427 lines
17 KiB
JavaScript
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
|