135 lines
5.7 KiB
JavaScript
135 lines
5.7 KiB
JavaScript
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
|