Skip to content

Commit 76cd409

Browse files
committed
initial working version
Signed-off-by: Jesse Sanford <[email protected]>
1 parent 929c97c commit 76cd409

File tree

7 files changed

+125
-12
lines changed

7 files changed

+125
-12
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
.mcp-cli
33
dist
4+
.pnpm-store

.pkg-pr-new.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"packages": {
3+
".": {
4+
"name": "mcp-remote",
5+
"template": {
6+
"name": "mcp-remote-example",
7+
"label": "Try mcp-remote",
8+
"description": "Test the mcp-remote package with this example"
9+
}
10+
}
11+
},
12+
"comment": {
13+
"header": "📦 **Package Preview Available**",
14+
"footer": "Install this preview: `npm install https://pkg.pr.new/mcp-remote@{{pr}}`"
15+
}
16+
}

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# `mcp-remote`
22

3+
[![pkg.pr.new](https://pkg.pr.new/badge/geelen/mcp-remote)](https://pkg.pr.new/~/geelen/mcp-remote)
4+
35
Connect an MCP Client that only supports local (stdio) servers to a Remote MCP Server, with auth support:
46

57
**Note: this is a working proof-of-concept** but should be considered **experimental**.
@@ -135,6 +137,16 @@ To bypass authentication, or to emit custom headers on all requests to your remo
135137
]
136138
```
137139

140+
* To allow connections to servers with self-signed or invalid TLS certificates, add the `--insecure` flag. **⚠️ Warning**: This disables certificate verification and should only be used in development environments or trusted networks. Do not use this flag when connecting to untrusted servers as it makes your connection vulnerable to man-in-the-middle attacks.
141+
142+
```json
143+
"args": [
144+
"mcp-remote",
145+
"https://self-signed-server.example.com/sse",
146+
"--insecure"
147+
]
148+
```
149+
138150
### Transport Strategies
139151

140152
MCP Remote supports different transport strategies when connecting to an MCP server. This allows you to control whether it uses Server-Sent Events (SSE) or HTTP transport, and in what order it tries them.
@@ -271,6 +283,8 @@ this might look like:
271283
}
272284
```
273285

286+
Alternatively, for development or trusted internal servers with self-signed certificates, you can use the `--insecure` flag to bypass certificate validation entirely. **Note**: This should only be used when you trust the server and network.
287+
274288
### Check the logs
275289

276290
* [Follow Claude Desktop logs in real-time](https://modelcontextprotocol.io/docs/tools/debugging#debugging-in-claude-desktop)

src/client.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ async function runClient(
3636
host: string,
3737
staticOAuthClientMetadata: StaticOAuthClientMetadata,
3838
staticOAuthClientInfo: StaticOAuthClientInformationFull,
39+
insecure: boolean = false,
3940
) {
4041
// Set up event emitter for auth flow
4142
const events = new EventEmitter()
@@ -93,7 +94,16 @@ async function runClient(
9394

9495
try {
9596
// Connect to remote server with lazy authentication
96-
const transport = await connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy)
97+
const transport = await connectToRemoteServer(
98+
client,
99+
serverUrl,
100+
authProvider,
101+
headers,
102+
authInitializer,
103+
transportStrategy,
104+
new Set(),
105+
insecure,
106+
)
97107

98108
// Set up message and error handlers
99109
transport.onmessage = (message) => {
@@ -159,8 +169,8 @@ async function runClient(
159169

160170
// Parse command-line arguments and run the client
161171
parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx client.ts <https://server-url> [callback-port] [--debug]')
162-
.then(({ serverUrl, callbackPort, headers, transportStrategy, host, staticOAuthClientMetadata, staticOAuthClientInfo }) => {
163-
return runClient(serverUrl, callbackPort, headers, transportStrategy, host, staticOAuthClientMetadata, staticOAuthClientInfo)
172+
.then(({ serverUrl, callbackPort, headers, transportStrategy, host, staticOAuthClientMetadata, staticOAuthClientInfo, insecure }) => {
173+
return runClient(serverUrl, callbackPort, headers, transportStrategy, host, staticOAuthClientMetadata, staticOAuthClientInfo, insecure)
164174
})
165175
.catch((error) => {
166176
console.error('Fatal error:', error)

src/lib/utils.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,31 @@
11
import { describe, it, expect } from 'vitest'
2+
import { parseCommandLineArgs } from './utils'
23

34
// All sanitizeUrl tests have been moved to the strict-url-sanitise package
5+
6+
describe('parseCommandLineArgs', () => {
7+
it('should parse --insecure flag correctly', async () => {
8+
const args = ['https://example.com', '--insecure']
9+
const result = await parseCommandLineArgs(args, 'Test usage')
10+
11+
expect(result.insecure).toBe(true)
12+
expect(result.serverUrl).toBe('https://example.com')
13+
})
14+
15+
it('should default insecure to false when not provided', async () => {
16+
const args = ['https://example.com']
17+
const result = await parseCommandLineArgs(args, 'Test usage')
18+
19+
expect(result.insecure).toBe(false)
20+
expect(result.serverUrl).toBe('https://example.com')
21+
})
22+
23+
it('should work with multiple flags including --insecure', async () => {
24+
const args = ['https://example.com', '--debug', '--insecure', '--allow-http']
25+
const result = await parseCommandLineArgs(args, 'Test usage')
26+
27+
expect(result.insecure).toBe(true)
28+
expect(result.debug).toBe(true)
29+
expect(result.serverUrl).toBe('https://example.com')
30+
})
31+
})

src/lib/utils.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import crypto from 'crypto'
1313
import fs from 'fs'
1414
import { readFile, rm } from 'fs/promises'
1515
import path from 'path'
16+
import https from 'https'
1617
import { version as MCP_REMOTE_VERSION } from '../../package.json'
1718

1819
// Global type declaration for typescript
@@ -191,24 +192,34 @@ export async function connectToRemoteServer(
191192
authInitializer: AuthInitializer,
192193
transportStrategy: TransportStrategy = 'http-first',
193194
recursionReasons: Set<string> = new Set(),
195+
insecure: boolean = false,
194196
): Promise<Transport> {
195197
log(`[${pid}] Connecting to remote server: ${serverUrl}`)
196198
const url = new URL(serverUrl)
197199

198200
// Create transport with eventSourceInit to pass Authorization header if present
199201
const eventSourceInit = {
200202
fetch: (url: string | URL, init?: RequestInit) => {
201-
return Promise.resolve(authProvider?.tokens?.()).then((tokens) =>
202-
fetch(url, {
203+
return Promise.resolve(authProvider?.tokens?.()).then((tokens) => {
204+
const requestInit: RequestInit = {
203205
...init,
204206
headers: {
205207
...(init?.headers as Record<string, string> | undefined),
206208
...headers,
207209
...(tokens?.access_token ? { Authorization: `Bearer ${tokens.access_token}` } : {}),
208210
Accept: 'text/event-stream',
209211
} as Record<string, string>,
210-
}),
211-
)
212+
}
213+
214+
// Add insecure HTTPS agent if insecure flag is enabled
215+
if (insecure && new URL(url).protocol === 'https:') {
216+
;(requestInit as any).agent = new https.Agent({
217+
rejectUnauthorized: false,
218+
})
219+
}
220+
221+
return fetch(url, requestInit)
222+
})
212223
},
213224
}
214225

@@ -219,15 +230,24 @@ export async function connectToRemoteServer(
219230

220231
// Create transport instance based on the strategy
221232
const sseTransport = transportStrategy === 'sse-only' || transportStrategy === 'sse-first'
233+
234+
// Create requestInit with insecure agent if needed
235+
const requestInit: RequestInit = { headers }
236+
if (insecure && url.protocol === 'https:') {
237+
;(requestInit as any).agent = new https.Agent({
238+
rejectUnauthorized: false,
239+
})
240+
}
241+
222242
const transport = sseTransport
223243
? new SSEClientTransport(url, {
224244
authProvider,
225-
requestInit: { headers },
245+
requestInit,
226246
eventSourceInit,
227247
})
228248
: new StreamableHTTPClientTransport(url, {
229249
authProvider,
230-
requestInit: { headers },
250+
requestInit,
231251
})
232252

233253
try {
@@ -245,7 +265,7 @@ export async function connectToRemoteServer(
245265
// the client is already connected. So let's just create a one-off client to make a single request and figure
246266
// out if we're actually talking to an HTTP server or not.
247267
if (DEBUG) debugLog('Creating test transport for HTTP-only connection test')
248-
const testTransport = new StreamableHTTPClientTransport(url, { authProvider, requestInit: { headers } })
268+
const testTransport = new StreamableHTTPClientTransport(url, { authProvider, requestInit })
249269
const testClient = new Client({ name: 'mcp-remote-fallback-test', version: '0.0.0' }, { capabilities: {} })
250270
await testClient.connect(testTransport)
251271
}
@@ -286,6 +306,7 @@ export async function connectToRemoteServer(
286306
authInitializer,
287307
sseTransport ? 'http-only' : 'sse-only',
288308
recursionReasons,
309+
insecure,
289310
)
290311
} else if (error instanceof UnauthorizedError || (error instanceof Error && error.message.includes('Unauthorized'))) {
291312
log('Authentication required. Initializing auth...')
@@ -333,7 +354,16 @@ export async function connectToRemoteServer(
333354
if (DEBUG) debugLog('Recursively reconnecting after auth', { recursionReasons: Array.from(recursionReasons) })
334355

335356
// Recursively call connectToRemoteServer with the updated recursion tracking
336-
return connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy, recursionReasons)
357+
return connectToRemoteServer(
358+
client,
359+
serverUrl,
360+
authProvider,
361+
headers,
362+
authInitializer,
363+
transportStrategy,
364+
recursionReasons,
365+
insecure,
366+
)
337367
} catch (authError: any) {
338368
log('Authorization error:', authError)
339369
if (DEBUG)
@@ -550,6 +580,7 @@ export async function parseCommandLineArgs(args: string[], usage: string) {
550580
const serverUrl = args[0]
551581
const specifiedPort = args[1] ? parseInt(args[1]) : undefined
552582
const allowHttp = args.includes('--allow-http')
583+
const insecure = args.includes('--insecure')
553584

554585
// Check for debug flag
555586
const debug = args.includes('--debug')
@@ -691,6 +722,7 @@ export async function parseCommandLineArgs(args: string[], usage: string) {
691722
staticOAuthClientMetadata,
692723
staticOAuthClientInfo,
693724
authorizeResource,
725+
insecure,
694726
}
695727
}
696728

src/proxy.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ async function runProxy(
3636
staticOAuthClientMetadata: StaticOAuthClientMetadata,
3737
staticOAuthClientInfo: StaticOAuthClientInformationFull,
3838
authorizeResource: string,
39+
insecure: boolean = false,
3940
) {
4041
// Set up event emitter for auth flow
4142
const events = new EventEmitter()
@@ -86,7 +87,16 @@ async function runProxy(
8687

8788
try {
8889
// Connect to remote server with lazy authentication
89-
const remoteTransport = await connectToRemoteServer(null, serverUrl, authProvider, headers, authInitializer, transportStrategy)
90+
const remoteTransport = await connectToRemoteServer(
91+
null,
92+
serverUrl,
93+
authProvider,
94+
headers,
95+
authInitializer,
96+
transportStrategy,
97+
new Set(),
98+
insecure,
99+
)
90100

91101
// Set up bidirectional proxy between local and remote transports
92102
mcpProxy({
@@ -155,6 +165,7 @@ parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx proxy.ts <https://se
155165
staticOAuthClientMetadata,
156166
staticOAuthClientInfo,
157167
authorizeResource,
168+
insecure,
158169
}) => {
159170
return runProxy(
160171
serverUrl,
@@ -165,6 +176,7 @@ parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx proxy.ts <https://se
165176
staticOAuthClientMetadata,
166177
staticOAuthClientInfo,
167178
authorizeResource,
179+
insecure,
168180
)
169181
},
170182
)

0 commit comments

Comments
 (0)