Skip to content

Commit d39a31e

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

File tree

7 files changed

+386
-13
lines changed

7 files changed

+386
-13
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: 52 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,23 @@ 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+
150+
**Important**: The `--insecure` flag will conflict with the `NODE_TLS_REJECT_UNAUTHORIZED` environment variable if it's set to enable certificate verification (any value other than `0`). If you encounter an error about this conflict, either:
151+
- Remove the `--insecure` flag, or
152+
- Set `NODE_TLS_REJECT_UNAUTHORIZED=0` in your environment, or
153+
- Unset the `NODE_TLS_REJECT_UNAUTHORIZED` environment variable entirely
154+
155+
When `NODE_TLS_REJECT_UNAUTHORIZED` is unset and you use `--insecure`, mcp-remote will temporarily set it to `0` during the connection and restore it afterwards.
156+
138157
### Transport Strategies
139158

140159
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 +290,10 @@ this might look like:
271290
}
272291
```
273292

293+
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.
294+
295+
If you have `NODE_TLS_REJECT_UNAUTHORIZED` set in your environment, ensure it's compatible with the `--insecure` flag (see the `--insecure` flag documentation above for details).
296+
274297
### Check the logs
275298

276299
* [Follow Claude Desktop logs in real-time](https://modelcontextprotocol.io/docs/tools/debugging#debugging-in-claude-desktop)
@@ -294,6 +317,35 @@ For troubleshooting complex issues, especially with token refreshing or authenti
294317

295318
This creates detailed logs in `~/.mcp-auth/{server_hash}_debug.log` with timestamps and complete information about every step of the connection and authentication process. When you find issues with token refreshing, laptop sleep/resume issues, or auth problems, provide these logs when seeking support.
296319

320+
### NODE_TLS_REJECT_UNAUTHORIZED Conflicts
321+
322+
If you see an error message like:
323+
324+
```
325+
Error: Cannot use --insecure flag while NODE_TLS_REJECT_UNAUTHORIZED environment variable is set to enable certificate verification.
326+
```
327+
328+
This means your environment has `NODE_TLS_REJECT_UNAUTHORIZED` set to a value other than `0`, which conflicts with the `--insecure` flag. To resolve this:
329+
330+
1. **Remove the `--insecure` flag** if you want to keep certificate verification enabled, or
331+
2. **Set `NODE_TLS_REJECT_UNAUTHORIZED=0`** in your MCP client configuration to disable certificate verification globally, or
332+
3. **Unset the environment variable** by removing it from your shell profile or MCP client configuration
333+
334+
Example of setting it to `0` in Claude Desktop config:
335+
```json
336+
{
337+
"mcpServers": {
338+
"remote-example": {
339+
"command": "npx",
340+
"args": ["mcp-remote", "https://example.com/sse", "--insecure"],
341+
"env": {
342+
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
343+
}
344+
}
345+
}
346+
}
347+
```
348+
297349
### Authentication Errors
298350

299351
If you encounter the following error, returned by the `/callback` URL:

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: 211 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,213 @@
1-
import { describe, it, expect } from 'vitest'
1+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2+
import { parseCommandLineArgs, connectToRemoteServer } from './utils'
3+
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'
24

35
// All sanitizeUrl tests have been moved to the strict-url-sanitise package
6+
7+
describe('parseCommandLineArgs', () => {
8+
it('should parse --insecure flag correctly', async () => {
9+
const args = ['https://example.com', '--insecure']
10+
const result = await parseCommandLineArgs(args, 'Test usage')
11+
12+
expect(result.insecure).toBe(true)
13+
expect(result.serverUrl).toBe('https://example.com')
14+
})
15+
16+
it('should default insecure to false when not provided', async () => {
17+
const args = ['https://example.com']
18+
const result = await parseCommandLineArgs(args, 'Test usage')
19+
20+
expect(result.insecure).toBe(false)
21+
expect(result.serverUrl).toBe('https://example.com')
22+
})
23+
24+
it('should work with multiple flags including --insecure', async () => {
25+
const args = ['https://example.com', '--debug', '--insecure', '--allow-http']
26+
const result = await parseCommandLineArgs(args, 'Test usage')
27+
28+
expect(result.insecure).toBe(true)
29+
expect(result.debug).toBe(true)
30+
expect(result.serverUrl).toBe('https://example.com')
31+
})
32+
})
33+
34+
describe('connectToRemoteServer insecure flag', () => {
35+
let originalTlsReject: string | undefined
36+
let mockExit: any
37+
let mockLog: any
38+
39+
beforeEach(() => {
40+
// Save original environment variable
41+
originalTlsReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED
42+
43+
// Mock process.exit to prevent actual exits during tests
44+
mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
45+
throw new Error('process.exit called')
46+
})
47+
48+
// Mock console.error to capture log messages
49+
mockLog = vi.spyOn(console, 'error').mockImplementation(() => {})
50+
})
51+
52+
afterEach(() => {
53+
// Restore original environment variable
54+
if (originalTlsReject !== undefined) {
55+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalTlsReject
56+
} else {
57+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
58+
}
59+
60+
// Restore mocks
61+
mockExit.mockRestore()
62+
mockLog.mockRestore()
63+
})
64+
65+
it('should fail when --insecure conflicts with NODE_TLS_REJECT_UNAUTHORIZED=1', async () => {
66+
// Set environment variable to enable cert verification (conflicts with --insecure)
67+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'
68+
69+
const mockAuthProvider = {} as OAuthClientProvider
70+
const mockAuthInitializer = async () => ({ waitForAuthCode: async () => 'test', skipBrowserAuth: false })
71+
72+
await expect(
73+
connectToRemoteServer(
74+
null,
75+
'https://example.com',
76+
mockAuthProvider,
77+
{},
78+
mockAuthInitializer,
79+
'http-first',
80+
new Set(),
81+
true // insecure flag
82+
)
83+
).rejects.toThrow('process.exit called')
84+
85+
// Check that error message was logged
86+
expect(mockLog).toHaveBeenCalledWith(
87+
expect.stringContaining('Cannot use --insecure flag while NODE_TLS_REJECT_UNAUTHORIZED')
88+
)
89+
})
90+
91+
it('should fail when --insecure conflicts with NODE_TLS_REJECT_UNAUTHORIZED=true', async () => {
92+
// Set environment variable to enable cert verification (conflicts with --insecure)
93+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 'true'
94+
95+
const mockAuthProvider = {} as OAuthClientProvider
96+
const mockAuthInitializer = async () => ({ waitForAuthCode: async () => 'test', skipBrowserAuth: false })
97+
98+
await expect(
99+
connectToRemoteServer(
100+
null,
101+
'https://example.com',
102+
mockAuthProvider,
103+
{},
104+
mockAuthInitializer,
105+
'http-first',
106+
new Set(),
107+
true // insecure flag
108+
)
109+
).rejects.toThrow('process.exit called')
110+
111+
// Check that error message was logged
112+
expect(mockLog).toHaveBeenCalledWith(
113+
expect.stringContaining('Cannot use --insecure flag while NODE_TLS_REJECT_UNAUTHORIZED')
114+
)
115+
})
116+
117+
it('should proceed when --insecure is compatible with NODE_TLS_REJECT_UNAUTHORIZED=0', async () => {
118+
// Set environment variable to disable cert verification (compatible with --insecure)
119+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
120+
121+
const mockAuthProvider = {} as OAuthClientProvider
122+
const mockAuthInitializer = async () => ({ waitForAuthCode: async () => 'test', skipBrowserAuth: false })
123+
124+
// This test will likely fail due to network issues, but we're just testing that
125+
// the conflict detection doesn't trigger process.exit
126+
try {
127+
await connectToRemoteServer(
128+
null,
129+
'https://example.com',
130+
mockAuthProvider,
131+
{},
132+
mockAuthInitializer,
133+
'http-first',
134+
new Set(),
135+
true // insecure flag
136+
)
137+
} catch (error) {
138+
// We expect this to fail due to network/connection issues, not conflict detection
139+
expect(error).not.toEqual(new Error('process.exit called'))
140+
}
141+
142+
// Should not have called process.exit
143+
expect(mockExit).not.toHaveBeenCalled()
144+
})
145+
146+
it('should set and restore NODE_TLS_REJECT_UNAUTHORIZED when unset with --insecure', async () => {
147+
// Ensure environment variable is unset
148+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
149+
150+
const mockAuthProvider = {} as OAuthClientProvider
151+
const mockAuthInitializer = async () => ({ waitForAuthCode: async () => 'test', skipBrowserAuth: false })
152+
153+
// This test will likely fail due to network issues, but we're testing env var handling
154+
try {
155+
await connectToRemoteServer(
156+
null,
157+
'https://example.com',
158+
mockAuthProvider,
159+
{},
160+
mockAuthInitializer,
161+
'http-first',
162+
new Set(),
163+
true // insecure flag
164+
)
165+
} catch (error) {
166+
// We expect this to fail due to network/connection issues
167+
expect(error).not.toEqual(new Error('process.exit called'))
168+
}
169+
170+
// Environment variable should be restored to unset state
171+
expect(process.env.NODE_TLS_REJECT_UNAUTHORIZED).toBeUndefined()
172+
173+
// Should not have called process.exit
174+
expect(mockExit).not.toHaveBeenCalled()
175+
176+
// Should have logged that we're setting the env var
177+
expect(mockLog).toHaveBeenCalledWith(
178+
expect.stringContaining('Setting NODE_TLS_REJECT_UNAUTHORIZED=0 for --insecure connection')
179+
)
180+
})
181+
182+
it('should not modify NODE_TLS_REJECT_UNAUTHORIZED when --insecure is false', async () => {
183+
// Set a specific value
184+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'
185+
const originalValue = process.env.NODE_TLS_REJECT_UNAUTHORIZED
186+
187+
const mockAuthProvider = {} as OAuthClientProvider
188+
const mockAuthInitializer = async () => ({ waitForAuthCode: async () => 'test', skipBrowserAuth: false })
189+
190+
// This test will likely fail due to network issues
191+
try {
192+
await connectToRemoteServer(
193+
null,
194+
'https://example.com',
195+
mockAuthProvider,
196+
{},
197+
mockAuthInitializer,
198+
'http-first',
199+
new Set(),
200+
false // insecure flag is false
201+
)
202+
} catch (error) {
203+
// We expect this to fail due to network/connection issues
204+
expect(error).not.toEqual(new Error('process.exit called'))
205+
}
206+
207+
// Environment variable should remain unchanged
208+
expect(process.env.NODE_TLS_REJECT_UNAUTHORIZED).toBe(originalValue)
209+
210+
// Should not have called process.exit
211+
expect(mockExit).not.toHaveBeenCalled()
212+
})
213+
})

0 commit comments

Comments
 (0)