#!/usr/bin/env node
/**
* index.ts
*
* Run MCP stdio servers over SSE or vice versa
*
* Usage:
* # stdio -> SSE
* npx -y supergateway --stdio "npx -y @modelcontextprotocol/server-filesystem /some/folder" \
* --port 8000 --baseUrl http://localhost:8000 --ssePath /sse --messagePath /message
*
* # SSE -> stdio
* npx -y supergateway --sse "https://mcp-server.superinterface.app"
*/
import express from 'express';
import bodyParser from 'body-parser';
import { spawn } from 'child_process';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { z } from 'zod';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { fileURLToPath } from 'url';
import { join, dirname } from 'path';
import { readFileSync } from 'fs';
const log = (...args) => console.log('[supergateway]', ...args);
const logStderr = (...args) => console.error('[supergateway]', ...args);
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
function getVersion() {
try {
const packageJsonPath = join(__dirname, '../package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
return packageJson.version || '1.0.0';
}
catch (err) {
console.error('[supergateway] Unable to retrieve version:', err);
return 'unknown';
}
}
const stdioToSse = async (stdioCmd, port, baseUrl, ssePath, messagePath) => {
log('Starting...');
log('Supergateway is supported by Superinterface - https://superinterface.ai');
log(` - port: ${port}`);
log(` - stdio: ${stdioCmd}`);
if (baseUrl) {
log(` - baseUrl: ${baseUrl}`);
}
log(` - ssePath: ${ssePath}`);
log(` - messagePath: ${messagePath}`);
const child = spawn(stdioCmd, { shell: true });
child.on('exit', (code, signal) => {
logStderr(`Child exited: code=${code}, signal=${signal}`);
process.exit(code ?? 1);
});
const server = new Server({ name: 'supergateway', version: getVersion() }, { capabilities: {} });
const sessions = {};
const app = express();
app.use((req, res, next) => {
if (req.path === messagePath)
return next();
return bodyParser.json()(req, res, next);
});
app.get(ssePath, async (req, res) => {
log(`New SSE connection from ${req.ip}`);
const sseTransport = new SSEServerTransport(`${baseUrl}${messagePath}`, res);
await server.connect(sseTransport);
const sessionId = sseTransport.sessionId;
if (sessionId) {
sessions[sessionId] = { transport: sseTransport, response: res };
}
sseTransport.onmessage = (msg) => {
const line = JSON.stringify(msg);
log(`SSE → Child (session ${sessionId}): ${line}`);
child.stdin.write(line + '\n');
};
sseTransport.onclose = () => {
log(`SSE connection closed (session ${sessionId})`);
delete sessions[sessionId];
};
sseTransport.onerror = err => {
logStderr(`SSE error (session ${sessionId}):`, err);
delete sessions[sessionId];
};
req.on('close', () => {
log(`Client disconnected (session ${sessionId})`);
delete sessions[sessionId];
});
});
// @ts-ignore: ignoring potential type mismatch from express
app.post(messagePath, async (req, res) => {
const sessionId = req.query.sessionId;
if (!sessionId) {
return res.status(400).send('Missing sessionId parameter');
}
const session = sessions[sessionId];
if (session?.transport?.handlePostMessage) {
log(`POST to SSE transport (session ${sessionId})`);
await session.transport.handlePostMessage(req, res);
}
else {
res.status(503).send(`No active SSE connection for session ${sessionId}`);
}
});
app.listen(port, () => {
log(`Listening on port ${port}`);
log(`SSE endpoint: http://localhost:${port}${ssePath}`);
log(`POST messages: http://localhost:${port}${messagePath}`);
});
let buffer = '';
child.stdout.on('data', (chunk) => {
buffer += chunk.toString('utf8');
const lines = buffer.split(/\r?\n/);
buffer = lines.pop() ?? '';
lines.forEach(line => {
if (!line.trim())
return;
try {
const jsonMsg = JSON.parse(line);
log('Child → SSE:', jsonMsg);
// Broadcast to all sessions
for (const [sid, session] of Object.entries(sessions)) {
try {
session.transport.send(jsonMsg);
}
catch (err) {
logStderr(`Failed to send to session ${sid}:`, err);
delete sessions[sid];
}
}
}
catch {
logStderr(`Child non-JSON: ${line}`);
}
});
});
child.stderr.on('data', (chunk) => {
logStderr(`Child stderr: ${chunk.toString('utf8')}`);
});
};
const sseToStdio = async (sseUrl) => {
logStderr('Starting...');
logStderr('Supergateway is supported by Superinterface - https://superinterface.ai');
logStderr(` - sse: ${sseUrl}`);
logStderr('Connecting to SSE...');
const sseTransport = new SSEClientTransport(new URL(sseUrl));
const sseClient = new Client({ name: 'supergateway', version: getVersion() }, { capabilities: {} });
sseTransport.onerror = err => {
logStderr('SSE error:', err);
};
sseTransport.onclose = () => {
logStderr('SSE connection closed');
process.exit(1);
};
await sseClient.connect(sseTransport);
logStderr('SSE connected');
const stdioServer = new Server(sseClient.getServerVersion() ?? { name: 'supergateway', version: getVersion() }, { capabilities: sseClient.getServerCapabilities() });
const stdioTransport = new StdioServerTransport();
await stdioServer.connect(stdioTransport);
// Build a response envelope using the JSON-RPC version from the request if present.
const wrapResponse = (req, payload) => ({
jsonrpc: req.jsonrpc || '2.0',
id: req.id,
...payload,
});
stdioServer.transport.onmessage = async (message) => {
const isRequest = 'method' in message && 'id' in message;
if (isRequest) {
logStderr('Stdio → SSE:', message);
const req = message;
let result;
try {
result = await sseClient.request(req, z.any());
}
catch (err) {
logStderr('Request error:', err);
const errorCode = err && typeof err === 'object' && 'code' in err
? err.code
: -32000;
let errorMsg = err && typeof err === 'object' && 'message' in err
? err.message
: 'Internal error';
// Remove the prefix if it is already present.
const prefix = `MCP error ${errorCode}:`;
if (errorMsg.startsWith(prefix)) {
errorMsg = errorMsg.slice(prefix.length).trim();
}
const errorResp = wrapResponse(req, {
error: {
code: errorCode,
message: errorMsg,
},
});
process.stdout.write(JSON.stringify(errorResp) + '\n');
return;
}
const response = wrapResponse(req, result.hasOwnProperty('error')
? { error: { ...result.error } }
: { result: { ...result } });
logStderr('Response:', response);
process.stdout.write(JSON.stringify(response) + '\n');
}
else {
logStderr('SSE → Stdio:', message);
process.stdout.write(JSON.stringify(message) + '\n');
}
};
logStderr('Stdio server listening');
};
const main = async () => {
const argv = yargs(hideBin(process.argv))
.option('stdio', {
type: 'string',
description: 'Command to run an MCP server over Stdio'
})
.option('sse', {
type: 'string',
description: 'SSE URL to connect to'
})
.option('port', {
type: 'number',
default: 8000,
description: '(stdio to SSE) Port to run on'
})
.option('baseUrl', {
type: 'string',
default: '',
description: '(stdio to SSE) Base URL for SSE clients'
})
.option('ssePath', {
type: 'string',
default: '/sse',
description: '(stdio to SSE) Path for SSE subscriptions'
})
.option('messagePath', {
type: 'string',
default: '/message',
description: '(stdio to SSE) Path for SSE messages'
})
.help()
.parseSync();
const hasStdio = Boolean(argv.stdio);
const hasSse = Boolean(argv.sse);
if (hasStdio && hasSse) {
logStderr('Error: Specify only one of --stdio or --sse, not both');
process.exit(1);
}
else if (!hasStdio && !hasSse) {
logStderr('Error: You must specify one of --stdio or --sse');
process.exit(1);
}
if (hasStdio) {
await stdioToSse(argv.stdio, argv.port, argv.baseUrl, argv.ssePath, argv.messagePath);
}
else {
await sseToStdio(argv.sse);
}
};
main().catch(err => {
logStderr('Fatal error:', err);
process.exit(1);
});