diff --git a/README.md b/README.md new file mode 100644 index 0000000..4097ba9 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# MCP SuperAssistant Proxy + +MCP SuperAssistant Proxy lets you run multiple **MCP stdio-based** and **SSE-based** servers and expose them through a single SSE endpoint. This allows MCP SuperAssistant and other tools to connect to multiple remote MCP servers and tools via a unified proxy. + +## Installation & Usage + +Run MCP SuperAssistant Proxy via `npx`: + +```bash +npx -y @srbhptl39/mcp-superassistant-proxy@latest --config path/to/config.json +``` + +### CLI Options + +- `--config, -c `: **(required)** Path to a JSON configuration file (see below) +- `--port `: Port to run the proxy server on (default: `3006`) +- `--baseUrl `: Base URL for SSE clients (default: `http://localhost:`) +- `--ssePath `: Path for SSE subscriptions (default: `/sse`) +- `--messagePath `: Path for SSE messages (default: `/message`) +- `--logLevel `: Set logging level (default: `info`) +- `--outputTransport stdio | sse | ws | streamableHttp`: Output MCP transport (default: `sse` with `--stdio` or `--config`, `stdio` with `--sse` or `--streamableHttp`) +- `--ssePath "/sse"`: Path for SSE subscriptions (stdio→SSE, config→SSE modes, default: `/sse`) +- `--messagePath "/message"`: Path for messages (stdio→SSE, stdio→WS, config→SSE, config→WS modes, default: `/message`) +- `--streamableHttpPath "/mcp"`: Path for Streamable HTTP (stdio→StreamableHttp, config→StreamableHttp modes, default: `/mcp`) +- `--stateful`: Run StreamableHttp in stateful mode (stdio→StreamableHttp, config→StreamableHttp modes) +- `--sessionTimeout 60000`: Session timeout in milliseconds (stateful StreamableHttp modes only) +- `--header "x-user-id: 123"`: Add one or more headers (stdio→SSE, SSE→stdio, or Streamable HTTP→stdio mode; can be used multiple times) +- `--oauth2Bearer "some-access-token"`: Adds an `Authorization` header with the provided Bearer token +- `--logLevel debug | info | none`: Controls logging level (default: `info`). Use `debug` for more verbose logs, `none` to suppress all logs. + +- `--cors`: Enable CORS (default: `true`) +- `--healthEndpoint `: One or more endpoints returning `"ok"` (can be used multiple times) +- `--timeout `: Connection timeout in milliseconds (default: `30000`) + +## Configuration File + +The configuration file is a JSON file specifying which MCP servers to connect to. Each server can be either a stdio-based server (run as a subprocess) or an SSE-based server (remote URL). + +### Example `config.json` + +```json +{ + "mcpServers": { + "notion": { + "command": "npx", + "args": ["-y", "@suekou/mcp-notion-server"], + "env": { + "NOTION_API_TOKEN": "" + } + }, + "gmail": { + "url": "https://mcp.composio.dev/gmail/xxxx" + }, + "youtube-subtitle-downloader": { + "command": "bun", + "args": [ + "run", + "/path/to/mcp-youtube/src/index.ts" + ] + }, + "desktop-commander": { + "command": "npx", + "args": ["-y", "@wonderwhy-er/desktop-commander"] + }, + "iterm-mcp": { + "command": "npx", + "args": ["-y", "iterm-mcp"] + } + } +} +``` + +- Each key under `mcpServers` is a unique name for the server. +- For stdio-based servers, specify `command`, `args`, and optionally `env`. +- For SSE-based servers, specify `url`. + +## Endpoints + +Once started, the proxy exposes: +- **SSE endpoint**: `http://localhost:/sse` +- **POST messages**: `http://localhost:/message` +- **Streamable HTTP endpoint**: `http://localhost:/mcp` +- **Websocket endpoint**: `ws://localhost:/message` + +(You can customize the paths with `--ssePath` and `--messagePath`.) + +## Example + +1. **Create a config file** (e.g., `config.json`) as shown above. +2. **Run MCP SuperAssistant Proxy**: + ```bash + npx -y @srbhptl39/mcp-superassistant-proxy@latest --config config.json --port 3006 + ``` + +## Why MCP? + +[Model Context Protocol](https://spec.modelcontextprotocol.io/) standardizes how AI tools exchange data. If your MCP server only speaks stdio, MCP SuperAssistant Proxy exposes an SSE-based interface so remote clients (and tools like MCP Inspector or Claude Desktop) can connect without extra server changes. It also allows you to aggregate multiple MCP servers behind a single endpoint. + +## Advanced Configuration + +MCP SuperAssistant Proxy is designed with modularity in mind: +- Supports both stdio and SSE MCP servers in one config. +- Automatically derives the JSON‑RPC version from incoming requests, ensuring future compatibility. +- Package information (name and version) is retransmitted where possible. +- Stdio-to-SSE mode uses standard logs and SSE-to-Stdio mode logs via stderr (as otherwise it would prevent stdio functionality). +- The SSE-to-SSE mode provides automatic reconnection with backoff if the remote server connection is lost. +- Health endpoints can be added for monitoring. + +--- + +For more details, see the [Model Context Protocol documentation](https://modelcontextprotocol.io/). + +## Acknowledgments + +Special thanks to [SuperGateway](https://github.com/supercorp-ai/supergateway) for starter code. \ No newline at end of file diff --git a/dist/gateways/configToSse.d.ts b/dist/gateways/configToSse.d.ts new file mode 100644 index 0000000..24fd7d1 --- /dev/null +++ b/dist/gateways/configToSse.d.ts @@ -0,0 +1,16 @@ +import { type CorsOptions } from 'cors'; +import { Logger } from '../types.js'; +export interface ConfigToSseArgs { + configPath: string; + port: number; + host: string; + baseUrl: string; + ssePath: string; + messagePath: string; + logger: Logger; + corsOrigin: CorsOptions['origin']; + healthEndpoints: string[]; + headers: Record; +} +export declare function configToSse(args: ConfigToSseArgs): Promise; +//# sourceMappingURL=configToSse.d.ts.map \ No newline at end of file diff --git a/dist/gateways/configToSse.d.ts.map b/dist/gateways/configToSse.d.ts.map new file mode 100644 index 0000000..34b825c --- /dev/null +++ b/dist/gateways/configToSse.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"configToSse.d.ts","sourceRoot":"","sources":["../../src/gateways/configToSse.ts"],"names":[],"mappings":"AAEA,OAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAA;AAO7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAOpC,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjC,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAaD,wBAAsB,WAAW,CAAC,IAAI,EAAE,eAAe,iBAoLtD"} \ No newline at end of file diff --git a/dist/gateways/configToSse.js b/dist/gateways/configToSse.js new file mode 100644 index 0000000..b2d7e1e --- /dev/null +++ b/dist/gateways/configToSse.js @@ -0,0 +1,144 @@ +import express from 'express'; +import bodyParser from 'body-parser'; +import cors from 'cors'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { getVersion } from '../lib/getVersion.js'; +import { onSignals } from '../lib/onSignals.js'; +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; +import { loadConfig } from '../lib/config.js'; +import { McpServerManager } from '../lib/mcpServerManager.js'; +const setResponseHeaders = ({ res, headers, }) => Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); +}); +export async function configToSse(args) { + const { configPath, port, host, baseUrl, ssePath, messagePath, logger, corsOrigin, healthEndpoints, headers, } = args; + logger.info(` - config: ${configPath}`); + logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info(` - host: ${host}`); + logger.info(` - port: ${port}`); + if (baseUrl) { + logger.info(` - baseUrl: ${baseUrl}`); + } + logger.info(` - ssePath: ${ssePath}`); + logger.info(` - messagePath: ${messagePath}`); + logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); + logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); + const serverManager = new McpServerManager(logger); + const cleanup = async () => { + await serverManager.cleanup(); + }; + onSignals({ logger, cleanup }); + let config; + try { + config = loadConfig(configPath); + logger.info(`Loaded config with ${Object.keys(config.mcpServers).length} servers`); + } + catch (err) { + logger.error(`Failed to load config: ${err}`); + process.exit(1); + } + for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) { + try { + await serverManager.addServer(serverName, serverConfig); + logger.info(`Successfully initialized server: ${serverName}`); + } + catch (err) { + logger.error(`Failed to initialize server ${serverName}: ${err}`); + process.exit(1); + } + } + const server = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + const sessions = {}; + const app = express(); + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })); + } + app.use((req, res, next) => { + if (req.path === messagePath) + return next(); + return bodyParser.json()(req, res, next); + }); + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }); + res.send('ok'); + }); + } + app.get(ssePath, async (req, res) => { + logger.info(`New SSE connection from ${req.ip}`); + setResponseHeaders({ + res, + headers, + }); + 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 = async (msg) => { + logger.info(`SSE → Servers (session ${sessionId}): ${JSON.stringify(msg)}`); + if ('method' in msg && 'id' in msg) { + try { + const response = await serverManager.handleRequest(msg); + logger.info(`Servers → SSE (session ${sessionId}):`); + logger.debug(`Servers → SSE (session ${sessionId}):`, response); + sseTransport.send(response); + } + catch (err) { + logger.error(`Error handling request in session ${sessionId}:`, err); + const errorResponse = { + jsonrpc: '2.0', + id: msg.id, + error: { + code: -32000, + message: 'Internal error', + }, + }; + sseTransport.send(errorResponse); + } + } + }; + sseTransport.onclose = () => { + logger.info(`SSE connection closed (session ${sessionId})`); + delete sessions[sessionId]; + }; + sseTransport.onerror = (err) => { + logger.error(`SSE error (session ${sessionId}):`, err); + delete sessions[sessionId]; + }; + req.on('close', () => { + logger.info(`Client disconnected (session ${sessionId})`); + delete sessions[sessionId]; + }); + }); + app.post(messagePath, async (req, res) => { + const sessionId = req.query.sessionId; + setResponseHeaders({ + res, + headers, + }); + if (!sessionId) { + return res.status(400).send('Missing sessionId parameter'); + } + const session = sessions[sessionId]; + if (session?.transport?.handlePostMessage) { + logger.info(`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, host, () => { + logger.info(`Listening on ${host}:${port}`); + logger.info(`SSE endpoint: http://${host}:${port}${ssePath}`); + logger.info(`POST messages: http://${host}:${port}${messagePath}`); + }); + logger.info('Config-to-SSE gateway ready'); +} +//# sourceMappingURL=configToSse.js.map \ No newline at end of file diff --git a/dist/gateways/configToSse.js.map b/dist/gateways/configToSse.js.map new file mode 100644 index 0000000..65739fc --- /dev/null +++ b/dist/gateways/configToSse.js.map @@ -0,0 +1 @@ +{"version":3,"file":"configToSse.js","sourceRoot":"","sources":["../../src/gateways/configToSse.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/gateways/configToStreamableHttp.d.ts b/dist/gateways/configToStreamableHttp.d.ts new file mode 100644 index 0000000..f37b641 --- /dev/null +++ b/dist/gateways/configToStreamableHttp.d.ts @@ -0,0 +1,16 @@ +import { type CorsOptions } from 'cors'; +import { Logger } from '../types.js'; +export interface ConfigToStreamableHttpArgs { + configPath: string; + port: number; + host: string; + streamableHttpPath: string; + logger: Logger; + corsOrigin: CorsOptions['origin']; + healthEndpoints: string[]; + headers: Record; + stateless?: boolean; + sessionTimeout?: number | null; +} +export declare function configToStreamableHttp(args: ConfigToStreamableHttpArgs): Promise; +//# sourceMappingURL=configToStreamableHttp.d.ts.map \ No newline at end of file diff --git a/dist/gateways/configToStreamableHttp.d.ts.map b/dist/gateways/configToStreamableHttp.d.ts.map new file mode 100644 index 0000000..e9b1b62 --- /dev/null +++ b/dist/gateways/configToStreamableHttp.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"configToStreamableHttp.d.ts","sourceRoot":"","sources":["../../src/gateways/configToStreamableHttp.ts"],"names":[],"mappings":"AACA,OAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAA;AAQ7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AASpC,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,kBAAkB,EAAE,MAAM,CAAA;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjC,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAaD,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,0BAA0B,iBA6W5E"} \ No newline at end of file diff --git a/dist/gateways/configToStreamableHttp.js b/dist/gateways/configToStreamableHttp.js new file mode 100644 index 0000000..a18a6f4 --- /dev/null +++ b/dist/gateways/configToStreamableHttp.js @@ -0,0 +1,287 @@ +import express from 'express'; +import cors from 'cors'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { isInitializeRequest, } from '@modelcontextprotocol/sdk/types.js'; +import { getVersion } from '../lib/getVersion.js'; +import { onSignals } from '../lib/onSignals.js'; +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; +import { loadConfig } from '../lib/config.js'; +import { McpServerManager } from '../lib/mcpServerManager.js'; +import { randomUUID } from 'node:crypto'; +import { SessionAccessCounter } from '../lib/sessionAccessCounter.js'; +const setResponseHeaders = ({ res, headers, }) => Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); +}); +export async function configToStreamableHttp(args) { + const { configPath, port, host, streamableHttpPath, logger, corsOrigin, healthEndpoints, headers, stateless = false, sessionTimeout, } = args; + logger.info(` - config: ${configPath}`); + logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info(` - host: ${host}`); + logger.info(` - port: ${port}`); + logger.info(` - streamableHttpPath: ${streamableHttpPath}`); + logger.info(` - stateless: ${stateless}`); + logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); + logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); + if (!stateless && sessionTimeout) { + logger.info(` - Session timeout: ${sessionTimeout}ms`); + } + const serverManager = new McpServerManager(logger); + const cleanup = async () => { + await serverManager.cleanup(); + }; + onSignals({ logger, cleanup }); + let config; + try { + config = loadConfig(configPath); + logger.info(`Loaded config with ${Object.keys(config.mcpServers).length} servers`); + } + catch (err) { + logger.error(`Failed to load config: ${err}`); + process.exit(1); + } + for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) { + try { + await serverManager.addServer(serverName, serverConfig); + logger.info(`Successfully initialized server: ${serverName}`); + } + catch (err) { + logger.error(`Failed to initialize server ${serverName}: ${err}`); + process.exit(1); + } + } + const app = express(); + app.use(express.json()); + if (corsOrigin) { + app.use(cors({ + origin: corsOrigin, + exposedHeaders: stateless ? [] : ['Mcp-Session-Id'], + })); + } + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }); + res.send('ok'); + }); + } + if (stateless) { + // Stateless mode - create new transport for each request + app.post(streamableHttpPath, async (req, res) => { + logger.info('Received stateless StreamableHttp request'); + setResponseHeaders({ + res, + headers, + }); + try { + const server = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + await server.connect(transport); + transport.onmessage = async (msg) => { + logger.info(`StreamableHttp → Servers: ${JSON.stringify(msg)}`); + if ('method' in msg && 'id' in msg) { + try { + const response = await serverManager.handleRequest(msg); + logger.info('Servers → StreamableHttp:'); + logger.debug('Servers → StreamableHttp:', response); + transport.send(response); + } + catch (err) { + logger.error('Error handling request:', err); + const errorResponse = { + jsonrpc: '2.0', + id: msg.id, + error: { + code: -32000, + message: 'Internal error', + }, + }; + transport.send(errorResponse); + } + } + }; + transport.onclose = () => { + logger.info('StreamableHttp connection closed'); + }; + transport.onerror = (err) => { + logger.error('StreamableHttp error:', err); + }; + await transport.handleRequest(req, res, req.body); + } + catch (error) { + logger.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } + }); + // Stateless mode doesn't support GET/DELETE + app.get(streamableHttpPath, async (req, res) => { + setResponseHeaders({ res, headers }); + res.status(405).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed in stateless mode', + }, + id: null, + }); + }); + app.delete(streamableHttpPath, async (req, res) => { + setResponseHeaders({ res, headers }); + res.status(405).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed in stateless mode', + }, + id: null, + }); + }); + } + else { + // Stateful mode - maintain sessions + const transports = {}; + const sessionCounter = sessionTimeout + ? new SessionAccessCounter(sessionTimeout, (sessionId) => { + logger.info(`Session ${sessionId} timed out, cleaning up`); + const transport = transports[sessionId]; + if (transport) { + transport.close(); + } + delete transports[sessionId]; + }, logger) + : null; + app.post(streamableHttpPath, async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + let transport; + setResponseHeaders({ + res, + headers, + }); + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId]; + sessionCounter?.inc(sessionId, 'POST request for existing session'); + } + else if (!sessionId && isInitializeRequest(req.body)) { + // New initialization request + const server = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sessionId) => { + transports[sessionId] = transport; + sessionCounter?.inc(sessionId, 'session initialization'); + }, + }); + await server.connect(transport); + transport.onmessage = async (msg) => { + logger.info(`StreamableHttp → Servers (session ${sessionId}): ${JSON.stringify(msg)}`); + if ('method' in msg && 'id' in msg) { + try { + const response = await serverManager.handleRequest(msg); + logger.info(`Servers → StreamableHttp (session ${sessionId}):`); + logger.debug(`Servers → StreamableHttp (session ${sessionId}):`, response); + transport.send(response); + } + catch (err) { + logger.error(`Error handling request in session ${sessionId}:`, err); + const errorResponse = { + jsonrpc: '2.0', + id: msg.id, + error: { + code: -32000, + message: 'Internal error', + }, + }; + transport.send(errorResponse); + } + } + }; + transport.onclose = () => { + logger.info(`StreamableHttp connection closed (session ${sessionId})`); + if (transport.sessionId) { + sessionCounter?.clear(transport.sessionId, false, 'transport being closed'); + delete transports[transport.sessionId]; + } + }; + transport.onerror = (err) => { + logger.error(`StreamableHttp error (session ${sessionId}):`, err); + if (transport.sessionId) { + sessionCounter?.clear(transport.sessionId, false, 'transport emitting error'); + delete transports[transport.sessionId]; + } + }; + } + else { + // Invalid request + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Bad Request: No valid session ID provided', + }, + id: null, + }); + return; + } + // Decrement session access count when response ends + let responseEnded = false; + const handleResponseEnd = (event) => { + if (!responseEnded && transport.sessionId) { + responseEnded = true; + logger.info(`Response ${event}`, transport.sessionId); + sessionCounter?.dec(transport.sessionId, `POST response ${event}`); + } + }; + res.on('finish', () => handleResponseEnd('finished')); + res.on('close', () => handleResponseEnd('closed')); + await transport.handleRequest(req, res, req.body); + }); + // Reusable handler for GET and DELETE requests in stateful mode + const handleSessionRequest = async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + setResponseHeaders({ + res, + headers, + }); + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + sessionCounter?.inc(sessionId, `${req.method} request for existing session`); + let responseEnded = false; + const handleResponseEnd = (event) => { + if (!responseEnded) { + responseEnded = true; + logger.info(`Response ${event}`, sessionId); + sessionCounter?.dec(sessionId, `${req.method} response ${event}`); + } + }; + res.on('finish', () => handleResponseEnd('finished')); + res.on('close', () => handleResponseEnd('closed')); + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + }; + app.get(streamableHttpPath, handleSessionRequest); + app.delete(streamableHttpPath, handleSessionRequest); + } + app.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`); + logger.info(`StreamableHttp endpoint: http://${host}:${port}${streamableHttpPath}`); + logger.info(`Mode: ${stateless ? 'stateless' : 'stateful'}`); + }); + logger.info('Config-to-StreamableHttp gateway ready'); +} +//# sourceMappingURL=configToStreamableHttp.js.map \ No newline at end of file diff --git a/dist/gateways/configToStreamableHttp.js.map b/dist/gateways/configToStreamableHttp.js.map new file mode 100644 index 0000000..3d5ee50 --- /dev/null +++ b/dist/gateways/configToStreamableHttp.js.map @@ -0,0 +1 @@ +{"version":3,"file":"configToStreamableHttp.js","sourceRoot":"","sources":["../../src/gateways/configToStreamableHttp.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,IAA0B,MAAM,MAAM,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAClE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAA;AAClG,OAAO,EAGL,mBAAmB,GACpB,MAAM,oCAAoC,CAAA;AAE3C,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAA;AACnE,OAAO,EAAU,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAA;AAerE,MAAM,kBAAkB,GAAG,CAAC,EAC1B,GAAG,EACH,OAAO,GAIR,EAAE,EAAE,CACH,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;IAC/C,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;AAC3B,CAAC,CAAC,CAAA;AAEJ,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,IAAgC;IAC3E,MAAM,EACJ,UAAU,EACV,IAAI,EACJ,IAAI,EACJ,kBAAkB,EAClB,MAAM,EACN,UAAU,EACV,eAAe,EACf,OAAO,EACP,SAAS,GAAG,KAAK,EACjB,cAAc,GACf,GAAG,IAAI,CAAA;IAER,MAAM,CAAC,IAAI,CAAC,eAAe,UAAU,EAAE,CAAC,CAAA;IACxC,MAAM,CAAC,IAAI,CACT,gBAAgB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CACnF,CAAA;IACD,MAAM,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAA;IAChC,MAAM,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAA;IAChC,MAAM,CAAC,IAAI,CAAC,2BAA2B,kBAAkB,EAAE,CAAC,CAAA;IAC5D,MAAM,CAAC,IAAI,CAAC,kBAAkB,SAAS,EAAE,CAAC,CAAA;IAC1C,MAAM,CAAC,IAAI,CACT,aAAa,UAAU,CAAC,CAAC,CAAC,YAAY,mBAAmB,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,CAC5F,CAAA;IACD,MAAM,CAAC,IAAI,CACT,yBAAyB,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAC1F,CAAA;IAED,IAAI,CAAC,SAAS,IAAI,cAAc,EAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,wBAAwB,cAAc,IAAI,CAAC,CAAA;IACzD,CAAC;IAED,MAAM,aAAa,GAAG,IAAI,gBAAgB,CAAC,MAAM,CAAC,CAAA;IAElD,MAAM,OAAO,GAAG,KAAK,IAAI,EAAE;QACzB,MAAM,aAAa,CAAC,OAAO,EAAE,CAAA;IAC/B,CAAC,CAAA;IAED,SAAS,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;IAE9B,IAAI,MAAc,CAAA;IAClB,IAAI,CAAC;QACH,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAA;QAC/B,MAAM,CAAC,IAAI,CACT,sBAAsB,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,MAAM,UAAU,CACtE,CAAA;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CAAC,0BAA0B,GAAG,EAAE,CAAC,CAAA;QAC7C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;IAED,KAAK,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3E,IAAI,CAAC;YACH,MAAM,aAAa,CAAC,SAAS,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;YACvD,MAAM,CAAC,IAAI,CAAC,oCAAoC,UAAU,EAAE,CAAC,CAAA;QAC/D,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,KAAK,CAAC,+BAA+B,UAAU,KAAK,GAAG,EAAE,CAAC,CAAA;YACjE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjB,CAAC;IACH,CAAC;IAED,MAAM,GAAG,GAAG,OAAO,EAAE,CAAA;IACrB,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;IAEvB,IAAI,UAAU,EAAE,CAAC;QACf,GAAG,CAAC,GAAG,CACL,IAAI,CAAC;YACH,MAAM,EAAE,UAAU;YAClB,cAAc,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC;SACpD,CAAC,CACH,CAAA;IACH,CAAC;IAED,KAAK,MAAM,EAAE,IAAI,eAAe,EAAE,CAAC;QACjC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;YACxB,kBAAkB,CAAC;gBACjB,GAAG;gBACH,OAAO;aACR,CAAC,CAAA;YACF,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAChB,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,IAAI,SAAS,EAAE,CAAC;QACd,yDAAyD;QACzD,GAAG,CAAC,IAAI,CAAC,kBAAkB,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;YAC9C,MAAM,CAAC,IAAI,CAAC,2CAA2C,CAAC,CAAA;YAExD,kBAAkB,CAAC;gBACjB,GAAG;gBACH,OAAO;aACR,CAAC,CAAA;YAEF,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAC3D,EAAE,YAAY,EAAE,EAAE,EAAE,CACrB,CAAA;gBAED,MAAM,SAAS,GAAG,IAAI,6BAA6B,CAAC;oBAClD,kBAAkB,EAAE,SAAS;iBAC9B,CAAC,CAAA;gBAEF,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;gBAE/B,SAAS,CAAC,SAAS,GAAG,KAAK,EAAE,GAAmB,EAAE,EAAE;oBAClD,MAAM,CAAC,IAAI,CAAC,6BAA6B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;oBAE/D,IAAI,QAAQ,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,EAAE,CAAC;wBACnC,IAAI,CAAC;4BACH,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,aAAa,CAChD,GAAqB,CACtB,CAAA;4BACD,MAAM,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAA;4BACxC,MAAM,CAAC,KAAK,CAAC,2BAA2B,EAAE,QAAQ,CAAC,CAAA;4BACnD,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;wBAC1B,CAAC;wBAAC,OAAO,GAAG,EAAE,CAAC;4BACb,MAAM,CAAC,KAAK,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAA;4BAC5C,MAAM,aAAa,GAAG;gCACpB,OAAO,EAAE,KAAc;gCACvB,EAAE,EAAG,GAAsB,CAAC,EAAE;gCAC9B,KAAK,EAAE;oCACL,IAAI,EAAE,CAAC,KAAK;oCACZ,OAAO,EAAE,gBAAgB;iCAC1B;6BACF,CAAA;4BACD,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;wBAC/B,CAAC;oBACH,CAAC;gBACH,CAAC,CAAA;gBAED,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE;oBACvB,MAAM,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAA;gBACjD,CAAC,CAAA;gBAED,SAAS,CAAC,OAAO,GAAG,CAAC,GAAG,EAAE,EAAE;oBAC1B,MAAM,CAAC,KAAK,CAAC,uBAAuB,EAAE,GAAG,CAAC,CAAA;gBAC5C,CAAC,CAAA;gBAED,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,CAAA;YACnD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAA;gBAClD,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;oBACrB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;wBACnB,OAAO,EAAE,KAAK;wBACd,KAAK,EAAE;4BACL,IAAI,EAAE,CAAC,KAAK;4BACZ,OAAO,EAAE,uBAAuB;yBACjC;wBACD,EAAE,EAAE,IAAI;qBACT,CAAC,CAAA;gBACJ,CAAC;YACH,CAAC;QACH,CAAC,CAAC,CAAA;QAEF,4CAA4C;QAC5C,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;YAC7C,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;YACpC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,sCAAsC;iBAChD;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QAEF,GAAG,CAAC,MAAM,CAAC,kBAAkB,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;YAChD,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;YACpC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,sCAAsC;iBAChD;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC;SAAM,CAAC;QACN,oCAAoC;QACpC,MAAM,UAAU,GACd,EAAE,CAAA;QACJ,MAAM,cAAc,GAAG,cAAc;YACnC,CAAC,CAAC,IAAI,oBAAoB,CACtB,cAAc,EACd,CAAC,SAAiB,EAAE,EAAE;gBACpB,MAAM,CAAC,IAAI,CAAC,WAAW,SAAS,yBAAyB,CAAC,CAAA;gBAC1D,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC,CAAA;gBACvC,IAAI,SAAS,EAAE,CAAC;oBACd,SAAS,CAAC,KAAK,EAAE,CAAA;gBACnB,CAAC;gBACD,OAAO,UAAU,CAAC,SAAS,CAAC,CAAA;YAC9B,CAAC,EACD,MAAM,CACP;YACH,CAAC,CAAC,IAAI,CAAA;QAER,GAAG,CAAC,IAAI,CAAC,kBAAkB,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;YAC9C,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAuB,CAAA;YACrE,IAAI,SAAwC,CAAA;YAE5C,kBAAkB,CAAC;gBACjB,GAAG;gBACH,OAAO;aACR,CAAC,CAAA;YAEF,IAAI,SAAS,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBACvC,2BAA2B;gBAC3B,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC,CAAA;gBACjC,cAAc,EAAE,GAAG,CAAC,SAAS,EAAE,mCAAmC,CAAC,CAAA;YACrE,CAAC;iBAAM,IAAI,CAAC,SAAS,IAAI,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACvD,6BAA6B;gBAC7B,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAC3D,EAAE,YAAY,EAAE,EAAE,EAAE,CACrB,CAAA;gBAED,SAAS,GAAG,IAAI,6BAA6B,CAAC;oBAC5C,kBAAkB,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE;oBACtC,oBAAoB,EAAE,CAAC,SAAS,EAAE,EAAE;wBAClC,UAAU,CAAC,SAAS,CAAC,GAAG,SAAS,CAAA;wBACjC,cAAc,EAAE,GAAG,CAAC,SAAS,EAAE,wBAAwB,CAAC,CAAA;oBAC1D,CAAC;iBACF,CAAC,CAAA;gBACF,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;gBAE/B,SAAS,CAAC,SAAS,GAAG,KAAK,EAAE,GAAmB,EAAE,EAAE;oBAClD,MAAM,CAAC,IAAI,CACT,qCAAqC,SAAS,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAC1E,CAAA;oBAED,IAAI,QAAQ,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,EAAE,CAAC;wBACnC,IAAI,CAAC;4BACH,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,aAAa,CAChD,GAAqB,CACtB,CAAA;4BACD,MAAM,CAAC,IAAI,CAAC,qCAAqC,SAAS,IAAI,CAAC,CAAA;4BAC/D,MAAM,CAAC,KAAK,CACV,qCAAqC,SAAS,IAAI,EAClD,QAAQ,CACT,CAAA;4BACD,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;wBAC1B,CAAC;wBAAC,OAAO,GAAG,EAAE,CAAC;4BACb,MAAM,CAAC,KAAK,CACV,qCAAqC,SAAS,GAAG,EACjD,GAAG,CACJ,CAAA;4BACD,MAAM,aAAa,GAAG;gCACpB,OAAO,EAAE,KAAc;gCACvB,EAAE,EAAG,GAAsB,CAAC,EAAE;gCAC9B,KAAK,EAAE;oCACL,IAAI,EAAE,CAAC,KAAK;oCACZ,OAAO,EAAE,gBAAgB;iCAC1B;6BACF,CAAA;4BACD,SAAS,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;wBAC/B,CAAC;oBACH,CAAC;gBACH,CAAC,CAAA;gBAED,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE;oBACvB,MAAM,CAAC,IAAI,CAAC,6CAA6C,SAAS,GAAG,CAAC,CAAA;oBACtE,IAAI,SAAS,CAAC,SAAS,EAAE,CAAC;wBACxB,cAAc,EAAE,KAAK,CACnB,SAAS,CAAC,SAAS,EACnB,KAAK,EACL,wBAAwB,CACzB,CAAA;wBACD,OAAO,UAAU,CAAC,SAAS,CAAC,SAAS,CAAC,CAAA;oBACxC,CAAC;gBACH,CAAC,CAAA;gBAED,SAAS,CAAC,OAAO,GAAG,CAAC,GAAG,EAAE,EAAE;oBAC1B,MAAM,CAAC,KAAK,CAAC,iCAAiC,SAAS,IAAI,EAAE,GAAG,CAAC,CAAA;oBACjE,IAAI,SAAS,CAAC,SAAS,EAAE,CAAC;wBACxB,cAAc,EAAE,KAAK,CACnB,SAAS,CAAC,SAAS,EACnB,KAAK,EACL,0BAA0B,CAC3B,CAAA;wBACD,OAAO,UAAU,CAAC,SAAS,CAAC,SAAS,CAAC,CAAA;oBACxC,CAAC;gBACH,CAAC,CAAA;YACH,CAAC;iBAAM,CAAC;gBACN,kBAAkB;gBAClB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE;wBACL,IAAI,EAAE,CAAC,KAAK;wBACZ,OAAO,EAAE,2CAA2C;qBACrD;oBACD,EAAE,EAAE,IAAI;iBACT,CAAC,CAAA;gBACF,OAAM;YACR,CAAC;YAED,oDAAoD;YACpD,IAAI,aAAa,GAAG,KAAK,CAAA;YACzB,MAAM,iBAAiB,GAAG,CAAC,KAAa,EAAE,EAAE;gBAC1C,IAAI,CAAC,aAAa,IAAI,SAAS,CAAC,SAAS,EAAE,CAAC;oBAC1C,aAAa,GAAG,IAAI,CAAA;oBACpB,MAAM,CAAC,IAAI,CAAC,YAAY,KAAK,EAAE,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;oBACrD,cAAc,EAAE,GAAG,CAAC,SAAS,CAAC,SAAS,EAAE,iBAAiB,KAAK,EAAE,CAAC,CAAA;gBACpE,CAAC;YACH,CAAC,CAAA;YAED,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC,CAAA;YACrD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAA;YAElD,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,CAAA;QACnD,CAAC,CAAC,CAAA;QAEF,gEAAgE;QAChE,MAAM,oBAAoB,GAAG,KAAK,EAChC,GAAoB,EACpB,GAAqB,EACrB,EAAE;YACF,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAuB,CAAA;YAErE,kBAAkB,CAAC;gBACjB,GAAG;gBACH,OAAO;aACR,CAAC,CAAA;YAEF,IAAI,CAAC,SAAS,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBACzC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAA;gBACrD,OAAM;YACR,CAAC;YAED,cAAc,EAAE,GAAG,CACjB,SAAS,EACT,GAAG,GAAG,CAAC,MAAM,+BAA+B,CAC7C,CAAA;YAED,IAAI,aAAa,GAAG,KAAK,CAAA;YACzB,MAAM,iBAAiB,GAAG,CAAC,KAAa,EAAE,EAAE;gBAC1C,IAAI,CAAC,aAAa,EAAE,CAAC;oBACnB,aAAa,GAAG,IAAI,CAAA;oBACpB,MAAM,CAAC,IAAI,CAAC,YAAY,KAAK,EAAE,EAAE,SAAS,CAAC,CAAA;oBAC3C,cAAc,EAAE,GAAG,CAAC,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,aAAa,KAAK,EAAE,CAAC,CAAA;gBACnE,CAAC;YACH,CAAC,CAAA;YAED,GAAG,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC,CAAA;YACrD,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,QAAQ,CAAC,CAAC,CAAA;YAElD,MAAM,SAAS,GAAG,UAAU,CAAC,SAAS,CAAC,CAAA;YACvC,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACzC,CAAC,CAAA;QAED,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,oBAAoB,CAAC,CAAA;QACjD,GAAG,CAAC,MAAM,CAAC,kBAAkB,EAAE,oBAAoB,CAAC,CAAA;IACtD,CAAC;IAED,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;QAC1B,MAAM,CAAC,IAAI,CAAC,gBAAgB,IAAI,IAAI,IAAI,EAAE,CAAC,CAAA;QAC3C,MAAM,CAAC,IAAI,CACT,mCAAmC,IAAI,IAAI,IAAI,GAAG,kBAAkB,EAAE,CACvE,CAAA;QACD,MAAM,CAAC,IAAI,CAAC,SAAS,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,MAAM,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAA;AACvD,CAAC"} \ No newline at end of file diff --git a/dist/gateways/configToWs.d.ts b/dist/gateways/configToWs.d.ts new file mode 100644 index 0000000..6d2b4fb --- /dev/null +++ b/dist/gateways/configToWs.d.ts @@ -0,0 +1,14 @@ +import { type CorsOptions } from 'cors'; +import { Logger } from '../types.js'; +export interface ConfigToWsArgs { + configPath: string; + port: number; + host: string; + messagePath: string; + logger: Logger; + corsOrigin: CorsOptions['origin']; + healthEndpoints: string[]; + headers: Record; +} +export declare function configToWs(args: ConfigToWsArgs): Promise; +//# sourceMappingURL=configToWs.d.ts.map \ No newline at end of file diff --git a/dist/gateways/configToWs.d.ts.map b/dist/gateways/configToWs.d.ts.map new file mode 100644 index 0000000..c13edb5 --- /dev/null +++ b/dist/gateways/configToWs.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"configToWs.d.ts","sourceRoot":"","sources":["../../src/gateways/configToWs.ts"],"names":[],"mappings":"AACA,OAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAA;AAO7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAQpC,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjC,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAaD,wBAAsB,UAAU,CAAC,IAAI,EAAE,cAAc,iBA8KpD"} \ No newline at end of file diff --git a/dist/gateways/configToWs.js b/dist/gateways/configToWs.js new file mode 100644 index 0000000..b9bfd0d --- /dev/null +++ b/dist/gateways/configToWs.js @@ -0,0 +1,146 @@ +import express from 'express'; +import cors from 'cors'; +import { createServer } from 'http'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { getVersion } from '../lib/getVersion.js'; +import { WebSocketServerTransport } from '../server/websocket.js'; +import { onSignals } from '../lib/onSignals.js'; +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; +import { loadConfig } from '../lib/config.js'; +import { McpServerManager } from '../lib/mcpServerManager.js'; +const setResponseHeaders = ({ res, headers, }) => Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); +}); +export async function configToWs(args) { + const { configPath, port, host, messagePath, logger, corsOrigin, healthEndpoints, headers, } = args; + logger.info(` - config: ${configPath}`); + logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info(` - host: ${host}`); + logger.info(` - port: ${port}`); + logger.info(` - messagePath: ${messagePath}`); + logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); + logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); + const serverManager = new McpServerManager(logger); + let wsTransport = null; + let isReady = false; + const cleanup = async () => { + await serverManager.cleanup(); + if (wsTransport) { + wsTransport.close().catch((err) => { + logger.error(`Error stopping WebSocket server: ${err.message}`); + }); + } + }; + onSignals({ logger, cleanup }); + let config; + try { + config = loadConfig(configPath); + logger.info(`Loaded config with ${Object.keys(config.mcpServers).length} servers`); + } + catch (err) { + logger.error(`Failed to load config: ${err}`); + process.exit(1); + } + for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) { + try { + await serverManager.addServer(serverName, serverConfig); + logger.info(`Successfully initialized server: ${serverName}`); + } + catch (err) { + logger.error(`Failed to initialize server ${serverName}: ${err}`); + process.exit(1); + } + } + try { + const server = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + const app = express(); + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })); + } + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }); + if (!isReady) { + res.status(500).send('Server is not ready'); + } + else { + res.send('ok'); + } + }); + } + const httpServer = createServer(app); + wsTransport = new WebSocketServerTransport({ + path: messagePath, + server: httpServer, + }); + await server.connect(wsTransport); + wsTransport.onmessage = async (message) => { + // Extract client ID from the modified message ID + const messageId = message.id; + let clientId; + let originalId; + if (typeof messageId === 'string' && messageId.includes(':')) { + const parts = messageId.split(':'); + clientId = parts[0]; + originalId = parts.slice(1).join(':'); + message.id = isNaN(Number(originalId)) + ? originalId + : Number(originalId); + } + const isRequest = 'method' in message && 'id' in message; + if (isRequest) { + logger.info(`WebSocket → Servers (client ${clientId}):`, message); + try { + const response = await serverManager.handleRequest(message); + logger.info(`Servers → WebSocket (client ${clientId}):`); + logger.debug(`Servers → WebSocket (client ${clientId}):`, response); + await wsTransport.send(response, clientId); + } + catch (err) { + logger.error(`Error handling request from client ${clientId}:`, err); + const errorResponse = { + jsonrpc: '2.0', + id: message.id, + error: { + code: -32000, + message: 'Internal error', + }, + }; + try { + await wsTransport.send(errorResponse, clientId); + } + catch (sendErr) { + logger.error(`Failed to send error response to client ${clientId}:`, sendErr); + } + } + } + else { + logger.info(`Notification from client ${clientId}:`, message); + } + }; + wsTransport.onconnection = (clientId) => { + logger.info(`New WebSocket connection: ${clientId}`); + }; + wsTransport.ondisconnection = (clientId) => { + logger.info(`WebSocket connection closed: ${clientId}`); + }; + wsTransport.onerror = (err) => { + logger.error(`WebSocket error: ${err.message}`); + }; + isReady = true; + httpServer.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`); + logger.info(`WebSocket endpoint: ws://${host}:${port}${messagePath}`); + }); + logger.info('Config-to-WebSocket gateway ready'); + } + catch (err) { + logger.error(`Failed to start: ${err.message}`); + cleanup(); + process.exit(1); + } +} +//# sourceMappingURL=configToWs.js.map \ No newline at end of file diff --git a/dist/gateways/configToWs.js.map b/dist/gateways/configToWs.js.map new file mode 100644 index 0000000..9795c73 --- /dev/null +++ b/dist/gateways/configToWs.js.map @@ -0,0 +1 @@ +{"version":3,"file":"configToWs.js","sourceRoot":"","sources":["../../src/gateways/configToWs.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/gateways/sseToSse.d.ts b/dist/gateways/sseToSse.d.ts new file mode 100644 index 0000000..3820d64 --- /dev/null +++ b/dist/gateways/sseToSse.d.ts @@ -0,0 +1,16 @@ +import { type CorsOptions } from 'cors'; +import { Logger } from '../types.js'; +export interface SseToSseArgs { + inputSseUrl: string; + port: number; + host: string; + baseUrl: string; + ssePath: string; + messagePath: string; + logger: Logger; + corsOrigin: CorsOptions['origin']; + healthEndpoints: string[]; + headers: Record; +} +export declare function sseToSse(args: SseToSseArgs): Promise; +//# sourceMappingURL=sseToSse.d.ts.map \ No newline at end of file diff --git a/dist/gateways/sseToSse.d.ts.map b/dist/gateways/sseToSse.d.ts.map new file mode 100644 index 0000000..60b1d04 --- /dev/null +++ b/dist/gateways/sseToSse.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"sseToSse.d.ts","sourceRoot":"","sources":["../../src/gateways/sseToSse.ts"],"names":[],"mappings":"AAEA,OAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAA;AAa7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAIpC,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjC,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAmDD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,iBAyQhD"} \ No newline at end of file diff --git a/dist/gateways/sseToSse.js b/dist/gateways/sseToSse.js new file mode 100644 index 0000000..46f8a69 --- /dev/null +++ b/dist/gateways/sseToSse.js @@ -0,0 +1,234 @@ +import express from 'express'; +import bodyParser from 'body-parser'; +import cors from 'cors'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { z } from 'zod'; +import { getVersion } from '../lib/getVersion.js'; +import { onSignals } from '../lib/onSignals.js'; +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; +let sseClient; +const newInitializeSseClient = ({ message }) => { + const clientInfo = message.params?.clientInfo; + const clientCapabilities = message.params?.capabilities; + return new Client({ + name: clientInfo?.name ?? 'mcp-superassistant-proxy', + version: clientInfo?.version ?? getVersion(), + }, { + capabilities: clientCapabilities ?? {}, + }); +}; +const newFallbackSseClient = async ({ sseTransport, }) => { + const fallbackSseClient = new Client({ + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, { + capabilities: {}, + }); + await fallbackSseClient.connect(sseTransport); + return fallbackSseClient; +}; +const setResponseHeaders = ({ res, headers, }) => Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); +}); +export async function sseToSse(args) { + const { inputSseUrl, port, host, baseUrl, ssePath, messagePath, logger, corsOrigin, healthEndpoints, headers, } = args; + logger.info(` - input SSE: ${inputSseUrl}`); + logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info(` - host: ${host}`); + logger.info(` - port: ${port}`); + if (baseUrl) { + logger.info(` - baseUrl: ${baseUrl}`); + } + logger.info(` - ssePath: ${ssePath}`); + logger.info(` - messagePath: ${messagePath}`); + logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); + logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); + onSignals({ logger }); + const inputSseTransport = new SSEClientTransport(new URL(inputSseUrl), { + eventSourceInit: { + fetch: (...props) => { + const [url, init = {}] = props; + return fetch(url, { ...init, headers: { ...init.headers, ...headers } }); + }, + }, + requestInit: { + headers, + }, + }); + inputSseTransport.onerror = (err) => { + logger.error('Input SSE error:', err); + }; + inputSseTransport.onclose = () => { + logger.error('Input SSE connection closed'); + process.exit(1); + }; + const outputServer = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + const sessions = {}; + const app = express(); + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })); + } + app.use((req, res, next) => { + if (req.path === messagePath) + return next(); + return bodyParser.json()(req, res, next); + }); + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }); + res.send('ok'); + }); + } + app.get(ssePath, async (req, res) => { + logger.info(`New SSE connection from ${req.ip}`); + setResponseHeaders({ + res, + headers, + }); + const outputSseTransport = new SSEServerTransport(`${baseUrl}${messagePath}`, res); + await outputServer.connect(outputSseTransport); + const sessionId = outputSseTransport.sessionId; + if (sessionId) { + sessions[sessionId] = { transport: outputSseTransport, response: res }; + } + const wrapResponse = (req, payload) => ({ + jsonrpc: req.jsonrpc || '2.0', + id: req.id, + ...payload, + }); + outputSseTransport.onmessage = async (message) => { + const isRequest = 'method' in message && 'id' in message; + if (isRequest) { + logger.info(`Output SSE → Input SSE (session ${sessionId}):`, message); + const req = message; + let result; + try { + if (!sseClient) { + if (message.method === 'initialize') { + sseClient = newInitializeSseClient({ + message, + }); + const originalRequest = sseClient.request; + sseClient.request = async function (requestMessage, ...restArgs) { + if (requestMessage.method === 'initialize' && + message.params?.protocolVersion && + requestMessage.params?.protocolVersion) { + requestMessage.params.protocolVersion = + message.params.protocolVersion; + } + result = await originalRequest.apply(this, [ + requestMessage, + ...restArgs, + ]); + return result; + }; + await sseClient.connect(inputSseTransport); + sseClient.request = originalRequest; + } + else { + logger.info('SSE client not initialized, creating fallback client'); + sseClient = await newFallbackSseClient({ + sseTransport: inputSseTransport, + }); + } + logger.info('Input SSE connected'); + } + else { + result = await sseClient.request(req, z.any()); + } + } + catch (err) { + logger.error('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'; + const prefix = `MCP error ${errorCode}:`; + if (errorMsg.startsWith(prefix)) { + errorMsg = errorMsg.slice(prefix.length).trim(); + } + const errorResp = wrapResponse(req, { + error: { + code: errorCode, + message: errorMsg, + }, + }); + try { + outputSseTransport.send(errorResp); + } + catch (sendErr) { + logger.error(`Failed to send error response to session ${sessionId}:`, sendErr); + delete sessions[sessionId]; + } + return; + } + const response = wrapResponse(req, result.hasOwnProperty('error') + ? { error: { ...result.error } } + : { result: { ...result } }); + logger.info(`Response (session ${sessionId}):`, response); + try { + outputSseTransport.send(response); + } + catch (sendErr) { + logger.error(`Failed to send response to session ${sessionId}:`, sendErr); + delete sessions[sessionId]; + } + } + else { + logger.info(`Input SSE → Output SSE (session ${sessionId}):`, message); + try { + outputSseTransport.send(message); + } + catch (sendErr) { + logger.error(`Failed to send message to session ${sessionId}:`, sendErr); + delete sessions[sessionId]; + } + } + }; + outputSseTransport.onclose = () => { + logger.info(`Output SSE connection closed (session ${sessionId})`); + delete sessions[sessionId]; + }; + outputSseTransport.onerror = (err) => { + logger.error(`Output SSE error (session ${sessionId}):`, err); + delete sessions[sessionId]; + }; + req.on('close', () => { + logger.info(`Client disconnected (session ${sessionId})`); + delete sessions[sessionId]; + }); + }); + app.post(messagePath, async (req, res) => { + const sessionId = req.query.sessionId; + setResponseHeaders({ + res, + headers, + }); + if (!sessionId) { + return res.status(400).send('Missing sessionId parameter'); + } + const session = sessions[sessionId]; + if (session?.transport?.handlePostMessage) { + logger.info(`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, host, () => { + logger.info(`Listening on ${host}:${port}`); + logger.info(`SSE endpoint: http://${host}:${port}${ssePath}`); + logger.info(`POST messages: http://${host}:${port}${messagePath}`); + }); + logger.info('SSE-to-SSE gateway ready'); +} +//# sourceMappingURL=sseToSse.js.map \ No newline at end of file diff --git a/dist/gateways/sseToSse.js.map b/dist/gateways/sseToSse.js.map new file mode 100644 index 0000000..c652587 --- /dev/null +++ b/dist/gateways/sseToSse.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sseToSse.js","sourceRoot":"","sources":["../../src/gateways/sseToSse.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/gateways/sseToStdio.d.ts b/dist/gateways/sseToStdio.d.ts new file mode 100644 index 0000000..59b558b --- /dev/null +++ b/dist/gateways/sseToStdio.d.ts @@ -0,0 +1,8 @@ +import { Logger } from '../types.js'; +export interface SseToStdioArgs { + sseUrl: string; + logger: Logger; + headers: Record; +} +export declare function sseToStdio(args: SseToStdioArgs): Promise; +//# sourceMappingURL=sseToStdio.d.ts.map \ No newline at end of file diff --git a/dist/gateways/sseToStdio.d.ts.map b/dist/gateways/sseToStdio.d.ts.map new file mode 100644 index 0000000..1259146 --- /dev/null +++ b/dist/gateways/sseToStdio.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"sseToStdio.d.ts","sourceRoot":"","sources":["../../src/gateways/sseToStdio.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGpC,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAwCD,wBAAsB,UAAU,CAAC,IAAI,EAAE,cAAc,iBAuIpD"} \ No newline at end of file diff --git a/dist/gateways/sseToStdio.js b/dist/gateways/sseToStdio.js new file mode 100644 index 0000000..bd2533a --- /dev/null +++ b/dist/gateways/sseToStdio.js @@ -0,0 +1,140 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { z } from 'zod'; +import { getVersion } from '../lib/getVersion.js'; +import { onSignals } from '../lib/onSignals.js'; +let sseClient; +const newInitializeSseClient = ({ message }) => { + const clientInfo = message.params?.clientInfo; + const clientCapabilities = message.params?.capabilities; + return new Client({ + name: clientInfo?.name ?? 'mcp-superassistant-proxy', + version: clientInfo?.version ?? getVersion(), + }, { + capabilities: clientCapabilities ?? {}, + }); +}; +const newFallbackSseClient = async ({ sseTransport, }) => { + const fallbackSseClient = new Client({ + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, { + capabilities: {}, + }); + await fallbackSseClient.connect(sseTransport); + return fallbackSseClient; +}; +export async function sseToStdio(args) { + const { sseUrl, logger, headers } = args; + logger.info(` - sse: ${sseUrl}`); + logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info('Connecting to SSE...'); + onSignals({ logger }); + const sseTransport = new SSEClientTransport(new URL(sseUrl), { + eventSourceInit: { + fetch: (...props) => { + const [url, init = {}] = props; + return fetch(url, { ...init, headers: { ...init.headers, ...headers } }); + }, + }, + requestInit: { + headers, + }, + }); + sseTransport.onerror = (err) => { + logger.error('SSE error:', err); + }; + sseTransport.onclose = () => { + logger.error('SSE connection closed'); + process.exit(1); + }; + const stdioServer = new Server({ + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, { + capabilities: {}, + }); + const stdioTransport = new StdioServerTransport(); + await stdioServer.connect(stdioTransport); + 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) { + logger.info('Stdio → SSE:', message); + const req = message; + let result; + try { + if (!sseClient) { + if (message.method === 'initialize') { + sseClient = newInitializeSseClient({ + message, + }); + const originalRequest = sseClient.request; + sseClient.request = async function (requestMessage, ...restArgs) { + // pass protocol version from original client + if (requestMessage.method === 'initialize' && + message.params?.protocolVersion && + requestMessage.params?.protocolVersion) { + requestMessage.params.protocolVersion = + message.params.protocolVersion; + } + result = await originalRequest.apply(this, [ + requestMessage, + ...restArgs, + ]); + return result; + }; + await sseClient.connect(sseTransport); + sseClient.request = originalRequest; + } + else { + logger.info('SSE client not initialized, creating fallback client'); + sseClient = await newFallbackSseClient({ sseTransport }); + } + logger.info('SSE connected'); + } + else { + result = await sseClient.request(req, z.any()); + } + } + catch (err) { + logger.error('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'; + 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 } }); + logger.info('Response:', response); + process.stdout.write(JSON.stringify(response) + '\n'); + } + else { + logger.info('SSE → Stdio:', message); + process.stdout.write(JSON.stringify(message) + '\n'); + } + }; + logger.info('Stdio server listening'); +} +//# sourceMappingURL=sseToStdio.js.map \ No newline at end of file diff --git a/dist/gateways/sseToStdio.js.map b/dist/gateways/sseToStdio.js.map new file mode 100644 index 0000000..ff89d9e --- /dev/null +++ b/dist/gateways/sseToStdio.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sseToStdio.js","sourceRoot":"","sources":["../../src/gateways/sseToStdio.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/gateways/sseToWs.d.ts b/dist/gateways/sseToWs.d.ts new file mode 100644 index 0000000..1d55f59 --- /dev/null +++ b/dist/gateways/sseToWs.d.ts @@ -0,0 +1,14 @@ +import { type CorsOptions } from 'cors'; +import { Logger } from '../types.js'; +export interface SseToWsArgs { + inputSseUrl: string; + port: number; + host: string; + messagePath: string; + logger: Logger; + corsOrigin: CorsOptions['origin']; + healthEndpoints: string[]; + headers: Record; +} +export declare function sseToWs(args: SseToWsArgs): Promise; +//# sourceMappingURL=sseToWs.d.ts.map \ No newline at end of file diff --git a/dist/gateways/sseToWs.d.ts.map b/dist/gateways/sseToWs.d.ts.map new file mode 100644 index 0000000..931f766 --- /dev/null +++ b/dist/gateways/sseToWs.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"sseToWs.d.ts","sourceRoot":"","sources":["../../src/gateways/sseToWs.ts"],"names":[],"mappings":"AACA,OAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAA;AAa7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAKpC,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjC,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAwCD,wBAAsB,OAAO,CAAC,IAAI,EAAE,WAAW,iBAqP9C"} \ No newline at end of file diff --git a/dist/gateways/sseToWs.js b/dist/gateways/sseToWs.js new file mode 100644 index 0000000..bedf8c8 --- /dev/null +++ b/dist/gateways/sseToWs.js @@ -0,0 +1,223 @@ +import express from 'express'; +import cors from 'cors'; +import { createServer } from 'http'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { z } from 'zod'; +import { getVersion } from '../lib/getVersion.js'; +import { WebSocketServerTransport } from '../server/websocket.js'; +import { onSignals } from '../lib/onSignals.js'; +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; +let sseClient; +const newInitializeSseClient = ({ message }) => { + const clientInfo = message.params?.clientInfo; + const clientCapabilities = message.params?.capabilities; + return new Client({ + name: clientInfo?.name ?? 'mcp-superassistant-proxy', + version: clientInfo?.version ?? getVersion(), + }, { + capabilities: clientCapabilities ?? {}, + }); +}; +const newFallbackSseClient = async ({ sseTransport, }) => { + const fallbackSseClient = new Client({ + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, { + capabilities: {}, + }); + await fallbackSseClient.connect(sseTransport); + return fallbackSseClient; +}; +export async function sseToWs(args) { + const { inputSseUrl, port, host, messagePath, logger, corsOrigin, healthEndpoints, headers, } = args; + logger.info(` - input SSE: ${inputSseUrl}`); + logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info(` - host: ${host}`); + logger.info(` - port: ${port}`); + logger.info(` - messagePath: ${messagePath}`); + logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); + logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); + let wsTransport = null; + let isReady = false; + const cleanup = () => { + if (wsTransport) { + wsTransport.close().catch((err) => { + logger.error(`Error stopping WebSocket server: ${err.message}`); + }); + } + }; + onSignals({ + logger, + cleanup, + }); + const inputSseTransport = new SSEClientTransport(new URL(inputSseUrl), { + eventSourceInit: { + fetch: (...props) => { + const [url, init = {}] = props; + return fetch(url, { ...init, headers: { ...init.headers, ...headers } }); + }, + }, + requestInit: { + headers, + }, + }); + inputSseTransport.onerror = (err) => { + logger.error('Input SSE error:', err); + }; + inputSseTransport.onclose = () => { + logger.error('Input SSE connection closed'); + cleanup(); + process.exit(1); + }; + try { + const outputServer = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + const app = express(); + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })); + } + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + if (!isReady) { + res.status(500).send('Server is not ready'); + } + else { + res.send('ok'); + } + }); + } + const httpServer = createServer(app); + wsTransport = new WebSocketServerTransport({ + path: messagePath, + server: httpServer, + }); + await outputServer.connect(wsTransport); + const wrapResponse = (req, payload) => ({ + jsonrpc: req.jsonrpc || '2.0', + id: req.id, + ...payload, + }); + wsTransport.onmessage = async (message) => { + // Extract client ID from the modified message ID + const messageId = message.id; + let clientId; + let originalId; + if (typeof messageId === 'string' && messageId.includes(':')) { + const parts = messageId.split(':'); + clientId = parts[0]; + originalId = parts.slice(1).join(':'); + message.id = isNaN(Number(originalId)) + ? originalId + : Number(originalId); + } + const isRequest = 'method' in message && 'id' in message; + if (isRequest) { + logger.info(`WebSocket → SSE (client ${clientId}):`, message); + const req = message; + let result; + try { + if (!sseClient) { + if (message.method === 'initialize') { + sseClient = newInitializeSseClient({ + message, + }); + const originalRequest = sseClient.request; + sseClient.request = async function (requestMessage, ...restArgs) { + if (requestMessage.method === 'initialize' && + message.params?.protocolVersion && + requestMessage.params?.protocolVersion) { + requestMessage.params.protocolVersion = + message.params.protocolVersion; + } + result = await originalRequest.apply(this, [ + requestMessage, + ...restArgs, + ]); + return result; + }; + await sseClient.connect(inputSseTransport); + sseClient.request = originalRequest; + } + else { + logger.info('SSE client not initialized, creating fallback client'); + sseClient = await newFallbackSseClient({ + sseTransport: inputSseTransport, + }); + } + logger.info('Input SSE connected'); + } + else { + result = await sseClient.request(req, z.any()); + } + } + catch (err) { + logger.error('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'; + const prefix = `MCP error ${errorCode}:`; + if (errorMsg.startsWith(prefix)) { + errorMsg = errorMsg.slice(prefix.length).trim(); + } + const errorResp = wrapResponse(req, { + error: { + code: errorCode, + message: errorMsg, + }, + }); + try { + await wsTransport.send(errorResp, clientId); + } + catch (sendErr) { + logger.error(`Failed to send error response to client ${clientId}:`, sendErr); + } + return; + } + const response = wrapResponse(req, result.hasOwnProperty('error') + ? { error: { ...result.error } } + : { result: { ...result } }); + logger.info(`Response (client ${clientId}):`, response); + try { + await wsTransport.send(response, clientId); + } + catch (sendErr) { + logger.error(`Failed to send response to client ${clientId}:`, sendErr); + } + } + else { + logger.info(`SSE → WebSocket (client ${clientId}):`, message); + try { + await wsTransport.send(message, clientId); + } + catch (sendErr) { + logger.error(`Failed to send message to client ${clientId}:`, sendErr); + } + } + }; + wsTransport.onconnection = (clientId) => { + logger.info(`New WebSocket connection: ${clientId}`); + }; + wsTransport.ondisconnection = (clientId) => { + logger.info(`WebSocket connection closed: ${clientId}`); + }; + wsTransport.onerror = (err) => { + logger.error(`WebSocket error: ${err.message}`); + }; + isReady = true; + httpServer.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`); + logger.info(`WebSocket endpoint: ws://${host}:${port}${messagePath}`); + }); + logger.info('SSE-to-WebSocket gateway ready'); + } + catch (err) { + logger.error(`Failed to start: ${err.message}`); + cleanup(); + process.exit(1); + } +} +//# sourceMappingURL=sseToWs.js.map \ No newline at end of file diff --git a/dist/gateways/sseToWs.js.map b/dist/gateways/sseToWs.js.map new file mode 100644 index 0000000..8ab3c62 --- /dev/null +++ b/dist/gateways/sseToWs.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sseToWs.js","sourceRoot":"","sources":["../../src/gateways/sseToWs.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/gateways/stdioToSse.d.ts b/dist/gateways/stdioToSse.d.ts new file mode 100644 index 0000000..aab7d5f --- /dev/null +++ b/dist/gateways/stdioToSse.d.ts @@ -0,0 +1,16 @@ +import { type CorsOptions } from 'cors'; +import { Logger } from '../types.js'; +export interface StdioToSseArgs { + stdioCmd: string; + port: number; + host: string; + baseUrl: string; + ssePath: string; + messagePath: string; + logger: Logger; + corsOrigin: CorsOptions['origin']; + healthEndpoints: string[]; + headers: Record; +} +export declare function stdioToSse(args: StdioToSseArgs): Promise; +//# sourceMappingURL=stdioToSse.d.ts.map \ No newline at end of file diff --git a/dist/gateways/stdioToSse.d.ts.map b/dist/gateways/stdioToSse.d.ts.map new file mode 100644 index 0000000..baa15c4 --- /dev/null +++ b/dist/gateways/stdioToSse.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"stdioToSse.d.ts","sourceRoot":"","sources":["../../src/gateways/stdioToSse.ts"],"names":[],"mappings":"AAEA,OAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAA;AAK7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAKpC,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjC,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAaD,wBAAsB,UAAU,CAAC,IAAI,EAAE,cAAc,iBAmKpD"} \ No newline at end of file diff --git a/dist/gateways/stdioToSse.js b/dist/gateways/stdioToSse.js new file mode 100644 index 0000000..f959a74 --- /dev/null +++ b/dist/gateways/stdioToSse.js @@ -0,0 +1,134 @@ +import express from 'express'; +import bodyParser from 'body-parser'; +import cors from 'cors'; +import { spawn } from 'child_process'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { getVersion } from '../lib/getVersion.js'; +import { onSignals } from '../lib/onSignals.js'; +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; +const setResponseHeaders = ({ res, headers, }) => Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); +}); +export async function stdioToSse(args) { + const { stdioCmd, port, host, baseUrl, ssePath, messagePath, logger, corsOrigin, healthEndpoints, headers, } = args; + logger.info(` - Headers: ${Object(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info(` - port: ${port}`); + logger.info(` - stdio: ${stdioCmd}`); + if (baseUrl) { + logger.info(` - baseUrl: ${baseUrl}`); + } + logger.info(` - ssePath: ${ssePath}`); + logger.info(` - messagePath: ${messagePath}`); + logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); + logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); + onSignals({ logger }); + const child = spawn(stdioCmd, { shell: true }); + child.on('exit', (code, signal) => { + logger.error(`Child exited: code=${code}, signal=${signal}`); + process.exit(code ?? 1); + }); + const server = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + const sessions = {}; + const app = express(); + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })); + } + app.use((req, res, next) => { + if (req.path === messagePath) + return next(); + return bodyParser.json()(req, res, next); + }); + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }); + res.send('ok'); + }); + } + app.get(ssePath, async (req, res) => { + logger.info(`New SSE connection from ${req.ip}`); + setResponseHeaders({ + res, + headers, + }); + 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) => { + logger.info(`SSE → Child (session ${sessionId}): ${JSON.stringify(msg)}`); + child.stdin.write(JSON.stringify(msg) + '\n'); + }; + sseTransport.onclose = () => { + logger.info(`SSE connection closed (session ${sessionId})`); + delete sessions[sessionId]; + }; + sseTransport.onerror = (err) => { + logger.error(`SSE error (session ${sessionId}):`, err); + delete sessions[sessionId]; + }; + req.on('close', () => { + logger.info(`Client disconnected (session ${sessionId})`); + delete sessions[sessionId]; + }); + }); + // @ts-ignore + app.post(messagePath, async (req, res) => { + const sessionId = req.query.sessionId; + setResponseHeaders({ + res, + headers, + }); + if (!sessionId) { + return res.status(400).send('Missing sessionId parameter'); + } + const session = sessions[sessionId]; + if (session?.transport?.handlePostMessage) { + logger.info(`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, host, () => { + logger.info(`Listening on ${host}:${port}`); + logger.info(`SSE endpoint: http://${host}:${port}${ssePath}`); + logger.info(`POST messages: http://${host}:${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); + logger.info('Child → SSE:', jsonMsg); + for (const [sid, session] of Object.entries(sessions)) { + try { + session.transport.send(jsonMsg); + } + catch (err) { + logger.error(`Failed to send to session ${sid}:`, err); + delete sessions[sid]; + } + } + } + catch { + logger.error(`Child non-JSON: ${line}`); + } + }); + }); + child.stderr.on('data', (chunk) => { + logger.error(`Child stderr: ${chunk.toString('utf8')}`); + }); +} +//# sourceMappingURL=stdioToSse.js.map \ No newline at end of file diff --git a/dist/gateways/stdioToSse.js.map b/dist/gateways/stdioToSse.js.map new file mode 100644 index 0000000..e567711 --- /dev/null +++ b/dist/gateways/stdioToSse.js.map @@ -0,0 +1 @@ +{"version":3,"file":"stdioToSse.js","sourceRoot":"","sources":["../../src/gateways/stdioToSse.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/gateways/stdioToStatefulStreamableHttp.d.ts b/dist/gateways/stdioToStatefulStreamableHttp.d.ts new file mode 100644 index 0000000..444e57b --- /dev/null +++ b/dist/gateways/stdioToStatefulStreamableHttp.d.ts @@ -0,0 +1,15 @@ +import { type CorsOptions } from 'cors'; +import { Logger } from '../types.js'; +export interface StdioToStreamableHttpArgs { + stdioCmd: string; + port: number; + host: string; + streamableHttpPath: string; + logger: Logger; + corsOrigin: CorsOptions['origin']; + healthEndpoints: string[]; + headers: Record; + sessionTimeout: number | null; +} +export declare function stdioToStatefulStreamableHttp(args: StdioToStreamableHttpArgs): Promise; +//# sourceMappingURL=stdioToStatefulStreamableHttp.d.ts.map \ No newline at end of file diff --git a/dist/gateways/stdioToStatefulStreamableHttp.d.ts.map b/dist/gateways/stdioToStatefulStreamableHttp.d.ts.map new file mode 100644 index 0000000..08b703a --- /dev/null +++ b/dist/gateways/stdioToStatefulStreamableHttp.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"stdioToStatefulStreamableHttp.d.ts","sourceRoot":"","sources":["../../src/gateways/stdioToStatefulStreamableHttp.ts"],"names":[],"mappings":"AACA,OAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAA;AAK7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAQpC,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,kBAAkB,EAAE,MAAM,CAAA;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjC,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAaD,wBAAsB,6BAA6B,CACjD,IAAI,EAAE,yBAAyB,iBA6OhC"} \ No newline at end of file diff --git a/dist/gateways/stdioToStatefulStreamableHttp.js b/dist/gateways/stdioToStatefulStreamableHttp.js new file mode 100644 index 0000000..da4d0aa --- /dev/null +++ b/dist/gateways/stdioToStatefulStreamableHttp.js @@ -0,0 +1,189 @@ +import express from 'express'; +import cors from 'cors'; +import { spawn } from 'child_process'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { getVersion } from '../lib/getVersion.js'; +import { onSignals } from '../lib/onSignals.js'; +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; +import { randomUUID } from 'node:crypto'; +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; +import { SessionAccessCounter } from '../lib/sessionAccessCounter.js'; +const setResponseHeaders = ({ res, headers, }) => Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); +}); +export async function stdioToStatefulStreamableHttp(args) { + const { stdioCmd, port, host, streamableHttpPath, logger, corsOrigin, healthEndpoints, headers, sessionTimeout, } = args; + logger.info(` - Headers: ${Object(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info(` - port: ${port}`); + logger.info(` - stdio: ${stdioCmd}`); + logger.info(` - streamableHttpPath: ${streamableHttpPath}`); + logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); + logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); + logger.info(` - Session timeout: ${sessionTimeout ? `${sessionTimeout}ms` : 'disabled'}`); + onSignals({ logger }); + const app = express(); + app.use(express.json()); + if (corsOrigin) { + app.use(cors({ + origin: corsOrigin, + exposedHeaders: ['Mcp-Session-Id'], + })); + } + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }); + res.send('ok'); + }); + } + // Map to store transports by session ID + const transports = {}; + // Session access counter for timeout management + const sessionCounter = sessionTimeout + ? new SessionAccessCounter(sessionTimeout, (sessionId) => { + logger.info(`Session ${sessionId} timed out, cleaning up`); + const transport = transports[sessionId]; + if (transport) { + transport.close(); + } + delete transports[sessionId]; + }, logger) + : null; + // Handle POST requests for client-to-server communication + app.post(streamableHttpPath, async (req, res) => { + // Check for existing session ID + const sessionId = req.headers['mcp-session-id']; + let transport; + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId]; + // Increment session access count + sessionCounter?.inc(sessionId, 'POST request for existing session'); + } + else if (!sessionId && isInitializeRequest(req.body)) { + // New initialization request + const server = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sessionId) => { + // Store the transport by session ID + transports[sessionId] = transport; + // Initialize session access count + sessionCounter?.inc(sessionId, 'session initialization'); + }, + }); + await server.connect(transport); + const child = spawn(stdioCmd, { shell: true }); + child.on('exit', (code, signal) => { + logger.error(`Child exited: code=${code}, signal=${signal}`); + transport.close(); + }); + 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); + logger.info('Child → StreamableHttp:', line); + try { + transport.send(jsonMsg); + } + catch (e) { + logger.error(`Failed to send to StreamableHttp`, e); + } + } + catch { + logger.error(`Child non-JSON: ${line}`); + } + }); + }); + child.stderr.on('data', (chunk) => { + logger.error(`Child stderr: ${chunk.toString('utf8')}`); + }); + transport.onmessage = (msg) => { + logger.info(`StreamableHttp → Child: ${JSON.stringify(msg)}`); + child.stdin.write(JSON.stringify(msg) + '\n'); + }; + transport.onclose = () => { + logger.info(`StreamableHttp connection closed (session ${sessionId})`); + if (transport.sessionId) { + sessionCounter?.clear(transport.sessionId, false, 'transport being closed'); + delete transports[transport.sessionId]; + } + child.kill(); + }; + transport.onerror = (err) => { + logger.error(`StreamableHttp error (session ${sessionId}):`, err); + if (transport.sessionId) { + sessionCounter?.clear(transport.sessionId, false, 'transport emitting error'); + delete transports[transport.sessionId]; + } + child.kill(); + }; + } + else { + // Invalid request + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Bad Request: No valid session ID provided', + }, + id: null, + }); + return; + } + // Decrement session access count when response ends + let responseEnded = false; + const handleResponseEnd = (event) => { + if (!responseEnded && transport.sessionId) { + responseEnded = true; + logger.info(`Response ${event}`, transport.sessionId); + sessionCounter?.dec(transport.sessionId, `POST response ${event}`); + } + }; + res.on('finish', () => handleResponseEnd('finished')); + res.on('close', () => handleResponseEnd('closed')); + // Handle the request + await transport.handleRequest(req, res, req.body); + }); + // Reusable handler for GET and DELETE requests + const handleSessionRequest = async (req, res) => { + const sessionId = req.headers['mcp-session-id']; + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID'); + return; + } + // Increment session access count + sessionCounter?.inc(sessionId, `${req.method} request for existing session`); + // Decrement session access count when response ends + let responseEnded = false; + const handleResponseEnd = (event) => { + if (!responseEnded) { + responseEnded = true; + logger.info(`Response ${event}`, sessionId); + sessionCounter?.dec(sessionId, `${req.method} response ${event}`); + } + }; + res.on('finish', () => handleResponseEnd('finished')); + res.on('close', () => handleResponseEnd('closed')); + const transport = transports[sessionId]; + await transport.handleRequest(req, res); + }; + // Handle GET requests for server-to-client notifications via SSE + app.get(streamableHttpPath, handleSessionRequest); + // Handle DELETE requests for session termination + app.delete(streamableHttpPath, handleSessionRequest); + app.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`); + logger.info(`StreamableHttp endpoint: http://${host}:${port}${streamableHttpPath}`); + }); +} +//# sourceMappingURL=stdioToStatefulStreamableHttp.js.map \ No newline at end of file diff --git a/dist/gateways/stdioToStatefulStreamableHttp.js.map b/dist/gateways/stdioToStatefulStreamableHttp.js.map new file mode 100644 index 0000000..dec2f91 --- /dev/null +++ b/dist/gateways/stdioToStatefulStreamableHttp.js.map @@ -0,0 +1 @@ +{"version":3,"file":"stdioToStatefulStreamableHttp.js","sourceRoot":"","sources":["../../src/gateways/stdioToStatefulStreamableHttp.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/gateways/stdioToStatelessStreamableHttp.d.ts b/dist/gateways/stdioToStatelessStreamableHttp.d.ts new file mode 100644 index 0000000..ab235df --- /dev/null +++ b/dist/gateways/stdioToStatelessStreamableHttp.d.ts @@ -0,0 +1,14 @@ +import { type CorsOptions } from 'cors'; +import { Logger } from '../types.js'; +export interface StdioToStreamableHttpArgs { + stdioCmd: string; + port: number; + host: string; + streamableHttpPath: string; + logger: Logger; + corsOrigin: CorsOptions['origin']; + healthEndpoints: string[]; + headers: Record; +} +export declare function stdioToStatelessStreamableHttp(args: StdioToStreamableHttpArgs): Promise; +//# sourceMappingURL=stdioToStatelessStreamableHttp.d.ts.map \ No newline at end of file diff --git a/dist/gateways/stdioToStatelessStreamableHttp.d.ts.map b/dist/gateways/stdioToStatelessStreamableHttp.d.ts.map new file mode 100644 index 0000000..7a480f2 --- /dev/null +++ b/dist/gateways/stdioToStatelessStreamableHttp.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"stdioToStatelessStreamableHttp.d.ts","sourceRoot":"","sources":["../../src/gateways/stdioToStatelessStreamableHttp.ts"],"names":[],"mappings":"AACA,OAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAA;AAK7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAKpC,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,kBAAkB,EAAE,MAAM,CAAA;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjC,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAaD,wBAAsB,8BAA8B,CAClD,IAAI,EAAE,yBAAyB,iBA8JhC"} \ No newline at end of file diff --git a/dist/gateways/stdioToStatelessStreamableHttp.js b/dist/gateways/stdioToStatelessStreamableHttp.js new file mode 100644 index 0000000..51fee68 --- /dev/null +++ b/dist/gateways/stdioToStatelessStreamableHttp.js @@ -0,0 +1,132 @@ +import express from 'express'; +import cors from 'cors'; +import { spawn } from 'child_process'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { getVersion } from '../lib/getVersion.js'; +import { onSignals } from '../lib/onSignals.js'; +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; +const setResponseHeaders = ({ res, headers, }) => Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); +}); +export async function stdioToStatelessStreamableHttp(args) { + const { stdioCmd, port, host, streamableHttpPath, logger, corsOrigin, healthEndpoints, headers, } = args; + logger.info(` - Headers: ${Object(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info(` - host: ${host}`); + logger.info(` - port: ${port}`); + logger.info(` - stdio: ${stdioCmd}`); + logger.info(` - streamableHttpPath: ${streamableHttpPath}`); + logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); + logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); + onSignals({ logger }); + const app = express(); + app.use(express.json()); + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })); + } + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }); + res.send('ok'); + }); + } + app.post(streamableHttpPath, async (req, res) => { + // In stateless mode, create a new instance of transport and server for each request + // to ensure complete isolation. A single instance would cause request ID collisions + // when multiple clients connect concurrently. + try { + const server = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }); + await server.connect(transport); + const child = spawn(stdioCmd, { shell: true }); + child.on('exit', (code, signal) => { + logger.error(`Child exited: code=${code}, signal=${signal}`); + transport.close(); + }); + 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); + logger.info('Child → StreamableHttp:', line); + try { + transport.send(jsonMsg); + } + catch (e) { + logger.error(`Failed to send to StreamableHttp`, e); + } + } + catch { + logger.error(`Child non-JSON: ${line}`); + } + }); + }); + child.stderr.on('data', (chunk) => { + logger.error(`Child stderr: ${chunk.toString('utf8')}`); + }); + transport.onmessage = (msg) => { + logger.info(`StreamableHttp → Child: ${JSON.stringify(msg)}`); + child.stdin.write(JSON.stringify(msg) + '\n'); + }; + transport.onclose = () => { + logger.info('StreamableHttp connection closed'); + child.kill(); + }; + transport.onerror = (err) => { + logger.error(`StreamableHttp error:`, err); + child.kill(); + }; + await transport.handleRequest(req, res, req.body); + } + catch (error) { + logger.error('Error handling MCP request:', error); + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }); + } + } + }); + app.get(streamableHttpPath, async (req, res) => { + logger.info('Received GET MCP request'); + res.writeHead(405).end(JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed.', + }, + id: null, + })); + }); + app.delete(streamableHttpPath, async (req, res) => { + logger.info('Received DELETE MCP request'); + res.writeHead(405).end(JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed.', + }, + id: null, + })); + }); + app.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`); + logger.info(`StreamableHttp endpoint: http://${host}:${port}${streamableHttpPath}`); + }); +} +//# sourceMappingURL=stdioToStatelessStreamableHttp.js.map \ No newline at end of file diff --git a/dist/gateways/stdioToStatelessStreamableHttp.js.map b/dist/gateways/stdioToStatelessStreamableHttp.js.map new file mode 100644 index 0000000..b3aabd1 --- /dev/null +++ b/dist/gateways/stdioToStatelessStreamableHttp.js.map @@ -0,0 +1 @@ +{"version":3,"file":"stdioToStatelessStreamableHttp.js","sourceRoot":"","sources":["../../src/gateways/stdioToStatelessStreamableHttp.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/gateways/stdioToWs.d.ts b/dist/gateways/stdioToWs.d.ts new file mode 100644 index 0000000..ec2fc40 --- /dev/null +++ b/dist/gateways/stdioToWs.d.ts @@ -0,0 +1,13 @@ +import { type CorsOptions } from 'cors'; +import { Logger } from '../types.js'; +export interface StdioToWsArgs { + stdioCmd: string; + port: number; + host: string; + messagePath: string; + logger: Logger; + corsOrigin: CorsOptions['origin']; + healthEndpoints: string[]; +} +export declare function stdioToWs(args: StdioToWsArgs): Promise; +//# sourceMappingURL=stdioToWs.d.ts.map \ No newline at end of file diff --git a/dist/gateways/stdioToWs.d.ts.map b/dist/gateways/stdioToWs.d.ts.map new file mode 100644 index 0000000..5b52b3b --- /dev/null +++ b/dist/gateways/stdioToWs.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"stdioToWs.d.ts","sourceRoot":"","sources":["../../src/gateways/stdioToWs.ts"],"names":[],"mappings":"AACA,OAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAA;AAK7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAMpC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjC,eAAe,EAAE,MAAM,EAAE,CAAA;CAC1B;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,iBAyIlD"} \ No newline at end of file diff --git a/dist/gateways/stdioToWs.js b/dist/gateways/stdioToWs.js new file mode 100644 index 0000000..ff9e80a --- /dev/null +++ b/dist/gateways/stdioToWs.js @@ -0,0 +1,115 @@ +import express from 'express'; +import cors from 'cors'; +import { createServer } from 'http'; +import { spawn } from 'child_process'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { getVersion } from '../lib/getVersion.js'; +import { WebSocketServerTransport } from '../server/websocket.js'; +import { onSignals } from '../lib/onSignals.js'; +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; +export async function stdioToWs(args) { + const { stdioCmd, port, host, messagePath, logger, healthEndpoints, corsOrigin, } = args; + logger.info(` - host: ${host}`); + logger.info(` - port: ${port}`); + logger.info(` - stdio: ${stdioCmd}`); + logger.info(` - messagePath: ${messagePath}`); + logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); + logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); + let wsTransport = null; + let child = null; + let isReady = false; + const cleanup = () => { + if (wsTransport) { + wsTransport.close().catch((err) => { + logger.error(`Error stopping WebSocket server: ${err.message}`); + }); + } + if (child) { + child.kill(); + } + }; + onSignals({ + logger, + cleanup, + }); + try { + child = spawn(stdioCmd, { shell: true }); + child.on('exit', (code, signal) => { + logger.error(`Child exited: code=${code}, signal=${signal}`); + cleanup(); + process.exit(code ?? 1); + }); + const server = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + // Handle child process output + 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); + logger.info(`Child → WebSocket: ${JSON.stringify(jsonMsg)}`); + // Broadcast to all connected clients + wsTransport?.send(jsonMsg, jsonMsg.id).catch((err) => { + logger.error('Failed to broadcast message:', err); + }); + } + catch { + logger.error(`Child non-JSON: ${line}`); + } + }); + }); + child.stderr.on('data', (chunk) => { + logger.info(`Child stderr: ${chunk.toString('utf8')}`); + }); + const app = express(); + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })); + } + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + if (child?.killed) { + res.status(500).send('Child process has been killed'); + } + if (!isReady) { + res.status(500).send('Server is not ready'); + } + res.send('ok'); + }); + } + const httpServer = createServer(app); + wsTransport = new WebSocketServerTransport({ + path: messagePath, + server: httpServer, + }); + await server.connect(wsTransport); + wsTransport.onmessage = (msg) => { + const line = JSON.stringify(msg); + logger.info(`WebSocket → Child: ${line}`); + child.stdin.write(line + '\n'); + }; + wsTransport.onconnection = (clientId) => { + logger.info(`New WebSocket connection: ${clientId}`); + }; + wsTransport.ondisconnection = (clientId) => { + logger.info(`WebSocket connection closed: ${clientId}`); + }; + wsTransport.onerror = (err) => { + logger.error(`WebSocket error: ${err.message}`); + }; + isReady = true; + httpServer.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`); + logger.info(`WebSocket endpoint: ws://${host}:${port}${messagePath}`); + }); + } + catch (err) { + logger.error(`Failed to start: ${err.message}`); + cleanup(); + process.exit(1); + } +} +//# sourceMappingURL=stdioToWs.js.map \ No newline at end of file diff --git a/dist/gateways/stdioToWs.js.map b/dist/gateways/stdioToWs.js.map new file mode 100644 index 0000000..41ccd5c --- /dev/null +++ b/dist/gateways/stdioToWs.js.map @@ -0,0 +1 @@ +{"version":3,"file":"stdioToWs.js","sourceRoot":"","sources":["../../src/gateways/stdioToWs.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/gateways/streamableHttpToSse.d.ts b/dist/gateways/streamableHttpToSse.d.ts new file mode 100644 index 0000000..4ff089d --- /dev/null +++ b/dist/gateways/streamableHttpToSse.d.ts @@ -0,0 +1,16 @@ +import { type CorsOptions } from 'cors'; +import { Logger } from '../types.js'; +export interface StreamableHttpToSseArgs { + streamableHttpUrl: string; + port: number; + host: string; + baseUrl: string; + ssePath: string; + messagePath: string; + logger: Logger; + corsOrigin: CorsOptions['origin']; + healthEndpoints: string[]; + headers: Record; +} +export declare function streamableHttpToSse(args: StreamableHttpToSseArgs): Promise; +//# sourceMappingURL=streamableHttpToSse.d.ts.map \ No newline at end of file diff --git a/dist/gateways/streamableHttpToSse.d.ts.map b/dist/gateways/streamableHttpToSse.d.ts.map new file mode 100644 index 0000000..324f82e --- /dev/null +++ b/dist/gateways/streamableHttpToSse.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"streamableHttpToSse.d.ts","sourceRoot":"","sources":["../../src/gateways/streamableHttpToSse.ts"],"names":[],"mappings":"AAEA,OAAa,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAA;AAc7C,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAIpC,MAAM,WAAW,uBAAuB;IACtC,iBAAiB,EAAE,MAAM,CAAA;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;IACjC,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAuDD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,uBAAuB,iBA+QtE"} \ No newline at end of file diff --git a/dist/gateways/streamableHttpToSse.js b/dist/gateways/streamableHttpToSse.js new file mode 100644 index 0000000..510e6a8 --- /dev/null +++ b/dist/gateways/streamableHttpToSse.js @@ -0,0 +1,229 @@ +import express from 'express'; +import bodyParser from 'body-parser'; +import cors from 'cors'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; +import { InitializeRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; +import { getVersion } from '../lib/getVersion.js'; +import { onSignals } from '../lib/onSignals.js'; +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js'; +let streamableHttpClient; +const newInitializeStreamableHttpClient = ({ message, }) => { + const clientInfo = message.params?.clientInfo; + const clientCapabilities = message.params?.capabilities; + return new Client({ + name: clientInfo?.name ?? 'mcp-superassistant-proxy', + version: clientInfo?.version ?? getVersion(), + }, { + capabilities: clientCapabilities ?? {}, + }); +}; +const newFallbackStreamableHttpClient = async ({ streamableHttpTransport, }) => { + const fallbackStreamableHttpClient = new Client({ + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, { + capabilities: {}, + }); + await fallbackStreamableHttpClient.connect(streamableHttpTransport); + return fallbackStreamableHttpClient; +}; +const setResponseHeaders = ({ res, headers, }) => Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value); +}); +export async function streamableHttpToSse(args) { + const { streamableHttpUrl, port, host, baseUrl, ssePath, messagePath, logger, corsOrigin, healthEndpoints, headers, } = args; + logger.info(` - input StreamableHttp: ${streamableHttpUrl}`); + logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info(` - host: ${host}`); + logger.info(` - port: ${port}`); + if (baseUrl) { + logger.info(` - baseUrl: ${baseUrl}`); + } + logger.info(` - ssePath: ${ssePath}`); + logger.info(` - messagePath: ${messagePath}`); + logger.info(` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`); + logger.info(` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`); + onSignals({ logger }); + const inputStreamableHttpTransport = new StreamableHTTPClientTransport(new URL(streamableHttpUrl), { + requestInit: { + headers, + }, + }); + inputStreamableHttpTransport.onerror = (err) => { + logger.error('Input StreamableHttp error:', err); + }; + inputStreamableHttpTransport.onclose = () => { + logger.error('Input StreamableHttp connection closed'); + process.exit(1); + }; + const outputServer = new Server({ name: 'mcp-superassistant-proxy', version: getVersion() }, { capabilities: {} }); + const sessions = {}; + const app = express(); + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })); + } + app.use((req, res, next) => { + if (req.path === messagePath) + return next(); + return bodyParser.json()(req, res, next); + }); + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }); + res.send('ok'); + }); + } + app.get(ssePath, async (req, res) => { + logger.info(`New SSE connection from ${req.ip}`); + setResponseHeaders({ + res, + headers, + }); + const outputSseTransport = new SSEServerTransport(`${baseUrl}${messagePath}`, res); + await outputServer.connect(outputSseTransport); + const sessionId = outputSseTransport.sessionId; + if (sessionId) { + sessions[sessionId] = { transport: outputSseTransport, response: res }; + } + const wrapResponse = (req, payload) => ({ + jsonrpc: req.jsonrpc || '2.0', + id: req.id, + ...payload, + }); + outputSseTransport.onmessage = async (message) => { + const isRequest = 'method' in message && 'id' in message; + if (isRequest) { + logger.info(`Output SSE → Input StreamableHttp (session ${sessionId}):`, message); + const req = message; + let result; + try { + if (!streamableHttpClient) { + if (message.method === 'initialize') { + streamableHttpClient = newInitializeStreamableHttpClient({ + message, + }); + const originalRequest = streamableHttpClient.request; + streamableHttpClient.request = async function (requestMessage, ...restArgs) { + if (InitializeRequestSchema.safeParse(requestMessage).success && + message.params?.protocolVersion && + requestMessage.params?.protocolVersion) { + requestMessage.params.protocolVersion = + message.params.protocolVersion; + } + result = await originalRequest.apply(this, [ + requestMessage, + ...restArgs, + ]); + return result; + }; + await streamableHttpClient.connect(inputStreamableHttpTransport); + streamableHttpClient.request = originalRequest; + } + else { + logger.info('StreamableHttp client not initialized, creating fallback client'); + streamableHttpClient = await newFallbackStreamableHttpClient({ + streamableHttpTransport: inputStreamableHttpTransport, + }); + } + logger.info('Input StreamableHttp connected'); + } + else { + result = await streamableHttpClient.request(req, z.any()); + } + } + catch (err) { + logger.error('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'; + const prefix = `MCP error ${errorCode}:`; + if (errorMsg.startsWith(prefix)) { + errorMsg = errorMsg.slice(prefix.length).trim(); + } + const errorResp = wrapResponse(req, { + error: { + code: errorCode, + message: errorMsg, + }, + }); + try { + outputSseTransport.send(errorResp); + } + catch (sendErr) { + logger.error(`Failed to send error response to session ${sessionId}:`, sendErr); + delete sessions[sessionId]; + } + return; + } + const response = wrapResponse(req, result.hasOwnProperty('error') + ? { error: { ...result.error } } + : { result: { ...result } }); + logger.info(`Response (session ${sessionId}):`, response); + try { + outputSseTransport.send(response); + } + catch (sendErr) { + logger.error(`Failed to send response to session ${sessionId}:`, sendErr); + delete sessions[sessionId]; + } + } + else { + logger.info(`Input StreamableHttp → Output SSE (session ${sessionId}):`, message); + try { + outputSseTransport.send(message); + } + catch (sendErr) { + logger.error(`Failed to send message to session ${sessionId}:`, sendErr); + delete sessions[sessionId]; + } + } + }; + outputSseTransport.onclose = () => { + logger.info(`Output SSE connection closed (session ${sessionId})`); + delete sessions[sessionId]; + }; + outputSseTransport.onerror = (err) => { + logger.error(`Output SSE error (session ${sessionId}):`, err); + delete sessions[sessionId]; + }; + req.on('close', () => { + logger.info(`Client disconnected (session ${sessionId})`); + delete sessions[sessionId]; + }); + }); + app.post(messagePath, async (req, res) => { + const sessionId = req.query.sessionId; + setResponseHeaders({ + res, + headers, + }); + if (!sessionId) { + return res.status(400).send('Missing sessionId parameter'); + } + const session = sessions[sessionId]; + if (session?.transport?.handlePostMessage) { + logger.info(`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, host, () => { + logger.info(`Listening on ${host}:${port}`); + logger.info(`SSE endpoint: http://${host}:${port}${ssePath}`); + logger.info(`POST messages: http://${host}:${port}${messagePath}`); + }); + logger.info('StreamableHttp-to-SSE gateway ready'); +} +//# sourceMappingURL=streamableHttpToSse.js.map \ No newline at end of file diff --git a/dist/gateways/streamableHttpToSse.js.map b/dist/gateways/streamableHttpToSse.js.map new file mode 100644 index 0000000..730c4f6 --- /dev/null +++ b/dist/gateways/streamableHttpToSse.js.map @@ -0,0 +1 @@ +{"version":3,"file":"streamableHttpToSse.js","sourceRoot":"","sources":["../../src/gateways/streamableHttpToSse.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAA;AAC7B,OAAO,UAAU,MAAM,aAAa,CAAA;AACpC,OAAO,IAA0B,MAAM,MAAM,CAAA;AAC7C,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAClE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAA;AAClG,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAA;AAO5E,OAAO,EAAE,uBAAuB,EAAE,MAAM,oCAAoC,CAAA;AAC5E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAEjD,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAA;AAC/C,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAA;AAenE,IAAI,oBAAwC,CAAA;AAE5C,MAAM,iCAAiC,GAAG,CAAC,EACzC,OAAO,GAGR,EAAE,EAAE;IACH,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,UAAwC,CAAA;IAC3E,MAAM,kBAAkB,GAAG,OAAO,CAAC,MAAM,EAAE,YAE9B,CAAA;IAEb,OAAO,IAAI,MAAM,CACf;QACE,IAAI,EAAE,UAAU,EAAE,IAAI,IAAI,0BAA0B;QACpD,OAAO,EAAE,UAAU,EAAE,OAAO,IAAI,UAAU,EAAE;KAC7C,EACD;QACE,YAAY,EAAE,kBAAkB,IAAI,EAAE;KACvC,CACF,CAAA;AACH,CAAC,CAAA;AAED,MAAM,+BAA+B,GAAG,KAAK,EAAE,EAC7C,uBAAuB,GAGxB,EAAE,EAAE;IACH,MAAM,4BAA4B,GAAG,IAAI,MAAM,CAC7C;QACE,IAAI,EAAE,0BAA0B;QAChC,OAAO,EAAE,UAAU,EAAE;KACtB,EACD;QACE,YAAY,EAAE,EAAE;KACjB,CACF,CAAA;IAED,MAAM,4BAA4B,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAA;IACnE,OAAO,4BAA4B,CAAA;AACrC,CAAC,CAAA;AAED,MAAM,kBAAkB,GAAG,CAAC,EAC1B,GAAG,EACH,OAAO,GAIR,EAAE,EAAE,CACH,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;IAC/C,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;AAC3B,CAAC,CAAC,CAAA;AAEJ,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,IAA6B;IACrE,MAAM,EACJ,iBAAiB,EACjB,IAAI,EACJ,IAAI,EACJ,OAAO,EACP,OAAO,EACP,WAAW,EACX,MAAM,EACN,UAAU,EACV,eAAe,EACf,OAAO,GACR,GAAG,IAAI,CAAA;IAER,MAAM,CAAC,IAAI,CAAC,6BAA6B,iBAAiB,EAAE,CAAC,CAAA;IAC7D,MAAM,CAAC,IAAI,CACT,gBAAgB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CACnF,CAAA;IACD,MAAM,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAA;IAChC,MAAM,CAAC,IAAI,CAAC,aAAa,IAAI,EAAE,CAAC,CAAA;IAChC,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,CAAC,IAAI,CAAC,gBAAgB,OAAO,EAAE,CAAC,CAAA;IACxC,CAAC;IACD,MAAM,CAAC,IAAI,CAAC,gBAAgB,OAAO,EAAE,CAAC,CAAA;IACtC,MAAM,CAAC,IAAI,CAAC,oBAAoB,WAAW,EAAE,CAAC,CAAA;IAC9C,MAAM,CAAC,IAAI,CACT,aAAa,UAAU,CAAC,CAAC,CAAC,YAAY,mBAAmB,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,EAAE,CAC5F,CAAA;IACD,MAAM,CAAC,IAAI,CACT,yBAAyB,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,CAC1F,CAAA;IAED,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;IAErB,MAAM,4BAA4B,GAAG,IAAI,6BAA6B,CACpE,IAAI,GAAG,CAAC,iBAAiB,CAAC,EAC1B;QACE,WAAW,EAAE;YACX,OAAO;SACR;KACF,CACF,CAAA;IAED,4BAA4B,CAAC,OAAO,GAAG,CAAC,GAAG,EAAE,EAAE;QAC7C,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAA;IAClD,CAAC,CAAA;IAED,4BAA4B,CAAC,OAAO,GAAG,GAAG,EAAE;QAC1C,MAAM,CAAC,KAAK,CAAC,wCAAwC,CAAC,CAAA;QACtD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAA;IAED,MAAM,YAAY,GAAG,IAAI,MAAM,CAC7B,EAAE,IAAI,EAAE,0BAA0B,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAC3D,EAAE,YAAY,EAAE,EAAE,EAAE,CACrB,CAAA;IAED,MAAM,QAAQ,GAGV,EAAE,CAAA;IAEN,MAAM,GAAG,GAAG,OAAO,EAAE,CAAA;IAErB,IAAI,UAAU,EAAE,CAAC;QACf,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC,CAAA;IACvC,CAAC;IAED,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW;YAAE,OAAO,IAAI,EAAE,CAAA;QAC3C,OAAO,UAAU,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,KAAK,MAAM,EAAE,IAAI,eAAe,EAAE,CAAC;QACjC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;YACxB,kBAAkB,CAAC;gBACjB,GAAG;gBACH,OAAO;aACR,CAAC,CAAA;YACF,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAChB,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QAClC,MAAM,CAAC,IAAI,CAAC,2BAA2B,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;QAEhD,kBAAkB,CAAC;YACjB,GAAG;YACH,OAAO;SACR,CAAC,CAAA;QAEF,MAAM,kBAAkB,GAAG,IAAI,kBAAkB,CAC/C,GAAG,OAAO,GAAG,WAAW,EAAE,EAC1B,GAAG,CACJ,CAAA;QACD,MAAM,YAAY,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;QAE9C,MAAM,SAAS,GAAG,kBAAkB,CAAC,SAAS,CAAA;QAC9C,IAAI,SAAS,EAAE,CAAC;YACd,QAAQ,CAAC,SAAS,CAAC,GAAG,EAAE,SAAS,EAAE,kBAAkB,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAA;QACxE,CAAC;QAED,MAAM,YAAY,GAAG,CAAC,GAAmB,EAAE,OAAe,EAAE,EAAE,CAAC,CAAC;YAC9D,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,KAAK;YAC7B,EAAE,EAAE,GAAG,CAAC,EAAE;YACV,GAAG,OAAO;SACX,CAAC,CAAA;QAEF,kBAAkB,CAAC,SAAS,GAAG,KAAK,EAAE,OAAuB,EAAE,EAAE;YAC/D,MAAM,SAAS,GAAG,QAAQ,IAAI,OAAO,IAAI,IAAI,IAAI,OAAO,CAAA;YACxD,IAAI,SAAS,EAAE,CAAC;gBACd,MAAM,CAAC,IAAI,CACT,8CAA8C,SAAS,IAAI,EAC3D,OAAO,CACR,CAAA;gBACD,MAAM,GAAG,GAAG,OAAyB,CAAA;gBACrC,IAAI,MAAM,CAAA;gBAEV,IAAI,CAAC;oBACH,IAAI,CAAC,oBAAoB,EAAE,CAAC;wBAC1B,IAAI,OAAO,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;4BACpC,oBAAoB,GAAG,iCAAiC,CAAC;gCACvD,OAAO;6BACR,CAAC,CAAA;4BAEF,MAAM,eAAe,GAAG,oBAAoB,CAAC,OAAO,CAAA;4BAEpD,oBAAoB,CAAC,OAAO,GAAG,KAAK,WAClC,cAAc,EACd,GAAG,QAAQ;gCAEX,IACE,uBAAuB,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC,OAAO;oCACzD,OAAO,CAAC,MAAM,EAAE,eAAe;oCAC/B,cAAc,CAAC,MAAM,EAAE,eAAe,EACtC,CAAC;oCACD,cAAc,CAAC,MAAM,CAAC,eAAe;wCACnC,OAAO,CAAC,MAAM,CAAC,eAAe,CAAA;gCAClC,CAAC;gCAED,MAAM,GAAG,MAAM,eAAe,CAAC,KAAK,CAAC,IAAI,EAAE;oCACzC,cAAc;oCACd,GAAG,QAAQ;iCACZ,CAAC,CAAA;gCAEF,OAAO,MAAM,CAAA;4BACf,CAAC,CAAA;4BAED,MAAM,oBAAoB,CAAC,OAAO,CAAC,4BAA4B,CAAC,CAAA;4BAChE,oBAAoB,CAAC,OAAO,GAAG,eAAe,CAAA;wBAChD,CAAC;6BAAM,CAAC;4BACN,MAAM,CAAC,IAAI,CACT,iEAAiE,CAClE,CAAA;4BACD,oBAAoB,GAAG,MAAM,+BAA+B,CAAC;gCAC3D,uBAAuB,EAAE,4BAA4B;6BACtD,CAAC,CAAA;wBACJ,CAAC;wBAED,MAAM,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAA;oBAC/C,CAAC;yBAAM,CAAC;wBACN,MAAM,GAAG,MAAM,oBAAoB,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,CAAA;oBAC3D,CAAC;gBACH,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,MAAM,CAAC,KAAK,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAA;oBACnC,MAAM,SAAS,GACb,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,IAAI,GAAG;wBAC7C,CAAC,CAAE,GAAW,CAAC,IAAI;wBACnB,CAAC,CAAC,CAAC,KAAK,CAAA;oBACZ,IAAI,QAAQ,GACV,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,SAAS,IAAI,GAAG;wBAChD,CAAC,CAAE,GAAW,CAAC,OAAO;wBACtB,CAAC,CAAC,gBAAgB,CAAA;oBACtB,MAAM,MAAM,GAAG,aAAa,SAAS,GAAG,CAAA;oBACxC,IAAI,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;wBAChC,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAA;oBACjD,CAAC;oBACD,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,EAAE;wBAClC,KAAK,EAAE;4BACL,IAAI,EAAE,SAAS;4BACf,OAAO,EAAE,QAAQ;yBAClB;qBACF,CAAC,CAAA;oBACF,IAAI,CAAC;wBACH,kBAAkB,CAAC,IAAI,CAAC,SAAgB,CAAC,CAAA;oBAC3C,CAAC;oBAAC,OAAO,OAAO,EAAE,CAAC;wBACjB,MAAM,CAAC,KAAK,CACV,4CAA4C,SAAS,GAAG,EACxD,OAAO,CACR,CAAA;wBACD,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAA;oBAC5B,CAAC;oBACD,OAAM;gBACR,CAAC;gBACD,MAAM,QAAQ,GAAG,YAAY,CAC3B,GAAG,EACH,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC;oBAC5B,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,EAAE;oBAChC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,EAAE,CAC9B,CAAA;gBACD,MAAM,CAAC,IAAI,CAAC,qBAAqB,SAAS,IAAI,EAAE,QAAQ,CAAC,CAAA;gBACzD,IAAI,CAAC;oBACH,kBAAkB,CAAC,IAAI,CAAC,QAAe,CAAC,CAAA;gBAC1C,CAAC;gBAAC,OAAO,OAAO,EAAE,CAAC;oBACjB,MAAM,CAAC,KAAK,CACV,sCAAsC,SAAS,GAAG,EAClD,OAAO,CACR,CAAA;oBACD,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAA;gBAC5B,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,IAAI,CACT,8CAA8C,SAAS,IAAI,EAC3D,OAAO,CACR,CAAA;gBACD,IAAI,CAAC;oBACH,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;gBAClC,CAAC;gBAAC,OAAO,OAAO,EAAE,CAAC;oBACjB,MAAM,CAAC,KAAK,CACV,qCAAqC,SAAS,GAAG,EACjD,OAAO,CACR,CAAA;oBACD,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAA;gBAC5B,CAAC;YACH,CAAC;QACH,CAAC,CAAA;QAED,kBAAkB,CAAC,OAAO,GAAG,GAAG,EAAE;YAChC,MAAM,CAAC,IAAI,CAAC,yCAAyC,SAAS,GAAG,CAAC,CAAA;YAClE,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAA;QAC5B,CAAC,CAAA;QAED,kBAAkB,CAAC,OAAO,GAAG,CAAC,GAAG,EAAE,EAAE;YACnC,MAAM,CAAC,KAAK,CAAC,6BAA6B,SAAS,IAAI,EAAE,GAAG,CAAC,CAAA;YAC7D,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAA;QAC5B,CAAC,CAAA;QAED,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACnB,MAAM,CAAC,IAAI,CAAC,gCAAgC,SAAS,GAAG,CAAC,CAAA;YACzD,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAA;QAC5B,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,GAAQ,EAAE,GAAQ,EAAE,EAAE;QACjD,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,SAAmB,CAAA;QAE/C,kBAAkB,CAAC;YACjB,GAAG;YACH,OAAO;SACR,CAAC,CAAA;QAEF,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAA;QAC5D,CAAC;QAED,MAAM,OAAO,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAA;QACnC,IAAI,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,CAAC;YAC1C,MAAM,CAAC,IAAI,CAAC,kCAAkC,SAAS,GAAG,CAAC,CAAA;YAC3D,MAAM,OAAO,CAAC,SAAS,CAAC,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QACrD,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,wCAAwC,SAAS,EAAE,CAAC,CAAA;QAC3E,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;QAC1B,MAAM,CAAC,IAAI,CAAC,gBAAgB,IAAI,IAAI,IAAI,EAAE,CAAC,CAAA;QAC3C,MAAM,CAAC,IAAI,CAAC,wBAAwB,IAAI,IAAI,IAAI,GAAG,OAAO,EAAE,CAAC,CAAA;QAC7D,MAAM,CAAC,IAAI,CAAC,yBAAyB,IAAI,IAAI,IAAI,GAAG,WAAW,EAAE,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,MAAM,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAA;AACpD,CAAC"} \ No newline at end of file diff --git a/dist/gateways/streamableHttpToStdio.d.ts b/dist/gateways/streamableHttpToStdio.d.ts new file mode 100644 index 0000000..d3d70ff --- /dev/null +++ b/dist/gateways/streamableHttpToStdio.d.ts @@ -0,0 +1,8 @@ +import { Logger } from '../types.js'; +export interface StreamableHttpToStdioArgs { + streamableHttpUrl: string; + logger: Logger; + headers: Record; +} +export declare function streamableHttpToStdio(args: StreamableHttpToStdioArgs): Promise; +//# sourceMappingURL=streamableHttpToStdio.d.ts.map \ No newline at end of file diff --git a/dist/gateways/streamableHttpToStdio.d.ts.map b/dist/gateways/streamableHttpToStdio.d.ts.map new file mode 100644 index 0000000..a8dce1f --- /dev/null +++ b/dist/gateways/streamableHttpToStdio.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"streamableHttpToStdio.d.ts","sourceRoot":"","sources":["../../src/gateways/streamableHttpToStdio.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAGpC,MAAM,WAAW,yBAAyB;IACxC,iBAAiB,EAAE,MAAM,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC;AAwCD,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,yBAAyB,iBAuI1E"} \ No newline at end of file diff --git a/dist/gateways/streamableHttpToStdio.js b/dist/gateways/streamableHttpToStdio.js new file mode 100644 index 0000000..5519dfc --- /dev/null +++ b/dist/gateways/streamableHttpToStdio.js @@ -0,0 +1,135 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { InitializeRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import { z } from 'zod'; +import { getVersion } from '../lib/getVersion.js'; +import { onSignals } from '../lib/onSignals.js'; +let mcpClient; +const newInitializeMcpClient = ({ message }) => { + const clientInfo = message.params?.clientInfo; + const clientCapabilities = message.params?.capabilities; + return new Client({ + name: clientInfo?.name ?? 'mcp-superassistant-proxy', + version: clientInfo?.version ?? getVersion(), + }, { + capabilities: clientCapabilities ?? {}, + }); +}; +const newFallbackMcpClient = async ({ mcpTransport, }) => { + const fallbackMcpClient = new Client({ + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, { + capabilities: {}, + }); + await fallbackMcpClient.connect(mcpTransport); + return fallbackMcpClient; +}; +export async function streamableHttpToStdio(args) { + const { streamableHttpUrl, logger, headers } = args; + logger.info(` - streamableHttp: ${streamableHttpUrl}`); + logger.info(` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`); + logger.info('Connecting to Streamable HTTP...'); + onSignals({ logger }); + const mcpTransport = new StreamableHTTPClientTransport(new URL(streamableHttpUrl), { + requestInit: { + headers, + }, + }); + mcpTransport.onerror = (err) => { + logger.error('Streamable HTTP error:', err); + }; + mcpTransport.onclose = () => { + logger.error('Streamable HTTP connection closed'); + process.exit(1); + }; + const stdioServer = new Server({ + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, { + capabilities: {}, + }); + const stdioTransport = new StdioServerTransport(); + await stdioServer.connect(stdioTransport); + 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) { + logger.info('Stdio → Streamable HTTP:', message); + const req = message; + let result; + try { + if (!mcpClient) { + if (message.method === 'initialize') { + mcpClient = newInitializeMcpClient({ + message, + }); + const originalRequest = mcpClient.request; + mcpClient.request = async function (possibleInitRequestMessage, ...restArgs) { + if (InitializeRequestSchema.safeParse(possibleInitRequestMessage) + .success && + message.params?.protocolVersion) { + // respect the protocol version from the stdio client's init request + possibleInitRequestMessage.params.protocolVersion = + message.params.protocolVersion; + } + result = await originalRequest.apply(this, [ + possibleInitRequestMessage, + ...restArgs, + ]); + return result; + }; + await mcpClient.connect(mcpTransport); + mcpClient.request = originalRequest; + } + else { + logger.info('Streamable HTTP client not initialized, creating fallback client'); + mcpClient = await newFallbackMcpClient({ mcpTransport }); + } + logger.info('Streamable HTTP connected'); + } + else { + result = await mcpClient.request(req, z.any()); + } + } + catch (err) { + logger.error('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'; + 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 } }); + logger.info('Response:', response); + process.stdout.write(JSON.stringify(response) + '\n'); + } + else { + logger.info('Streamable HTTP → Stdio:', message); + process.stdout.write(JSON.stringify(message) + '\n'); + } + }; + logger.info('Stdio server listening'); +} +//# sourceMappingURL=streamableHttpToStdio.js.map \ No newline at end of file diff --git a/dist/gateways/streamableHttpToStdio.js.map b/dist/gateways/streamableHttpToStdio.js.map new file mode 100644 index 0000000..83bcf15 --- /dev/null +++ b/dist/gateways/streamableHttpToStdio.js.map @@ -0,0 +1 @@ +{"version":3,"file":"streamableHttpToStdio.js","sourceRoot":"","sources":["../../src/gateways/streamableHttpToStdio.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/index.d.ts b/dist/index.d.ts new file mode 100644 index 0000000..306b377 --- /dev/null +++ b/dist/index.d.ts @@ -0,0 +1,46 @@ +#!/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 + */ +export {}; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/index.d.ts.map b/dist/index.d.ts.map new file mode 100644 index 0000000..354a0c1 --- /dev/null +++ b/dist/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG"} \ No newline at end of file diff --git a/dist/index.js b/dist/index.js new file mode 100644 index 0000000..c309766 --- /dev/null +++ b/dist/index.js @@ -0,0 +1,424 @@ +#!/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, + 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, + 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, + 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 \ No newline at end of file diff --git a/dist/index.js.map b/dist/index.js.map new file mode 100644 index 0000000..02205b3 --- /dev/null +++ b/dist/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/lib/config.d.ts b/dist/lib/config.d.ts new file mode 100644 index 0000000..f1e38eb --- /dev/null +++ b/dist/lib/config.d.ts @@ -0,0 +1,78 @@ +import { z } from 'zod'; +export declare const McpServerConfigSchema: z.ZodObject<{ + name: z.ZodOptional; + type: z.ZodOptional>; + command: z.ZodOptional; + args: z.ZodOptional>; + url: z.ZodOptional; + env: z.ZodOptional>; + headers: z.ZodOptional>; +}, "strip", z.ZodTypeAny, { + headers?: Record; + name?: string; + type?: "stdio" | "sse" | "streamable-http"; + url?: string; + command?: string; + args?: string[]; + env?: Record; +}, { + headers?: Record; + name?: string; + type?: "stdio" | "sse" | "streamable-http"; + url?: string; + command?: string; + args?: string[]; + env?: Record; +}>; +export declare const ConfigSchema: z.ZodObject<{ + mcpServers: z.ZodRecord; + type: z.ZodOptional>; + command: z.ZodOptional; + args: z.ZodOptional>; + url: z.ZodOptional; + env: z.ZodOptional>; + headers: z.ZodOptional>; + }, "strip", z.ZodTypeAny, { + headers?: Record; + name?: string; + type?: "stdio" | "sse" | "streamable-http"; + url?: string; + command?: string; + args?: string[]; + env?: Record; + }, { + headers?: Record; + name?: string; + type?: "stdio" | "sse" | "streamable-http"; + url?: string; + command?: string; + args?: string[]; + env?: Record; + }>>; +}, "strip", z.ZodTypeAny, { + mcpServers?: Record; + name?: string; + type?: "stdio" | "sse" | "streamable-http"; + url?: string; + command?: string; + args?: string[]; + env?: Record; + }>; +}, { + mcpServers?: Record; + name?: string; + type?: "stdio" | "sse" | "streamable-http"; + url?: string; + command?: string; + args?: string[]; + env?: Record; + }>; +}>; +export type McpServerConfig = z.infer; +export type Config = z.infer; +export declare function detectServerType(config: McpServerConfig): 'stdio' | 'sse' | 'streamable-http'; +export declare function loadConfig(configPath: string): Config; +//# sourceMappingURL=config.d.ts.map \ No newline at end of file diff --git a/dist/lib/config.d.ts.map b/dist/lib/config.d.ts.map new file mode 100644 index 0000000..5ee1ddf --- /dev/null +++ b/dist/lib/config.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;EAQhC,CAAA;AAEF,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEvB,CAAA;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAA;AACnE,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAEjD,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,eAAe,GACtB,OAAO,GAAG,KAAK,GAAG,iBAAiB,CAgCrC;AAED,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAgBrD"} \ No newline at end of file diff --git a/dist/lib/config.js b/dist/lib/config.js new file mode 100644 index 0000000..a2732e6 --- /dev/null +++ b/dist/lib/config.js @@ -0,0 +1,60 @@ +import { readFileSync } from 'fs'; +import { z } from 'zod'; +export const McpServerConfigSchema = z.object({ + name: z.string().optional(), + type: z.enum(['stdio', 'sse', 'streamable-http']).optional(), + command: z.string().optional(), + args: z.array(z.string()).optional(), + url: z.string().optional(), + env: z.record(z.string()).optional(), + headers: z.record(z.string()).optional(), +}); +export const ConfigSchema = z.object({ + mcpServers: z.record(z.string(), McpServerConfigSchema), +}); +export function detectServerType(config) { + if (config.type) { + return config.type; + } + if (config.url) { + // Auto-detect based on URL path + try { + const url = new URL(config.url); + if (url.pathname.endsWith('/mcp') || url.pathname.includes('/mcp')) { + return 'streamable-http'; + } + else if (url.pathname.endsWith('/sse') || + url.pathname.includes('/sse')) { + return 'sse'; + } + else { + // Default to streamable-http for unrecognized HTTP endpoints + return 'streamable-http'; + } + } + catch { + return 'streamable-http'; + } + } + if (config.command) { + return 'stdio'; + } + throw new Error(`Cannot detect server type for ${config.name}. Please specify type explicitly.`); +} +export function loadConfig(configPath) { + try { + const configContent = readFileSync(configPath, 'utf8'); + const configData = JSON.parse(configContent); + return ConfigSchema.parse(configData); + } + catch (err) { + if (err instanceof SyntaxError) { + throw new Error(`Invalid JSON in config file: ${err.message}`); + } + if (err instanceof z.ZodError) { + throw new Error(`Invalid config format: ${err.errors.map((e) => e.message).join(', ')}`); + } + throw new Error(`Failed to load config: ${err}`); + } +} +//# sourceMappingURL=config.js.map \ No newline at end of file diff --git a/dist/lib/config.js.map b/dist/lib/config.js.map new file mode 100644 index 0000000..6359445 --- /dev/null +++ b/dist/lib/config.js.map @@ -0,0 +1 @@ +{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/lib/corsOrigin.d.ts b/dist/lib/corsOrigin.d.ts new file mode 100644 index 0000000..7733e5b --- /dev/null +++ b/dist/lib/corsOrigin.d.ts @@ -0,0 +1,6 @@ +export declare const corsOrigin: ({ argv, }: { + argv: { + cors: (string | number)[] | undefined; + }; +}) => (string | RegExp)[] | "*"; +//# sourceMappingURL=corsOrigin.d.ts.map \ No newline at end of file diff --git a/dist/lib/corsOrigin.d.ts.map b/dist/lib/corsOrigin.d.ts.map new file mode 100644 index 0000000..f646347 --- /dev/null +++ b/dist/lib/corsOrigin.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"corsOrigin.d.ts","sourceRoot":"","sources":["../../src/lib/corsOrigin.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,UAAU,GAAI,WAExB;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,GAAG,SAAS,CAAA;KACtC,CAAA;CACF,8BAwBA,CAAA"} \ No newline at end of file diff --git a/dist/lib/corsOrigin.js b/dist/lib/corsOrigin.js new file mode 100644 index 0000000..5d6ef40 --- /dev/null +++ b/dist/lib/corsOrigin.js @@ -0,0 +1,24 @@ +export const corsOrigin = ({ argv, }) => { + if (!argv.cors) { + return '*'; + } + if (argv.cors.length === 0) { + return '*'; + } + const origins = argv.cors.map((item) => `${item}`); + if (origins.includes('*')) + return '*'; + return origins.map((origin) => { + if (/^\/.*\/$/.test(origin)) { + const pattern = origin.slice(1, -1); + try { + return new RegExp(pattern); + } + catch (error) { + return origin; + } + } + return origin; + }); +}; +//# sourceMappingURL=corsOrigin.js.map \ No newline at end of file diff --git a/dist/lib/corsOrigin.js.map b/dist/lib/corsOrigin.js.map new file mode 100644 index 0000000..3734d03 --- /dev/null +++ b/dist/lib/corsOrigin.js.map @@ -0,0 +1 @@ +{"version":3,"file":"corsOrigin.js","sourceRoot":"","sources":["../../src/lib/corsOrigin.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,EACzB,IAAI,GAKL,EAAE,EAAE;IACH,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,CAAA;IAElD,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,GAAG,CAAA;IAErC,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;QAC5B,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YAC5B,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YACnC,IAAI,CAAC;gBACH,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,CAAA;YAC5B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,OAAO,MAAM,CAAA;YACf,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAA;IACf,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA"} \ No newline at end of file diff --git a/dist/lib/getLogger.d.ts b/dist/lib/getLogger.d.ts new file mode 100644 index 0000000..1d714ea --- /dev/null +++ b/dist/lib/getLogger.d.ts @@ -0,0 +1,6 @@ +import { Logger } from '../types.js'; +export declare const getLogger: ({ logLevel, outputTransport, }: { + logLevel: string; + outputTransport: string; +}) => Logger; +//# sourceMappingURL=getLogger.d.ts.map \ No newline at end of file diff --git a/dist/lib/getLogger.d.ts.map b/dist/lib/getLogger.d.ts.map new file mode 100644 index 0000000..44f992c --- /dev/null +++ b/dist/lib/getLogger.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"getLogger.d.ts","sourceRoot":"","sources":["../../src/lib/getLogger.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AA0EpC,eAAO,MAAM,SAAS,GAAI,gCAGvB;IACD,QAAQ,EAAE,MAAM,CAAA;IAChB,eAAe,EAAE,MAAM,CAAA;CACxB,KAAG,MAWH,CAAA"} \ No newline at end of file diff --git a/dist/lib/getLogger.js b/dist/lib/getLogger.js new file mode 100644 index 0000000..a778257 --- /dev/null +++ b/dist/lib/getLogger.js @@ -0,0 +1,55 @@ +import util from 'node:util'; +const defaultFormatArgs = (args) => args; +const log = ({ formatArgs = defaultFormatArgs, } = { formatArgs: defaultFormatArgs }) => (...args) => console.log('[mcp-superassistant-proxy]', ...formatArgs(args)); +const logStderr = ({ formatArgs = defaultFormatArgs, } = { formatArgs: defaultFormatArgs }) => (...args) => console.error('[mcp-superassistant-proxy]', ...formatArgs(args)); +const noneLogger = { + info: () => { }, + error: () => { }, + debug: () => { }, + warn: () => { }, +}; +const infoLogger = { + info: log(), + error: logStderr(), + debug: () => { }, + warn: logStderr(), +}; +const infoLoggerStdio = { + info: logStderr(), + error: logStderr(), + debug: () => { }, + warn: logStderr(), +}; +const debugFormatArgs = (args) => args.map((arg) => { + if (typeof arg === 'object') { + return util.inspect(arg, { + depth: null, + colors: process.stderr.isTTY, + compact: false, + }); + } + return arg; +}); +const debugLogger = { + info: log({ formatArgs: debugFormatArgs }), + error: logStderr({ formatArgs: debugFormatArgs }), + debug: log({ formatArgs: debugFormatArgs }), + warn: logStderr({ formatArgs: debugFormatArgs }), +}; +const debugLoggerStdio = { + info: logStderr({ formatArgs: debugFormatArgs }), + error: logStderr({ formatArgs: debugFormatArgs }), + debug: logStderr({ formatArgs: debugFormatArgs }), + warn: logStderr({ formatArgs: debugFormatArgs }), +}; +export const getLogger = ({ logLevel, outputTransport, }) => { + if (logLevel === 'none') { + return noneLogger; + } + if (logLevel === 'debug') { + return outputTransport === 'stdio' ? debugLoggerStdio : debugLogger; + } + // info logLevel + return outputTransport === 'stdio' ? infoLoggerStdio : infoLogger; +}; +//# sourceMappingURL=getLogger.js.map \ No newline at end of file diff --git a/dist/lib/getLogger.js.map b/dist/lib/getLogger.js.map new file mode 100644 index 0000000..eadd8d2 --- /dev/null +++ b/dist/lib/getLogger.js.map @@ -0,0 +1 @@ +{"version":3,"file":"getLogger.js","sourceRoot":"","sources":["../../src/lib/getLogger.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/lib/getVersion.d.ts b/dist/lib/getVersion.d.ts new file mode 100644 index 0000000..0ea5040 --- /dev/null +++ b/dist/lib/getVersion.d.ts @@ -0,0 +1,2 @@ +export declare function getVersion(): string; +//# sourceMappingURL=getVersion.d.ts.map \ No newline at end of file diff --git a/dist/lib/getVersion.d.ts.map b/dist/lib/getVersion.d.ts.map new file mode 100644 index 0000000..4e2846c --- /dev/null +++ b/dist/lib/getVersion.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"getVersion.d.ts","sourceRoot":"","sources":["../../src/lib/getVersion.ts"],"names":[],"mappings":"AAOA,wBAAgB,UAAU,IAAI,MAAM,CAanC"} \ No newline at end of file diff --git a/dist/lib/getVersion.js b/dist/lib/getVersion.js new file mode 100644 index 0000000..6384798 --- /dev/null +++ b/dist/lib/getVersion.js @@ -0,0 +1,17 @@ +import { fileURLToPath } from 'url'; +import { join, dirname } from 'path'; +import { readFileSync } from 'fs'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +export 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('[mcp-superassistant-proxy]', 'Unable to retrieve version:', err); + return 'unknown'; + } +} +//# sourceMappingURL=getVersion.js.map \ No newline at end of file diff --git a/dist/lib/getVersion.js.map b/dist/lib/getVersion.js.map new file mode 100644 index 0000000..3a15c52 --- /dev/null +++ b/dist/lib/getVersion.js.map @@ -0,0 +1 @@ +{"version":3,"file":"getVersion.js","sourceRoot":"","sources":["../../src/lib/getVersion.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAA;AACnC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAA;AAEjC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACjD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AAErC,MAAM,UAAU,UAAU;IACxB,IAAI,CAAC;QACH,MAAM,eAAe,GAAG,IAAI,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAA;QAC7D,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC,CAAA;QACtE,OAAO,WAAW,CAAC,OAAO,IAAI,OAAO,CAAA;IACvC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CACX,4BAA4B,EAC5B,6BAA6B,EAC7B,GAAG,CACJ,CAAA;QACD,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/dist/lib/headers.d.ts b/dist/lib/headers.d.ts new file mode 100644 index 0000000..11cc0d8 --- /dev/null +++ b/dist/lib/headers.d.ts @@ -0,0 +1,9 @@ +import { Logger } from '../types.js'; +export declare const headers: ({ argv, logger, }: { + argv: { + header: (string | number)[]; + oauth2Bearer: string | undefined; + }; + logger: Logger; +}) => Record; +//# sourceMappingURL=headers.d.ts.map \ No newline at end of file diff --git a/dist/lib/headers.d.ts.map b/dist/lib/headers.d.ts.map new file mode 100644 index 0000000..e24768b --- /dev/null +++ b/dist/lib/headers.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"headers.d.ts","sourceRoot":"","sources":["../../src/lib/headers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AA+BpC,eAAO,MAAM,OAAO,GAAI,mBAGrB;IACD,IAAI,EAAE;QACJ,MAAM,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,EAAE,CAAA;QAC3B,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;KACjC,CAAA;IACD,MAAM,EAAE,MAAM,CAAA;CACf,KAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAcxB,CAAA"} \ No newline at end of file diff --git a/dist/lib/headers.js b/dist/lib/headers.js new file mode 100644 index 0000000..915a470 --- /dev/null +++ b/dist/lib/headers.js @@ -0,0 +1,32 @@ +const parseHeaders = ({ argvHeader, logger, }) => { + return argvHeader.reduce((acc, rawHeader) => { + const header = `${rawHeader}`; + const colonIndex = header.indexOf(':'); + if (colonIndex === -1) { + logger.error(`Invalid header format: ${header}, ignoring`); + return acc; + } + const key = header.slice(0, colonIndex).trim(); + const value = header.slice(colonIndex + 1).trim(); + if (!key || !value) { + logger.error(`Invalid header format: ${header}, ignoring`); + return acc; + } + acc[key] = value; + return acc; + }, {}); +}; +export const headers = ({ argv, logger, }) => { + const headers = parseHeaders({ + argvHeader: argv.header, + logger, + }); + if ('oauth2Bearer' in argv) { + return { + ...headers, + Authorization: `Bearer ${argv.oauth2Bearer}`, + }; + } + return headers; +}; +//# sourceMappingURL=headers.js.map \ No newline at end of file diff --git a/dist/lib/headers.js.map b/dist/lib/headers.js.map new file mode 100644 index 0000000..f9197e2 --- /dev/null +++ b/dist/lib/headers.js.map @@ -0,0 +1 @@ +{"version":3,"file":"headers.js","sourceRoot":"","sources":["../../src/lib/headers.ts"],"names":[],"mappings":"AAEA,MAAM,YAAY,GAAG,CAAC,EACpB,UAAU,EACV,MAAM,GAIP,EAA0B,EAAE;IAC3B,OAAO,UAAU,CAAC,MAAM,CAAyB,CAAC,GAAG,EAAE,SAAS,EAAE,EAAE;QAClE,MAAM,MAAM,GAAG,GAAG,SAAS,EAAE,CAAA;QAE7B,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;QACtC,IAAI,UAAU,KAAK,CAAC,CAAC,EAAE,CAAC;YACtB,MAAM,CAAC,KAAK,CAAC,0BAA0B,MAAM,YAAY,CAAC,CAAA;YAC1D,OAAO,GAAG,CAAA;QACZ,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,EAAE,CAAA;QAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;QAEjD,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;YACnB,MAAM,CAAC,KAAK,CAAC,0BAA0B,MAAM,YAAY,CAAC,CAAA;YAC1D,OAAO,GAAG,CAAA;QACZ,CAAC;QAED,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QAChB,OAAO,GAAG,CAAA;IACZ,CAAC,EAAE,EAAE,CAAC,CAAA;AACR,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,EACtB,IAAI,EACJ,MAAM,GAOP,EAA0B,EAAE;IAC3B,MAAM,OAAO,GAAG,YAAY,CAAC;QAC3B,UAAU,EAAE,IAAI,CAAC,MAAM;QACvB,MAAM;KACP,CAAC,CAAA;IAEF,IAAI,cAAc,IAAI,IAAI,EAAE,CAAC;QAC3B,OAAO;YACL,GAAG,OAAO;YACV,aAAa,EAAE,UAAU,IAAI,CAAC,YAAY,EAAE;SAC7C,CAAA;IACH,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"} \ No newline at end of file diff --git a/dist/lib/mcpServerManager.d.ts b/dist/lib/mcpServerManager.d.ts new file mode 100644 index 0000000..bed12a4 --- /dev/null +++ b/dist/lib/mcpServerManager.d.ts @@ -0,0 +1,24 @@ +import { ChildProcessWithoutNullStreams } from 'child_process'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { JSONRPCRequest, JSONRPCResponse, Tool, Resource } from '@modelcontextprotocol/sdk/types.js'; +import { McpServerConfig } from './config.js'; +import { Logger } from '../types.js'; +export interface ManagedServer { + name: string; + config: McpServerConfig; + client: Client; + tools: Tool[]; + resources: Resource[]; + connected: boolean; + child?: ChildProcessWithoutNullStreams; +} +export declare class McpServerManager { + private servers; + private logger; + constructor(logger: Logger); + addServer(name: string, config: McpServerConfig): Promise; + handleRequest(request: JSONRPCRequest): Promise; + getServers(): Map; + cleanup(): Promise; +} +//# sourceMappingURL=mcpServerManager.d.ts.map \ No newline at end of file diff --git a/dist/lib/mcpServerManager.d.ts.map b/dist/lib/mcpServerManager.d.ts.map new file mode 100644 index 0000000..67b0ff8 --- /dev/null +++ b/dist/lib/mcpServerManager.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"mcpServerManager.d.ts","sourceRoot":"","sources":["../../src/lib/mcpServerManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,8BAA8B,EAAE,MAAM,eAAe,CAAA;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAIlE,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,IAAI,EACJ,QAAQ,EAGT,MAAM,oCAAoC,CAAA;AAE3C,OAAO,EAAE,eAAe,EAAoB,MAAM,aAAa,CAAA;AAE/D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,eAAe,CAAA;IACvB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,IAAI,EAAE,CAAA;IACb,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,CAAC,EAAE,8BAA8B,CAAA;CACvC;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAwC;IACvD,OAAO,CAAC,MAAM,CAAQ;gBAEV,MAAM,EAAE,MAAM;IAIpB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAmJ/D,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;IAgOtE,UAAU,IAAI,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC;IAIlC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAa/B"} \ No newline at end of file diff --git a/dist/lib/mcpServerManager.js b/dist/lib/mcpServerManager.js new file mode 100644 index 0000000..0bd1504 --- /dev/null +++ b/dist/lib/mcpServerManager.js @@ -0,0 +1,349 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { z } from 'zod'; +import { detectServerType } from './config.js'; +import { getVersion } from './getVersion.js'; +export class McpServerManager { + servers = new Map(); + logger; + constructor(logger) { + this.logger = logger; + } + async addServer(name, config) { + const serverType = detectServerType(config); + const client = new Client({ + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, { + capabilities: {}, + }); + let transport; + let child; + if (serverType === 'stdio') { + if (!config.command) { + throw new Error(`Stdio server ${name} missing command`); + } + const args = config.args || []; + this.logger.info(`Starting server ${name}: ${config.command} ${args.join(' ')}`); + this.logger.debug(`Command: "${config.command}", Args: [${args.map((a) => `"${a}"`).join(', ')}]`); + this.logger.debug(`Creating StdioClientTransport for ${name}`); + try { + // Use command and args to create the transport, similar to test files + // StdioClientTransport will handle spawning the process internally + transport = new StdioClientTransport({ + command: config.command, + args: args, + env: config.env ? { ...process.env, ...config.env } : process.env, + }); + this.logger.debug(`StdioClientTransport created for ${name}`); + } + catch (transportErr) { + this.logger.error(`Failed to create StdioClientTransport for ${name}:`, transportErr); + throw transportErr; + } + } + else if (serverType === 'sse') { + if (!config.url) { + throw new Error(`HTTP server ${name} missing URL`); + } + const url = new URL(config.url); + if (url.pathname.endsWith('/sse') || url.pathname.includes('/sse')) { + const headers = config.headers || {}; + this.logger.info(`Connecting to SSE server ${name} with headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`); + transport = new SSEClientTransport(url, { + eventSourceInit: { + fetch: (...props) => { + const [url, init = {}] = props; + return fetch(url, { + ...init, + headers: { ...init.headers, ...headers } + }); + }, + }, + requestInit: { + headers, + }, + }); + } + else { + throw new Error(`HTTP server ${name} URL must be an SSE endpoint (path should end with /sse)`); + } + } + else if (serverType === 'streamable-http') { + if (!config.url) { + throw new Error(`Streamable HTTP server ${name} missing URL`); + } + const headers = config.headers || {}; + this.logger.info(`Connecting to streamable HTTP server ${name}: ${config.url}`); + this.logger.info(`With headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`); + const url = new URL(config.url); + transport = new StreamableHTTPClientTransport(url, { + requestInit: { + headers, + }, + }); + } + else { + throw new Error(`Unsupported server type: ${serverType}`); + } + try { + this.logger.debug(`Attempting to connect client to transport for ${name}`); + await client.connect(transport); + this.logger.info(`Connected to server: ${name}`); + const server = { + name, + config, + client, + tools: [], + resources: [], + connected: true, + child: child || undefined, + }; + try { + const toolsResponse = (await client.request({ method: 'tools/list', params: {} }, z.object({ tools: z.array(z.any()) }))); + server.tools = toolsResponse.tools || []; + this.logger.info(`Server ${name} has ${server.tools.length} tools`); + } + catch (err) { + this.logger.warn(`Server ${name} does not support tools: ${err}`); + } + try { + const resourcesResponse = (await client.request({ method: 'resources/list', params: {} }, z.object({ resources: z.array(z.any()) }))); + server.resources = resourcesResponse.resources || []; + this.logger.info(`Server ${name} has ${server.resources.length} resources`); + } + catch (err) { + this.logger.debug(`Server ${name} does not support resources: ${err}`); + } + this.servers.set(name, server); + } + catch (err) { + if (child) { + child.kill(); + } + throw new Error(`Failed to connect to server ${name}: ${err}`); + } + } + async handleRequest(request) { + const { method, params, id } = request; + if (method === 'initialize') { + return { + jsonrpc: '2.0', + id, + result: { + protocolVersion: '2024-11-05', + capabilities: { + tools: {}, + resources: {}, + }, + serverInfo: { + name: 'mcp-superassistant-proxy-unified', + version: getVersion(), + }, + }, + }; + } + if (method === 'tools/list') { + const allTools = []; + for (const [serverName, server] of this.servers) { + if (server.connected) { + for (const tool of server.tools) { + allTools.push({ + ...tool, + name: `${serverName}.${tool.name}`, + }); + } + } + } + return { + jsonrpc: '2.0', + id, + result: { tools: allTools }, + }; + } + if (method === 'resources/list') { + const allResources = []; + for (const [serverName, server] of this.servers) { + if (server.connected) { + for (const resource of server.resources) { + allResources.push({ + ...resource, + name: `${serverName}.${resource.name}`, + uri: `${serverName}://${resource.uri}`, + }); + } + } + } + return { + jsonrpc: '2.0', + id, + result: { resources: allResources }, + }; + } + if (method === 'tools/call') { + const toolName = params?.name; + if (!toolName) { + return { + jsonrpc: '2.0', + id, + error: { + code: -32602, + message: 'Tool name is required', + }, + }; + } + let serverName; + let originalToolName; + if (toolName.includes('.')) { + // Tool name includes server prefix + ; + [serverName, originalToolName] = toolName.split('.', 2); + } + else { + // No server prefix, find which server has this tool + let foundServer = null; + for (const [sName, server] of this.servers) { + if (server.connected && + server.tools.some((tool) => tool.name === toolName)) { + if (foundServer) { + // Tool exists in multiple servers, require explicit server name + return { + jsonrpc: '2.0', + id, + error: { + code: -32602, + message: `Tool '${toolName}' exists in multiple servers (${foundServer}, ${sName}). Use format: servername.toolname`, + }, + }; + } + foundServer = sName; + } + } + if (!foundServer) { + return { + jsonrpc: '2.0', + id, + error: { + code: -32601, + message: `Tool '${toolName}' not found in any connected server`, + }, + }; + } + serverName = foundServer; + originalToolName = toolName; + } + const server = this.servers.get(serverName); + if (!server || !server.connected) { + return { + jsonrpc: '2.0', + id, + error: { + code: -32601, + message: `Server ${serverName} not found or not connected`, + }, + }; + } + try { + const response = await server.client.request({ + method: 'tools/call', + params: { + ...params, + name: originalToolName, + }, + }, z.any()); + return { + jsonrpc: '2.0', + id, + result: response, + }; + } + catch (err) { + return { + jsonrpc: '2.0', + id, + error: { + code: err.code || -32000, + message: err.message || 'Tool call failed', + }, + }; + } + } + if (method === 'resources/read') { + const uri = params?.uri; + if (!uri || !uri.includes('://')) { + return { + jsonrpc: '2.0', + id, + error: { + code: -32602, + message: 'Invalid resource URI. Expected format: servername://resource-uri', + }, + }; + } + const [serverName, originalUri] = uri.split('://', 2); + const server = this.servers.get(serverName); + if (!server || !server.connected) { + return { + jsonrpc: '2.0', + id, + error: { + code: -32601, + message: `Server ${serverName} not found or not connected`, + }, + }; + } + try { + const response = await server.client.request({ + method: 'resources/read', + params: { + ...params, + uri: originalUri, + }, + }, z.any()); + return { + jsonrpc: '2.0', + id, + result: response, + }; + } + catch (err) { + return { + jsonrpc: '2.0', + id, + error: { + code: err.code || -32000, + message: err.message || 'Resource read failed', + }, + }; + } + } + return { + jsonrpc: '2.0', + id, + error: { + code: -32601, + message: `Method not found: ${method}`, + }, + }; + } + getServers() { + return this.servers; + } + async cleanup() { + for (const [name, server] of this.servers) { + try { + if (server.child) { + server.child.kill(); + } + this.logger.info(`Cleaned up server: ${name}`); + } + catch (err) { + this.logger.error(`Error cleaning up server ${name}: ${err}`); + } + } + this.servers.clear(); + } +} +//# sourceMappingURL=mcpServerManager.js.map \ No newline at end of file diff --git a/dist/lib/mcpServerManager.js.map b/dist/lib/mcpServerManager.js.map new file mode 100644 index 0000000..40e3524 --- /dev/null +++ b/dist/lib/mcpServerManager.js.map @@ -0,0 +1 @@ +{"version":3,"file":"mcpServerManager.js","sourceRoot":"","sources":["../../src/lib/mcpServerManager.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/lib/onSignals.d.ts b/dist/lib/onSignals.d.ts new file mode 100644 index 0000000..18d4490 --- /dev/null +++ b/dist/lib/onSignals.d.ts @@ -0,0 +1,14 @@ +import { Logger } from '../types.js'; +export interface OnSignalsOptions { + logger: Logger; + cleanup?: () => void; +} +/** + * Sets up signal handlers for graceful shutdown. + * + * @param options Configuration options + * @param options.logger Logger instance + * @param options.cleanup Optional cleanup function to be called before exit + */ +export declare function onSignals(options: OnSignalsOptions): void; +//# sourceMappingURL=onSignals.d.ts.map \ No newline at end of file diff --git a/dist/lib/onSignals.d.ts.map b/dist/lib/onSignals.d.ts.map new file mode 100644 index 0000000..6a8265e --- /dev/null +++ b/dist/lib/onSignals.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"onSignals.d.ts","sourceRoot":"","sources":["../../src/lib/onSignals.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAwBzD"} \ No newline at end of file diff --git a/dist/lib/onSignals.js b/dist/lib/onSignals.js new file mode 100644 index 0000000..9788bb7 --- /dev/null +++ b/dist/lib/onSignals.js @@ -0,0 +1,28 @@ +/** + * Sets up signal handlers for graceful shutdown. + * + * @param options Configuration options + * @param options.logger Logger instance + * @param options.cleanup Optional cleanup function to be called before exit + */ +export function onSignals(options) { + const { logger, cleanup } = options; + const handleSignal = (signal) => { + logger.info(`Caught ${signal}. Exiting...`); + if (cleanup) { + cleanup(); + } + process.exit(0); + }; + process.on('SIGINT', () => handleSignal('SIGINT')); + process.on('SIGTERM', () => handleSignal('SIGTERM')); + process.on('SIGHUP', () => handleSignal('SIGHUP')); + process.stdin.on('close', () => { + logger.info('stdin closed. Exiting...'); + if (cleanup) { + cleanup(); + } + process.exit(0); + }); +} +//# sourceMappingURL=onSignals.js.map \ No newline at end of file diff --git a/dist/lib/onSignals.js.map b/dist/lib/onSignals.js.map new file mode 100644 index 0000000..bb05c7a --- /dev/null +++ b/dist/lib/onSignals.js.map @@ -0,0 +1 @@ +{"version":3,"file":"onSignals.js","sourceRoot":"","sources":["../../src/lib/onSignals.ts"],"names":[],"mappings":"AAOA;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,OAAyB;IACjD,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAA;IAEnC,MAAM,YAAY,GAAG,CAAC,MAAc,EAAE,EAAE;QACtC,MAAM,CAAC,IAAI,CAAC,UAAU,MAAM,cAAc,CAAC,CAAA;QAC3C,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,EAAE,CAAA;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAA;IAED,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAA;IAElD,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAA;IAEpD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAA;IAElD,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QAC7B,MAAM,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAA;QACvC,IAAI,OAAO,EAAE,CAAC;YACZ,OAAO,EAAE,CAAA;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC,CAAC,CAAA;AACJ,CAAC"} \ No newline at end of file diff --git a/dist/lib/serializeCorsOrigin.d.ts b/dist/lib/serializeCorsOrigin.d.ts new file mode 100644 index 0000000..34f99b6 --- /dev/null +++ b/dist/lib/serializeCorsOrigin.d.ts @@ -0,0 +1,5 @@ +import type { CorsOptions } from 'cors'; +export declare const serializeCorsOrigin: ({ corsOrigin, }: { + corsOrigin: CorsOptions["origin"]; +}) => string; +//# sourceMappingURL=serializeCorsOrigin.d.ts.map \ No newline at end of file diff --git a/dist/lib/serializeCorsOrigin.d.ts.map b/dist/lib/serializeCorsOrigin.d.ts.map new file mode 100644 index 0000000..1b9922f --- /dev/null +++ b/dist/lib/serializeCorsOrigin.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"serializeCorsOrigin.d.ts","sourceRoot":"","sources":["../../src/lib/serializeCorsOrigin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,MAAM,CAAA;AAEvC,eAAO,MAAM,mBAAmB,GAAI,iBAEjC;IACD,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAA;CAClC,WAOG,CAAA"} \ No newline at end of file diff --git a/dist/lib/serializeCorsOrigin.js b/dist/lib/serializeCorsOrigin.js new file mode 100644 index 0000000..561f68a --- /dev/null +++ b/dist/lib/serializeCorsOrigin.js @@ -0,0 +1,7 @@ +export const serializeCorsOrigin = ({ corsOrigin, }) => JSON.stringify(corsOrigin, (_key, value) => { + if (value instanceof RegExp) { + return value.toString(); + } + return value; +}); +//# sourceMappingURL=serializeCorsOrigin.js.map \ No newline at end of file diff --git a/dist/lib/serializeCorsOrigin.js.map b/dist/lib/serializeCorsOrigin.js.map new file mode 100644 index 0000000..4c8a408 --- /dev/null +++ b/dist/lib/serializeCorsOrigin.js.map @@ -0,0 +1 @@ +{"version":3,"file":"serializeCorsOrigin.js","sourceRoot":"","sources":["../../src/lib/serializeCorsOrigin.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAClC,UAAU,GAGX,EAAE,EAAE,CACH,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;IACzC,IAAI,KAAK,YAAY,MAAM,EAAE,CAAC;QAC5B,OAAO,KAAK,CAAC,QAAQ,EAAE,CAAA;IACzB,CAAC;IAED,OAAO,KAAK,CAAA;AACd,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/dist/lib/sessionAccessCounter.d.ts b/dist/lib/sessionAccessCounter.d.ts new file mode 100644 index 0000000..db9dbf8 --- /dev/null +++ b/dist/lib/sessionAccessCounter.d.ts @@ -0,0 +1,12 @@ +import { Logger } from '../types.js'; +export declare class SessionAccessCounter { + timeout: number; + cleanup: (sessionId: string) => unknown; + logger: Logger; + private sessions; + constructor(timeout: number, cleanup: (sessionId: string) => unknown, logger: Logger); + inc(sessionId: string, reason: string): void; + dec(sessionId: string, reason: string): void; + clear(sessionId: string, runCleanup: boolean, reason: string): void; +} +//# sourceMappingURL=sessionAccessCounter.d.ts.map \ No newline at end of file diff --git a/dist/lib/sessionAccessCounter.d.ts.map b/dist/lib/sessionAccessCounter.d.ts.map new file mode 100644 index 0000000..0bcb9ce --- /dev/null +++ b/dist/lib/sessionAccessCounter.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"sessionAccessCounter.d.ts","sourceRoot":"","sources":["../../src/lib/sessionAccessCounter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,qBAAa,oBAAoB;IAOtB,OAAO,EAAE,MAAM;IACf,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO;IACvC,MAAM,EAAE,MAAM;IARvB,OAAO,CAAC,QAAQ,CAGH;gBAGJ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,EACvC,MAAM,EAAE,MAAM;IAGvB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAgCrC,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IA+CrC,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM;CAwB7D"} \ No newline at end of file diff --git a/dist/lib/sessionAccessCounter.js b/dist/lib/sessionAccessCounter.js new file mode 100644 index 0000000..172ea8c --- /dev/null +++ b/dist/lib/sessionAccessCounter.js @@ -0,0 +1,78 @@ +export class SessionAccessCounter { + timeout; + cleanup; + logger; + sessions = new Map(); + constructor(timeout, cleanup, logger) { + this.timeout = timeout; + this.cleanup = cleanup; + this.logger = logger; + } + inc(sessionId, reason) { + this.logger.info(`SessionAccessCounter.inc() ${sessionId}, caused by ${reason}`); + const session = this.sessions.get(sessionId); + if (!session) { + // New session + this.logger.info(`Session access count 0 -> 1 for ${sessionId} (new session)`); + this.sessions.set(sessionId, { accessCount: 1 }); + return; + } + if ('timeout' in session) { + // Clear pending cleanup and reactivate + this.logger.info(`Session access count 0 -> 1, clearing cleanup timeout for ${sessionId}`); + clearTimeout(session.timeout); + this.sessions.set(sessionId, { accessCount: 1 }); + } + else { + // Increment active session + this.logger.info(`Session access count ${session.accessCount} -> ${session.accessCount + 1} for ${sessionId}`); + session.accessCount++; + } + } + dec(sessionId, reason) { + this.logger.info(`SessionAccessCounter.dec() ${sessionId}, caused by ${reason}`); + const session = this.sessions.get(sessionId); + if (!session) { + this.logger.error(`Called dec() on non-existent session ${sessionId}, ignoring`); + return; + } + if ('timeout' in session) { + this.logger.error(`Called dec() on session ${sessionId} that is already pending cleanup, ignoring`); + return; + } + if (session.accessCount <= 0) { + throw new Error(`Invalid access count ${session.accessCount} for session ${sessionId}`); + } + session.accessCount--; + this.logger.info(`Session access count ${session.accessCount + 1} -> ${session.accessCount} for ${sessionId}`); + if (session.accessCount === 0) { + this.logger.info(`Session access count reached 0, setting cleanup timeout for ${sessionId}`); + this.sessions.set(sessionId, { + timeout: setTimeout(() => { + this.logger.info(`Session ${sessionId} timed out, cleaning up`); + this.sessions.delete(sessionId); + this.cleanup(sessionId); + }, this.timeout), + }); + } + } + clear(sessionId, runCleanup, reason) { + this.logger.info(`SessionAccessCounter.clear() ${sessionId}, caused by ${reason}`); + const session = this.sessions.get(sessionId); + if (!session) { + this.logger.info(`Attempted to clear non-existent session ${sessionId}`); + return; + } + // Clear any pending timeout + if ('timeout' in session) { + clearTimeout(session.timeout); + } + // Remove from tracking + this.sessions.delete(sessionId); + // Run cleanup if requested + if (runCleanup) { + this.cleanup(sessionId); + } + } +} +//# sourceMappingURL=sessionAccessCounter.js.map \ No newline at end of file diff --git a/dist/lib/sessionAccessCounter.js.map b/dist/lib/sessionAccessCounter.js.map new file mode 100644 index 0000000..b4bf705 --- /dev/null +++ b/dist/lib/sessionAccessCounter.js.map @@ -0,0 +1 @@ +{"version":3,"file":"sessionAccessCounter.js","sourceRoot":"","sources":["../../src/lib/sessionAccessCounter.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/server/websocket.d.ts b/dist/server/websocket.d.ts new file mode 100644 index 0000000..06e0415 --- /dev/null +++ b/dist/server/websocket.d.ts @@ -0,0 +1,22 @@ +import { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js'; +import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'; +import { Server } from 'http'; +export declare class WebSocketServerTransport implements Transport { + private wss; + private clients; + onclose?: () => void; + onerror?: (err: Error) => void; + private messageHandler?; + onconnection?: (clientId: string) => void; + ondisconnection?: (clientId: string) => void; + set onmessage(handler: ((message: JSONRPCMessage) => void) | undefined); + constructor({ path, server }: { + path: string; + server: Server; + }); + start(): Promise; + send(msg: JSONRPCMessage, options?: TransportSendOptions | string): Promise; + broadcast(msg: JSONRPCMessage): Promise; + close(): Promise; +} +//# sourceMappingURL=websocket.d.ts.map \ No newline at end of file diff --git a/dist/server/websocket.d.ts.map b/dist/server/websocket.d.ts.map new file mode 100644 index 0000000..74dac85 --- /dev/null +++ b/dist/server/websocket.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/server/websocket.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,oBAAoB,EACrB,MAAM,+CAA+C,CAAA;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AAGnE,OAAO,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAE7B,qBAAa,wBAAyB,YAAW,SAAS;IACxD,OAAO,CAAC,GAAG,CAAkB;IAC7B,OAAO,CAAC,OAAO,CAAoC;IAEnD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;IAC9B,OAAO,CAAC,cAAc,CAAC,CAAiD;IACxE,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IACzC,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;IAE5C,IAAI,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAC,GAAG,SAAS,EAgBrE;gBAEW,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE;IAOxD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BtB,IAAI,CACR,GAAG,EAAE,cAAc,EACnB,OAAO,CAAC,EAAE,oBAAoB,GAAG,MAAM,GACtC,OAAO,CAAC,IAAI,CAAC;IAmCV,SAAS,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAQ7B"} \ No newline at end of file diff --git a/dist/server/websocket.js b/dist/server/websocket.js new file mode 100644 index 0000000..ee69ed1 --- /dev/null +++ b/dist/server/websocket.js @@ -0,0 +1,103 @@ +import { v4 as uuidv4 } from 'uuid'; +import { WebSocket, WebSocketServer } from 'ws'; +export class WebSocketServerTransport { + wss; + clients = new Map(); + onclose; + onerror; + messageHandler; + onconnection; + ondisconnection; + set onmessage(handler) { + this.messageHandler = handler + ? (msg, clientId) => { + // @ts-ignore + if (msg.id === undefined) { + console.log('Broadcast message:', msg); + return handler(msg); + } + // @ts-ignore + return handler({ + ...msg, + // @ts-ignore + id: clientId + ':' + msg.id, + }); + } + : undefined; + } + constructor({ path, server }) { + this.wss = new WebSocketServer({ + path, + server, + }); + } + async start() { + this.wss.on('connection', (ws) => { + const clientId = uuidv4(); + this.clients.set(clientId, ws); + this.onconnection?.(clientId); + ws.on('message', (data) => { + try { + const msg = JSON.parse(data.toString()); + this.messageHandler?.(msg, clientId); + } + catch (err) { + this.onerror?.(new Error(`Failed to parse message: ${err}`)); + } + }); + ws.on('close', () => { + this.clients.delete(clientId); + this.ondisconnection?.(clientId); + }); + ws.on('error', (err) => { + this.onerror?.(err); + }); + }); + } + async send(msg, options) { + // decide if they passed a raw clientId (legacy) or options object + const clientId = typeof options === 'string' ? options : undefined; + // if your protocol mangles IDs to include clientId, strip it off + const [cId, rawId] = clientId?.split(':') ?? []; + if (rawId !== undefined) { + // @ts-ignore + msg.id = parseInt(rawId, 10); + } + const payload = JSON.stringify(msg); + if (cId) { + // send only to the one client + const ws = this.clients.get(cId); + if (ws?.readyState === WebSocket.OPEN) { + ws.send(payload); + } + else { + this.clients.delete(cId); + this.ondisconnection?.(cId); + } + } + else { + // broadcast to everyone + for (const [id, ws] of this.clients) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(payload); + } + else { + this.clients.delete(id); + this.ondisconnection?.(id); + } + } + } + } + async broadcast(msg) { + return this.send(msg); + } + async close() { + return new Promise((resolve) => { + this.wss.close(() => { + this.clients.clear(); + resolve(); + }); + }); + } +} +//# sourceMappingURL=websocket.js.map \ No newline at end of file diff --git a/dist/server/websocket.js.map b/dist/server/websocket.js.map new file mode 100644 index 0000000..f7ca0d6 --- /dev/null +++ b/dist/server/websocket.js.map @@ -0,0 +1 @@ +{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/server/websocket.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/types.d.ts b/dist/types.d.ts new file mode 100644 index 0000000..9e1ea8f --- /dev/null +++ b/dist/types.d.ts @@ -0,0 +1,7 @@ +export interface Logger { + info: (...args: any[]) => void; + error: (...args: any[]) => void; + debug: (...args: any[]) => void; + warn: (...args: any[]) => void; +} +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/dist/types.d.ts.map b/dist/types.d.ts.map new file mode 100644 index 0000000..5345580 --- /dev/null +++ b/dist/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,MAAM;IACrB,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC9B,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,KAAK,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;IAC/B,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;CAC/B"} \ No newline at end of file diff --git a/dist/types.js b/dist/types.js new file mode 100644 index 0000000..718fd38 --- /dev/null +++ b/dist/types.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/dist/types.js.map b/dist/types.js.map new file mode 100644 index 0000000..c768b79 --- /dev/null +++ b/dist/types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/mcpconfig-example.json b/mcpconfig-example.json new file mode 100644 index 0000000..7ff4da0 --- /dev/null +++ b/mcpconfig-example.json @@ -0,0 +1,37 @@ +{ + "mcpServers": { + "filesystem": { + "name": "filesystem", + "type": "stdio", + "command": "npx", + "args": [ + "@modelcontextprotocol/server-filesystem", + "/tmp" + ] + }, + "brave_search": { + "name": "brave_search", + "type": "stdio", + "command": "npx", + "args": [ + "@modelcontextprotocol/server-brave-search" + ] + }, + "modern_remote_server": { + "name": "modern_remote_server", + "type": "stream", + "url": "http://localhost:8080/mcp" + }, + "legacy_remote_server": { + "name": "legacy_remote_server", + "type": "http", + "url": "http://localhost:9090/sse" + }, + "hub": { + "url": "https://mcp.xyz.com/sse", + "headers": { + "X-API-KEY": "XXXXXX" + } + } + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..323c470 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "@srbhptl39/mcp-superassistant-proxy", + "version": "0.1.8", + "description": "Run MCP SuperAssistant over SSE or visa versa", + "keywords": [ + "mcp", + "stdio", + "sse", + "http", + "websocket", + "stream", + "mcp-superassistant", + "gateway", + "proxy", + "bridge" + ], + "type": "module", + "bin": { + "@srbhptl39/mcp-superassistant-proxy": "dist/index.js" + }, + "scripts": { + "build": "tsc", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.15.1", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "express": "^4.21.2", + "uuid": "^11.1.0", + "ws": "^8.18.2", + "yargs": "^17.7.2", + "zod": "^3.24.4" + }, + "devDependencies": { + "@types/body-parser": "^1.19.5", + "prev-modelcontextprotocol-sdk": "npm:@modelcontextprotocol/sdk@1.4.0", + "@types/cors": "^2.8.18", + "@types/express": "^5.0.2", + "@types/node": "^22.15.18", + "@types/ws": "^8.18.1", + "@types/yargs": "^17.0.33", + "husky": "^9.1.7", + "lint-staged": "^16.0.0", + "prettier": "^3.5.3", + "ts-node": "^10.9.2", + "tsx": "^4.19.4", + "typescript": "^5.8.3" + } +} diff --git a/src/gateways/configToSse.ts b/src/gateways/configToSse.ts new file mode 100644 index 0000000..0895bff --- /dev/null +++ b/src/gateways/configToSse.ts @@ -0,0 +1,221 @@ +import express from 'express' +import bodyParser from 'body-parser' +import cors, { type CorsOptions } from 'cors' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js' +import { + JSONRPCMessage, + JSONRPCRequest, +} from '@modelcontextprotocol/sdk/types.js' +import { Logger } from '../types.js' +import { getVersion } from '../lib/getVersion.js' +import { onSignals } from '../lib/onSignals.js' +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js' +import { Config, loadConfig } from '../lib/config.js' +import { McpServerManager } from '../lib/mcpServerManager.js' + +export interface ConfigToSseArgs { + configPath: string + port: number + host: string + baseUrl: string + ssePath: string + messagePath: string + logger: Logger + corsOrigin: CorsOptions['origin'] + healthEndpoints: string[] + headers: Record +} + +const setResponseHeaders = ({ + res, + headers, +}: { + res: express.Response + headers: Record +}) => + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value) + }) + +export async function configToSse(args: ConfigToSseArgs) { + const { + configPath, + port, + host, + baseUrl, + ssePath, + messagePath, + logger, + corsOrigin, + healthEndpoints, + headers, + } = args + + logger.info(` - config: ${configPath}`) + logger.info( + ` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info(` - host: ${host}`) + logger.info(` - port: ${port}`) + if (baseUrl) { + logger.info(` - baseUrl: ${baseUrl}`) + } + logger.info(` - ssePath: ${ssePath}`) + logger.info(` - messagePath: ${messagePath}`) + logger.info( + ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`, + ) + logger.info( + ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`, + ) + + const serverManager = new McpServerManager(logger) + + const cleanup = async () => { + await serverManager.cleanup() + } + + onSignals({ logger, cleanup }) + + let config: Config + try { + config = loadConfig(configPath) + logger.info( + `Loaded config with ${Object.keys(config.mcpServers).length} servers`, + ) + } catch (err) { + logger.error(`Failed to load config: ${err}`) + process.exit(1) + } + + for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) { + try { + await serverManager.addServer(serverName, serverConfig) + logger.info(`Successfully initialized server: ${serverName}`) + } catch (err) { + logger.error(`Failed to initialize server ${serverName}: ${err}`) + process.exit(1) + } + } + + const server = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + + const sessions: Record< + string, + { transport: SSEServerTransport; response: express.Response } + > = {} + + const app = express() + + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })) + } + + app.use((req, res, next) => { + if (req.path === messagePath) return next() + return bodyParser.json()(req, res, next) + }) + + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }) + res.send('ok') + }) + } + + app.get(ssePath, async (req, res) => { + logger.info(`New SSE connection from ${req.ip}`) + + setResponseHeaders({ + res, + headers, + }) + + 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 = async (msg: JSONRPCMessage) => { + logger.info( + `SSE → Servers (session ${sessionId}): ${JSON.stringify(msg)}`, + ) + + if ('method' in msg && 'id' in msg) { + try { + const response = await serverManager.handleRequest( + msg as JSONRPCRequest, + ) + logger.info(`Servers → SSE (session ${sessionId}):`) + logger.debug(`Servers → SSE (session ${sessionId}):`, response) + sseTransport.send(response) + } catch (err) { + logger.error(`Error handling request in session ${sessionId}:`, err) + const errorResponse = { + jsonrpc: '2.0' as const, + id: (msg as JSONRPCRequest).id, + error: { + code: -32000, + message: 'Internal error', + }, + } + sseTransport.send(errorResponse) + } + } + } + + sseTransport.onclose = () => { + logger.info(`SSE connection closed (session ${sessionId})`) + delete sessions[sessionId] + } + + sseTransport.onerror = (err) => { + logger.error(`SSE error (session ${sessionId}):`, err) + delete sessions[sessionId] + } + + req.on('close', () => { + logger.info(`Client disconnected (session ${sessionId})`) + delete sessions[sessionId] + }) + }) + + app.post(messagePath, async (req: any, res: any) => { + const sessionId = req.query.sessionId as string + + setResponseHeaders({ + res, + headers, + }) + + if (!sessionId) { + return res.status(400).send('Missing sessionId parameter') + } + + const session = sessions[sessionId] + if (session?.transport?.handlePostMessage) { + logger.info(`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, host, () => { + logger.info(`Listening on ${host}:${port}`) + logger.info(`SSE endpoint: http://${host}:${port}${ssePath}`) + logger.info(`POST messages: http://${host}:${port}${messagePath}`) + }) + + logger.info('Config-to-SSE gateway ready') +} diff --git a/src/gateways/configToStreamableHttp.ts b/src/gateways/configToStreamableHttp.ts new file mode 100644 index 0000000..e1ff966 --- /dev/null +++ b/src/gateways/configToStreamableHttp.ts @@ -0,0 +1,408 @@ +import express from 'express' +import cors, { type CorsOptions } from 'cors' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import { + JSONRPCMessage, + JSONRPCRequest, + isInitializeRequest, +} from '@modelcontextprotocol/sdk/types.js' +import { Logger } from '../types.js' +import { getVersion } from '../lib/getVersion.js' +import { onSignals } from '../lib/onSignals.js' +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js' +import { Config, loadConfig } from '../lib/config.js' +import { McpServerManager } from '../lib/mcpServerManager.js' +import { randomUUID } from 'node:crypto' +import { SessionAccessCounter } from '../lib/sessionAccessCounter.js' + +export interface ConfigToStreamableHttpArgs { + configPath: string + port: number + host: string + streamableHttpPath: string + logger: Logger + corsOrigin: CorsOptions['origin'] + healthEndpoints: string[] + headers: Record + stateless?: boolean + sessionTimeout?: number | null +} + +const setResponseHeaders = ({ + res, + headers, +}: { + res: express.Response + headers: Record +}) => + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value) + }) + +export async function configToStreamableHttp(args: ConfigToStreamableHttpArgs) { + const { + configPath, + port, + host, + streamableHttpPath, + logger, + corsOrigin, + healthEndpoints, + headers, + stateless = false, + sessionTimeout, + } = args + + logger.info(` - config: ${configPath}`) + logger.info( + ` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info(` - host: ${host}`) + logger.info(` - port: ${port}`) + logger.info(` - streamableHttpPath: ${streamableHttpPath}`) + logger.info(` - stateless: ${stateless}`) + logger.info( + ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`, + ) + logger.info( + ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`, + ) + + if (!stateless && sessionTimeout) { + logger.info(` - Session timeout: ${sessionTimeout}ms`) + } + + const serverManager = new McpServerManager(logger) + + const cleanup = async () => { + await serverManager.cleanup() + } + + onSignals({ logger, cleanup }) + + let config: Config + try { + config = loadConfig(configPath) + logger.info( + `Loaded config with ${Object.keys(config.mcpServers).length} servers`, + ) + } catch (err) { + logger.error(`Failed to load config: ${err}`) + process.exit(1) + } + + for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) { + try { + await serverManager.addServer(serverName, serverConfig) + logger.info(`Successfully initialized server: ${serverName}`) + } catch (err) { + logger.error(`Failed to initialize server ${serverName}: ${err}`) + process.exit(1) + } + } + + const app = express() + app.use(express.json()) + + if (corsOrigin) { + app.use( + cors({ + origin: corsOrigin, + exposedHeaders: stateless ? [] : ['Mcp-Session-Id'], + }), + ) + } + + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }) + res.send('ok') + }) + } + + if (stateless) { + // Stateless mode - create new transport for each request + app.post(streamableHttpPath, async (req, res) => { + logger.info('Received stateless StreamableHttp request') + + setResponseHeaders({ + res, + headers, + }) + + try { + const server = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }) + + await server.connect(transport) + + transport.onmessage = async (msg: JSONRPCMessage) => { + logger.info(`StreamableHttp → Servers: ${JSON.stringify(msg)}`) + + if ('method' in msg && 'id' in msg) { + try { + const response = await serverManager.handleRequest( + msg as JSONRPCRequest, + ) + logger.info('Servers → StreamableHttp:') + logger.debug('Servers → StreamableHttp:', response) + transport.send(response) + } catch (err) { + logger.error('Error handling request:', err) + const errorResponse = { + jsonrpc: '2.0' as const, + id: (msg as JSONRPCRequest).id, + error: { + code: -32000, + message: 'Internal error', + }, + } + transport.send(errorResponse) + } + } + } + + transport.onclose = () => { + logger.info('StreamableHttp connection closed') + } + + transport.onerror = (err) => { + logger.error('StreamableHttp error:', err) + } + + await transport.handleRequest(req, res, req.body) + } catch (error) { + logger.error('Error handling MCP request:', error) + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }) + } + } + }) + + // Stateless mode doesn't support GET/DELETE + app.get(streamableHttpPath, async (req, res) => { + setResponseHeaders({ res, headers }) + res.status(405).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed in stateless mode', + }, + id: null, + }) + }) + + app.delete(streamableHttpPath, async (req, res) => { + setResponseHeaders({ res, headers }) + res.status(405).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed in stateless mode', + }, + id: null, + }) + }) + } else { + // Stateful mode - maintain sessions + const transports: { [sessionId: string]: StreamableHTTPServerTransport } = + {} + const sessionCounter = sessionTimeout + ? new SessionAccessCounter( + sessionTimeout, + (sessionId: string) => { + logger.info(`Session ${sessionId} timed out, cleaning up`) + const transport = transports[sessionId] + if (transport) { + transport.close() + } + delete transports[sessionId] + }, + logger, + ) + : null + + app.post(streamableHttpPath, async (req, res) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined + let transport: StreamableHTTPServerTransport + + setResponseHeaders({ + res, + headers, + }) + + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId] + sessionCounter?.inc(sessionId, 'POST request for existing session') + } else if (!sessionId && isInitializeRequest(req.body)) { + // New initialization request + const server = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sessionId) => { + transports[sessionId] = transport + sessionCounter?.inc(sessionId, 'session initialization') + }, + }) + await server.connect(transport) + + transport.onmessage = async (msg: JSONRPCMessage) => { + logger.info( + `StreamableHttp → Servers (session ${sessionId}): ${JSON.stringify(msg)}`, + ) + + if ('method' in msg && 'id' in msg) { + try { + const response = await serverManager.handleRequest( + msg as JSONRPCRequest, + ) + logger.info(`Servers → StreamableHttp (session ${sessionId}):`) + logger.debug( + `Servers → StreamableHttp (session ${sessionId}):`, + response, + ) + transport.send(response) + } catch (err) { + logger.error( + `Error handling request in session ${sessionId}:`, + err, + ) + const errorResponse = { + jsonrpc: '2.0' as const, + id: (msg as JSONRPCRequest).id, + error: { + code: -32000, + message: 'Internal error', + }, + } + transport.send(errorResponse) + } + } + } + + transport.onclose = () => { + logger.info(`StreamableHttp connection closed (session ${sessionId})`) + if (transport.sessionId) { + sessionCounter?.clear( + transport.sessionId, + false, + 'transport being closed', + ) + delete transports[transport.sessionId] + } + } + + transport.onerror = (err) => { + logger.error(`StreamableHttp error (session ${sessionId}):`, err) + if (transport.sessionId) { + sessionCounter?.clear( + transport.sessionId, + false, + 'transport emitting error', + ) + delete transports[transport.sessionId] + } + } + } else { + // Invalid request + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Bad Request: No valid session ID provided', + }, + id: null, + }) + return + } + + // Decrement session access count when response ends + let responseEnded = false + const handleResponseEnd = (event: string) => { + if (!responseEnded && transport.sessionId) { + responseEnded = true + logger.info(`Response ${event}`, transport.sessionId) + sessionCounter?.dec(transport.sessionId, `POST response ${event}`) + } + } + + res.on('finish', () => handleResponseEnd('finished')) + res.on('close', () => handleResponseEnd('closed')) + + await transport.handleRequest(req, res, req.body) + }) + + // Reusable handler for GET and DELETE requests in stateful mode + const handleSessionRequest = async ( + req: express.Request, + res: express.Response, + ) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined + + setResponseHeaders({ + res, + headers, + }) + + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID') + return + } + + sessionCounter?.inc( + sessionId, + `${req.method} request for existing session`, + ) + + let responseEnded = false + const handleResponseEnd = (event: string) => { + if (!responseEnded) { + responseEnded = true + logger.info(`Response ${event}`, sessionId) + sessionCounter?.dec(sessionId, `${req.method} response ${event}`) + } + } + + res.on('finish', () => handleResponseEnd('finished')) + res.on('close', () => handleResponseEnd('closed')) + + const transport = transports[sessionId] + await transport.handleRequest(req, res) + } + + app.get(streamableHttpPath, handleSessionRequest) + app.delete(streamableHttpPath, handleSessionRequest) + } + + app.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`) + logger.info( + `StreamableHttp endpoint: http://${host}:${port}${streamableHttpPath}`, + ) + logger.info(`Mode: ${stateless ? 'stateless' : 'stateful'}`) + }) + + logger.info('Config-to-StreamableHttp gateway ready') +} diff --git a/src/gateways/configToWs.ts b/src/gateways/configToWs.ts new file mode 100644 index 0000000..94cff76 --- /dev/null +++ b/src/gateways/configToWs.ts @@ -0,0 +1,213 @@ +import express from 'express' +import cors, { type CorsOptions } from 'cors' +import { createServer } from 'http' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { + JSONRPCMessage, + JSONRPCRequest, +} from '@modelcontextprotocol/sdk/types.js' +import { Logger } from '../types.js' +import { getVersion } from '../lib/getVersion.js' +import { WebSocketServerTransport } from '../server/websocket.js' +import { onSignals } from '../lib/onSignals.js' +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js' +import { Config, loadConfig } from '../lib/config.js' +import { McpServerManager } from '../lib/mcpServerManager.js' + +export interface ConfigToWsArgs { + configPath: string + port: number + host: string + messagePath: string + logger: Logger + corsOrigin: CorsOptions['origin'] + healthEndpoints: string[] + headers: Record +} + +const setResponseHeaders = ({ + res, + headers, +}: { + res: express.Response + headers: Record +}) => + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value) + }) + +export async function configToWs(args: ConfigToWsArgs) { + const { + configPath, + port, + host, + messagePath, + logger, + corsOrigin, + healthEndpoints, + headers, + } = args + + logger.info(` - config: ${configPath}`) + logger.info( + ` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info(` - host: ${host}`) + logger.info(` - port: ${port}`) + logger.info(` - messagePath: ${messagePath}`) + logger.info( + ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`, + ) + logger.info( + ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`, + ) + + const serverManager = new McpServerManager(logger) + let wsTransport: WebSocketServerTransport | null = null + let isReady = false + + const cleanup = async () => { + await serverManager.cleanup() + if (wsTransport) { + wsTransport.close().catch((err) => { + logger.error(`Error stopping WebSocket server: ${err.message}`) + }) + } + } + + onSignals({ logger, cleanup }) + + let config: Config + try { + config = loadConfig(configPath) + logger.info( + `Loaded config with ${Object.keys(config.mcpServers).length} servers`, + ) + } catch (err) { + logger.error(`Failed to load config: ${err}`) + process.exit(1) + } + + for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) { + try { + await serverManager.addServer(serverName, serverConfig) + logger.info(`Successfully initialized server: ${serverName}`) + } catch (err) { + logger.error(`Failed to initialize server ${serverName}: ${err}`) + process.exit(1) + } + } + + try { + const server = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + + const app = express() + + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })) + } + + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }) + if (!isReady) { + res.status(500).send('Server is not ready') + } else { + res.send('ok') + } + }) + } + + const httpServer = createServer(app) + + wsTransport = new WebSocketServerTransport({ + path: messagePath, + server: httpServer, + }) + + await server.connect(wsTransport) + + wsTransport.onmessage = async (message: JSONRPCMessage) => { + // Extract client ID from the modified message ID + const messageId = (message as any).id + let clientId: string | undefined + let originalId: string | number | undefined + + if (typeof messageId === 'string' && messageId.includes(':')) { + const parts = messageId.split(':') + clientId = parts[0] + originalId = parts.slice(1).join(':') + // Restore original ID for the request + ;(message as any).id = isNaN(Number(originalId)) + ? originalId + : Number(originalId) + } + + const isRequest = 'method' in message && 'id' in message + if (isRequest) { + logger.info(`WebSocket → Servers (client ${clientId}):`, message) + + try { + const response = await serverManager.handleRequest( + message as JSONRPCRequest, + ) + logger.info(`Servers → WebSocket (client ${clientId}):`) + logger.debug(`Servers → WebSocket (client ${clientId}):`, response) + + await wsTransport!.send(response, clientId) + } catch (err) { + logger.error(`Error handling request from client ${clientId}:`, err) + const errorResponse = { + jsonrpc: '2.0' as const, + id: (message as JSONRPCRequest).id, + error: { + code: -32000, + message: 'Internal error', + }, + } + try { + await wsTransport!.send(errorResponse, clientId) + } catch (sendErr) { + logger.error( + `Failed to send error response to client ${clientId}:`, + sendErr, + ) + } + } + } else { + logger.info(`Notification from client ${clientId}:`, message) + } + } + + wsTransport.onconnection = (clientId: string) => { + logger.info(`New WebSocket connection: ${clientId}`) + } + + wsTransport.ondisconnection = (clientId: string) => { + logger.info(`WebSocket connection closed: ${clientId}`) + } + + wsTransport.onerror = (err: Error) => { + logger.error(`WebSocket error: ${err.message}`) + } + + isReady = true + + httpServer.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`) + logger.info(`WebSocket endpoint: ws://${host}:${port}${messagePath}`) + }) + + logger.info('Config-to-WebSocket gateway ready') + } catch (err: any) { + logger.error(`Failed to start: ${err.message}`) + cleanup() + process.exit(1) + } +} diff --git a/src/gateways/sseToSse.ts b/src/gateways/sseToSse.ts new file mode 100644 index 0000000..b6e6418 --- /dev/null +++ b/src/gateways/sseToSse.ts @@ -0,0 +1,347 @@ +import express from 'express' +import bodyParser from 'body-parser' +import cors, { type CorsOptions } from 'cors' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js' +import type { + JSONRPCMessage, + JSONRPCRequest, + ClientCapabilities, + Implementation, +} from '@modelcontextprotocol/sdk/types.js' +import { z } from 'zod' +import { getVersion } from '../lib/getVersion.js' +import { Logger } from '../types.js' +import { onSignals } from '../lib/onSignals.js' +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js' + +export interface SseToSseArgs { + inputSseUrl: string + port: number + host: string + baseUrl: string + ssePath: string + messagePath: string + logger: Logger + corsOrigin: CorsOptions['origin'] + healthEndpoints: string[] + headers: Record +} + +let sseClient: Client | undefined + +const newInitializeSseClient = ({ message }: { message: JSONRPCRequest }) => { + const clientInfo = message.params?.clientInfo as Implementation | undefined + const clientCapabilities = message.params?.capabilities as + | ClientCapabilities + | undefined + + return new Client( + { + name: clientInfo?.name ?? 'mcp-superassistant-proxy', + version: clientInfo?.version ?? getVersion(), + }, + { + capabilities: clientCapabilities ?? {}, + }, + ) +} + +const newFallbackSseClient = async ({ + sseTransport, +}: { + sseTransport: SSEClientTransport +}) => { + const fallbackSseClient = new Client( + { + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, + { + capabilities: {}, + }, + ) + + await fallbackSseClient.connect(sseTransport) + return fallbackSseClient +} + +const setResponseHeaders = ({ + res, + headers, +}: { + res: express.Response + headers: Record +}) => + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value) + }) + +export async function sseToSse(args: SseToSseArgs) { + const { + inputSseUrl, + port, + host, + baseUrl, + ssePath, + messagePath, + logger, + corsOrigin, + healthEndpoints, + headers, + } = args + + logger.info(` - input SSE: ${inputSseUrl}`) + logger.info( + ` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info(` - host: ${host}`) + logger.info(` - port: ${port}`) + if (baseUrl) { + logger.info(` - baseUrl: ${baseUrl}`) + } + logger.info(` - ssePath: ${ssePath}`) + logger.info(` - messagePath: ${messagePath}`) + logger.info( + ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`, + ) + logger.info( + ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`, + ) + + onSignals({ logger }) + + const inputSseTransport = new SSEClientTransport(new URL(inputSseUrl), { + eventSourceInit: { + fetch: (...props: Parameters) => { + const [url, init = {}] = props + return fetch(url, { ...init, headers: { ...init.headers, ...headers } }) + }, + }, + requestInit: { + headers, + }, + }) + + inputSseTransport.onerror = (err) => { + logger.error('Input SSE error:', err) + } + + inputSseTransport.onclose = () => { + logger.error('Input SSE connection closed') + process.exit(1) + } + + const outputServer = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + + const sessions: Record< + string, + { transport: SSEServerTransport; response: express.Response } + > = {} + + const app = express() + + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })) + } + + app.use((req, res, next) => { + if (req.path === messagePath) return next() + return bodyParser.json()(req, res, next) + }) + + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }) + res.send('ok') + }) + } + + app.get(ssePath, async (req, res) => { + logger.info(`New SSE connection from ${req.ip}`) + + setResponseHeaders({ + res, + headers, + }) + + const outputSseTransport = new SSEServerTransport( + `${baseUrl}${messagePath}`, + res, + ) + await outputServer.connect(outputSseTransport) + + const sessionId = outputSseTransport.sessionId + if (sessionId) { + sessions[sessionId] = { transport: outputSseTransport, response: res } + } + + const wrapResponse = (req: JSONRPCRequest, payload: object) => ({ + jsonrpc: req.jsonrpc || '2.0', + id: req.id, + ...payload, + }) + + outputSseTransport.onmessage = async (message: JSONRPCMessage) => { + const isRequest = 'method' in message && 'id' in message + if (isRequest) { + logger.info(`Output SSE → Input SSE (session ${sessionId}):`, message) + const req = message as JSONRPCRequest + let result + + try { + if (!sseClient) { + if (message.method === 'initialize') { + sseClient = newInitializeSseClient({ + message, + }) + + const originalRequest = sseClient.request + + sseClient.request = async function (requestMessage, ...restArgs) { + if ( + requestMessage.method === 'initialize' && + message.params?.protocolVersion && + requestMessage.params?.protocolVersion + ) { + requestMessage.params.protocolVersion = + message.params.protocolVersion + } + + result = await originalRequest.apply(this, [ + requestMessage, + ...restArgs, + ]) + + return result + } + + await sseClient.connect(inputSseTransport) + sseClient.request = originalRequest + } else { + logger.info( + 'SSE client not initialized, creating fallback client', + ) + sseClient = await newFallbackSseClient({ + sseTransport: inputSseTransport, + }) + } + + logger.info('Input SSE connected') + } else { + result = await sseClient.request(req, z.any()) + } + } catch (err) { + logger.error('Request error:', err) + const errorCode = + err && typeof err === 'object' && 'code' in err + ? (err as any).code + : -32000 + let errorMsg = + err && typeof err === 'object' && 'message' in err + ? (err as any).message + : 'Internal error' + const prefix = `MCP error ${errorCode}:` + if (errorMsg.startsWith(prefix)) { + errorMsg = errorMsg.slice(prefix.length).trim() + } + const errorResp = wrapResponse(req, { + error: { + code: errorCode, + message: errorMsg, + }, + }) + try { + outputSseTransport.send(errorResp as any) + } catch (sendErr) { + logger.error( + `Failed to send error response to session ${sessionId}:`, + sendErr, + ) + delete sessions[sessionId] + } + return + } + const response = wrapResponse( + req, + result.hasOwnProperty('error') + ? { error: { ...result.error } } + : { result: { ...result } }, + ) + logger.info(`Response (session ${sessionId}):`, response) + try { + outputSseTransport.send(response as any) + } catch (sendErr) { + logger.error( + `Failed to send response to session ${sessionId}:`, + sendErr, + ) + delete sessions[sessionId] + } + } else { + logger.info(`Input SSE → Output SSE (session ${sessionId}):`, message) + try { + outputSseTransport.send(message) + } catch (sendErr) { + logger.error( + `Failed to send message to session ${sessionId}:`, + sendErr, + ) + delete sessions[sessionId] + } + } + } + + outputSseTransport.onclose = () => { + logger.info(`Output SSE connection closed (session ${sessionId})`) + delete sessions[sessionId] + } + + outputSseTransport.onerror = (err) => { + logger.error(`Output SSE error (session ${sessionId}):`, err) + delete sessions[sessionId] + } + + req.on('close', () => { + logger.info(`Client disconnected (session ${sessionId})`) + delete sessions[sessionId] + }) + }) + + app.post(messagePath, async (req: any, res: any) => { + const sessionId = req.query.sessionId as string + + setResponseHeaders({ + res, + headers, + }) + + if (!sessionId) { + return res.status(400).send('Missing sessionId parameter') + } + + const session = sessions[sessionId] + if (session?.transport?.handlePostMessage) { + logger.info(`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, host, () => { + logger.info(`Listening on ${host}:${port}`) + logger.info(`SSE endpoint: http://${host}:${port}${ssePath}`) + logger.info(`POST messages: http://${host}:${port}${messagePath}`) + }) + + logger.info('SSE-to-SSE gateway ready') +} diff --git a/src/gateways/sseToStdio.ts b/src/gateways/sseToStdio.ts new file mode 100644 index 0000000..3439fcd --- /dev/null +++ b/src/gateways/sseToStdio.ts @@ -0,0 +1,195 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import type { + JSONRPCMessage, + JSONRPCRequest, + ClientCapabilities, + Implementation, +} from '@modelcontextprotocol/sdk/types.js' +import { z } from 'zod' +import { getVersion } from '../lib/getVersion.js' +import { Logger } from '../types.js' +import { onSignals } from '../lib/onSignals.js' + +export interface SseToStdioArgs { + sseUrl: string + logger: Logger + headers: Record +} + +let sseClient: Client | undefined + +const newInitializeSseClient = ({ message }: { message: JSONRPCRequest }) => { + const clientInfo = message.params?.clientInfo as Implementation | undefined + const clientCapabilities = message.params?.capabilities as + | ClientCapabilities + | undefined + + return new Client( + { + name: clientInfo?.name ?? 'mcp-superassistant-proxy', + version: clientInfo?.version ?? getVersion(), + }, + { + capabilities: clientCapabilities ?? {}, + }, + ) +} + +const newFallbackSseClient = async ({ + sseTransport, +}: { + sseTransport: SSEClientTransport +}) => { + const fallbackSseClient = new Client( + { + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, + { + capabilities: {}, + }, + ) + + await fallbackSseClient.connect(sseTransport) + return fallbackSseClient +} + +export async function sseToStdio(args: SseToStdioArgs) { + const { sseUrl, logger, headers } = args + + logger.info(` - sse: ${sseUrl}`) + logger.info( + ` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info('Connecting to SSE...') + + onSignals({ logger }) + + const sseTransport = new SSEClientTransport(new URL(sseUrl), { + eventSourceInit: { + fetch: (...props: Parameters) => { + const [url, init = {}] = props + return fetch(url, { ...init, headers: { ...init.headers, ...headers } }) + }, + }, + requestInit: { + headers, + }, + }) + + sseTransport.onerror = (err) => { + logger.error('SSE error:', err) + } + + sseTransport.onclose = () => { + logger.error('SSE connection closed') + process.exit(1) + } + + const stdioServer = new Server( + { + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, + { + capabilities: {}, + }, + ) + + const stdioTransport = new StdioServerTransport() + await stdioServer.connect(stdioTransport) + + const wrapResponse = (req: JSONRPCRequest, payload: object) => ({ + jsonrpc: req.jsonrpc || '2.0', + id: req.id, + ...payload, + }) + + stdioServer.transport!.onmessage = async (message: JSONRPCMessage) => { + const isRequest = 'method' in message && 'id' in message + if (isRequest) { + logger.info('Stdio → SSE:', message) + const req = message as JSONRPCRequest + let result + + try { + if (!sseClient) { + if (message.method === 'initialize') { + sseClient = newInitializeSseClient({ + message, + }) + + const originalRequest = sseClient.request + + sseClient.request = async function (requestMessage, ...restArgs) { + // pass protocol version from original client + if ( + requestMessage.method === 'initialize' && + message.params?.protocolVersion && + requestMessage.params?.protocolVersion + ) { + requestMessage.params.protocolVersion = + message.params.protocolVersion + } + + result = await originalRequest.apply(this, [ + requestMessage, + ...restArgs, + ]) + + return result + } + + await sseClient.connect(sseTransport) + sseClient.request = originalRequest + } else { + logger.info('SSE client not initialized, creating fallback client') + sseClient = await newFallbackSseClient({ sseTransport }) + } + + logger.info('SSE connected') + } else { + result = await sseClient.request(req, z.any()) + } + } catch (err) { + logger.error('Request error:', err) + const errorCode = + err && typeof err === 'object' && 'code' in err + ? (err as any).code + : -32000 + let errorMsg = + err && typeof err === 'object' && 'message' in err + ? (err as any).message + : 'Internal error' + 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 } }, + ) + logger.info('Response:', response) + process.stdout.write(JSON.stringify(response) + '\n') + } else { + logger.info('SSE → Stdio:', message) + process.stdout.write(JSON.stringify(message) + '\n') + } + } + + logger.info('Stdio server listening') +} diff --git a/src/gateways/sseToWs.ts b/src/gateways/sseToWs.ts new file mode 100644 index 0000000..37ac583 --- /dev/null +++ b/src/gateways/sseToWs.ts @@ -0,0 +1,314 @@ +import express from 'express' +import cors, { type CorsOptions } from 'cors' +import { createServer } from 'http' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import type { + JSONRPCMessage, + JSONRPCRequest, + ClientCapabilities, + Implementation, +} from '@modelcontextprotocol/sdk/types.js' +import { z } from 'zod' +import { getVersion } from '../lib/getVersion.js' +import { Logger } from '../types.js' +import { WebSocketServerTransport } from '../server/websocket.js' +import { onSignals } from '../lib/onSignals.js' +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js' + +export interface SseToWsArgs { + inputSseUrl: string + port: number + host: string + messagePath: string + logger: Logger + corsOrigin: CorsOptions['origin'] + healthEndpoints: string[] + headers: Record +} + +let sseClient: Client | undefined + +const newInitializeSseClient = ({ message }: { message: JSONRPCRequest }) => { + const clientInfo = message.params?.clientInfo as Implementation | undefined + const clientCapabilities = message.params?.capabilities as + | ClientCapabilities + | undefined + + return new Client( + { + name: clientInfo?.name ?? 'mcp-superassistant-proxy', + version: clientInfo?.version ?? getVersion(), + }, + { + capabilities: clientCapabilities ?? {}, + }, + ) +} + +const newFallbackSseClient = async ({ + sseTransport, +}: { + sseTransport: SSEClientTransport +}) => { + const fallbackSseClient = new Client( + { + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, + { + capabilities: {}, + }, + ) + + await fallbackSseClient.connect(sseTransport) + return fallbackSseClient +} + +export async function sseToWs(args: SseToWsArgs) { + const { + inputSseUrl, + port, + host, + messagePath, + logger, + corsOrigin, + healthEndpoints, + headers, + } = args + + logger.info(` - input SSE: ${inputSseUrl}`) + logger.info( + ` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info(` - host: ${host}`) + logger.info(` - port: ${port}`) + logger.info(` - messagePath: ${messagePath}`) + logger.info( + ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`, + ) + logger.info( + ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`, + ) + + let wsTransport: WebSocketServerTransport | null = null + let isReady = false + + const cleanup = () => { + if (wsTransport) { + wsTransport.close().catch((err) => { + logger.error(`Error stopping WebSocket server: ${err.message}`) + }) + } + } + + onSignals({ + logger, + cleanup, + }) + + const inputSseTransport = new SSEClientTransport(new URL(inputSseUrl), { + eventSourceInit: { + fetch: (...props: Parameters) => { + const [url, init = {}] = props + return fetch(url, { ...init, headers: { ...init.headers, ...headers } }) + }, + }, + requestInit: { + headers, + }, + }) + + inputSseTransport.onerror = (err) => { + logger.error('Input SSE error:', err) + } + + inputSseTransport.onclose = () => { + logger.error('Input SSE connection closed') + cleanup() + process.exit(1) + } + + try { + const outputServer = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + + const app = express() + + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })) + } + + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + if (!isReady) { + res.status(500).send('Server is not ready') + } else { + res.send('ok') + } + }) + } + + const httpServer = createServer(app) + + wsTransport = new WebSocketServerTransport({ + path: messagePath, + server: httpServer, + }) + + await outputServer.connect(wsTransport) + + const wrapResponse = (req: JSONRPCRequest, payload: object) => ({ + jsonrpc: req.jsonrpc || '2.0', + id: req.id, + ...payload, + }) + + wsTransport.onmessage = async (message: JSONRPCMessage) => { + // Extract client ID from the modified message ID + const messageId = (message as any).id + let clientId: string | undefined + let originalId: string | number | undefined + + if (typeof messageId === 'string' && messageId.includes(':')) { + const parts = messageId.split(':') + clientId = parts[0] + originalId = parts.slice(1).join(':') + // Restore original ID for the request + ;(message as any).id = isNaN(Number(originalId)) + ? originalId + : Number(originalId) + } + + const isRequest = 'method' in message && 'id' in message + if (isRequest) { + logger.info(`WebSocket → SSE (client ${clientId}):`, message) + const req = message as JSONRPCRequest + let result + + try { + if (!sseClient) { + if (message.method === 'initialize') { + sseClient = newInitializeSseClient({ + message, + }) + + const originalRequest = sseClient.request + + sseClient.request = async function (requestMessage, ...restArgs) { + if ( + requestMessage.method === 'initialize' && + message.params?.protocolVersion && + requestMessage.params?.protocolVersion + ) { + requestMessage.params.protocolVersion = + message.params.protocolVersion + } + + result = await originalRequest.apply(this, [ + requestMessage, + ...restArgs, + ]) + + return result + } + + await sseClient.connect(inputSseTransport) + sseClient.request = originalRequest + } else { + logger.info( + 'SSE client not initialized, creating fallback client', + ) + sseClient = await newFallbackSseClient({ + sseTransport: inputSseTransport, + }) + } + + logger.info('Input SSE connected') + } else { + result = await sseClient.request(req, z.any()) + } + } catch (err) { + logger.error('Request error:', err) + const errorCode = + err && typeof err === 'object' && 'code' in err + ? (err as any).code + : -32000 + let errorMsg = + err && typeof err === 'object' && 'message' in err + ? (err as any).message + : 'Internal error' + const prefix = `MCP error ${errorCode}:` + if (errorMsg.startsWith(prefix)) { + errorMsg = errorMsg.slice(prefix.length).trim() + } + const errorResp = wrapResponse(req, { + error: { + code: errorCode, + message: errorMsg, + }, + }) + try { + await wsTransport!.send(errorResp as any, clientId) + } catch (sendErr) { + logger.error( + `Failed to send error response to client ${clientId}:`, + sendErr, + ) + } + return + } + const response = wrapResponse( + req, + result.hasOwnProperty('error') + ? { error: { ...result.error } } + : { result: { ...result } }, + ) + logger.info(`Response (client ${clientId}):`, response) + try { + await wsTransport!.send(response as any, clientId) + } catch (sendErr) { + logger.error( + `Failed to send response to client ${clientId}:`, + sendErr, + ) + } + } else { + logger.info(`SSE → WebSocket (client ${clientId}):`, message) + try { + await wsTransport!.send(message, clientId) + } catch (sendErr) { + logger.error(`Failed to send message to client ${clientId}:`, sendErr) + } + } + } + + wsTransport.onconnection = (clientId: string) => { + logger.info(`New WebSocket connection: ${clientId}`) + } + + wsTransport.ondisconnection = (clientId: string) => { + logger.info(`WebSocket connection closed: ${clientId}`) + } + + wsTransport.onerror = (err: Error) => { + logger.error(`WebSocket error: ${err.message}`) + } + + isReady = true + + httpServer.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`) + logger.info(`WebSocket endpoint: ws://${host}:${port}${messagePath}`) + }) + + logger.info('SSE-to-WebSocket gateway ready') + } catch (err: any) { + logger.error(`Failed to start: ${err.message}`) + cleanup() + process.exit(1) + } +} diff --git a/src/gateways/stdioToSse.ts b/src/gateways/stdioToSse.ts new file mode 100644 index 0000000..919f611 --- /dev/null +++ b/src/gateways/stdioToSse.ts @@ -0,0 +1,200 @@ +import express from 'express' +import bodyParser from 'body-parser' +import cors, { type CorsOptions } from 'cors' +import { spawn, ChildProcessWithoutNullStreams } from 'child_process' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js' +import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js' +import { Logger } from '../types.js' +import { getVersion } from '../lib/getVersion.js' +import { onSignals } from '../lib/onSignals.js' +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js' + +export interface StdioToSseArgs { + stdioCmd: string + port: number + host: string + baseUrl: string + ssePath: string + messagePath: string + logger: Logger + corsOrigin: CorsOptions['origin'] + healthEndpoints: string[] + headers: Record +} + +const setResponseHeaders = ({ + res, + headers, +}: { + res: express.Response + headers: Record +}) => + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value) + }) + +export async function stdioToSse(args: StdioToSseArgs) { + const { + stdioCmd, + port, + host, + baseUrl, + ssePath, + messagePath, + logger, + corsOrigin, + healthEndpoints, + headers, + } = args + + logger.info( + ` - Headers: ${Object(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info(` - port: ${port}`) + logger.info(` - stdio: ${stdioCmd}`) + if (baseUrl) { + logger.info(` - baseUrl: ${baseUrl}`) + } + logger.info(` - ssePath: ${ssePath}`) + logger.info(` - messagePath: ${messagePath}`) + + logger.info( + ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`, + ) + logger.info( + ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`, + ) + + onSignals({ logger }) + + const child: ChildProcessWithoutNullStreams = spawn(stdioCmd, { shell: true }) + child.on('exit', (code, signal) => { + logger.error(`Child exited: code=${code}, signal=${signal}`) + process.exit(code ?? 1) + }) + + const server = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + + const sessions: Record< + string, + { transport: SSEServerTransport; response: express.Response } + > = {} + + const app = express() + + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })) + } + + app.use((req, res, next) => { + if (req.path === messagePath) return next() + return bodyParser.json()(req, res, next) + }) + + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }) + res.send('ok') + }) + } + + app.get(ssePath, async (req, res) => { + logger.info(`New SSE connection from ${req.ip}`) + + setResponseHeaders({ + res, + headers, + }) + + 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: JSONRPCMessage) => { + logger.info(`SSE → Child (session ${sessionId}): ${JSON.stringify(msg)}`) + child.stdin.write(JSON.stringify(msg) + '\n') + } + + sseTransport.onclose = () => { + logger.info(`SSE connection closed (session ${sessionId})`) + delete sessions[sessionId] + } + + sseTransport.onerror = (err) => { + logger.error(`SSE error (session ${sessionId}):`, err) + delete sessions[sessionId] + } + + req.on('close', () => { + logger.info(`Client disconnected (session ${sessionId})`) + delete sessions[sessionId] + }) + }) + + // @ts-ignore + app.post(messagePath, async (req, res) => { + const sessionId = req.query.sessionId as string + + setResponseHeaders({ + res, + headers, + }) + + if (!sessionId) { + return res.status(400).send('Missing sessionId parameter') + } + + const session = sessions[sessionId] + if (session?.transport?.handlePostMessage) { + logger.info(`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, host, () => { + logger.info(`Listening on ${host}:${port}`) + logger.info(`SSE endpoint: http://${host}:${port}${ssePath}`) + logger.info(`POST messages: http://${host}:${port}${messagePath}`) + }) + + let buffer = '' + child.stdout.on('data', (chunk: Buffer) => { + 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) + logger.info('Child → SSE:', jsonMsg) + for (const [sid, session] of Object.entries(sessions)) { + try { + session.transport.send(jsonMsg) + } catch (err) { + logger.error(`Failed to send to session ${sid}:`, err) + delete sessions[sid] + } + } + } catch { + logger.error(`Child non-JSON: ${line}`) + } + }) + }) + + child.stderr.on('data', (chunk: Buffer) => { + logger.error(`Child stderr: ${chunk.toString('utf8')}`) + }) +} diff --git a/src/gateways/stdioToStatefulStreamableHttp.ts b/src/gateways/stdioToStatefulStreamableHttp.ts new file mode 100644 index 0000000..b028dff --- /dev/null +++ b/src/gateways/stdioToStatefulStreamableHttp.ts @@ -0,0 +1,276 @@ +import express from 'express' +import cors, { type CorsOptions } from 'cors' +import { spawn } from 'child_process' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js' +import { Logger } from '../types.js' +import { getVersion } from '../lib/getVersion.js' +import { onSignals } from '../lib/onSignals.js' +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js' +import { randomUUID } from 'node:crypto' +import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js' +import { SessionAccessCounter } from '../lib/sessionAccessCounter.js' + +export interface StdioToStreamableHttpArgs { + stdioCmd: string + port: number + host: string + streamableHttpPath: string + logger: Logger + corsOrigin: CorsOptions['origin'] + healthEndpoints: string[] + headers: Record + sessionTimeout: number | null +} + +const setResponseHeaders = ({ + res, + headers, +}: { + res: express.Response + headers: Record +}) => + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value) + }) + +export async function stdioToStatefulStreamableHttp( + args: StdioToStreamableHttpArgs, +) { + const { + stdioCmd, + port, + host, + streamableHttpPath, + logger, + corsOrigin, + healthEndpoints, + headers, + sessionTimeout, + } = args + + logger.info( + ` - Headers: ${Object(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info(` - port: ${port}`) + logger.info(` - stdio: ${stdioCmd}`) + logger.info(` - streamableHttpPath: ${streamableHttpPath}`) + + logger.info( + ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`, + ) + logger.info( + ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`, + ) + logger.info( + ` - Session timeout: ${sessionTimeout ? `${sessionTimeout}ms` : 'disabled'}`, + ) + + onSignals({ logger }) + + const app = express() + app.use(express.json()) + + if (corsOrigin) { + app.use( + cors({ + origin: corsOrigin, + exposedHeaders: ['Mcp-Session-Id'], + }), + ) + } + + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }) + res.send('ok') + }) + } + + // Map to store transports by session ID + const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {} + + // Session access counter for timeout management + const sessionCounter = sessionTimeout + ? new SessionAccessCounter( + sessionTimeout, + (sessionId: string) => { + logger.info(`Session ${sessionId} timed out, cleaning up`) + const transport = transports[sessionId] + if (transport) { + transport.close() + } + delete transports[sessionId] + }, + logger, + ) + : null + + // Handle POST requests for client-to-server communication + app.post(streamableHttpPath, async (req, res) => { + // Check for existing session ID + const sessionId = req.headers['mcp-session-id'] as string | undefined + let transport: StreamableHTTPServerTransport + + if (sessionId && transports[sessionId]) { + // Reuse existing transport + transport = transports[sessionId] + // Increment session access count + sessionCounter?.inc(sessionId, 'POST request for existing session') + } else if (!sessionId && isInitializeRequest(req.body)) { + // New initialization request + + const server = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + + transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: () => randomUUID(), + onsessioninitialized: (sessionId) => { + // Store the transport by session ID + transports[sessionId] = transport + // Initialize session access count + sessionCounter?.inc(sessionId, 'session initialization') + }, + }) + await server.connect(transport) + const child = spawn(stdioCmd, { shell: true }) + child.on('exit', (code, signal) => { + logger.error(`Child exited: code=${code}, signal=${signal}`) + transport.close() + }) + + let buffer = '' + child.stdout.on('data', (chunk: Buffer) => { + 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) + logger.info('Child → StreamableHttp:', line) + try { + transport.send(jsonMsg) + } catch (e) { + logger.error(`Failed to send to StreamableHttp`, e) + } + } catch { + logger.error(`Child non-JSON: ${line}`) + } + }) + }) + + child.stderr.on('data', (chunk: Buffer) => { + logger.error(`Child stderr: ${chunk.toString('utf8')}`) + }) + + transport.onmessage = (msg: JSONRPCMessage) => { + logger.info(`StreamableHttp → Child: ${JSON.stringify(msg)}`) + child.stdin.write(JSON.stringify(msg) + '\n') + } + + transport.onclose = () => { + logger.info(`StreamableHttp connection closed (session ${sessionId})`) + if (transport.sessionId) { + sessionCounter?.clear( + transport.sessionId, + false, + 'transport being closed', + ) + delete transports[transport.sessionId] + } + child.kill() + } + + transport.onerror = (err) => { + logger.error(`StreamableHttp error (session ${sessionId}):`, err) + if (transport.sessionId) { + sessionCounter?.clear( + transport.sessionId, + false, + 'transport emitting error', + ) + delete transports[transport.sessionId] + } + child.kill() + } + } else { + // Invalid request + res.status(400).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Bad Request: No valid session ID provided', + }, + id: null, + }) + return + } + + // Decrement session access count when response ends + let responseEnded = false + const handleResponseEnd = (event: string) => { + if (!responseEnded && transport.sessionId) { + responseEnded = true + logger.info(`Response ${event}`, transport.sessionId) + sessionCounter?.dec(transport.sessionId, `POST response ${event}`) + } + } + + res.on('finish', () => handleResponseEnd('finished')) + res.on('close', () => handleResponseEnd('closed')) + + // Handle the request + await transport.handleRequest(req, res, req.body) + }) + + // Reusable handler for GET and DELETE requests + const handleSessionRequest = async ( + req: express.Request, + res: express.Response, + ) => { + const sessionId = req.headers['mcp-session-id'] as string | undefined + if (!sessionId || !transports[sessionId]) { + res.status(400).send('Invalid or missing session ID') + return + } + + // Increment session access count + sessionCounter?.inc(sessionId, `${req.method} request for existing session`) + + // Decrement session access count when response ends + let responseEnded = false + const handleResponseEnd = (event: string) => { + if (!responseEnded) { + responseEnded = true + logger.info(`Response ${event}`, sessionId) + sessionCounter?.dec(sessionId, `${req.method} response ${event}`) + } + } + + res.on('finish', () => handleResponseEnd('finished')) + res.on('close', () => handleResponseEnd('closed')) + + const transport = transports[sessionId] + await transport.handleRequest(req, res) + } + + // Handle GET requests for server-to-client notifications via SSE + app.get(streamableHttpPath, handleSessionRequest) + + // Handle DELETE requests for session termination + app.delete(streamableHttpPath, handleSessionRequest) + + app.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`) + logger.info( + `StreamableHttp endpoint: http://${host}:${port}${streamableHttpPath}`, + ) + }) +} diff --git a/src/gateways/stdioToStatelessStreamableHttp.ts b/src/gateways/stdioToStatelessStreamableHttp.ts new file mode 100644 index 0000000..432f126 --- /dev/null +++ b/src/gateways/stdioToStatelessStreamableHttp.ts @@ -0,0 +1,193 @@ +import express from 'express' +import cors, { type CorsOptions } from 'cors' +import { spawn } from 'child_process' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' +import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js' +import { Logger } from '../types.js' +import { getVersion } from '../lib/getVersion.js' +import { onSignals } from '../lib/onSignals.js' +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js' + +export interface StdioToStreamableHttpArgs { + stdioCmd: string + port: number + host: string + streamableHttpPath: string + logger: Logger + corsOrigin: CorsOptions['origin'] + healthEndpoints: string[] + headers: Record +} + +const setResponseHeaders = ({ + res, + headers, +}: { + res: express.Response + headers: Record +}) => + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value) + }) + +export async function stdioToStatelessStreamableHttp( + args: StdioToStreamableHttpArgs, +) { + const { + stdioCmd, + port, + host, + streamableHttpPath, + logger, + corsOrigin, + healthEndpoints, + headers, + } = args + + logger.info( + ` - Headers: ${Object(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info(` - host: ${host}`) + logger.info(` - port: ${port}`) + logger.info(` - stdio: ${stdioCmd}`) + logger.info(` - streamableHttpPath: ${streamableHttpPath}`) + + logger.info( + ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`, + ) + logger.info( + ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`, + ) + + onSignals({ logger }) + + const app = express() + app.use(express.json()) + + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })) + } + + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }) + res.send('ok') + }) + } + + app.post(streamableHttpPath, async (req, res) => { + // In stateless mode, create a new instance of transport and server for each request + // to ensure complete isolation. A single instance would cause request ID collisions + // when multiple clients connect concurrently. + + try { + const server = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + }) + + await server.connect(transport) + const child = spawn(stdioCmd, { shell: true }) + child.on('exit', (code, signal) => { + logger.error(`Child exited: code=${code}, signal=${signal}`) + transport.close() + }) + + let buffer = '' + child.stdout.on('data', (chunk: Buffer) => { + 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) + logger.info('Child → StreamableHttp:', line) + try { + transport.send(jsonMsg) + } catch (e) { + logger.error(`Failed to send to StreamableHttp`, e) + } + } catch { + logger.error(`Child non-JSON: ${line}`) + } + }) + }) + + child.stderr.on('data', (chunk: Buffer) => { + logger.error(`Child stderr: ${chunk.toString('utf8')}`) + }) + + transport.onmessage = (msg: JSONRPCMessage) => { + logger.info(`StreamableHttp → Child: ${JSON.stringify(msg)}`) + child.stdin.write(JSON.stringify(msg) + '\n') + } + + transport.onclose = () => { + logger.info('StreamableHttp connection closed') + child.kill() + } + + transport.onerror = (err) => { + logger.error(`StreamableHttp error:`, err) + child.kill() + } + + await transport.handleRequest(req, res, req.body) + } catch (error) { + logger.error('Error handling MCP request:', error) + if (!res.headersSent) { + res.status(500).json({ + jsonrpc: '2.0', + error: { + code: -32603, + message: 'Internal server error', + }, + id: null, + }) + } + } + }) + + app.get(streamableHttpPath, async (req, res) => { + logger.info('Received GET MCP request') + res.writeHead(405).end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed.', + }, + id: null, + }), + ) + }) + + app.delete(streamableHttpPath, async (req, res) => { + logger.info('Received DELETE MCP request') + res.writeHead(405).end( + JSON.stringify({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Method not allowed.', + }, + id: null, + }), + ) + }) + + app.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`) + logger.info( + `StreamableHttp endpoint: http://${host}:${port}${streamableHttpPath}`, + ) + }) +} diff --git a/src/gateways/stdioToWs.ts b/src/gateways/stdioToWs.ts new file mode 100644 index 0000000..cc93c6b --- /dev/null +++ b/src/gateways/stdioToWs.ts @@ -0,0 +1,160 @@ +import express from 'express' +import cors, { type CorsOptions } from 'cors' +import { createServer } from 'http' +import { spawn, ChildProcessWithoutNullStreams } from 'child_process' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js' +import { Logger } from '../types.js' +import { getVersion } from '../lib/getVersion.js' +import { WebSocketServerTransport } from '../server/websocket.js' +import { onSignals } from '../lib/onSignals.js' +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js' + +export interface StdioToWsArgs { + stdioCmd: string + port: number + host: string + messagePath: string + logger: Logger + corsOrigin: CorsOptions['origin'] + healthEndpoints: string[] +} + +export async function stdioToWs(args: StdioToWsArgs) { + const { + stdioCmd, + port, + host, + messagePath, + logger, + healthEndpoints, + corsOrigin, + } = args + logger.info(` - host: ${host}`) + logger.info(` - port: ${port}`) + logger.info(` - stdio: ${stdioCmd}`) + logger.info(` - messagePath: ${messagePath}`) + logger.info( + ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`, + ) + logger.info( + ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`, + ) + + let wsTransport: WebSocketServerTransport | null = null + let child: ChildProcessWithoutNullStreams | null = null + let isReady = false + + const cleanup = () => { + if (wsTransport) { + wsTransport.close().catch((err) => { + logger.error(`Error stopping WebSocket server: ${err.message}`) + }) + } + if (child) { + child.kill() + } + } + + onSignals({ + logger, + cleanup, + }) + + try { + child = spawn(stdioCmd, { shell: true }) + child.on('exit', (code, signal) => { + logger.error(`Child exited: code=${code}, signal=${signal}`) + cleanup() + process.exit(code ?? 1) + }) + + const server = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + + // Handle child process output + let buffer = '' + child.stdout.on('data', (chunk: Buffer) => { + 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) + logger.info(`Child → WebSocket: ${JSON.stringify(jsonMsg)}`) + // Broadcast to all connected clients + wsTransport?.send(jsonMsg, jsonMsg.id).catch((err) => { + logger.error('Failed to broadcast message:', err) + }) + } catch { + logger.error(`Child non-JSON: ${line}`) + } + }) + }) + + child.stderr.on('data', (chunk: Buffer) => { + logger.info(`Child stderr: ${chunk.toString('utf8')}`) + }) + + const app = express() + + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })) + } + + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + if (child?.killed) { + res.status(500).send('Child process has been killed') + } + + if (!isReady) { + res.status(500).send('Server is not ready') + } + + res.send('ok') + }) + } + + const httpServer = createServer(app) + + wsTransport = new WebSocketServerTransport({ + path: messagePath, + server: httpServer, + }) + + await server.connect(wsTransport) + + wsTransport.onmessage = (msg: JSONRPCMessage) => { + const line = JSON.stringify(msg) + logger.info(`WebSocket → Child: ${line}`) + child!.stdin.write(line + '\n') + } + + wsTransport.onconnection = (clientId: string) => { + logger.info(`New WebSocket connection: ${clientId}`) + } + + wsTransport.ondisconnection = (clientId: string) => { + logger.info(`WebSocket connection closed: ${clientId}`) + } + + wsTransport.onerror = (err: Error) => { + logger.error(`WebSocket error: ${err.message}`) + } + + isReady = true + + httpServer.listen(port, host, () => { + logger.info(`Listening on ${host}:${port}`) + logger.info(`WebSocket endpoint: ws://${host}:${port}${messagePath}`) + }) + } catch (err: any) { + logger.error(`Failed to start: ${err.message}`) + cleanup() + process.exit(1) + } +} diff --git a/src/gateways/streamableHttpToSse.ts b/src/gateways/streamableHttpToSse.ts new file mode 100644 index 0000000..071831c --- /dev/null +++ b/src/gateways/streamableHttpToSse.ts @@ -0,0 +1,358 @@ +import express from 'express' +import bodyParser from 'body-parser' +import cors, { type CorsOptions } from 'cors' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js' +import type { + JSONRPCMessage, + JSONRPCRequest, + ClientCapabilities, + Implementation, +} from '@modelcontextprotocol/sdk/types.js' +import { InitializeRequestSchema } from '@modelcontextprotocol/sdk/types.js' +import { z } from 'zod' +import { getVersion } from '../lib/getVersion.js' +import { Logger } from '../types.js' +import { onSignals } from '../lib/onSignals.js' +import { serializeCorsOrigin } from '../lib/serializeCorsOrigin.js' + +export interface StreamableHttpToSseArgs { + streamableHttpUrl: string + port: number + host: string + baseUrl: string + ssePath: string + messagePath: string + logger: Logger + corsOrigin: CorsOptions['origin'] + healthEndpoints: string[] + headers: Record +} + +let streamableHttpClient: Client | undefined + +const newInitializeStreamableHttpClient = ({ + message, +}: { + message: JSONRPCRequest +}) => { + const clientInfo = message.params?.clientInfo as Implementation | undefined + const clientCapabilities = message.params?.capabilities as + | ClientCapabilities + | undefined + + return new Client( + { + name: clientInfo?.name ?? 'mcp-superassistant-proxy', + version: clientInfo?.version ?? getVersion(), + }, + { + capabilities: clientCapabilities ?? {}, + }, + ) +} + +const newFallbackStreamableHttpClient = async ({ + streamableHttpTransport, +}: { + streamableHttpTransport: StreamableHTTPClientTransport +}) => { + const fallbackStreamableHttpClient = new Client( + { + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, + { + capabilities: {}, + }, + ) + + await fallbackStreamableHttpClient.connect(streamableHttpTransport) + return fallbackStreamableHttpClient +} + +const setResponseHeaders = ({ + res, + headers, +}: { + res: express.Response + headers: Record +}) => + Object.entries(headers).forEach(([key, value]) => { + res.setHeader(key, value) + }) + +export async function streamableHttpToSse(args: StreamableHttpToSseArgs) { + const { + streamableHttpUrl, + port, + host, + baseUrl, + ssePath, + messagePath, + logger, + corsOrigin, + healthEndpoints, + headers, + } = args + + logger.info(` - input StreamableHttp: ${streamableHttpUrl}`) + logger.info( + ` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info(` - host: ${host}`) + logger.info(` - port: ${port}`) + if (baseUrl) { + logger.info(` - baseUrl: ${baseUrl}`) + } + logger.info(` - ssePath: ${ssePath}`) + logger.info(` - messagePath: ${messagePath}`) + logger.info( + ` - CORS: ${corsOrigin ? `enabled (${serializeCorsOrigin({ corsOrigin })})` : 'disabled'}`, + ) + logger.info( + ` - Health endpoints: ${healthEndpoints.length ? healthEndpoints.join(', ') : '(none)'}`, + ) + + onSignals({ logger }) + + const inputStreamableHttpTransport = new StreamableHTTPClientTransport( + new URL(streamableHttpUrl), + { + requestInit: { + headers, + }, + }, + ) + + inputStreamableHttpTransport.onerror = (err) => { + logger.error('Input StreamableHttp error:', err) + } + + inputStreamableHttpTransport.onclose = () => { + logger.error('Input StreamableHttp connection closed') + process.exit(1) + } + + const outputServer = new Server( + { name: 'mcp-superassistant-proxy', version: getVersion() }, + { capabilities: {} }, + ) + + const sessions: Record< + string, + { transport: SSEServerTransport; response: express.Response } + > = {} + + const app = express() + + if (corsOrigin) { + app.use(cors({ origin: corsOrigin })) + } + + app.use((req, res, next) => { + if (req.path === messagePath) return next() + return bodyParser.json()(req, res, next) + }) + + for (const ep of healthEndpoints) { + app.get(ep, (_req, res) => { + setResponseHeaders({ + res, + headers, + }) + res.send('ok') + }) + } + + app.get(ssePath, async (req, res) => { + logger.info(`New SSE connection from ${req.ip}`) + + setResponseHeaders({ + res, + headers, + }) + + const outputSseTransport = new SSEServerTransport( + `${baseUrl}${messagePath}`, + res, + ) + await outputServer.connect(outputSseTransport) + + const sessionId = outputSseTransport.sessionId + if (sessionId) { + sessions[sessionId] = { transport: outputSseTransport, response: res } + } + + const wrapResponse = (req: JSONRPCRequest, payload: object) => ({ + jsonrpc: req.jsonrpc || '2.0', + id: req.id, + ...payload, + }) + + outputSseTransport.onmessage = async (message: JSONRPCMessage) => { + const isRequest = 'method' in message && 'id' in message + if (isRequest) { + logger.info( + `Output SSE → Input StreamableHttp (session ${sessionId}):`, + message, + ) + const req = message as JSONRPCRequest + let result + + try { + if (!streamableHttpClient) { + if (message.method === 'initialize') { + streamableHttpClient = newInitializeStreamableHttpClient({ + message, + }) + + const originalRequest = streamableHttpClient.request + + streamableHttpClient.request = async function ( + requestMessage, + ...restArgs + ) { + if ( + InitializeRequestSchema.safeParse(requestMessage).success && + message.params?.protocolVersion && + requestMessage.params?.protocolVersion + ) { + requestMessage.params.protocolVersion = + message.params.protocolVersion + } + + result = await originalRequest.apply(this, [ + requestMessage, + ...restArgs, + ]) + + return result + } + + await streamableHttpClient.connect(inputStreamableHttpTransport) + streamableHttpClient.request = originalRequest + } else { + logger.info( + 'StreamableHttp client not initialized, creating fallback client', + ) + streamableHttpClient = await newFallbackStreamableHttpClient({ + streamableHttpTransport: inputStreamableHttpTransport, + }) + } + + logger.info('Input StreamableHttp connected') + } else { + result = await streamableHttpClient.request(req, z.any()) + } + } catch (err) { + logger.error('Request error:', err) + const errorCode = + err && typeof err === 'object' && 'code' in err + ? (err as any).code + : -32000 + let errorMsg = + err && typeof err === 'object' && 'message' in err + ? (err as any).message + : 'Internal error' + const prefix = `MCP error ${errorCode}:` + if (errorMsg.startsWith(prefix)) { + errorMsg = errorMsg.slice(prefix.length).trim() + } + const errorResp = wrapResponse(req, { + error: { + code: errorCode, + message: errorMsg, + }, + }) + try { + outputSseTransport.send(errorResp as any) + } catch (sendErr) { + logger.error( + `Failed to send error response to session ${sessionId}:`, + sendErr, + ) + delete sessions[sessionId] + } + return + } + const response = wrapResponse( + req, + result.hasOwnProperty('error') + ? { error: { ...result.error } } + : { result: { ...result } }, + ) + logger.info(`Response (session ${sessionId}):`, response) + try { + outputSseTransport.send(response as any) + } catch (sendErr) { + logger.error( + `Failed to send response to session ${sessionId}:`, + sendErr, + ) + delete sessions[sessionId] + } + } else { + logger.info( + `Input StreamableHttp → Output SSE (session ${sessionId}):`, + message, + ) + try { + outputSseTransport.send(message) + } catch (sendErr) { + logger.error( + `Failed to send message to session ${sessionId}:`, + sendErr, + ) + delete sessions[sessionId] + } + } + } + + outputSseTransport.onclose = () => { + logger.info(`Output SSE connection closed (session ${sessionId})`) + delete sessions[sessionId] + } + + outputSseTransport.onerror = (err) => { + logger.error(`Output SSE error (session ${sessionId}):`, err) + delete sessions[sessionId] + } + + req.on('close', () => { + logger.info(`Client disconnected (session ${sessionId})`) + delete sessions[sessionId] + }) + }) + + app.post(messagePath, async (req: any, res: any) => { + const sessionId = req.query.sessionId as string + + setResponseHeaders({ + res, + headers, + }) + + if (!sessionId) { + return res.status(400).send('Missing sessionId parameter') + } + + const session = sessions[sessionId] + if (session?.transport?.handlePostMessage) { + logger.info(`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, host, () => { + logger.info(`Listening on ${host}:${port}`) + logger.info(`SSE endpoint: http://${host}:${port}${ssePath}`) + logger.info(`POST messages: http://${host}:${port}${messagePath}`) + }) + + logger.info('StreamableHttp-to-SSE gateway ready') +} diff --git a/src/gateways/streamableHttpToStdio.ts b/src/gateways/streamableHttpToStdio.ts new file mode 100644 index 0000000..60d2c98 --- /dev/null +++ b/src/gateways/streamableHttpToStdio.ts @@ -0,0 +1,196 @@ +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import type { + JSONRPCMessage, + JSONRPCRequest, + ClientCapabilities, + Implementation, +} from '@modelcontextprotocol/sdk/types.js' +import { InitializeRequestSchema } from '@modelcontextprotocol/sdk/types.js' +import { z } from 'zod' +import { getVersion } from '../lib/getVersion.js' +import { Logger } from '../types.js' +import { onSignals } from '../lib/onSignals.js' + +export interface StreamableHttpToStdioArgs { + streamableHttpUrl: string + logger: Logger + headers: Record +} + +let mcpClient: Client | undefined + +const newInitializeMcpClient = ({ message }: { message: JSONRPCRequest }) => { + const clientInfo = message.params?.clientInfo as Implementation | undefined + const clientCapabilities = message.params?.capabilities as + | ClientCapabilities + | undefined + + return new Client( + { + name: clientInfo?.name ?? 'mcp-superassistant-proxy', + version: clientInfo?.version ?? getVersion(), + }, + { + capabilities: clientCapabilities ?? {}, + }, + ) +} + +const newFallbackMcpClient = async ({ + mcpTransport, +}: { + mcpTransport: StreamableHTTPClientTransport +}) => { + const fallbackMcpClient = new Client( + { + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, + { + capabilities: {}, + }, + ) + + await fallbackMcpClient.connect(mcpTransport) + return fallbackMcpClient +} + +export async function streamableHttpToStdio(args: StreamableHttpToStdioArgs) { + const { streamableHttpUrl, logger, headers } = args + + logger.info(` - streamableHttp: ${streamableHttpUrl}`) + logger.info( + ` - Headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + logger.info('Connecting to Streamable HTTP...') + + onSignals({ logger }) + + const mcpTransport = new StreamableHTTPClientTransport( + new URL(streamableHttpUrl), + { + requestInit: { + headers, + }, + }, + ) + + mcpTransport.onerror = (err) => { + logger.error('Streamable HTTP error:', err) + } + + mcpTransport.onclose = () => { + logger.error('Streamable HTTP connection closed') + process.exit(1) + } + + const stdioServer = new Server( + { + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, + { + capabilities: {}, + }, + ) + + const stdioTransport = new StdioServerTransport() + await stdioServer.connect(stdioTransport) + + const wrapResponse = (req: JSONRPCRequest, payload: object) => ({ + jsonrpc: req.jsonrpc || '2.0', + id: req.id, + ...payload, + }) + + stdioServer.transport!.onmessage = async (message: JSONRPCMessage) => { + const isRequest = 'method' in message && 'id' in message + if (isRequest) { + logger.info('Stdio → Streamable HTTP:', message) + const req = message as JSONRPCRequest + let result + + try { + if (!mcpClient) { + if (message.method === 'initialize') { + mcpClient = newInitializeMcpClient({ + message, + }) + + const originalRequest = mcpClient.request + + mcpClient.request = async function ( + possibleInitRequestMessage, + ...restArgs + ) { + if ( + InitializeRequestSchema.safeParse(possibleInitRequestMessage) + .success && + message.params?.protocolVersion + ) { + // respect the protocol version from the stdio client's init request + possibleInitRequestMessage.params!.protocolVersion = + message.params.protocolVersion + } + result = await originalRequest.apply(this, [ + possibleInitRequestMessage, + ...restArgs, + ]) + return result + } + + await mcpClient.connect(mcpTransport) + mcpClient.request = originalRequest + } else { + logger.info( + 'Streamable HTTP client not initialized, creating fallback client', + ) + mcpClient = await newFallbackMcpClient({ mcpTransport }) + } + + logger.info('Streamable HTTP connected') + } else { + result = await mcpClient.request(req, z.any()) + } + } catch (err) { + logger.error('Request error:', err) + const errorCode = + err && typeof err === 'object' && 'code' in err + ? (err as any).code + : -32000 + let errorMsg = + err && typeof err === 'object' && 'message' in err + ? (err as any).message + : 'Internal error' + 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 } }, + ) + logger.info('Response:', response) + process.stdout.write(JSON.stringify(response) + '\n') + } else { + logger.info('Streamable HTTP → Stdio:', message) + process.stdout.write(JSON.stringify(message) + '\n') + } + } + + logger.info('Stdio server listening') +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..aac2e3c --- /dev/null +++ b/src/index.ts @@ -0,0 +1,443 @@ +#!/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'] as const, + 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 as string, + }) + + 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 as string[], + 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 as string[], + }) + } else if (argv.outputTransport === 'streamableHttp') { + const stateful = argv.stateful + if (stateful) { + logger.info('Running stateful server') + + let sessionTimeout: null | number + 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, + streamableHttpPath: argv.streamableHttpPath, + logger, + corsOrigin: corsOrigin({ argv }), + healthEndpoints: argv.healthEndpoint as string[], + headers: headers({ + argv, + logger, + }), + sessionTimeout, + }) + } else { + logger.info('Running stateless server') + + await stdioToStatelessStreamableHttp({ + stdioCmd: argv.stdio!, + port: argv.port, + host: argv.host, + streamableHttpPath: argv.streamableHttpPath, + logger, + corsOrigin: corsOrigin({ argv }), + healthEndpoints: argv.healthEndpoint as string[], + 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 as string[], + 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 as string[], + 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 as string[], + 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 as string[], + 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 as string[], + headers: headers({ + argv, + logger, + }), + }) + } else if (argv.outputTransport === 'streamableHttp') { + const stateless = !argv.stateful + + let sessionTimeout: null | number = 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, + streamableHttpPath: argv.streamableHttpPath, + logger, + corsOrigin: corsOrigin({ argv }), + healthEndpoints: argv.healthEndpoint as string[], + 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() diff --git a/src/lib/config.ts b/src/lib/config.ts new file mode 100644 index 0000000..dbbd78f --- /dev/null +++ b/src/lib/config.ts @@ -0,0 +1,73 @@ +import { readFileSync } from 'fs' +import { z } from 'zod' + +export const McpServerConfigSchema = z.object({ + name: z.string().optional(), + type: z.enum(['stdio', 'sse', 'streamable-http']).optional(), + command: z.string().optional(), + args: z.array(z.string()).optional(), + url: z.string().optional(), + env: z.record(z.string()).optional(), + headers: z.record(z.string()).optional(), +}) + +export const ConfigSchema = z.object({ + mcpServers: z.record(z.string(), McpServerConfigSchema), +}) + +export type McpServerConfig = z.infer +export type Config = z.infer + +export function detectServerType( + config: McpServerConfig, +): 'stdio' | 'sse' | 'streamable-http' { + if (config.type) { + return config.type + } + + if (config.url) { + // Auto-detect based on URL path + try { + const url = new URL(config.url) + if (url.pathname.endsWith('/mcp') || url.pathname.includes('/mcp')) { + return 'streamable-http' + } else if ( + url.pathname.endsWith('/sse') || + url.pathname.includes('/sse') + ) { + return 'sse' + } else { + // Default to streamable-http for unrecognized HTTP endpoints + return 'streamable-http' + } + } catch { + return 'streamable-http' + } + } + + if (config.command) { + return 'stdio' + } + + throw new Error( + `Cannot detect server type for ${config.name}. Please specify type explicitly.`, + ) +} + +export function loadConfig(configPath: string): Config { + try { + const configContent = readFileSync(configPath, 'utf8') + const configData = JSON.parse(configContent) + return ConfigSchema.parse(configData) + } catch (err) { + if (err instanceof SyntaxError) { + throw new Error(`Invalid JSON in config file: ${err.message}`) + } + if (err instanceof z.ZodError) { + throw new Error( + `Invalid config format: ${err.errors.map((e) => e.message).join(', ')}`, + ) + } + throw new Error(`Failed to load config: ${err}`) + } +} diff --git a/src/lib/corsOrigin.ts b/src/lib/corsOrigin.ts new file mode 100644 index 0000000..5ebefc5 --- /dev/null +++ b/src/lib/corsOrigin.ts @@ -0,0 +1,31 @@ +export const corsOrigin = ({ + argv, +}: { + argv: { + cors: (string | number)[] | undefined + } +}) => { + if (!argv.cors) { + return '*' + } + + if (argv.cors.length === 0) { + return '*' + } + + const origins = argv.cors.map((item) => `${item}`) + + if (origins.includes('*')) return '*' + + return origins.map((origin) => { + if (/^\/.*\/$/.test(origin)) { + const pattern = origin.slice(1, -1) + try { + return new RegExp(pattern) + } catch (error) { + return origin + } + } + return origin + }) +} diff --git a/src/lib/getLogger.ts b/src/lib/getLogger.ts new file mode 100644 index 0000000..7693e17 --- /dev/null +++ b/src/lib/getLogger.ts @@ -0,0 +1,93 @@ +import util from 'node:util' +import { Logger } from '../types.js' + +const defaultFormatArgs = (args: any[]) => args + +const log = + ( + { + formatArgs = defaultFormatArgs, + }: { + formatArgs?: typeof defaultFormatArgs + } = { formatArgs: defaultFormatArgs }, + ) => + (...args: any[]) => + console.log('[mcp-superassistant-proxy]', ...formatArgs(args)) + +const logStderr = + ( + { + formatArgs = defaultFormatArgs, + }: { + formatArgs?: typeof defaultFormatArgs + } = { formatArgs: defaultFormatArgs }, + ) => + (...args: any[]) => + console.error('[mcp-superassistant-proxy]', ...formatArgs(args)) + +const noneLogger: Logger = { + info: () => {}, + error: () => {}, + debug: () => {}, + warn: () => {}, +} + +const infoLogger: Logger = { + info: log(), + error: logStderr(), + debug: () => {}, + warn: logStderr(), +} + +const infoLoggerStdio: Logger = { + info: logStderr(), + error: logStderr(), + debug: () => {}, + warn: logStderr(), +} + +const debugFormatArgs = (args: any[]) => + args.map((arg) => { + if (typeof arg === 'object') { + return util.inspect(arg, { + depth: null, + colors: process.stderr.isTTY, + compact: false, + }) + } + + return arg + }) + +const debugLogger: Logger = { + info: log({ formatArgs: debugFormatArgs }), + error: logStderr({ formatArgs: debugFormatArgs }), + debug: log({ formatArgs: debugFormatArgs }), + warn: logStderr({ formatArgs: debugFormatArgs }), +} + +const debugLoggerStdio: Logger = { + info: logStderr({ formatArgs: debugFormatArgs }), + error: logStderr({ formatArgs: debugFormatArgs }), + debug: logStderr({ formatArgs: debugFormatArgs }), + warn: logStderr({ formatArgs: debugFormatArgs }), +} + +export const getLogger = ({ + logLevel, + outputTransport, +}: { + logLevel: string + outputTransport: string +}): Logger => { + if (logLevel === 'none') { + return noneLogger + } + + if (logLevel === 'debug') { + return outputTransport === 'stdio' ? debugLoggerStdio : debugLogger + } + + // info logLevel + return outputTransport === 'stdio' ? infoLoggerStdio : infoLogger +} diff --git a/src/lib/getVersion.ts b/src/lib/getVersion.ts new file mode 100644 index 0000000..aa6e552 --- /dev/null +++ b/src/lib/getVersion.ts @@ -0,0 +1,21 @@ +import { fileURLToPath } from 'url' +import { join, dirname } from 'path' +import { readFileSync } from 'fs' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +export function getVersion(): string { + 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( + '[mcp-superassistant-proxy]', + 'Unable to retrieve version:', + err, + ) + return 'unknown' + } +} diff --git a/src/lib/headers.ts b/src/lib/headers.ts new file mode 100644 index 0000000..afc3a4a --- /dev/null +++ b/src/lib/headers.ts @@ -0,0 +1,55 @@ +import { Logger } from '../types.js' + +const parseHeaders = ({ + argvHeader, + logger, +}: { + argvHeader: (string | number)[] + logger: Logger +}): Record => { + return argvHeader.reduce>((acc, rawHeader) => { + const header = `${rawHeader}` + + const colonIndex = header.indexOf(':') + if (colonIndex === -1) { + logger.error(`Invalid header format: ${header}, ignoring`) + return acc + } + + const key = header.slice(0, colonIndex).trim() + const value = header.slice(colonIndex + 1).trim() + + if (!key || !value) { + logger.error(`Invalid header format: ${header}, ignoring`) + return acc + } + + acc[key] = value + return acc + }, {}) +} + +export const headers = ({ + argv, + logger, +}: { + argv: { + header: (string | number)[] + oauth2Bearer: string | undefined + } + logger: Logger +}): Record => { + const headers = parseHeaders({ + argvHeader: argv.header, + logger, + }) + + if ('oauth2Bearer' in argv) { + return { + ...headers, + Authorization: `Bearer ${argv.oauth2Bearer}`, + } + } + + return headers +} diff --git a/src/lib/mcpServerManager.ts b/src/lib/mcpServerManager.ts new file mode 100644 index 0000000..246793f --- /dev/null +++ b/src/lib/mcpServerManager.ts @@ -0,0 +1,425 @@ +import { spawn, ChildProcessWithoutNullStreams } from 'child_process' +import { Client } from '@modelcontextprotocol/sdk/client/index.js' +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' +import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' +import type { + JSONRPCRequest, + JSONRPCResponse, + Tool, + Resource, + ListToolsResult, + ListResourcesResult, +} from '@modelcontextprotocol/sdk/types.js' +import { z } from 'zod' +import { McpServerConfig, detectServerType } from './config.js' +import { getVersion } from './getVersion.js' +import { Logger } from '../types.js' + +export interface ManagedServer { + name: string + config: McpServerConfig + client: Client + tools: Tool[] + resources: Resource[] + connected: boolean + child?: ChildProcessWithoutNullStreams +} + +export class McpServerManager { + private servers: Map = new Map() + private logger: Logger + + constructor(logger: Logger) { + this.logger = logger + } + + async addServer(name: string, config: McpServerConfig): Promise { + const serverType = detectServerType(config) + const client = new Client( + { + name: 'mcp-superassistant-proxy', + version: getVersion(), + }, + { + capabilities: {}, + }, + ) + + let transport + let child: ChildProcessWithoutNullStreams | undefined + + if (serverType === 'stdio') { + if (!config.command) { + throw new Error(`Stdio server ${name} missing command`) + } + + const args = config.args || [] + + this.logger.info( + `Starting server ${name}: ${config.command} ${args.join(' ')}`, + ) + this.logger.debug( + `Command: "${config.command}", Args: [${args.map((a) => `"${a}"`).join(', ')}]`, + ) + + this.logger.debug(`Creating StdioClientTransport for ${name}`) + + try { + // Use command and args to create the transport, similar to test files + // StdioClientTransport will handle spawning the process internally + transport = new StdioClientTransport({ + command: config.command, + args: args, + env: config.env ? { ...process.env, ...config.env } : process.env, + } as any) + this.logger.debug(`StdioClientTransport created for ${name}`) + } catch (transportErr) { + this.logger.error( + `Failed to create StdioClientTransport for ${name}:`, + transportErr, + ) + throw transportErr + } + } else if (serverType === 'sse') { + if (!config.url) { + throw new Error(`HTTP server ${name} missing URL`) + } + + const url = new URL(config.url) + if (url.pathname.endsWith('/sse') || url.pathname.includes('/sse')) { + const headers = config.headers || {} + this.logger.info( + `Connecting to SSE server ${name} with headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + transport = new SSEClientTransport(url, { + eventSourceInit: { + fetch: (...props: Parameters) => { + const [url, init = {}] = props + return fetch(url, { + ...init, + headers: { ...init.headers, ...headers } + }) + }, + }, + requestInit: { + headers, + }, + }) + } else { + throw new Error( + `HTTP server ${name} URL must be an SSE endpoint (path should end with /sse)`, + ) + } + } else if (serverType === 'streamable-http') { + if (!config.url) { + throw new Error(`Streamable HTTP server ${name} missing URL`) + } + + const headers = config.headers || {} + this.logger.info( + `Connecting to streamable HTTP server ${name}: ${config.url}`, + ) + this.logger.info( + `With headers: ${Object.keys(headers).length ? JSON.stringify(headers) : '(none)'}`, + ) + const url = new URL(config.url) + transport = new StreamableHTTPClientTransport(url, { + requestInit: { + headers, + }, + }) + } else { + throw new Error(`Unsupported server type: ${serverType}`) + } + + try { + this.logger.debug(`Attempting to connect client to transport for ${name}`) + await client.connect(transport) + this.logger.info(`Connected to server: ${name}`) + + const server: ManagedServer = { + name, + config, + client, + tools: [], + resources: [], + connected: true, + child: child || undefined, + } + + try { + const toolsResponse = (await client.request( + { method: 'tools/list', params: {} }, + z.object({ tools: z.array(z.any()) }), + )) as ListToolsResult + server.tools = toolsResponse.tools || [] + this.logger.info(`Server ${name} has ${server.tools.length} tools`) + } catch (err) { + this.logger.warn(`Server ${name} does not support tools: ${err}`) + } + + try { + const resourcesResponse = (await client.request( + { method: 'resources/list', params: {} }, + z.object({ resources: z.array(z.any()) }), + )) as ListResourcesResult + server.resources = resourcesResponse.resources || [] + this.logger.info( + `Server ${name} has ${server.resources.length} resources`, + ) + } catch (err) { + this.logger.debug(`Server ${name} does not support resources: ${err}`) + } + + this.servers.set(name, server) + } catch (err) { + if (child) { + child.kill() + } + throw new Error(`Failed to connect to server ${name}: ${err}`) + } + } + + async handleRequest(request: JSONRPCRequest): Promise { + const { method, params, id } = request + + if (method === 'initialize') { + return { + jsonrpc: '2.0', + id, + result: { + protocolVersion: '2024-11-05', + capabilities: { + tools: {}, + resources: {}, + }, + serverInfo: { + name: 'mcp-superassistant-proxy-unified', + version: getVersion(), + }, + }, + } + } + + if (method === 'tools/list') { + const allTools: Tool[] = [] + for (const [serverName, server] of this.servers) { + if (server.connected) { + for (const tool of server.tools) { + allTools.push({ + ...tool, + name: `${serverName}.${tool.name}`, + }) + } + } + } + return { + jsonrpc: '2.0', + id, + result: { tools: allTools }, + } + } + + if (method === 'resources/list') { + const allResources: Resource[] = [] + for (const [serverName, server] of this.servers) { + if (server.connected) { + for (const resource of server.resources) { + allResources.push({ + ...resource, + name: `${serverName}.${resource.name}`, + uri: `${serverName}://${resource.uri}`, + }) + } + } + } + return { + jsonrpc: '2.0', + id, + result: { resources: allResources }, + } + } + + if (method === 'tools/call') { + const toolName = params?.name as string + if (!toolName) { + return { + jsonrpc: '2.0', + id, + error: { + code: -32602, + message: 'Tool name is required', + }, + } as any + } + + let serverName: string + let originalToolName: string + + if (toolName.includes('.')) { + // Tool name includes server prefix + ;[serverName, originalToolName] = toolName.split('.', 2) + } else { + // No server prefix, find which server has this tool + let foundServer: string | null = null + for (const [sName, server] of this.servers) { + if ( + server.connected && + server.tools.some((tool) => tool.name === toolName) + ) { + if (foundServer) { + // Tool exists in multiple servers, require explicit server name + return { + jsonrpc: '2.0', + id, + error: { + code: -32602, + message: `Tool '${toolName}' exists in multiple servers (${foundServer}, ${sName}). Use format: servername.toolname`, + }, + } as any + } + foundServer = sName + } + } + + if (!foundServer) { + return { + jsonrpc: '2.0', + id, + error: { + code: -32601, + message: `Tool '${toolName}' not found in any connected server`, + }, + } as any + } + + serverName = foundServer + originalToolName = toolName + } + + const server = this.servers.get(serverName) + + if (!server || !server.connected) { + return { + jsonrpc: '2.0', + id, + error: { + code: -32601, + message: `Server ${serverName} not found or not connected`, + }, + } as any + } + + try { + const response = await server.client.request( + { + method: 'tools/call', + params: { + ...params, + name: originalToolName, + }, + }, + z.any(), + ) + return { + jsonrpc: '2.0', + id, + result: response, + } + } catch (err: any) { + return { + jsonrpc: '2.0', + id, + error: { + code: err.code || -32000, + message: err.message || 'Tool call failed', + }, + } as any + } + } + + if (method === 'resources/read') { + const uri = params?.uri as string + if (!uri || !uri.includes('://')) { + return { + jsonrpc: '2.0', + id, + error: { + code: -32602, + message: + 'Invalid resource URI. Expected format: servername://resource-uri', + }, + } as any + } + + const [serverName, originalUri] = uri.split('://', 2) + const server = this.servers.get(serverName) + + if (!server || !server.connected) { + return { + jsonrpc: '2.0', + id, + error: { + code: -32601, + message: `Server ${serverName} not found or not connected`, + }, + } as any + } + + try { + const response = await server.client.request( + { + method: 'resources/read', + params: { + ...params, + uri: originalUri, + }, + }, + z.any(), + ) + return { + jsonrpc: '2.0', + id, + result: response, + } + } catch (err: any) { + return { + jsonrpc: '2.0', + id, + error: { + code: err.code || -32000, + message: err.message || 'Resource read failed', + }, + } as any + } + } + + return { + jsonrpc: '2.0', + id, + error: { + code: -32601, + message: `Method not found: ${method}`, + }, + } as any + } + + getServers(): Map { + return this.servers + } + + async cleanup(): Promise { + for (const [name, server] of this.servers) { + try { + if (server.child) { + server.child.kill() + } + this.logger.info(`Cleaned up server: ${name}`) + } catch (err) { + this.logger.error(`Error cleaning up server ${name}: ${err}`) + } + } + this.servers.clear() + } +} diff --git a/src/lib/onSignals.ts b/src/lib/onSignals.ts new file mode 100644 index 0000000..3061560 --- /dev/null +++ b/src/lib/onSignals.ts @@ -0,0 +1,39 @@ +import { Logger } from '../types.js' + +export interface OnSignalsOptions { + logger: Logger + cleanup?: () => void +} + +/** + * Sets up signal handlers for graceful shutdown. + * + * @param options Configuration options + * @param options.logger Logger instance + * @param options.cleanup Optional cleanup function to be called before exit + */ +export function onSignals(options: OnSignalsOptions): void { + const { logger, cleanup } = options + + const handleSignal = (signal: string) => { + logger.info(`Caught ${signal}. Exiting...`) + if (cleanup) { + cleanup() + } + process.exit(0) + } + + process.on('SIGINT', () => handleSignal('SIGINT')) + + process.on('SIGTERM', () => handleSignal('SIGTERM')) + + process.on('SIGHUP', () => handleSignal('SIGHUP')) + + process.stdin.on('close', () => { + logger.info('stdin closed. Exiting...') + if (cleanup) { + cleanup() + } + process.exit(0) + }) +} diff --git a/src/lib/serializeCorsOrigin.ts b/src/lib/serializeCorsOrigin.ts new file mode 100644 index 0000000..15af6af --- /dev/null +++ b/src/lib/serializeCorsOrigin.ts @@ -0,0 +1,14 @@ +import type { CorsOptions } from 'cors' + +export const serializeCorsOrigin = ({ + corsOrigin, +}: { + corsOrigin: CorsOptions['origin'] +}) => + JSON.stringify(corsOrigin, (_key, value) => { + if (value instanceof RegExp) { + return value.toString() + } + + return value + }) diff --git a/src/lib/sessionAccessCounter.ts b/src/lib/sessionAccessCounter.ts new file mode 100644 index 0000000..670a2f1 --- /dev/null +++ b/src/lib/sessionAccessCounter.ts @@ -0,0 +1,118 @@ +import { Logger } from '../types.js' + +export class SessionAccessCounter { + private sessions: Map< + string, + { accessCount: number } | { timeout: NodeJS.Timeout } + > = new Map() + + constructor( + public timeout: number, + public cleanup: (sessionId: string) => unknown, + public logger: Logger, + ) {} + + inc(sessionId: string, reason: string) { + this.logger.info( + `SessionAccessCounter.inc() ${sessionId}, caused by ${reason}`, + ) + + const session = this.sessions.get(sessionId) + + if (!session) { + // New session + this.logger.info( + `Session access count 0 -> 1 for ${sessionId} (new session)`, + ) + this.sessions.set(sessionId, { accessCount: 1 }) + return + } + + if ('timeout' in session) { + // Clear pending cleanup and reactivate + this.logger.info( + `Session access count 0 -> 1, clearing cleanup timeout for ${sessionId}`, + ) + clearTimeout(session.timeout) + this.sessions.set(sessionId, { accessCount: 1 }) + } else { + // Increment active session + this.logger.info( + `Session access count ${session.accessCount} -> ${session.accessCount + 1} for ${sessionId}`, + ) + session.accessCount++ + } + } + + dec(sessionId: string, reason: string) { + this.logger.info( + `SessionAccessCounter.dec() ${sessionId}, caused by ${reason}`, + ) + + const session = this.sessions.get(sessionId) + + if (!session) { + this.logger.error( + `Called dec() on non-existent session ${sessionId}, ignoring`, + ) + return + } + + if ('timeout' in session) { + this.logger.error( + `Called dec() on session ${sessionId} that is already pending cleanup, ignoring`, + ) + return + } + + if (session.accessCount <= 0) { + throw new Error( + `Invalid access count ${session.accessCount} for session ${sessionId}`, + ) + } + + session.accessCount-- + this.logger.info( + `Session access count ${session.accessCount + 1} -> ${session.accessCount} for ${sessionId}`, + ) + + if (session.accessCount === 0) { + this.logger.info( + `Session access count reached 0, setting cleanup timeout for ${sessionId}`, + ) + + this.sessions.set(sessionId, { + timeout: setTimeout(() => { + this.logger.info(`Session ${sessionId} timed out, cleaning up`) + this.sessions.delete(sessionId) + this.cleanup(sessionId) + }, this.timeout), + }) + } + } + + clear(sessionId: string, runCleanup: boolean, reason: string) { + this.logger.info( + `SessionAccessCounter.clear() ${sessionId}, caused by ${reason}`, + ) + + const session = this.sessions.get(sessionId) + if (!session) { + this.logger.info(`Attempted to clear non-existent session ${sessionId}`) + return + } + + // Clear any pending timeout + if ('timeout' in session) { + clearTimeout(session.timeout) + } + + // Remove from tracking + this.sessions.delete(sessionId) + + // Run cleanup if requested + if (runCleanup) { + this.cleanup(sessionId) + } + } +} diff --git a/src/server/websocket.ts b/src/server/websocket.ts new file mode 100644 index 0000000..ad6be52 --- /dev/null +++ b/src/server/websocket.ts @@ -0,0 +1,121 @@ +import { + Transport, + TransportSendOptions, +} from '@modelcontextprotocol/sdk/shared/transport.js' +import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js' +import { v4 as uuidv4 } from 'uuid' +import { WebSocket, WebSocketServer } from 'ws' +import { Server } from 'http' + +export class WebSocketServerTransport implements Transport { + private wss!: WebSocketServer + private clients: Map = new Map() + + onclose?: () => void + onerror?: (err: Error) => void + private messageHandler?: (msg: JSONRPCMessage, clientId: string) => void + onconnection?: (clientId: string) => void + ondisconnection?: (clientId: string) => void + + set onmessage(handler: ((message: JSONRPCMessage) => void) | undefined) { + this.messageHandler = handler + ? (msg, clientId) => { + // @ts-ignore + if (msg.id === undefined) { + console.log('Broadcast message:', msg) + return handler(msg) + } + // @ts-ignore + return handler({ + ...msg, + // @ts-ignore + id: clientId + ':' + msg.id, + }) + } + : undefined + } + + constructor({ path, server }: { path: string; server: Server }) { + this.wss = new WebSocketServer({ + path, + server, + }) + } + + async start(): Promise { + this.wss.on('connection', (ws: WebSocket) => { + const clientId = uuidv4() + this.clients.set(clientId, ws) + this.onconnection?.(clientId) + + ws.on('message', (data: Buffer) => { + try { + const msg = JSON.parse(data.toString()) + this.messageHandler?.(msg, clientId) + } catch (err) { + this.onerror?.(new Error(`Failed to parse message: ${err}`)) + } + }) + + ws.on('close', () => { + this.clients.delete(clientId) + this.ondisconnection?.(clientId) + }) + + ws.on('error', (err: Error) => { + this.onerror?.(err) + }) + }) + } + + async send( + msg: JSONRPCMessage, + options?: TransportSendOptions | string, + ): Promise { + // decide if they passed a raw clientId (legacy) or options object + const clientId = typeof options === 'string' ? options : undefined + + // if your protocol mangles IDs to include clientId, strip it off + const [cId, rawId] = clientId?.split(':') ?? [] + if (rawId !== undefined) { + // @ts-ignore + msg.id = parseInt(rawId, 10) + } + + const payload = JSON.stringify(msg) + + if (cId) { + // send only to the one client + const ws = this.clients.get(cId) + if (ws?.readyState === WebSocket.OPEN) { + ws.send(payload) + } else { + this.clients.delete(cId) + this.ondisconnection?.(cId) + } + } else { + // broadcast to everyone + for (const [id, ws] of this.clients) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(payload) + } else { + this.clients.delete(id) + this.ondisconnection?.(id) + } + } + } + } + + async broadcast(msg: JSONRPCMessage): Promise { + return this.send(msg) + } + + async close(): Promise { + return new Promise((resolve) => { + this.wss.close(() => { + this.clients.clear() + resolve() + }) + }) + } +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..33a9b16 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,6 @@ +export interface Logger { + info: (...args: any[]) => void + error: (...args: any[]) => void + debug: (...args: any[]) => void + warn: (...args: any[]) => void +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..756b564 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,37 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "strict": false, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": false, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "downlevelIteration": true, + "allowImportingTsExtensions": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "types": ["node"], + "noImplicitAny": false, + "noImplicitReturns": false, + "noImplicitThis": false, + "resolveJsonModule": true, + "allowJs": true, + "noErrorTruncation": true, + "isolatedModules": false, + "exactOptionalPropertyTypes": false, + "noImplicitOverride": false, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedIndexedAccess": false + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "ts-node": { + "esm": true + } +}