mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat: support Streamable HTTP transport for downstream (#32)
This commit is contained in:
@@ -23,7 +23,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
"@modelcontextprotocol/sdk": "^1.10.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.3",
|
"@radix-ui/react-accordion": "^1.2.3",
|
||||||
"@radix-ui/react-slot": "^1.1.2",
|
"@radix-ui/react-slot": "^1.1.2",
|
||||||
"@shadcn/ui": "^0.0.4",
|
"@shadcn/ui": "^0.0.4",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -9,8 +9,8 @@ importers:
|
|||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@modelcontextprotocol/sdk':
|
'@modelcontextprotocol/sdk':
|
||||||
specifier: ^1.9.0
|
specifier: ^1.10.2
|
||||||
version: 1.9.0
|
version: 1.10.2
|
||||||
'@radix-ui/react-accordion':
|
'@radix-ui/react-accordion':
|
||||||
specifier: ^1.2.3
|
specifier: ^1.2.3
|
||||||
version: 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
version: 1.2.3(@types/react-dom@19.0.4(@types/react@19.0.12))(@types/react@19.0.12)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||||
@@ -867,8 +867,8 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.9':
|
'@jridgewell/trace-mapping@0.3.9':
|
||||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.9.0':
|
'@modelcontextprotocol/sdk@1.10.2':
|
||||||
resolution: {integrity: sha512-Jq2EUCQpe0iyO5FGpzVYDNFR6oR53AIrwph9yWl7uSc7IWUMsrmpmSaTGra5hQNunXpM+9oit85p924jWuHzUA==}
|
resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@next/env@15.2.4':
|
'@next/env@15.2.4':
|
||||||
@@ -4268,7 +4268,7 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.0
|
'@jridgewell/sourcemap-codec': 1.5.0
|
||||||
|
|
||||||
'@modelcontextprotocol/sdk@1.9.0':
|
'@modelcontextprotocol/sdk@1.10.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import path from 'path';
|
|||||||
import { initMcpServer } from './services/mcpService.js';
|
import { initMcpServer } from './services/mcpService.js';
|
||||||
import { initMiddlewares } from './middlewares/index.js';
|
import { initMiddlewares } from './middlewares/index.js';
|
||||||
import { initRoutes } from './routes/index.js';
|
import { initRoutes } from './routes/index.js';
|
||||||
import { handleSseConnection, handleSseMessage } from './services/sseService.js';
|
import {
|
||||||
|
handleSseConnection,
|
||||||
|
handleSseMessage,
|
||||||
|
handleMcpPostRequest,
|
||||||
|
handleMcpOtherRequest,
|
||||||
|
} from './services/sseService.js';
|
||||||
import { migrateUserData } from './utils/migration.js';
|
import { migrateUserData } from './utils/migration.js';
|
||||||
import { initializeDefaultUser } from './models/User.js';
|
import { initializeDefaultUser } from './models/User.js';
|
||||||
|
|
||||||
@@ -34,6 +39,9 @@ export class AppServer {
|
|||||||
console.log('MCP server initialized successfully');
|
console.log('MCP server initialized successfully');
|
||||||
this.app.get('/sse/:group?', (req, res) => handleSseConnection(req, res));
|
this.app.get('/sse/:group?', (req, res) => handleSseConnection(req, res));
|
||||||
this.app.post('/messages', handleSseMessage);
|
this.app.post('/messages', handleSseMessage);
|
||||||
|
this.app.post('/mcp/:group?', handleMcpPostRequest);
|
||||||
|
this.app.get('/mcp/:group?', handleMcpOtherRequest);
|
||||||
|
this.app.delete('/mcp/:group?', handleMcpOtherRequest);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Error initializing MCP server:', error);
|
console.error('Error initializing MCP server:', error);
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
||||||
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
||||||
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
||||||
|
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
||||||
import { getMcpServer } from './mcpService.js';
|
import { getMcpServer } from './mcpService.js';
|
||||||
import { loadSettings } from '../config/index.js';
|
import { loadSettings } from '../config/index.js';
|
||||||
|
|
||||||
const transports: { [sessionId: string]: { transport: SSEServerTransport; group: string } } = {};
|
const transports: { [sessionId: string]: { transport: Transport; group: string } } = {};
|
||||||
|
|
||||||
export const getGroup = (sessionId: string): string => {
|
export const getGroup = (sessionId: string): string => {
|
||||||
return transports[sessionId]?.group || '';
|
return transports[sessionId]?.group || '';
|
||||||
@@ -44,13 +48,72 @@ export const handleSseMessage = async (req: Request, res: Response): Promise<voi
|
|||||||
req.query.group = group;
|
req.query.group = group;
|
||||||
console.log(`Received message for sessionId: ${sessionId} in group: ${group}`);
|
console.log(`Received message for sessionId: ${sessionId} in group: ${group}`);
|
||||||
if (transport) {
|
if (transport) {
|
||||||
await transport.handlePostMessage(req, res);
|
await (transport as SSEServerTransport).handlePostMessage(req, res);
|
||||||
} else {
|
} else {
|
||||||
console.error(`No transport found for sessionId: ${sessionId}`);
|
console.error(`No transport found for sessionId: ${sessionId}`);
|
||||||
res.status(400).send('No transport found for sessionId');
|
res.status(400).send('No transport found for sessionId');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleMcpPostRequest = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
console.log('Handling MCP post request');
|
||||||
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||||
|
const group = req.params.group;
|
||||||
|
const settings = loadSettings();
|
||||||
|
const routingConfig = settings.systemConfig?.routing || {
|
||||||
|
enableGlobalRoute: true,
|
||||||
|
enableGroupNameRoute: true,
|
||||||
|
};
|
||||||
|
if (!group && !routingConfig.enableGlobalRoute) {
|
||||||
|
res.status(403).send('Global routes are disabled. Please specify a group ID.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let transport: StreamableHTTPServerTransport;
|
||||||
|
if (sessionId && transports[sessionId]) {
|
||||||
|
transport = transports[sessionId].transport as StreamableHTTPServerTransport;
|
||||||
|
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||||
|
transport = new StreamableHTTPServerTransport({
|
||||||
|
sessionIdGenerator: () => randomUUID(),
|
||||||
|
onsessioninitialized: (sessionId) => {
|
||||||
|
transports[sessionId] = { transport, group };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
transport.onclose = () => {
|
||||||
|
if (transport.sessionId) {
|
||||||
|
delete transports[transport.sessionId];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await getMcpServer().connect(transport);
|
||||||
|
} else {
|
||||||
|
res.status(400).json({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
error: {
|
||||||
|
code: -32000,
|
||||||
|
message: 'Bad Request: No valid session ID provided',
|
||||||
|
},
|
||||||
|
id: null,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await transport.handleRequest(req, res, req.body);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const handleMcpOtherRequest = async (req: Request, res: Response) => {
|
||||||
|
console.log('Handling MCP other request');
|
||||||
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||||
|
if (!sessionId || !transports[sessionId]) {
|
||||||
|
res.status(400).send('Invalid or missing session ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { transport } = transports[sessionId];
|
||||||
|
await (transport as StreamableHTTPServerTransport).handleRequest(req, res);
|
||||||
|
};
|
||||||
|
|
||||||
export const getConnectionCount = (): number => {
|
export const getConnectionCount = (): number => {
|
||||||
return Object.keys(transports).length;
|
return Object.keys(transports).length;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user