First commit
This commit is contained in:
parent
ea6581494c
commit
7fc5633fc4
115
README.md
Normal file
115
README.md
Normal file
@ -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 <path>`: **(required)** Path to a JSON configuration file (see below)
|
||||
- `--port <number>`: Port to run the proxy server on (default: `3006`)
|
||||
- `--baseUrl <url>`: Base URL for SSE clients (default: `http://localhost:<port>`)
|
||||
- `--ssePath <path>`: Path for SSE subscriptions (default: `/sse`)
|
||||
- `--messagePath <path>`: Path for SSE messages (default: `/message`)
|
||||
- `--logLevel <info|none>`: 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 <path>`: One or more endpoints returning `"ok"` (can be used multiple times)
|
||||
- `--timeout <ms>`: 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": "<your_notion_token_here>"
|
||||
}
|
||||
},
|
||||
"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:<port>/sse`
|
||||
- **POST messages**: `http://localhost:<port>/message`
|
||||
- **Streamable HTTP endpoint**: `http://localhost:<port>/mcp`
|
||||
- **Websocket endpoint**: `ws://localhost:<port>/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.
|
||||
16
dist/gateways/configToSse.d.ts
vendored
Normal file
16
dist/gateways/configToSse.d.ts
vendored
Normal file
@ -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<string, string>;
|
||||
}
|
||||
export declare function configToSse(args: ConfigToSseArgs): Promise<void>;
|
||||
//# sourceMappingURL=configToSse.d.ts.map
|
||||
1
dist/gateways/configToSse.d.ts.map
vendored
Normal file
1
dist/gateways/configToSse.d.ts.map
vendored
Normal file
@ -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"}
|
||||
144
dist/gateways/configToSse.js
vendored
Normal file
144
dist/gateways/configToSse.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/configToSse.js.map
vendored
Normal file
1
dist/gateways/configToSse.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
16
dist/gateways/configToStreamableHttp.d.ts
vendored
Normal file
16
dist/gateways/configToStreamableHttp.d.ts
vendored
Normal file
@ -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<string, string>;
|
||||
stateless?: boolean;
|
||||
sessionTimeout?: number | null;
|
||||
}
|
||||
export declare function configToStreamableHttp(args: ConfigToStreamableHttpArgs): Promise<void>;
|
||||
//# sourceMappingURL=configToStreamableHttp.d.ts.map
|
||||
1
dist/gateways/configToStreamableHttp.d.ts.map
vendored
Normal file
1
dist/gateways/configToStreamableHttp.d.ts.map
vendored
Normal file
@ -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"}
|
||||
287
dist/gateways/configToStreamableHttp.js
vendored
Normal file
287
dist/gateways/configToStreamableHttp.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/configToStreamableHttp.js.map
vendored
Normal file
1
dist/gateways/configToStreamableHttp.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
14
dist/gateways/configToWs.d.ts
vendored
Normal file
14
dist/gateways/configToWs.d.ts
vendored
Normal file
@ -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<string, string>;
|
||||
}
|
||||
export declare function configToWs(args: ConfigToWsArgs): Promise<void>;
|
||||
//# sourceMappingURL=configToWs.d.ts.map
|
||||
1
dist/gateways/configToWs.d.ts.map
vendored
Normal file
1
dist/gateways/configToWs.d.ts.map
vendored
Normal file
@ -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"}
|
||||
146
dist/gateways/configToWs.js
vendored
Normal file
146
dist/gateways/configToWs.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/configToWs.js.map
vendored
Normal file
1
dist/gateways/configToWs.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
16
dist/gateways/sseToSse.d.ts
vendored
Normal file
16
dist/gateways/sseToSse.d.ts
vendored
Normal file
@ -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<string, string>;
|
||||
}
|
||||
export declare function sseToSse(args: SseToSseArgs): Promise<void>;
|
||||
//# sourceMappingURL=sseToSse.d.ts.map
|
||||
1
dist/gateways/sseToSse.d.ts.map
vendored
Normal file
1
dist/gateways/sseToSse.d.ts.map
vendored
Normal file
@ -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"}
|
||||
234
dist/gateways/sseToSse.js
vendored
Normal file
234
dist/gateways/sseToSse.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/sseToSse.js.map
vendored
Normal file
1
dist/gateways/sseToSse.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
8
dist/gateways/sseToStdio.d.ts
vendored
Normal file
8
dist/gateways/sseToStdio.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import { Logger } from '../types.js';
|
||||
export interface SseToStdioArgs {
|
||||
sseUrl: string;
|
||||
logger: Logger;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
export declare function sseToStdio(args: SseToStdioArgs): Promise<void>;
|
||||
//# sourceMappingURL=sseToStdio.d.ts.map
|
||||
1
dist/gateways/sseToStdio.d.ts.map
vendored
Normal file
1
dist/gateways/sseToStdio.d.ts.map
vendored
Normal file
@ -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"}
|
||||
140
dist/gateways/sseToStdio.js
vendored
Normal file
140
dist/gateways/sseToStdio.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/sseToStdio.js.map
vendored
Normal file
1
dist/gateways/sseToStdio.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
14
dist/gateways/sseToWs.d.ts
vendored
Normal file
14
dist/gateways/sseToWs.d.ts
vendored
Normal file
@ -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<string, string>;
|
||||
}
|
||||
export declare function sseToWs(args: SseToWsArgs): Promise<void>;
|
||||
//# sourceMappingURL=sseToWs.d.ts.map
|
||||
1
dist/gateways/sseToWs.d.ts.map
vendored
Normal file
1
dist/gateways/sseToWs.d.ts.map
vendored
Normal file
@ -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"}
|
||||
223
dist/gateways/sseToWs.js
vendored
Normal file
223
dist/gateways/sseToWs.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/sseToWs.js.map
vendored
Normal file
1
dist/gateways/sseToWs.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
16
dist/gateways/stdioToSse.d.ts
vendored
Normal file
16
dist/gateways/stdioToSse.d.ts
vendored
Normal file
@ -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<string, string>;
|
||||
}
|
||||
export declare function stdioToSse(args: StdioToSseArgs): Promise<void>;
|
||||
//# sourceMappingURL=stdioToSse.d.ts.map
|
||||
1
dist/gateways/stdioToSse.d.ts.map
vendored
Normal file
1
dist/gateways/stdioToSse.d.ts.map
vendored
Normal file
@ -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"}
|
||||
134
dist/gateways/stdioToSse.js
vendored
Normal file
134
dist/gateways/stdioToSse.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/stdioToSse.js.map
vendored
Normal file
1
dist/gateways/stdioToSse.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
15
dist/gateways/stdioToStatefulStreamableHttp.d.ts
vendored
Normal file
15
dist/gateways/stdioToStatefulStreamableHttp.d.ts
vendored
Normal file
@ -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<string, string>;
|
||||
sessionTimeout: number | null;
|
||||
}
|
||||
export declare function stdioToStatefulStreamableHttp(args: StdioToStreamableHttpArgs): Promise<void>;
|
||||
//# sourceMappingURL=stdioToStatefulStreamableHttp.d.ts.map
|
||||
1
dist/gateways/stdioToStatefulStreamableHttp.d.ts.map
vendored
Normal file
1
dist/gateways/stdioToStatefulStreamableHttp.d.ts.map
vendored
Normal file
@ -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"}
|
||||
189
dist/gateways/stdioToStatefulStreamableHttp.js
vendored
Normal file
189
dist/gateways/stdioToStatefulStreamableHttp.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/stdioToStatefulStreamableHttp.js.map
vendored
Normal file
1
dist/gateways/stdioToStatefulStreamableHttp.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
14
dist/gateways/stdioToStatelessStreamableHttp.d.ts
vendored
Normal file
14
dist/gateways/stdioToStatelessStreamableHttp.d.ts
vendored
Normal file
@ -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<string, string>;
|
||||
}
|
||||
export declare function stdioToStatelessStreamableHttp(args: StdioToStreamableHttpArgs): Promise<void>;
|
||||
//# sourceMappingURL=stdioToStatelessStreamableHttp.d.ts.map
|
||||
1
dist/gateways/stdioToStatelessStreamableHttp.d.ts.map
vendored
Normal file
1
dist/gateways/stdioToStatelessStreamableHttp.d.ts.map
vendored
Normal file
@ -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"}
|
||||
132
dist/gateways/stdioToStatelessStreamableHttp.js
vendored
Normal file
132
dist/gateways/stdioToStatelessStreamableHttp.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/stdioToStatelessStreamableHttp.js.map
vendored
Normal file
1
dist/gateways/stdioToStatelessStreamableHttp.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
13
dist/gateways/stdioToWs.d.ts
vendored
Normal file
13
dist/gateways/stdioToWs.d.ts
vendored
Normal file
@ -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<void>;
|
||||
//# sourceMappingURL=stdioToWs.d.ts.map
|
||||
1
dist/gateways/stdioToWs.d.ts.map
vendored
Normal file
1
dist/gateways/stdioToWs.d.ts.map
vendored
Normal file
@ -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"}
|
||||
115
dist/gateways/stdioToWs.js
vendored
Normal file
115
dist/gateways/stdioToWs.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/stdioToWs.js.map
vendored
Normal file
1
dist/gateways/stdioToWs.js.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"stdioToWs.js","sourceRoot":"","sources":["../../src/gateways/stdioToWs.ts"],"names":[],"mappings":"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"}
|
||||
16
dist/gateways/streamableHttpToSse.d.ts
vendored
Normal file
16
dist/gateways/streamableHttpToSse.d.ts
vendored
Normal file
@ -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<string, string>;
|
||||
}
|
||||
export declare function streamableHttpToSse(args: StreamableHttpToSseArgs): Promise<void>;
|
||||
//# sourceMappingURL=streamableHttpToSse.d.ts.map
|
||||
1
dist/gateways/streamableHttpToSse.d.ts.map
vendored
Normal file
1
dist/gateways/streamableHttpToSse.d.ts.map
vendored
Normal file
@ -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"}
|
||||
229
dist/gateways/streamableHttpToSse.js
vendored
Normal file
229
dist/gateways/streamableHttpToSse.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/streamableHttpToSse.js.map
vendored
Normal file
1
dist/gateways/streamableHttpToSse.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
8
dist/gateways/streamableHttpToStdio.d.ts
vendored
Normal file
8
dist/gateways/streamableHttpToStdio.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
import { Logger } from '../types.js';
|
||||
export interface StreamableHttpToStdioArgs {
|
||||
streamableHttpUrl: string;
|
||||
logger: Logger;
|
||||
headers: Record<string, string>;
|
||||
}
|
||||
export declare function streamableHttpToStdio(args: StreamableHttpToStdioArgs): Promise<void>;
|
||||
//# sourceMappingURL=streamableHttpToStdio.d.ts.map
|
||||
1
dist/gateways/streamableHttpToStdio.d.ts.map
vendored
Normal file
1
dist/gateways/streamableHttpToStdio.d.ts.map
vendored
Normal file
@ -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"}
|
||||
135
dist/gateways/streamableHttpToStdio.js
vendored
Normal file
135
dist/gateways/streamableHttpToStdio.js
vendored
Normal file
@ -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
|
||||
1
dist/gateways/streamableHttpToStdio.js.map
vendored
Normal file
1
dist/gateways/streamableHttpToStdio.js.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"streamableHttpToStdio.js","sourceRoot":"","sources":["../../src/gateways/streamableHttpToStdio.ts"],"names":[],"mappings":"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"}
|
||||
46
dist/index.d.ts
vendored
Normal file
46
dist/index.d.ts
vendored
Normal file
@ -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
|
||||
1
dist/index.d.ts.map
vendored
Normal file
1
dist/index.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG"}
|
||||
424
dist/index.js
vendored
Normal file
424
dist/index.js
vendored
Normal file
@ -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
|
||||
1
dist/index.js.map
vendored
Normal file
1
dist/index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
78
dist/lib/config.d.ts
vendored
Normal file
78
dist/lib/config.d.ts
vendored
Normal file
@ -0,0 +1,78 @@
|
||||
import { z } from 'zod';
|
||||
export declare const McpServerConfigSchema: z.ZodObject<{
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
type: z.ZodOptional<z.ZodEnum<["stdio", "sse", "streamable-http"]>>;
|
||||
command: z.ZodOptional<z.ZodString>;
|
||||
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
||||
url: z.ZodOptional<z.ZodString>;
|
||||
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
||||
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
headers?: Record<string, string>;
|
||||
name?: string;
|
||||
type?: "stdio" | "sse" | "streamable-http";
|
||||
url?: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}, {
|
||||
headers?: Record<string, string>;
|
||||
name?: string;
|
||||
type?: "stdio" | "sse" | "streamable-http";
|
||||
url?: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}>;
|
||||
export declare const ConfigSchema: z.ZodObject<{
|
||||
mcpServers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
||||
name: z.ZodOptional<z.ZodString>;
|
||||
type: z.ZodOptional<z.ZodEnum<["stdio", "sse", "streamable-http"]>>;
|
||||
command: z.ZodOptional<z.ZodString>;
|
||||
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
||||
url: z.ZodOptional<z.ZodString>;
|
||||
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
||||
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
headers?: Record<string, string>;
|
||||
name?: string;
|
||||
type?: "stdio" | "sse" | "streamable-http";
|
||||
url?: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}, {
|
||||
headers?: Record<string, string>;
|
||||
name?: string;
|
||||
type?: "stdio" | "sse" | "streamable-http";
|
||||
url?: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}>>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
mcpServers?: Record<string, {
|
||||
headers?: Record<string, string>;
|
||||
name?: string;
|
||||
type?: "stdio" | "sse" | "streamable-http";
|
||||
url?: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}>;
|
||||
}, {
|
||||
mcpServers?: Record<string, {
|
||||
headers?: Record<string, string>;
|
||||
name?: string;
|
||||
type?: "stdio" | "sse" | "streamable-http";
|
||||
url?: string;
|
||||
command?: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}>;
|
||||
}>;
|
||||
export type McpServerConfig = z.infer<typeof McpServerConfigSchema>;
|
||||
export type Config = z.infer<typeof ConfigSchema>;
|
||||
export declare function detectServerType(config: McpServerConfig): 'stdio' | 'sse' | 'streamable-http';
|
||||
export declare function loadConfig(configPath: string): Config;
|
||||
//# sourceMappingURL=config.d.ts.map
|
||||
1
dist/lib/config.d.ts.map
vendored
Normal file
1
dist/lib/config.d.ts.map
vendored
Normal file
@ -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"}
|
||||
60
dist/lib/config.js
vendored
Normal file
60
dist/lib/config.js
vendored
Normal file
@ -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
|
||||
1
dist/lib/config.js.map
vendored
Normal file
1
dist/lib/config.js.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
|
||||
6
dist/lib/corsOrigin.d.ts
vendored
Normal file
6
dist/lib/corsOrigin.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
export declare const corsOrigin: ({ argv, }: {
|
||||
argv: {
|
||||
cors: (string | number)[] | undefined;
|
||||
};
|
||||
}) => (string | RegExp)[] | "*";
|
||||
//# sourceMappingURL=corsOrigin.d.ts.map
|
||||
1
dist/lib/corsOrigin.d.ts.map
vendored
Normal file
1
dist/lib/corsOrigin.d.ts.map
vendored
Normal file
@ -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"}
|
||||
24
dist/lib/corsOrigin.js
vendored
Normal file
24
dist/lib/corsOrigin.js
vendored
Normal file
@ -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
|
||||
1
dist/lib/corsOrigin.js.map
vendored
Normal file
1
dist/lib/corsOrigin.js.map
vendored
Normal file
@ -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"}
|
||||
6
dist/lib/getLogger.d.ts
vendored
Normal file
6
dist/lib/getLogger.d.ts
vendored
Normal file
@ -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
|
||||
1
dist/lib/getLogger.d.ts.map
vendored
Normal file
1
dist/lib/getLogger.d.ts.map
vendored
Normal file
@ -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"}
|
||||
55
dist/lib/getLogger.js
vendored
Normal file
55
dist/lib/getLogger.js
vendored
Normal file
@ -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
|
||||
1
dist/lib/getLogger.js.map
vendored
Normal file
1
dist/lib/getLogger.js.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"getLogger.js","sourceRoot":"","sources":["../../src/lib/getLogger.ts"],"names":[],"mappings":"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"}
|
||||
2
dist/lib/getVersion.d.ts
vendored
Normal file
2
dist/lib/getVersion.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare function getVersion(): string;
|
||||
//# sourceMappingURL=getVersion.d.ts.map
|
||||
1
dist/lib/getVersion.d.ts.map
vendored
Normal file
1
dist/lib/getVersion.d.ts.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"getVersion.d.ts","sourceRoot":"","sources":["../../src/lib/getVersion.ts"],"names":[],"mappings":"AAOA,wBAAgB,UAAU,IAAI,MAAM,CAanC"}
|
||||
17
dist/lib/getVersion.js
vendored
Normal file
17
dist/lib/getVersion.js
vendored
Normal file
@ -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
|
||||
1
dist/lib/getVersion.js.map
vendored
Normal file
1
dist/lib/getVersion.js.map
vendored
Normal file
@ -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"}
|
||||
9
dist/lib/headers.d.ts
vendored
Normal file
9
dist/lib/headers.d.ts
vendored
Normal file
@ -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<string, string>;
|
||||
//# sourceMappingURL=headers.d.ts.map
|
||||
1
dist/lib/headers.d.ts.map
vendored
Normal file
1
dist/lib/headers.d.ts.map
vendored
Normal file
@ -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"}
|
||||
32
dist/lib/headers.js
vendored
Normal file
32
dist/lib/headers.js
vendored
Normal file
@ -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
|
||||
1
dist/lib/headers.js.map
vendored
Normal file
1
dist/lib/headers.js.map
vendored
Normal file
@ -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"}
|
||||
24
dist/lib/mcpServerManager.d.ts
vendored
Normal file
24
dist/lib/mcpServerManager.d.ts
vendored
Normal file
@ -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<void>;
|
||||
handleRequest(request: JSONRPCRequest): Promise<JSONRPCResponse>;
|
||||
getServers(): Map<string, ManagedServer>;
|
||||
cleanup(): Promise<void>;
|
||||
}
|
||||
//# sourceMappingURL=mcpServerManager.d.ts.map
|
||||
1
dist/lib/mcpServerManager.d.ts.map
vendored
Normal file
1
dist/lib/mcpServerManager.d.ts.map
vendored
Normal file
@ -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"}
|
||||
349
dist/lib/mcpServerManager.js
vendored
Normal file
349
dist/lib/mcpServerManager.js
vendored
Normal file
@ -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
|
||||
1
dist/lib/mcpServerManager.js.map
vendored
Normal file
1
dist/lib/mcpServerManager.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
14
dist/lib/onSignals.d.ts
vendored
Normal file
14
dist/lib/onSignals.d.ts
vendored
Normal file
@ -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
|
||||
1
dist/lib/onSignals.d.ts.map
vendored
Normal file
1
dist/lib/onSignals.d.ts.map
vendored
Normal file
@ -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"}
|
||||
28
dist/lib/onSignals.js
vendored
Normal file
28
dist/lib/onSignals.js
vendored
Normal file
@ -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
|
||||
1
dist/lib/onSignals.js.map
vendored
Normal file
1
dist/lib/onSignals.js.map
vendored
Normal file
@ -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"}
|
||||
5
dist/lib/serializeCorsOrigin.d.ts
vendored
Normal file
5
dist/lib/serializeCorsOrigin.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
import type { CorsOptions } from 'cors';
|
||||
export declare const serializeCorsOrigin: ({ corsOrigin, }: {
|
||||
corsOrigin: CorsOptions["origin"];
|
||||
}) => string;
|
||||
//# sourceMappingURL=serializeCorsOrigin.d.ts.map
|
||||
1
dist/lib/serializeCorsOrigin.d.ts.map
vendored
Normal file
1
dist/lib/serializeCorsOrigin.d.ts.map
vendored
Normal file
@ -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"}
|
||||
7
dist/lib/serializeCorsOrigin.js
vendored
Normal file
7
dist/lib/serializeCorsOrigin.js
vendored
Normal file
@ -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
|
||||
1
dist/lib/serializeCorsOrigin.js.map
vendored
Normal file
1
dist/lib/serializeCorsOrigin.js.map
vendored
Normal file
@ -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"}
|
||||
12
dist/lib/sessionAccessCounter.d.ts
vendored
Normal file
12
dist/lib/sessionAccessCounter.d.ts
vendored
Normal file
@ -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
|
||||
1
dist/lib/sessionAccessCounter.d.ts.map
vendored
Normal file
1
dist/lib/sessionAccessCounter.d.ts.map
vendored
Normal file
@ -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"}
|
||||
78
dist/lib/sessionAccessCounter.js
vendored
Normal file
78
dist/lib/sessionAccessCounter.js
vendored
Normal file
@ -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
|
||||
1
dist/lib/sessionAccessCounter.js.map
vendored
Normal file
1
dist/lib/sessionAccessCounter.js.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"sessionAccessCounter.js","sourceRoot":"","sources":["../../src/lib/sessionAccessCounter.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,oBAAoB;IAOtB;IACA;IACA;IARD,QAAQ,GAGZ,IAAI,GAAG,EAAE,CAAA;IAEb,YACS,OAAe,EACf,OAAuC,EACvC,MAAc;QAFd,YAAO,GAAP,OAAO,CAAQ;QACf,YAAO,GAAP,OAAO,CAAgC;QACvC,WAAM,GAAN,MAAM,CAAQ;IACpB,CAAC;IAEJ,GAAG,CAAC,SAAiB,EAAE,MAAc;QACnC,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,8BAA8B,SAAS,eAAe,MAAM,EAAE,CAC/D,CAAA;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAE5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,cAAc;YACd,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,mCAAmC,SAAS,gBAAgB,CAC7D,CAAA;YACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,CAAA;YAChD,OAAM;QACR,CAAC;QAED,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC;YACzB,uCAAuC;YACvC,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,6DAA6D,SAAS,EAAE,CACzE,CAAA;YACD,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;YAC7B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC,CAAA;QAClD,CAAC;aAAM,CAAC;YACN,2BAA2B;YAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,wBAAwB,OAAO,CAAC,WAAW,OAAO,OAAO,CAAC,WAAW,GAAG,CAAC,QAAQ,SAAS,EAAE,CAC7F,CAAA;YACD,OAAO,CAAC,WAAW,EAAE,CAAA;QACvB,CAAC;IACH,CAAC;IAED,GAAG,CAAC,SAAiB,EAAE,MAAc;QACnC,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,8BAA8B,SAAS,eAAe,MAAM,EAAE,CAC/D,CAAA;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAE5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,wCAAwC,SAAS,YAAY,CAC9D,CAAA;YACD,OAAM;QACR,CAAC;QAED,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC;YACzB,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,2BAA2B,SAAS,4CAA4C,CACjF,CAAA;YACD,OAAM;QACR,CAAC;QAED,IAAI,OAAO,CAAC,WAAW,IAAI,CAAC,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,wBAAwB,OAAO,CAAC,WAAW,gBAAgB,SAAS,EAAE,CACvE,CAAA;QACH,CAAC;QAED,OAAO,CAAC,WAAW,EAAE,CAAA;QACrB,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,wBAAwB,OAAO,CAAC,WAAW,GAAG,CAAC,OAAO,OAAO,CAAC,WAAW,QAAQ,SAAS,EAAE,CAC7F,CAAA;QAED,IAAI,OAAO,CAAC,WAAW,KAAK,CAAC,EAAE,CAAC;YAC9B,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,+DAA+D,SAAS,EAAE,CAC3E,CAAA;YAED,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE;gBAC3B,OAAO,EAAE,UAAU,CAAC,GAAG,EAAE;oBACvB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,SAAS,yBAAyB,CAAC,CAAA;oBAC/D,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;oBAC/B,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;gBACzB,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC;aACjB,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAED,KAAK,CAAC,SAAiB,EAAE,UAAmB,EAAE,MAAc;QAC1D,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,gCAAgC,SAAS,eAAe,MAAM,EAAE,CACjE,CAAA;QAED,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,2CAA2C,SAAS,EAAE,CAAC,CAAA;YACxE,OAAM;QACR,CAAC;QAED,4BAA4B;QAC5B,IAAI,SAAS,IAAI,OAAO,EAAE,CAAC;YACzB,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QAC/B,CAAC;QAED,uBAAuB;QACvB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;QAE/B,2BAA2B;QAC3B,IAAI,UAAU,EAAE,CAAC;YACf,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;QACzB,CAAC;IACH,CAAC;CACF"}
|
||||
22
dist/server/websocket.d.ts
vendored
Normal file
22
dist/server/websocket.d.ts
vendored
Normal file
@ -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<void>;
|
||||
send(msg: JSONRPCMessage, options?: TransportSendOptions | string): Promise<void>;
|
||||
broadcast(msg: JSONRPCMessage): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
//# sourceMappingURL=websocket.d.ts.map
|
||||
1
dist/server/websocket.d.ts.map
vendored
Normal file
1
dist/server/websocket.d.ts.map
vendored
Normal file
@ -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"}
|
||||
103
dist/server/websocket.js
vendored
Normal file
103
dist/server/websocket.js
vendored
Normal file
@ -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
|
||||
1
dist/server/websocket.js.map
vendored
Normal file
1
dist/server/websocket.js.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/server/websocket.ts"],"names":[],"mappings":"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"}
|
||||
7
dist/types.d.ts
vendored
Normal file
7
dist/types.d.ts
vendored
Normal file
@ -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
|
||||
1
dist/types.d.ts.map
vendored
Normal file
1
dist/types.d.ts.map
vendored
Normal file
@ -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"}
|
||||
2
dist/types.js
vendored
Normal file
2
dist/types.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export {};
|
||||
//# sourceMappingURL=types.js.map
|
||||
1
dist/types.js.map
vendored
Normal file
1
dist/types.js.map
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
||||
37
mcpconfig-example.json
Normal file
37
mcpconfig-example.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
package.json
Normal file
50
package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
221
src/gateways/configToSse.ts
Normal file
221
src/gateways/configToSse.ts
Normal file
@ -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<string, string>
|
||||
}
|
||||
|
||||
const setResponseHeaders = ({
|
||||
res,
|
||||
headers,
|
||||
}: {
|
||||
res: express.Response
|
||||
headers: Record<string, string>
|
||||
}) =>
|
||||
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')
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user