Skip to content

Allow for insecure https connections to upstream MCP servers w/ --insecure flag #132

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
node_modules
.mcp-cli
dist
.pnpm-store
16 changes: 16 additions & 0 deletions .pkg-pr-new.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"packages": {
".": {
"name": "mcp-remote",
"template": {
"name": "mcp-remote-example",
"label": "Try mcp-remote",
"description": "Test the mcp-remote package with this example"
}
}
},
"comment": {
"header": "📦 **Package Preview Available**",
"footer": "Install this preview: `npm install https://pkg.pr.new/mcp-remote@{{pr}}`"
}
}
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# `mcp-remote`

[![pkg.pr.new](https://pkg.pr.new/badge/geelen/mcp-remote)](https://pkg.pr.new/~/geelen/mcp-remote)

Connect an MCP Client that only supports local (stdio) servers to a Remote MCP Server, with auth support:

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

* 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.

```json
"args": [
"mcp-remote",
"https://self-signed-server.example.com/sse",
"--insecure"
]
```

**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:
- Remove the `--insecure` flag, or
- Set `NODE_TLS_REJECT_UNAUTHORIZED=0` in your environment, or
- Unset the `NODE_TLS_REJECT_UNAUTHORIZED` environment variable entirely

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.

### Transport Strategies

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.
Expand Down Expand Up @@ -271,6 +290,10 @@ this might look like:
}
```

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.

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).

### Check the logs

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

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.

### NODE_TLS_REJECT_UNAUTHORIZED Conflicts

If you see an error message like:

```
Error: Cannot use --insecure flag while NODE_TLS_REJECT_UNAUTHORIZED environment variable is set to enable certificate verification.
```

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:

1. **Remove the `--insecure` flag** if you want to keep certificate verification enabled, or
2. **Set `NODE_TLS_REJECT_UNAUTHORIZED=0`** in your MCP client configuration to disable certificate verification globally, or
3. **Unset the environment variable** by removing it from your shell profile or MCP client configuration

Example of setting it to `0` in Claude Desktop config:
```json
{
"mcpServers": {
"remote-example": {
"command": "npx",
"args": ["mcp-remote", "https://example.com/sse", "--insecure"],
"env": {
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
}
}
}
```

### Authentication Errors

If you encounter the following error, returned by the `/callback` URL:
Expand Down
16 changes: 13 additions & 3 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ async function runClient(
host: string,
staticOAuthClientMetadata: StaticOAuthClientMetadata,
staticOAuthClientInfo: StaticOAuthClientInformationFull,
insecure: boolean = false,
) {
// Set up event emitter for auth flow
const events = new EventEmitter()
Expand Down Expand Up @@ -93,7 +94,16 @@ async function runClient(

try {
// Connect to remote server with lazy authentication
const transport = await connectToRemoteServer(client, serverUrl, authProvider, headers, authInitializer, transportStrategy)
const transport = await connectToRemoteServer(
client,
serverUrl,
authProvider,
headers,
authInitializer,
transportStrategy,
new Set(),
insecure,
)

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

// Parse command-line arguments and run the client
parseCommandLineArgs(process.argv.slice(2), 'Usage: npx tsx client.ts <https://server-url> [callback-port] [--debug]')
.then(({ serverUrl, callbackPort, headers, transportStrategy, host, staticOAuthClientMetadata, staticOAuthClientInfo }) => {
return runClient(serverUrl, callbackPort, headers, transportStrategy, host, staticOAuthClientMetadata, staticOAuthClientInfo)
.then(({ serverUrl, callbackPort, headers, transportStrategy, host, staticOAuthClientMetadata, staticOAuthClientInfo, insecure }) => {
return runClient(serverUrl, callbackPort, headers, transportStrategy, host, staticOAuthClientMetadata, staticOAuthClientInfo, insecure)
})
.catch((error) => {
console.error('Fatal error:', error)
Expand Down
212 changes: 211 additions & 1 deletion src/lib/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,213 @@
import { describe, it, expect } from 'vitest'
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { parseCommandLineArgs, connectToRemoteServer } from './utils'
import { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'

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

describe('parseCommandLineArgs', () => {
it('should parse --insecure flag correctly', async () => {
const args = ['https://example.com', '--insecure']
const result = await parseCommandLineArgs(args, 'Test usage')

expect(result.insecure).toBe(true)
expect(result.serverUrl).toBe('https://example.com')
})

it('should default insecure to false when not provided', async () => {
const args = ['https://example.com']
const result = await parseCommandLineArgs(args, 'Test usage')

expect(result.insecure).toBe(false)
expect(result.serverUrl).toBe('https://example.com')
})

it('should work with multiple flags including --insecure', async () => {
const args = ['https://example.com', '--debug', '--insecure', '--allow-http']
const result = await parseCommandLineArgs(args, 'Test usage')

expect(result.insecure).toBe(true)
expect(result.debug).toBe(true)
expect(result.serverUrl).toBe('https://example.com')
})
})

describe('connectToRemoteServer insecure flag', () => {
let originalTlsReject: string | undefined
let mockExit: any
let mockLog: any

beforeEach(() => {
// Save original environment variable
originalTlsReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED

// Mock process.exit to prevent actual exits during tests
mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called')
})

// Mock console.error to capture log messages
mockLog = vi.spyOn(console, 'error').mockImplementation(() => {})
})

afterEach(() => {
// Restore original environment variable
if (originalTlsReject !== undefined) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalTlsReject
} else {
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED
}

// Restore mocks
mockExit.mockRestore()
mockLog.mockRestore()
})

it('should fail when --insecure conflicts with NODE_TLS_REJECT_UNAUTHORIZED=1', async () => {
// Set environment variable to enable cert verification (conflicts with --insecure)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'

const mockAuthProvider = {} as OAuthClientProvider
const mockAuthInitializer = async () => ({ waitForAuthCode: async () => 'test', skipBrowserAuth: false })

await expect(
connectToRemoteServer(
null,
'https://example.com',
mockAuthProvider,
{},
mockAuthInitializer,
'http-first',
new Set(),
true // insecure flag
)
).rejects.toThrow('process.exit called')

// Check that error message was logged
expect(mockLog).toHaveBeenCalledWith(
expect.stringContaining('Cannot use --insecure flag while NODE_TLS_REJECT_UNAUTHORIZED')
)
})

it('should fail when --insecure conflicts with NODE_TLS_REJECT_UNAUTHORIZED=true', async () => {
// Set environment variable to enable cert verification (conflicts with --insecure)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = 'true'

const mockAuthProvider = {} as OAuthClientProvider
const mockAuthInitializer = async () => ({ waitForAuthCode: async () => 'test', skipBrowserAuth: false })

await expect(
connectToRemoteServer(
null,
'https://example.com',
mockAuthProvider,
{},
mockAuthInitializer,
'http-first',
new Set(),
true // insecure flag
)
).rejects.toThrow('process.exit called')

// Check that error message was logged
expect(mockLog).toHaveBeenCalledWith(
expect.stringContaining('Cannot use --insecure flag while NODE_TLS_REJECT_UNAUTHORIZED')
)
})

it('should proceed when --insecure is compatible with NODE_TLS_REJECT_UNAUTHORIZED=0', async () => {
// Set environment variable to disable cert verification (compatible with --insecure)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'

const mockAuthProvider = {} as OAuthClientProvider
const mockAuthInitializer = async () => ({ waitForAuthCode: async () => 'test', skipBrowserAuth: false })

// This test will likely fail due to network issues, but we're just testing that
// the conflict detection doesn't trigger process.exit
try {
await connectToRemoteServer(
null,
'https://example.com',
mockAuthProvider,
{},
mockAuthInitializer,
'http-first',
new Set(),
true // insecure flag
)
} catch (error) {
// We expect this to fail due to network/connection issues, not conflict detection
expect(error).not.toEqual(new Error('process.exit called'))
}

// Should not have called process.exit
expect(mockExit).not.toHaveBeenCalled()
})

it('should set and restore NODE_TLS_REJECT_UNAUTHORIZED when unset with --insecure', async () => {
// Ensure environment variable is unset
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED

const mockAuthProvider = {} as OAuthClientProvider
const mockAuthInitializer = async () => ({ waitForAuthCode: async () => 'test', skipBrowserAuth: false })

// This test will likely fail due to network issues, but we're testing env var handling
try {
await connectToRemoteServer(
null,
'https://example.com',
mockAuthProvider,
{},
mockAuthInitializer,
'http-first',
new Set(),
true // insecure flag
)
} catch (error) {
// We expect this to fail due to network/connection issues
expect(error).not.toEqual(new Error('process.exit called'))
}

// Environment variable should be restored to unset state
expect(process.env.NODE_TLS_REJECT_UNAUTHORIZED).toBeUndefined()

// Should not have called process.exit
expect(mockExit).not.toHaveBeenCalled()

// Should have logged that we're setting the env var
expect(mockLog).toHaveBeenCalledWith(
expect.stringContaining('Setting NODE_TLS_REJECT_UNAUTHORIZED=0 for --insecure connection')
)
})

it('should not modify NODE_TLS_REJECT_UNAUTHORIZED when --insecure is false', async () => {
// Set a specific value
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'
const originalValue = process.env.NODE_TLS_REJECT_UNAUTHORIZED

const mockAuthProvider = {} as OAuthClientProvider
const mockAuthInitializer = async () => ({ waitForAuthCode: async () => 'test', skipBrowserAuth: false })

// This test will likely fail due to network issues
try {
await connectToRemoteServer(
null,
'https://example.com',
mockAuthProvider,
{},
mockAuthInitializer,
'http-first',
new Set(),
false // insecure flag is false
)
} catch (error) {
// We expect this to fail due to network/connection issues
expect(error).not.toEqual(new Error('process.exit called'))
}

// Environment variable should remain unchanged
expect(process.env.NODE_TLS_REJECT_UNAUTHORIZED).toBe(originalValue)

// Should not have called process.exit
expect(mockExit).not.toHaveBeenCalled()
})
})
Loading