feat: convert sse to http transport
This commit is contained in:
parent
504aa26761
commit
4cc46bf8d1
6 changed files with 60 additions and 65 deletions
49
README.md
49
README.md
|
@ -23,10 +23,7 @@ All the most popular MCP clients (Claude Desktop, Cursor & Windsurf) use the fol
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"remote-example": {
|
"remote-example": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": [
|
"args": ["mcp-remote", "https://remote.mcp.server/sse"]
|
||||||
"mcp-remote",
|
|
||||||
"https://remote.mcp.server/sse"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,12 +38,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"remote-example": {
|
"remote-example": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": [
|
"args": ["mcp-remote", "https://remote.mcp.server/sse", "--header", "Authorization: Bearer ${AUTH_TOKEN}"]
|
||||||
"mcp-remote",
|
|
||||||
"https://remote.mcp.server/sse",
|
|
||||||
"--header",
|
|
||||||
"Authorization: Bearer ${AUTH_TOKEN}"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"env": {
|
"env": {
|
||||||
"AUTH_TOKEN": "..."
|
"AUTH_TOKEN": "..."
|
||||||
|
@ -74,7 +66,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
||||||
|
|
||||||
### Flags
|
### Flags
|
||||||
|
|
||||||
* If `npx` is producing errors, consider adding `-y` as the first argument to auto-accept the installation of the `mcp-remote` package.
|
- If `npx` is producing errors, consider adding `-y` as the first argument to auto-accept the installation of the `mcp-remote` package.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
|
@ -85,7 +77,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
* To force `npx` to always check for an updated version of `mcp-remote`, add the `@latest` flag:
|
- To force `npx` to always check for an updated version of `mcp-remote`, add the `@latest` flag:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"args": [
|
"args": [
|
||||||
|
@ -94,7 +86,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
* To change which port `mcp-remote` listens for an OAuth redirect (by default `3334`), add an additional argument after the server URL. Note that whatever port you specify, if it is unavailable an open port will be chosen at random.
|
- To change which port `mcp-remote` listens for an OAuth redirect (by default `3334`), add an additional argument after the server URL. Note that whatever port you specify, if it is unavailable an open port will be chosen at random.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"args": [
|
"args": [
|
||||||
|
@ -104,7 +96,7 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
* To allow HTTP connections in trusted private networks, add the `--allow-http` flag. Note: This should only be used in secure private networks where traffic cannot be intercepted.
|
- To allow HTTP connections in trusted private networks, add the `--allow-http` flag. Note: This should only be used in secure private networks where traffic cannot be intercepted.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"args": [
|
"args": [
|
||||||
|
@ -120,8 +112,8 @@ To bypass authentication, or to emit custom headers on all requests to your remo
|
||||||
|
|
||||||
In order to add an MCP server to Claude Desktop you need to edit the configuration file located at:
|
In order to add an MCP server to Claude Desktop you need to edit the configuration file located at:
|
||||||
|
|
||||||
* macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
* Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
If it does not exist yet, [you may need to enable it under Settings > Developer](https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server).
|
If it does not exist yet, [you may need to enable it under Settings > Developer](https://modelcontextprotocol.io/quickstart/user#2-add-the-filesystem-mcp-server).
|
||||||
|
|
||||||
|
@ -143,16 +135,16 @@ As of version `0.48.0`, Cursor supports unauthed SSE servers directly. If your M
|
||||||
|
|
||||||
For instructions on building & deploying remote MCP servers, including acting as a valid OAuth client, see the following resources:
|
For instructions on building & deploying remote MCP servers, including acting as a valid OAuth client, see the following resources:
|
||||||
|
|
||||||
* https://developers.cloudflare.com/agents/guides/remote-mcp-server/
|
- https://developers.cloudflare.com/agents/guides/remote-mcp-server/
|
||||||
|
|
||||||
In particular, see:
|
In particular, see:
|
||||||
|
|
||||||
* https://github.com/cloudflare/workers-oauth-provider for defining an MCP-comlpiant OAuth server in Cloudflare Workers
|
- https://github.com/cloudflare/workers-oauth-provider for defining an MCP-comlpiant OAuth server in Cloudflare Workers
|
||||||
* https://github.com/cloudflare/agents/tree/main/examples/mcp for defining an `McpAgent` using the [`agents`](https://npmjs.com/package/agents) framework.
|
- https://github.com/cloudflare/agents/tree/main/examples/mcp for defining an `McpAgent` using the [`agents`](https://npmjs.com/package/agents) framework.
|
||||||
|
|
||||||
For more information about testing these servers, see also:
|
For more information about testing these servers, see also:
|
||||||
|
|
||||||
* https://developers.cloudflare.com/agents/guides/test-remote-mcp-server/
|
- https://developers.cloudflare.com/agents/guides/test-remote-mcp-server/
|
||||||
|
|
||||||
Know of more resources you'd like to share? Please add them to this Readme and send a PR!
|
Know of more resources you'd like to share? Please add them to this Readme and send a PR!
|
||||||
|
|
||||||
|
@ -170,7 +162,7 @@ Then restarting your MCP client.
|
||||||
|
|
||||||
### Check your Node version
|
### Check your Node version
|
||||||
|
|
||||||
Make sure that the version of Node you have installed is [18 or
|
Make sure that the version of Node you have installed is [18 or
|
||||||
higher](https://modelcontextprotocol.io/quickstart/server). Claude
|
higher](https://modelcontextprotocol.io/quickstart/server). Claude
|
||||||
Desktop will use your system version of Node, even if you have a newer
|
Desktop will use your system version of Node, even if you have a newer
|
||||||
version installed elsewhere.
|
version installed elsewhere.
|
||||||
|
@ -187,13 +179,10 @@ this might look like:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"remote-example": {
|
"remote-example": {
|
||||||
"command": "npx",
|
"command": "npx",
|
||||||
"args": [
|
"args": ["mcp-remote", "https://remote.mcp.server/sse"],
|
||||||
"mcp-remote",
|
|
||||||
"https://remote.mcp.server/sse"
|
|
||||||
],
|
|
||||||
"env": {
|
"env": {
|
||||||
"NODE_EXTRA_CA_CERTS": "{your CA certificate file path}.pem"
|
"NODE_EXTRA_CA_CERTS": "{your CA certificate file path}.pem"
|
||||||
}
|
}
|
||||||
|
@ -204,10 +193,10 @@ this might look like:
|
||||||
|
|
||||||
### Check the logs
|
### Check the logs
|
||||||
|
|
||||||
* [Follow Claude Desktop logs in real-time](https://modelcontextprotocol.io/docs/tools/debugging#debugging-in-claude-desktop)
|
- [Follow Claude Desktop logs in real-time](https://modelcontextprotocol.io/docs/tools/debugging#debugging-in-claude-desktop)
|
||||||
* MacOS / Linux:<br/>`tail -n 20 -F ~/Library/Logs/Claude/mcp*.log`
|
- MacOS / Linux:<br/>`tail -n 20 -F ~/Library/Logs/Claude/mcp*.log`
|
||||||
* For bash on WSL:<br/>`tail -n 20 -f "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log"`
|
- For bash on WSL:<br/>`tail -n 20 -f "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log"`
|
||||||
* Powershell: <br/>`Get-Content "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log" -Wait -Tail 20`
|
- Powershell: <br/>`Get-Content "C:\Users\YourUsername\AppData\Local\Claude\Logs\mcp.log" -Wait -Tail 20`
|
||||||
|
|
||||||
## Debugging
|
## Debugging
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
"check": "prettier --check . && tsc"
|
"check": "prettier --check . && tsc"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"open": "^10.1.0"
|
"open": "^10.1.0"
|
||||||
},
|
},
|
||||||
|
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
@ -9,8 +9,8 @@ importers:
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@modelcontextprotocol/sdk':
|
'@modelcontextprotocol/sdk':
|
||||||
specifier: ^1.9.0
|
specifier: ^1.10.2
|
||||||
version: 1.9.0
|
version: 1.10.2
|
||||||
express:
|
express:
|
||||||
specifier: ^4.21.2
|
specifier: ^4.21.2
|
||||||
version: 4.21.2
|
version: 4.21.2
|
||||||
|
@ -217,8 +217,8 @@ packages:
|
||||||
'@jridgewell/trace-mapping@0.3.25':
|
'@jridgewell/trace-mapping@0.3.25':
|
||||||
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.9.0':
|
'@modelcontextprotocol/sdk@1.10.2':
|
||||||
resolution: {integrity: sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==}
|
resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@pkgjs/parseargs@0.11.0':
|
'@pkgjs/parseargs@0.11.0':
|
||||||
|
@ -1238,7 +1238,7 @@ snapshots:
|
||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.9.0':
|
'@modelcontextprotocol/sdk@1.10.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MCP Client with OAuth support
|
* MCP Client with OAuth support
|
||||||
* A command-line client that connects to an MCP server using SSE with OAuth authentication.
|
* A command-line client that connects to an MCP server using StreamableHTTP with OAuth authentication.
|
||||||
*
|
*
|
||||||
* Run with: npx tsx client.ts https://example.remote/server [callback-port]
|
* Run with: npx tsx client.ts https://example.remote/server [callback-port]
|
||||||
*
|
*
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
import { EventEmitter } from 'events'
|
import { EventEmitter } from 'events'
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||||
import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
|
import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||||
import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider'
|
import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider'
|
||||||
|
@ -60,7 +60,16 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor
|
||||||
// Create the transport factory
|
// Create the transport factory
|
||||||
const url = new URL(serverUrl)
|
const url = new URL(serverUrl)
|
||||||
function initTransport() {
|
function initTransport() {
|
||||||
const transport = new SSEClientTransport(url, { authProvider, requestInit: { headers } })
|
const transport = new StreamableHTTPClientTransport(url, {
|
||||||
|
authProvider,
|
||||||
|
requestInit: { headers },
|
||||||
|
reconnectionOptions: {
|
||||||
|
initialReconnectionDelay: 1000,
|
||||||
|
maxReconnectionDelay: 10000,
|
||||||
|
reconnectionDelayGrowFactor: 1.5,
|
||||||
|
maxRetries: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Set up message and error handlers
|
// Set up message and error handlers
|
||||||
transport.onmessage = (message) => {
|
transport.onmessage = (message) => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
import { OAuthClientProvider, UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'
|
||||||
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
||||||
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
|
||||||
import { OAuthCallbackServerOptions } from './types'
|
import { OAuthCallbackServerOptions } from './types'
|
||||||
import express from 'express'
|
import express from 'express'
|
||||||
|
@ -65,13 +65,13 @@ export function mcpProxy({ transportToClient, transportToServer }: { transportTo
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and connects to a remote SSE server with OAuth authentication
|
* Creates and connects to a remote server with OAuth authentication
|
||||||
* @param serverUrl The URL of the remote server
|
* @param serverUrl The URL of the remote server
|
||||||
* @param authProvider The OAuth client provider
|
* @param authProvider The OAuth client provider
|
||||||
* @param headers Additional headers to send with the request
|
* @param headers Additional headers to send with the request
|
||||||
* @param waitForAuthCode Function to wait for the auth code
|
* @param waitForAuthCode Function to wait for the auth code
|
||||||
* @param skipBrowserAuth Whether to skip browser auth and use shared auth
|
* @param skipBrowserAuth Whether to skip browser auth and use shared auth
|
||||||
* @returns The connected SSE client transport
|
* @returns The connected StreamableHTTP client transport
|
||||||
*/
|
*/
|
||||||
export async function connectToRemoteServer(
|
export async function connectToRemoteServer(
|
||||||
serverUrl: string,
|
serverUrl: string,
|
||||||
|
@ -79,31 +79,19 @@ export async function connectToRemoteServer(
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
waitForAuthCode: () => Promise<string>,
|
waitForAuthCode: () => Promise<string>,
|
||||||
skipBrowserAuth: boolean = false,
|
skipBrowserAuth: boolean = false,
|
||||||
): Promise<SSEClientTransport> {
|
): Promise<StreamableHTTPClientTransport> {
|
||||||
log(`[${pid}] Connecting to remote server: ${serverUrl}`)
|
log(`[${pid}] Connecting to remote server: ${serverUrl}`)
|
||||||
const url = new URL(serverUrl)
|
const url = new URL(serverUrl)
|
||||||
|
|
||||||
// Create transport with eventSourceInit to pass Authorization header if present
|
const transport = new StreamableHTTPClientTransport(url, {
|
||||||
const eventSourceInit = {
|
|
||||||
fetch: (url: string | URL, init?: RequestInit) => {
|
|
||||||
return Promise.resolve(authProvider?.tokens?.()).then((tokens) =>
|
|
||||||
fetch(url, {
|
|
||||||
...init,
|
|
||||||
headers: {
|
|
||||||
...(init?.headers as Record<string, string> | undefined),
|
|
||||||
...headers,
|
|
||||||
...(tokens?.access_token ? { Authorization: `Bearer ${tokens.access_token}` } : {}),
|
|
||||||
Accept: "text/event-stream",
|
|
||||||
} as Record<string, string>,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const transport = new SSEClientTransport(url, {
|
|
||||||
authProvider,
|
authProvider,
|
||||||
requestInit: { headers },
|
requestInit: { headers },
|
||||||
eventSourceInit,
|
reconnectionOptions: {
|
||||||
|
initialReconnectionDelay: 1000,
|
||||||
|
maxReconnectionDelay: 10000,
|
||||||
|
reconnectionDelayGrowFactor: 1.5,
|
||||||
|
maxRetries: 10,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -126,7 +114,16 @@ export async function connectToRemoteServer(
|
||||||
await transport.finishAuth(code)
|
await transport.finishAuth(code)
|
||||||
|
|
||||||
// Create a new transport after auth
|
// Create a new transport after auth
|
||||||
const newTransport = new SSEClientTransport(url, { authProvider, requestInit: { headers } })
|
const newTransport = new StreamableHTTPClientTransport(url, {
|
||||||
|
authProvider,
|
||||||
|
requestInit: { headers },
|
||||||
|
reconnectionOptions: {
|
||||||
|
initialReconnectionDelay: 1000,
|
||||||
|
maxReconnectionDelay: 10000,
|
||||||
|
reconnectionDelayGrowFactor: 1.5,
|
||||||
|
maxRetries: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
await newTransport.start()
|
await newTransport.start()
|
||||||
log('Connected to remote server after authentication')
|
log('Connected to remote server after authentication')
|
||||||
return newTransport
|
return newTransport
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MCP Proxy with OAuth support
|
* MCP Proxy with OAuth support
|
||||||
* A bidirectional proxy between a local STDIO MCP server and a remote SSE server with OAuth authentication.
|
* A bidirectional proxy between a local STDIO MCP server and a remote server with OAuth authentication.
|
||||||
*
|
*
|
||||||
* Run with: npx tsx proxy.ts https://example.remote/server [callback-port]
|
* Run with: npx tsx proxy.ts https://example.remote/server [callback-port]
|
||||||
*
|
*
|
||||||
|
@ -59,7 +59,7 @@ async function runProxy(serverUrl: string, callbackPort: number, headers: Record
|
||||||
// Start the local STDIO server
|
// Start the local STDIO server
|
||||||
await localTransport.start()
|
await localTransport.start()
|
||||||
log('Local STDIO server running')
|
log('Local STDIO server running')
|
||||||
log('Proxy established successfully between local STDIO and remote SSE')
|
log('Proxy established successfully between local STDIO and remote server')
|
||||||
log('Press Ctrl+C to exit')
|
log('Press Ctrl+C to exit')
|
||||||
|
|
||||||
// Setup cleanup handler
|
// Setup cleanup handler
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue