# With inspector:
npx -y @modelcontextprotocol/inspector \
docker -- run --rm -i node:latest \
npx -y https://gist.github.com/ochafik/3ba7eb0e5af9d250a77efbb2c948039b \
npx -y --silent @modelcontextprotocol/server-everything streamableHttp
# Or CC:
claude mcp add my-remote -- \
docker run --rm -i node:latest \
npx -y https://gist.github.com/ochafik/3ba7eb0e5af9d250a77efbb2c948039b \
npx -y --silent @modelcontextprotocol/server-everything streamableHttp-
-
Save ochafik/3ba7eb0e5af9d250a77efbb2c948039b to your computer and use it in GitHub Desktop.
http-as-stdio
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * HTTP to stdio converter for MCP protocol | |
| * | |
| * This converter allows an MCP server running over stdio to be accessed via HTTP. | |
| * It acts as a bridge between stdio transport (parent process) and HTTP transport (server). | |
| */ | |
| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; | |
| import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; | |
| import { CancelledNotificationSchema } from '@modelcontextprotocol/sdk/types.js'; | |
| import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; | |
| // Check if a message is a cancelled notification | |
| const isCancelledNotification = (value: any): boolean => | |
| CancelledNotificationSchema.safeParse(value).success; | |
| type NamedTransport<T extends Transport = Transport> = { | |
| name: string, | |
| transport: T, | |
| } | |
| // Bidirectionally propagates onclose & onmessage events between two transports | |
| function proxyTransports(client: NamedTransport<StreamableHTTPClientTransport>, server: NamedTransport<StdioServerTransport>, cleanup: () => Promise<void>): void { | |
| let closed = false; | |
| const propagateClose = (source: NamedTransport, target: NamedTransport) => { | |
| source.transport.onclose = async () => { | |
| console.info(`[http-as-stdio]: Transport closed: source=${source.name}, target=${target.name}`); | |
| if (!closed) { | |
| closed = true; | |
| try { | |
| target.transport.close(); | |
| // Clean up before exiting | |
| console.info(`[http-as-stdio]: Exiting due to ${source.name} closure`); | |
| await cleanup(); | |
| } catch (error) { | |
| console.error(`[http-as-stdio]: Error during cleanup after ${source.name} close:`, error); | |
| } | |
| process.exit(0); | |
| } | |
| }; | |
| }; | |
| propagateClose(server, client); | |
| propagateClose(client, server); | |
| const propagateMessage = (source: NamedTransport, target: NamedTransport) => { | |
| source.transport.onmessage = (message: any) => { | |
| console.info(`[http-as-stdio]: Message from ${source.name}: ${JSON.stringify(message)}`); | |
| if (!closed) { | |
| try { | |
| const relatedRequestId = isCancelledNotification(message) ? message.params.requestId : undefined; | |
| target.transport.send(message, {relatedRequestId}); | |
| } catch (error) { | |
| console.error(`[http-as-stdio]: Error sending message to ${target.name}: ${error}`); | |
| if (!closed) { | |
| closed = true; | |
| source.transport.close(); | |
| target.transport.close(); | |
| } | |
| } | |
| } | |
| }; | |
| }; | |
| propagateMessage(server, client); | |
| propagateMessage(client, server); | |
| server.transport.start(); | |
| client.transport.start(); | |
| } | |
| // Parse command line arguments | |
| const args = process.argv.slice(2); | |
| if (args.length === 0) { | |
| console.error('[http-as-stdio]: Usage: http-as-stdio <server-url>'); | |
| console.error('[http-as-stdio]: Example: http-as-stdio http://elicitation-proxy:3000/mcp'); | |
| process.exit(1); | |
| } | |
| const serverUrl = args[0]!; // We already checked args.length | |
| async function main() { | |
| let cleanupInProgress = false; | |
| console.info(`[http-as-stdio]: Starting stdio to HTTP bridge for ${serverUrl}`); | |
| // Create stdio transport for communication with parent process | |
| const stdioTransport = new StdioServerTransport(); | |
| // let retries = 0; | |
| // let delay = initialDelay; | |
| // while (retries < maxRetries) { | |
| // try { | |
| // console.error(`Attempting to connect to ${url} (attempt ${retries + 1}/${maxRetries})...`); | |
| // // Try a simple HTTP request to check if the server is ready | |
| // const response = await fetch(url, { | |
| // method: 'POST', | |
| // headers: { | |
| // 'Content-Type': 'application/json', | |
| // }, | |
| // body: JSON.stringify({ | |
| // jsonrpc: '2.0', | |
| // method: 'initialize', | |
| // params: { | |
| // protocolVersion: '1.0.0', | |
| // capabilities: {}, | |
| // clientInfo: { | |
| // name: 'http-as-stdio', | |
| // version: '1.0.0' | |
| // } | |
| // }, | |
| // id: 1 | |
| // }) | |
| // }); | |
| // // If we get any response (even an error), the server is at least running | |
| // if (response.status < 500) { | |
| // console.error(`Server is ready (status: ${response.status})`); | |
| // return; | |
| // } | |
| // throw new Error(`Server returned ${response.status}`); | |
| // } catch (error) { | |
| // retries++; | |
| // if (retries >= maxRetries) { | |
| // throw new Error(`Failed to connect to ${url} after ${maxRetries} attempts: ${error}`); | |
| // } | |
| // console.error(`Connection failed, retrying in ${delay}ms...`); | |
| // await new Promise(resolve => setTimeout(resolve, delay)); | |
| // // Exponential backoff with max delay of 10 seconds | |
| // delay = Math.min(delay * 1.5, 10000); | |
| // } | |
| // } | |
| // console.error(`Server is ready, establishing bridge...`); | |
| // Create HTTP client transport for communication with the server | |
| const httpTransport = new StreamableHTTPClientTransport(new URL(serverUrl)); | |
| // Handle graceful shutdown | |
| const cleanup = async () => { | |
| if (cleanupInProgress) { | |
| console.warn('[http-as-stdio]: Cleanup already in progress...'); | |
| return; | |
| } | |
| cleanupInProgress = true; | |
| console.info('[http-as-stdio]: Shutting down...'); | |
| // Kill any child processes in the same process group | |
| try { | |
| // Send SIGTERM to all processes in our process group | |
| process.kill(-process.pid, 'SIGTERM'); | |
| // Give processes time to exit gracefully | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| // Force kill if still running | |
| try { | |
| process.kill(-process.pid, 'SIGKILL'); | |
| } catch (e) { | |
| // Ignore errors - processes may have already exited | |
| } | |
| } catch (error: any) { | |
| if (error.code !== 'ESRCH') { // ESRCH = no such process | |
| console.error('[http-as-stdio]: Error killing process group:', error); | |
| } | |
| } | |
| try { | |
| await Promise.all([ | |
| stdioTransport.close(), | |
| httpTransport.close(), | |
| ]); | |
| } catch (error) { | |
| console.error('[http-as-stdio]: Error during cleanup:', error); | |
| } | |
| // Give a bit more time for cleanup | |
| await new Promise(resolve => setTimeout(resolve, 100)); | |
| process.exit(0); | |
| }; | |
| const client = {name: 'http', transport: httpTransport}; | |
| const server = {name: 'stdio', transport: stdioTransport}; | |
| const addErrorHandler = (transport: NamedTransport) => { | |
| transport.transport.onerror = async (error: Error) => { | |
| console.error(`[http-as-stdio]: Transport error on (${transport.name})`); | |
| console.error(error); | |
| await cleanup(); | |
| process.exit(1); | |
| }; | |
| }; | |
| addErrorHandler(client); | |
| addErrorHandler(server); | |
| proxyTransports(client, server, cleanup); | |
| console.info('[http-as-stdio]: Bridge established successfully'); | |
| process.on('SIGINT', cleanup); | |
| process.on('SIGTERM', cleanup); | |
| process.on('SIGHUP', cleanup); | |
| // Handle stdin closure (client disconnect) | |
| process.stdin.on('end', () => { | |
| console.info('[http-as-stdio]: stdin closed, client disconnected'); | |
| cleanup(); | |
| }); | |
| process.stdin.on('error', (error) => { | |
| console.error('[http-as-stdio]: stdin error:', error); | |
| cleanup(); | |
| }); | |
| // Keep the process alive | |
| // process.stdin.resume(); | |
| } | |
| // Run the converter | |
| main().catch((error) => { | |
| console.error(error); | |
| console.error('[http-as-stdio]: Failed to start HTTP to stdio bridge:', error); | |
| process.exit(1); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "name": "http-as-stdio", | |
| "version": "1.0.0", | |
| "description": "", | |
| "license": "ISC", | |
| "author": "", | |
| "type": "module", | |
| "bin": "dist/http-as-stdio.js", | |
| "scripts": { | |
| "prepare": "npm run build", | |
| "start": "bun run http-as-stdio.ts", | |
| "build": "bun build http-as-stdio.ts --outdir=dist --banner '#!/usr/bin/env node' --target=node --minify && shx chmod +x dist/http-as-stdio.js" | |
| }, | |
| "dependencies": { | |
| "@modelcontextprotocol/sdk": "^1.19.1", | |
| "zod-from-json-schema": "^0.5.0" | |
| }, | |
| "devDependencies": { | |
| "bun": "^1.2.23", | |
| "shx": "^0.4.0" | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment