Skip to content

Commit acbb2a4

Browse files
committed
fixed regression on newer node versions
Signed-off-by: Jesse Sanford <[email protected]>
1 parent 76cd409 commit acbb2a4

File tree

3 files changed

+274
-14
lines changed

3 files changed

+274
-14
lines changed

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,13 @@ To bypass authentication, or to emit custom headers on all requests to your remo
147147
]
148148
```
149149

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+
150157
### Transport Strategies
151158

152159
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.
@@ -285,6 +292,8 @@ this might look like:
285292

286293
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.
287294

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+
288297
### Check the logs
289298

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

309318
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.
310319

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+
311349
### Authentication Errors
312350

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

src/lib/utils.test.ts

Lines changed: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { describe, it, expect } from 'vitest'
2-
import { parseCommandLineArgs } from './utils'
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'
34

45
// All sanitizeUrl tests have been moved to the strict-url-sanitise package
56

@@ -29,3 +30,184 @@ describe('parseCommandLineArgs', () => {
2930
expect(result.serverUrl).toBe('https://example.com')
3031
})
3132
})
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+
})

src/lib/utils.ts

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,30 @@ export async function connectToRemoteServer(
196196
): Promise<Transport> {
197197
log(`[${pid}] Connecting to remote server: ${serverUrl}`)
198198
const url = new URL(serverUrl)
199+
200+
// Handle NODE_TLS_REJECT_UNAUTHORIZED environment variable for insecure connections
201+
const originalTlsReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED
202+
let shouldRestoreTlsEnv = false
203+
204+
if (insecure && url.protocol === 'https:') {
205+
if (originalTlsReject !== undefined) {
206+
// Environment variable is already set, check for conflicts
207+
if (originalTlsReject !== '0') {
208+
// User has cert verification enabled but wants --insecure, this is a conflict
209+
log('Error: Cannot use --insecure flag while NODE_TLS_REJECT_UNAUTHORIZED environment variable is set to enable certificate verification.')
210+
log('Either remove the --insecure flag or set NODE_TLS_REJECT_UNAUTHORIZED=0')
211+
process.exit(1)
212+
}
213+
// originalTlsReject === '0', compatible with --insecure, proceed without changes
214+
if (DEBUG) debugLog('NODE_TLS_REJECT_UNAUTHORIZED already set to 0, compatible with --insecure flag')
215+
} else {
216+
// Environment variable is unset, we can safely set it temporarily
217+
log('Setting NODE_TLS_REJECT_UNAUTHORIZED=0 for --insecure connection (will be restored after connection)')
218+
if (DEBUG) debugLog('Setting NODE_TLS_REJECT_UNAUTHORIZED=0 for insecure connection')
219+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
220+
shouldRestoreTlsEnv = true
221+
}
222+
}
199223

200224
// Create transport with eventSourceInit to pass Authorization header if present
201225
const eventSourceInit = {
@@ -211,12 +235,7 @@ export async function connectToRemoteServer(
211235
} as Record<string, string>,
212236
}
213237

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-
}
238+
// Note: insecure TLS handling is done via NODE_TLS_REJECT_UNAUTHORIZED environment variable
220239

221240
return fetch(url, requestInit)
222241
})
@@ -231,13 +250,8 @@ export async function connectToRemoteServer(
231250
// Create transport instance based on the strategy
232251
const sseTransport = transportStrategy === 'sse-only' || transportStrategy === 'sse-first'
233252

234-
// Create requestInit with insecure agent if needed
253+
// Create requestInit with headers
235254
const requestInit: RequestInit = { headers }
236-
if (insecure && url.protocol === 'https:') {
237-
;(requestInit as any).agent = new https.Agent({
238-
rejectUnauthorized: false,
239-
})
240-
}
241255

242256
const transport = sseTransport
243257
? new SSEClientTransport(url, {
@@ -272,6 +286,12 @@ export async function connectToRemoteServer(
272286
}
273287
log(`Connected to remote server using ${transport.constructor.name}`)
274288

289+
// Restore original TLS settings if we modified them
290+
if (shouldRestoreTlsEnv) {
291+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
292+
if (DEBUG) debugLog('Restored NODE_TLS_REJECT_UNAUTHORIZED to unset state')
293+
}
294+
275295
return transport
276296
} catch (error: any) {
277297
// Check if it's a protocol error and we should attempt fallback
@@ -289,6 +309,10 @@ export async function connectToRemoteServer(
289309
if (recursionReasons.has(REASON_TRANSPORT_FALLBACK)) {
290310
const errorMessage = `Already attempted transport fallback. Giving up.`
291311
log(errorMessage)
312+
// Restore original TLS settings before throwing
313+
if (shouldRestoreTlsEnv) {
314+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
315+
}
292316
throw new Error(errorMessage)
293317
}
294318

@@ -345,6 +369,14 @@ export async function connectToRemoteServer(
345369
debugLog('Already attempted auth reconnection, giving up', {
346370
recursionReasons: Array.from(recursionReasons),
347371
})
372+
// Restore original TLS settings before throwing
373+
if (insecure && url.protocol === 'https:') {
374+
if (originalTlsReject !== undefined) {
375+
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalTlsReject
376+
} else {
377+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
378+
}
379+
}
348380
throw new Error(errorMessage)
349381
}
350382

@@ -371,6 +403,10 @@ export async function connectToRemoteServer(
371403
errorMessage: authError.message,
372404
stack: authError.stack,
373405
})
406+
// Restore original TLS settings before throwing
407+
if (shouldRestoreTlsEnv) {
408+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
409+
}
374410
throw authError
375411
}
376412
} else {
@@ -381,6 +417,10 @@ export async function connectToRemoteServer(
381417
stack: error.stack,
382418
transportType: transport.constructor.name,
383419
})
420+
// Restore original TLS settings before throwing
421+
if (shouldRestoreTlsEnv) {
422+
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
423+
}
384424
throw error
385425
}
386426
}

0 commit comments

Comments
 (0)