427 lines
17 KiB
JavaScript

#!/usr/bin/env node
/**
* index.ts
*
* Run MCP stdio servers over SSE, convert between stdio, SSE, WS.
*
* Usage:
* # stdio→SSE
* npx -y mcp-superassistant-proxy --stdio "npx -y @modelcontextprotocol/server-filesystem /" \
* --port 3006 --baseUrl http://localhost:3006 --ssePath /sse --messagePath /message
*
* # SSE→stdio
* npx -y mcp-superassistant-proxy --sse "https://mcp-server-ab71a6b2-cd55-49d0-adba-562bc85956e3.supermachine.app"
*
* # SSE→SSE
* npx -y mcp-superassistant-proxy --sse "https://input-sse-server.example.com/sse" --outputTransport ssetosse \
* --port 3006 --baseUrl http://localhost:3006 --ssePath /sse --messagePath /message
*
* # SSE→WS
* npx -y mcp-superassistant-proxy --sse "https://input-sse-server.example.com/sse" --outputTransport ws \
* --port 3006 --messagePath /message
*
* # stdio→WS
* npx -y mcp-superassistant-proxy --stdio "npx -y @modelcontextprotocol/server-filesystem /" --outputTransport ws
*
* # Streamable HTTP→stdio
* npx -y mcp-superassistant-proxy --streamableHttp "https://mcp-server.example.com/mcp"
*
* # Streamable HTTP→SSE
* npx -y mcp-superassistant-proxy --streamableHttp "https://mcp-server.example.com/mcp" --outputTransport sse \
* --port 3006 --baseUrl http://localhost:3006 --ssePath /sse --messagePath /message
*
* # Config→SSE (unified multiple servers)
* npx -y mcp-superassistant-proxy --config ./config.json --outputTransport sse --port 3006
*
* # Config→WS (unified multiple servers)
* npx -y mcp-superassistant-proxy --config ./config.json --outputTransport ws --port 3006
*
* # Config→StreamableHttp (unified multiple servers - stateless)
* npx -y mcp-superassistant-proxy --config ./config.json --outputTransport streamableHttp --port 3006
*
* # Config→StreamableHttp (unified multiple servers - stateful with session timeout)
* npx -y mcp-superassistant-proxy --config ./config.json --outputTransport streamableHttp --port 3006 --stateful --sessionTimeout 300000
*/
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { stdioToSse } from './gateways/stdioToSse.js';
import { sseToStdio } from './gateways/sseToStdio.js';
import { sseToSse } from './gateways/sseToSse.js';
import { sseToWs } from './gateways/sseToWs.js';
import { stdioToWs } from './gateways/stdioToWs.js';
import { streamableHttpToStdio } from './gateways/streamableHttpToStdio.js';
import { streamableHttpToSse } from './gateways/streamableHttpToSse.js';
import { configToSse } from './gateways/configToSse.js';
import { configToWs } from './gateways/configToWs.js';
import { configToStreamableHttp } from './gateways/configToStreamableHttp.js';
import { headers } from './lib/headers.js';
import { corsOrigin } from './lib/corsOrigin.js';
import { getLogger } from './lib/getLogger.js';
import { stdioToStatelessStreamableHttp } from './gateways/stdioToStatelessStreamableHttp.js';
import { stdioToStatefulStreamableHttp } from './gateways/stdioToStatefulStreamableHttp.js';
async function main() {
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('streamableHttp', {
type: 'string',
description: 'Streamable HTTP URL to connect to',
})
.option('config', {
type: 'string',
description: 'Path to configuration file with multiple MCP servers',
})
.option('outputTransport', {
type: 'string',
choices: ['stdio', 'sse', 'ssetosse', 'ws', 'streamableHttp'],
default: () => {
const args = hideBin(process.argv);
if (args.includes('--stdio'))
return 'sse';
if (args.includes('--sse'))
return 'stdio';
if (args.includes('--streamableHttp'))
return 'stdio';
if (args.includes('--config'))
return 'sse';
return undefined;
},
description: 'Transport for output. Default is "sse" when using --stdio or --config and "stdio" when using --sse or --streamableHttp.',
})
.option('port', {
type: 'number',
default: 3006,
description: '(stdio→SSE, stdio→WS, stdio→StreamableHttp, SSE→SSE, SSE→WS, config→SSE, config→WS, config→StreamableHttp) Port for output MCP server',
})
.option('host', {
type: 'string',
default: 'localhost',
description: '(stdio→SSE, stdio→WS, stdio→StreamableHttp, SSE→SSE, SSE→WS, config→SSE, config→WS, config→StreamableHttp) Host to bind to. Use "0.0.0.0" to bind to all interfaces.',
})
.option('baseUrl', {
type: 'string',
default: '',
description: '(stdio→SSE, SSE→SSE, config→SSE) Base URL for output MCP server',
})
.option('ssePath', {
type: 'string',
default: '/sse',
description: '(stdio→SSE, SSE→SSE, config→SSE) Path for SSE subscriptions',
})
.option('messagePath', {
type: 'string',
default: '/message',
description: '(stdio→SSE, stdio→WS, SSE→SSE, SSE→WS, config→SSE, config→WS) Path for messages',
})
.option('streamableHttpPath', {
type: 'string',
default: '/mcp',
description: '(stdio→StreamableHttp, config→StreamableHttp) Path for StreamableHttp',
})
.option('streamableHttpPath', {
type: 'string',
default: '/mcp',
description: '(stdio→StreamableHttp, config→StreamableHttp) Path for StreamableHttp',
})
.option('logLevel', {
choices: ['debug', 'info', 'none'],
default: 'info',
description: 'Logging level',
})
.option('cors', {
type: 'array',
description: 'Configure CORS origins. CORS is enabled by default allowing all origins (*). Use --cors with no values to explicitly allow all origins, or supply one or more allowed origins (e.g. --cors "http://example.com" or --cors "/example\\.com$/" for regex matching).',
})
.option('healthEndpoint', {
type: 'array',
default: [],
description: 'One or more endpoints returning "ok", e.g. --healthEndpoint /healthz --healthEndpoint /readyz',
})
.option('header', {
type: 'array',
default: [],
description: 'Headers to be added to the request headers, e.g. --header "x-user-id: 123"',
})
.option('oauth2Bearer', {
type: 'string',
description: 'Authorization header to be added, e.g. --oauth2Bearer "some-access-token" adds "Authorization: Bearer some-access-token"',
})
.option('stateful', {
type: 'boolean',
default: false,
description: 'Whether the server is stateful. Only supported for stdio→StreamableHttp and config→StreamableHttp.',
})
.option('sessionTimeout', {
type: 'number',
description: 'Session timeout in milliseconds. Only supported for stateful stdio→StreamableHttp and config→StreamableHttp. If not set, the session will only be deleted when client transport explicitly terminates the session.',
})
.help()
.parseSync();
const hasStdio = Boolean(argv.stdio);
const hasSse = Boolean(argv.sse);
const hasStreamableHttp = Boolean(argv.streamableHttp);
const hasConfig = Boolean(argv.config);
const activeCount = [hasStdio, hasSse, hasStreamableHttp, hasConfig].filter(Boolean).length;
const logger = getLogger({
logLevel: argv.logLevel,
outputTransport: argv.outputTransport,
});
if (activeCount === 0) {
logger.error('Error: You must specify one of --stdio, --sse, --streamableHttp, or --config');
process.exit(1);
}
else if (activeCount > 1) {
logger.error('Error: Specify only one of --stdio, --sse, --streamableHttp, or --config, not multiple');
process.exit(1);
}
logger.info('Starting...');
logger.info('Starting mcp-superassistant-proxy ...');
logger.info(` - outputTransport: ${argv.outputTransport}`);
try {
if (hasStdio) {
if (argv.outputTransport === 'sse') {
await stdioToSse({
stdioCmd: argv.stdio,
port: argv.port,
host: argv.host,
baseUrl: argv.baseUrl,
ssePath: argv.ssePath,
messagePath: argv.messagePath,
logger,
corsOrigin: corsOrigin({ argv }),
healthEndpoints: argv.healthEndpoint,
headers: headers({
argv,
logger,
}),
});
}
else if (argv.outputTransport === 'ws') {
await stdioToWs({
stdioCmd: argv.stdio,
port: argv.port,
host: argv.host,
messagePath: argv.messagePath,
logger,
corsOrigin: corsOrigin({ argv }),
healthEndpoints: argv.healthEndpoint,
});
}
else if (argv.outputTransport === 'streamableHttp') {
const stateful = argv.stateful;
if (stateful) {
logger.info('Running stateful server');
let sessionTimeout;
if (typeof argv.sessionTimeout === 'number') {
if (argv.sessionTimeout <= 0) {
logger.error(`Error: \`sessionTimeout\` must be a positive number, received: ${argv.sessionTimeout}`);
process.exit(1);
}
sessionTimeout = argv.sessionTimeout;
}
else {
sessionTimeout = null;
}
await stdioToStatefulStreamableHttp({
stdioCmd: argv.stdio,
port: argv.port,
host: argv.host,
baseUrl: argv.baseUrl,
streamableHttpPath: argv.streamableHttpPath,
logger,
corsOrigin: corsOrigin({ argv }),
healthEndpoints: argv.healthEndpoint,
headers: headers({
argv,
logger,
}),
sessionTimeout,
});
}
else {
logger.info('Running stateless server');
await stdioToStatelessStreamableHttp({
stdioCmd: argv.stdio,
port: argv.port,
host: argv.host,
baseUrl: argv.baseUrl,
streamableHttpPath: argv.streamableHttpPath,
logger,
corsOrigin: corsOrigin({ argv }),
healthEndpoints: argv.healthEndpoint,
headers: headers({
argv,
logger,
}),
});
}
}
else {
logger.error(`Error: stdio→${argv.outputTransport} not supported`);
process.exit(1);
}
}
else if (hasSse) {
if (argv.outputTransport === 'stdio') {
await sseToStdio({
sseUrl: argv.sse,
logger,
headers: headers({
argv,
logger,
}),
});
}
else if (argv.outputTransport === 'ssetosse') {
await sseToSse({
inputSseUrl: argv.sse,
port: argv.port,
host: argv.host,
baseUrl: argv.baseUrl,
ssePath: argv.ssePath,
messagePath: argv.messagePath,
logger,
corsOrigin: corsOrigin({ argv }),
healthEndpoints: argv.healthEndpoint,
headers: headers({
argv,
logger,
}),
});
}
else if (argv.outputTransport === 'ws') {
await sseToWs({
inputSseUrl: argv.sse,
port: argv.port,
host: argv.host,
messagePath: argv.messagePath,
logger,
corsOrigin: corsOrigin({ argv }),
healthEndpoints: argv.healthEndpoint,
headers: headers({
argv,
logger,
}),
});
}
else {
logger.error(`Error: sse→${argv.outputTransport} not supported`);
process.exit(1);
}
}
else if (hasStreamableHttp) {
if (argv.outputTransport === 'stdio') {
await streamableHttpToStdio({
streamableHttpUrl: argv.streamableHttp,
logger,
headers: headers({
argv,
logger,
}),
});
}
else if (argv.outputTransport === 'sse') {
await streamableHttpToSse({
streamableHttpUrl: argv.streamableHttp,
port: argv.port,
host: argv.host,
baseUrl: argv.baseUrl,
ssePath: argv.ssePath,
messagePath: argv.messagePath,
logger,
corsOrigin: corsOrigin({ argv }),
healthEndpoints: argv.healthEndpoint,
headers: headers({
argv,
logger,
}),
});
}
else {
logger.error(`Error: streamableHttp→${argv.outputTransport} not supported`);
process.exit(1);
}
}
else if (hasConfig) {
if (argv.outputTransport === 'sse') {
await configToSse({
configPath: argv.config,
port: argv.port,
host: argv.host,
baseUrl: argv.baseUrl,
ssePath: argv.ssePath,
messagePath: argv.messagePath,
logger,
corsOrigin: corsOrigin({ argv }),
healthEndpoints: argv.healthEndpoint,
headers: headers({
argv,
logger,
}),
});
}
else if (argv.outputTransport === 'ws') {
await configToWs({
configPath: argv.config,
port: argv.port,
host: argv.host,
messagePath: argv.messagePath,
logger,
corsOrigin: corsOrigin({ argv }),
healthEndpoints: argv.healthEndpoint,
headers: headers({
argv,
logger,
}),
});
}
else if (argv.outputTransport === 'streamableHttp') {
const stateless = !argv.stateful;
let sessionTimeout = null;
if (argv.stateful && typeof argv.sessionTimeout === 'number') {
if (argv.sessionTimeout <= 0) {
logger.error(`Error: \`sessionTimeout\` must be a positive number, received: ${argv.sessionTimeout}`);
process.exit(1);
}
sessionTimeout = argv.sessionTimeout;
}
await configToStreamableHttp({
configPath: argv.config,
port: argv.port,
host: argv.host,
baseUrl: argv.baseUrl,
streamableHttpPath: argv.streamableHttpPath,
logger,
corsOrigin: corsOrigin({ argv }),
healthEndpoints: argv.healthEndpoint,
headers: headers({
argv,
logger,
}),
stateless,
sessionTimeout,
});
}
else {
logger.error(`Error: config→${argv.outputTransport} not supported`);
process.exit(1);
}
}
else {
logger.error('Error: Invalid input transport');
process.exit(1);
}
}
catch (err) {
logger.error('Fatal error:', err);
process.exit(1);
}
}
main();
//# sourceMappingURL=index.js.map