[v2] Decouple server from express and hono - http framework-agnostic MCP server (#1326)

This commit is contained in:
Konstantin Konstantinov 2026-01-16 12:13:25 +02:00 • committed by GitHub
commit f495077e08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 8193 additions and 15757 deletions

View file

@ -9,8 +9,10 @@
import { randomUUID } from 'node:crypto';
import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
import { type Request, type Response } from 'express';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
// Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults
// The validator supports format validation (email, date, etc.) if ajv-formats is installed
@ -321,7 +323,7 @@ async function main() {
const app = createMcpExpressApp();
// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
// MCP POST endpoint
const mcpPostHandler = async (req: Request, res: Response) => {
@ -331,13 +333,13 @@ async function main() {
}
try {
let transport: StreamableHTTPServerTransport;
let transport: NodeStreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport for this session
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request - create new transport
transport = new StreamableHTTPServerTransport({
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: sessionId => {
// Store the transport by session ID when session is initialized

View file

@ -9,19 +9,16 @@
import { randomUUID } from 'node:crypto';
import { setupAuthServer } from '@modelcontextprotocol/examples-shared';
import type { CallToolResult, ElicitRequestURLParams, ElicitResult, OAuthMetadata } from '@modelcontextprotocol/server';
import {
checkResourceAllowed,
createMcpExpressApp,
createProtectedResourceMetadataRouter,
getOAuthProtectedResourceMetadataUrl,
isInitializeRequest,
mcpAuthMetadataRouter,
McpServer,
requireBearerAuth,
StreamableHTTPServerTransport,
UrlElicitationRequiredError
} from '@modelcontextprotocol/server';
setupAuthServer
} from '@modelcontextprotocol/examples-shared';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult, ElicitRequestURLParams, ElicitResult } from '@modelcontextprotocol/server';
import { isInitializeRequest, McpServer, UrlElicitationRequiredError } from '@modelcontextprotocol/server';
import cors from 'cors';
import type { Request, Response } from 'express';
import express from 'express';
@ -238,63 +235,17 @@ let authMiddleware = null;
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
const oauthMetadata: OAuthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true });
setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: true, demoMode: true });
const tokenVerifier = {
verifyAccessToken: async (token: string) => {
const endpoint = oauthMetadata.introspection_endpoint;
if (!endpoint) {
throw new Error('No token verification endpoint available in metadata');
}
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
token: token
}).toString()
});
if (!response.ok) {
const text = await response.text().catch(() => null);
throw new Error(`Invalid or expired token: ${text}`);
}
const data = (await response.json()) as { aud: string; client_id: string; scope: string; exp: number };
if (!data.aud) {
throw new Error(`Resource Indicator (RFC8707) missing`);
}
if (!checkResourceAllowed({ requestedResource: data.aud, configuredResource: mcpServerUrl })) {
throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`);
}
// Convert the response to AuthInfo format
return {
token,
clientId: data.client_id,
scopes: data.scope ? data.scope.split(' ') : [],
expiresAt: data.exp
};
}
};
// Add metadata routes to the main MCP server
app.use(
mcpAuthMetadataRouter({
oauthMetadata,
resourceServerUrl: mcpServerUrl,
scopesSupported: ['mcp:tools'],
resourceName: 'MCP Demo Server'
})
);
// Add protected resource metadata route to the MCP server
// This allows clients to discover the auth server
app.use(createProtectedResourceMetadataRouter());
authMiddleware = requireBearerAuth({
verifier: tokenVerifier,
requiredScopes: [],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
strictResource: true,
expectedResource: mcpServerUrl
});
/**
@ -594,7 +545,7 @@ app.post('/confirm-payment', express.urlencoded(), (req: Request, res: Response)
});
// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
// Interface for a function that can send an elicitation request
type ElicitationSender = (params: ElicitRequestURLParams) => Promise<ElicitResult>;
@ -613,7 +564,7 @@ const mcpPostHandler = async (req: Request, res: Response) => {
console.debug(`Received MCP POST for session: ${sessionId || 'unknown'}`);
try {
let transport: StreamableHTTPServerTransport;
let transport: NodeStreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
@ -621,7 +572,7 @@ const mcpPostHandler = async (req: Request, res: Response) => {
const server = getServer();
// New initialization request
const eventStore = new InMemoryEventStore();
transport = new StreamableHTTPServerTransport({
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore, // Enable resumability
onsessioninitialized: sessionId => {

View file

@ -1,7 +1,9 @@
import { randomUUID } from 'node:crypto';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult } from '@modelcontextprotocol/server';
import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
import * as z from 'zod/v4';
@ -96,21 +98,21 @@ const getServer = () => {
const app = createMcpExpressApp();
// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
app.post('/mcp', async (req: Request, res: Response) => {
console.log('Received MCP request:', req.body);
try {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
let transport: NodeStreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request - use JSON response mode
transport = new StreamableHTTPServerTransport({
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableJsonResponse: true, // Enable JSON response mode
onsessioninitialized: sessionId => {

View file

@ -1,174 +0,0 @@
import type { CallToolResult } from '@modelcontextprotocol/server';
import { createMcpExpressApp, McpServer, SSEServerTransport } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
import * as z from 'zod/v4';
/**
* This example server demonstrates the deprecated HTTP+SSE transport
* (protocol version 2024-11-05). It mainly used for testing backward compatible clients.
*
* The server exposes two endpoints:
* - /mcp: For establishing the SSE stream (GET)
* - /messages: For receiving client messages (POST)
*
*/
// Create an MCP server instance
const getServer = () => {
const server = new McpServer(
{
name: 'simple-sse-server',
version: '1.0.0'
},
{ capabilities: { logging: {} } }
);
server.registerTool(
'start-notification-stream',
{
description: 'Starts sending periodic notifications',
inputSchema: {
interval: z.number().describe('Interval in milliseconds between notifications').default(1000),
count: z.number().describe('Number of notifications to send').default(10)
}
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
// Send the initial notification
await server.sendLoggingMessage(
{
level: 'info',
data: `Starting notification stream with ${count} messages every ${interval}ms`
},
extra.sessionId
);
// Send periodic notifications
while (counter < count) {
counter++;
await sleep(interval);
try {
await server.sendLoggingMessage(
{
level: 'info',
data: `Notification #${counter} at ${new Date().toISOString()}`
},
extra.sessionId
);
} catch (error) {
console.error('Error sending notification:', error);
}
}
return {
content: [
{
type: 'text',
text: `Completed sending ${count} notifications every ${interval}ms`
}
]
};
}
);
return server;
};
const app = createMcpExpressApp();
// Store transports by session ID
const transports: Record<string, SSEServerTransport> = {};
// SSE endpoint for establishing the stream
app.get('/mcp', async (req: Request, res: Response) => {
console.log('Received GET request to /sse (establishing SSE stream)');
try {
// Create a new SSE transport for the client
// The endpoint for POST messages is '/messages'
const transport = new SSEServerTransport('/messages', res);
// Store the transport by session ID
const sessionId = transport.sessionId;
transports[sessionId] = transport;
// Set up onclose handler to clean up transport when closed
transport.onclose = () => {
console.log(`SSE transport closed for session ${sessionId}`);
delete transports[sessionId];
};
// Connect the transport to the MCP server
const server = getServer();
await server.connect(transport);
console.log(`Established SSE stream with session ID: ${sessionId}`);
} catch (error) {
console.error('Error establishing SSE stream:', error);
if (!res.headersSent) {
res.status(500).send('Error establishing SSE stream');
}
}
});
// Messages endpoint for receiving client JSON-RPC requests
app.post('/messages', async (req: Request, res: Response) => {
console.log('Received POST request to /messages');
// Extract session ID from URL query parameter
// In the SSE protocol, this is added by the client based on the endpoint event
const sessionId = req.query.sessionId as string | undefined;
if (!sessionId) {
console.error('No session ID provided in request URL');
res.status(400).send('Missing sessionId parameter');
return;
}
const transport = transports[sessionId];
if (!transport) {
console.error(`No active transport found for session ID: ${sessionId}`);
res.status(404).send('Session not found');
return;
}
try {
// Handle the POST message with the transport
await transport.handlePostMessage(req, res, req.body);
} catch (error) {
console.error('Error handling request:', error);
if (!res.headersSent) {
res.status(500).send('Error handling request');
}
}
});
// Start the server
const PORT = 3000;
app.listen(PORT, error => {
if (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
console.log(`Simple SSE Server (deprecated protocol version 2024-11-05) listening on port ${PORT}`);
});
// Handle server shutdown
process.on('SIGINT', async () => {
console.log('Shutting down server...');
// Close all active transports to properly clean up resources
for (const sessionId in transports) {
try {
console.log(`Closing transport for session ${sessionId}`);
await transports[sessionId]!.close();
delete transports[sessionId];
} catch (error) {
console.error(`Error closing transport for session ${sessionId}:`, error);
}
}
console.log('Server shutdown complete');
process.exit(0);
});

View file

@ -1,5 +1,7 @@
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult, GetPromptResult, ReadResourceResult } from '@modelcontextprotocol/server';
import { createMcpExpressApp, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
import { McpServer } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
import * as z from 'zod/v4';
@ -103,7 +105,7 @@ const app = createMcpExpressApp();
app.post('/mcp', async (req: Request, res: Response) => {
const server = getServer();
try {
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
const transport: NodeStreamableHTTPServerTransport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined
});
await server.connect(transport);

View file

@ -1,26 +1,26 @@
import { randomUUID } from 'node:crypto';
import { setupAuthServer } from '@modelcontextprotocol/examples-shared';
import {
createProtectedResourceMetadataRouter,
getOAuthProtectedResourceMetadataUrl,
requireBearerAuth,
setupAuthServer
} from '@modelcontextprotocol/examples-shared';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type {
CallToolResult,
GetPromptResult,
OAuthMetadata,
PrimitiveSchemaDefinition,
ReadResourceResult,
ResourceLink
} from '@modelcontextprotocol/server';
import {
checkResourceAllowed,
createMcpExpressApp,
ElicitResultSchema,
getOAuthProtectedResourceMetadataUrl,
InMemoryTaskMessageQueue,
InMemoryTaskStore,
isInitializeRequest,
mcpAuthMetadataRouter,
McpServer,
requireBearerAuth,
StreamableHTTPServerTransport
McpServer
} from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
import * as z from 'zod/v4';
@ -527,70 +527,22 @@ if (useOAuth) {
const mcpServerUrl = new URL(`http://localhost:${MCP_PORT}/mcp`);
const authServerUrl = new URL(`http://localhost:${AUTH_PORT}`);
const oauthMetadata: OAuthMetadata = setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth });
setupAuthServer({ authServerUrl, mcpServerUrl, strictResource: strictOAuth, demoMode: true });
const tokenVerifier = {
verifyAccessToken: async (token: string) => {
const endpoint = oauthMetadata.introspection_endpoint;
if (!endpoint) {
throw new Error('No token verification endpoint available in metadata');
}
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
token: token
}).toString()
});
if (!response.ok) {
const text = await response.text().catch(() => null);
throw new Error(`Invalid or expired token: ${text}`);
}
const data = (await response.json()) as { aud: string; client_id: string; scope: string; exp: number };
if (strictOAuth) {
if (!data.aud) {
throw new Error(`Resource Indicator (RFC8707) missing`);
}
if (!checkResourceAllowed({ requestedResource: data.aud, configuredResource: mcpServerUrl })) {
throw new Error(`Expected resource indicator ${mcpServerUrl}, got: ${data.aud}`);
}
}
// Convert the response to AuthInfo format
return {
token,
clientId: data.client_id,
scopes: data.scope ? data.scope.split(' ') : [],
expiresAt: data.exp
};
}
};
// Add metadata routes to the main MCP server
app.use(
mcpAuthMetadataRouter({
oauthMetadata,
resourceServerUrl: mcpServerUrl,
scopesSupported: ['mcp:tools'],
resourceName: 'MCP Demo Server'
})
);
// Add protected resource metadata route to the MCP server
// This allows clients to discover the auth server
app.use(createProtectedResourceMetadataRouter());
authMiddleware = requireBearerAuth({
verifier: tokenVerifier,
requiredScopes: [],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
strictResource: strictOAuth,
expectedResource: mcpServerUrl
});
}
// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
// MCP POST endpoint with optional auth
const mcpPostHandler = async (req: Request, res: Response) => {
@ -601,18 +553,18 @@ const mcpPostHandler = async (req: Request, res: Response) => {
console.log('Request body:', req.body);
}
if (useOAuth && req.auth) {
console.log('Authenticated user:', req.auth);
if (useOAuth && req.app.locals.auth) {
console.log('Authenticated user:', req.app.locals.auth);
}
try {
let transport: StreamableHTTPServerTransport;
let transport: NodeStreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
const eventStore = new InMemoryEventStore();
transport = new StreamableHTTPServerTransport({
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore, // Enable resumability
onsessioninitialized: sessionId => {
@ -685,8 +637,8 @@ const mcpGetHandler = async (req: Request, res: Response) => {
return;
}
if (useOAuth && req.auth) {
console.log('Authenticated SSE connection from user:', req.auth);
if (useOAuth && req.app.locals.auth) {
console.log('Authenticated SSE connection from user:', req.app.locals.auth);
}
// Check for Last-Event-ID header for resumability

View file

@ -11,6 +11,8 @@
import { randomUUID } from 'node:crypto';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type {
CallToolResult,
CreateMessageRequest,
@ -35,15 +37,13 @@ import type {
} from '@modelcontextprotocol/server';
import {
CallToolRequestSchema,
createMcpExpressApp,
GetTaskPayloadRequestSchema,
GetTaskRequestSchema,
InMemoryTaskStore,
isTerminal,
ListToolsRequestSchema,
RELATED_TASK_META_KEY,
Server,
StreamableHTTPServerTransport
Server
} from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
@ -642,7 +642,7 @@ const createServer = (): Server => {
const app = createMcpExpressApp();
// Map to store transports by session ID
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
// Helper to check if request is initialize
const isInitializeRequest = (body: unknown): boolean => {
@ -654,12 +654,12 @@ app.post('/mcp', async (req: Request, res: Response) => {
const sessionId = req.headers['mcp-session-id'] as string | undefined;
try {
let transport: StreamableHTTPServerTransport;
let transport: NodeStreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
transport = new StreamableHTTPServerTransport({
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: sid => {
console.log(`Session initialized: ${sid}`);

View file

@ -1,258 +0,0 @@
import { randomUUID } from 'node:crypto';
import type { CallToolResult } from '@modelcontextprotocol/server';
import {
createMcpExpressApp,
isInitializeRequest,
McpServer,
SSEServerTransport,
StreamableHTTPServerTransport
} from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
import * as z from 'zod/v4';
import { InMemoryEventStore } from './inMemoryEventStore.js';
/**
* This example server demonstrates backwards compatibility with both:
* 1. The deprecated HTTP+SSE transport (protocol version 2024-11-05)
* 2. The Streamable HTTP transport (protocol version 2025-11-25)
*
* It maintains a single MCP server instance but exposes two transport options:
* - /mcp: The new Streamable HTTP endpoint (supports GET/POST/DELETE)
* - /sse: The deprecated SSE endpoint for older clients (GET to establish stream)
* - /messages: The deprecated POST endpoint for older clients (POST to send messages)
*/
const getServer = () => {
const server = new McpServer(
{
name: 'backwards-compatible-server',
version: '1.0.0'
},
{ capabilities: { logging: {} } }
);
// Register a simple tool that sends notifications over time
server.registerTool(
'start-notification-stream',
{
description: 'Starts sending periodic notifications for testing resumability',
inputSchema: {
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
count: z.number().describe('Number of notifications to send (0 for 100)').default(50)
}
},
async ({ interval, count }, extra): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
while (count === 0 || counter < count) {
counter++;
try {
await server.sendLoggingMessage(
{
level: 'info',
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
},
extra.sessionId
);
} catch (error) {
console.error('Error sending notification:', error);
}
// Wait for the specified interval
await sleep(interval);
}
return {
content: [
{
type: 'text',
text: `Started sending periodic notifications every ${interval}ms`
}
]
};
}
);
return server;
};
// Create Express application
const app = createMcpExpressApp();
// Store transports by session ID
const transports: Record<string, StreamableHTTPServerTransport | SSEServerTransport> = {};
//=============================================================================
// STREAMABLE HTTP TRANSPORT (PROTOCOL VERSION 2025-11-25)
//=============================================================================
// Handle all MCP Streamable HTTP requests (GET, POST, DELETE) on a single endpoint
app.all('/mcp', async (req: Request, res: Response) => {
console.log(`Received ${req.method} request to /mcp`);
try {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Check if the transport is of the correct type
const existingTransport = transports[sessionId];
if (existingTransport instanceof StreamableHTTPServerTransport) {
// Reuse existing transport
transport = existingTransport;
} else {
// Transport exists but is not a StreamableHTTPServerTransport (could be SSEServerTransport)
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: Session exists but uses a different transport protocol'
},
id: null
});
return;
}
} else if (!sessionId && req.method === 'POST' && isInitializeRequest(req.body)) {
const eventStore = new InMemoryEventStore();
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore, // Enable resumability
onsessioninitialized: sessionId => {
// Store the transport by session ID when session is initialized
console.log(`StreamableHTTP session initialized with ID: ${sessionId}`);
transports[sessionId] = transport;
}
});
// Set up onclose handler to clean up transport when closed
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
console.log(`Transport closed for session ${sid}, removing from transports map`);
delete transports[sid];
}
};
// Connect the transport to the MCP server
const server = getServer();
await server.connect(transport);
} else {
// Invalid request - no session ID or not initialization request
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided'
},
id: null
});
return;
}
// Handle the request with the transport
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.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
});
}
}
});
//=============================================================================
// DEPRECATED HTTP+SSE TRANSPORT (PROTOCOL VERSION 2024-11-05)
//=============================================================================
app.get('/sse', async (req: Request, res: Response) => {
console.log('Received GET request to /sse (deprecated SSE transport)');
const transport = new SSEServerTransport('/messages', res);
transports[transport.sessionId] = transport;
res.on('close', () => {
delete transports[transport.sessionId];
});
const server = getServer();
await server.connect(transport);
});
app.post('/messages', async (req: Request, res: Response) => {
const sessionId = req.query.sessionId as string;
let transport: SSEServerTransport;
const existingTransport = transports[sessionId];
if (existingTransport instanceof SSEServerTransport) {
// Reuse existing transport
transport = existingTransport;
} else {
// Transport exists but is not a SSEServerTransport (could be StreamableHTTPServerTransport)
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: Session exists but uses a different transport protocol'
},
id: null
});
return;
}
if (transport) {
await transport.handlePostMessage(req, res, req.body);
} else {
res.status(400).send('No transport found for sessionId');
}
});
// Start the server
const PORT = 3000;
app.listen(PORT, error => {
if (error) {
console.error('Failed to start server:', error);
process.exit(1);
}
console.log(`Backwards compatible MCP server listening on port ${PORT}`);
console.log(`
==============================================
SUPPORTED TRANSPORT OPTIONS:
1. Streamable Http(Protocol version: 2025-11-25)
Endpoint: /mcp
Methods: GET, POST, DELETE
Usage:
- Initialize with POST to /mcp
- Establish SSE stream with GET to /mcp
- Send requests with POST to /mcp
- Terminate session with DELETE to /mcp
2. Http + SSE (Protocol version: 2024-11-05)
Endpoints: /sse (GET) and /messages (POST)
Usage:
- Establish SSE stream with GET to /sse
- Send requests with POST to /messages?sessionId=<id>
==============================================
`);
});
// Handle server shutdown
process.on('SIGINT', async () => {
console.log('Shutting down server...');
// Close all active transports to properly clean up resources
for (const sessionId in transports) {
try {
console.log(`Closing transport for session ${sessionId}`);
await transports[sessionId]!.close();
delete transports[sessionId];
} catch (error) {
console.error(`Error closing transport for session ${sessionId}:`, error);
}
}
console.log('Server shutdown complete');
process.exit(0);
});

View file

@ -14,8 +14,10 @@
*/
import { randomUUID } from 'node:crypto';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { CallToolResult } from '@modelcontextprotocol/server';
import { createMcpExpressApp, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
import { McpServer } from '@modelcontextprotocol/server';
import cors from 'cors';
import type { Request, Response } from 'express';
@ -111,7 +113,7 @@ app.use(cors());
const eventStore = new InMemoryEventStore();
// Track transports by session ID for session reuse
const transports = new Map<string, StreamableHTTPServerTransport>();
const transports = new Map<string, NodeStreamableHTTPServerTransport>();
// Handle all MCP requests
app.all('/mcp', async (req: Request, res: Response) => {
@ -121,7 +123,7 @@ app.all('/mcp', async (req: Request, res: Response) => {
let transport = sessionId ? transports.get(sessionId) : undefined;
if (!transport) {
transport = new StreamableHTTPServerTransport({
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore,
retryInterval: 2000, // Default retry interval for priming events

View file

@ -1,7 +1,9 @@
import { randomUUID } from 'node:crypto';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
import type { ReadResourceResult } from '@modelcontextprotocol/server';
import { createMcpExpressApp, isInitializeRequest, McpServer, StreamableHTTPServerTransport } from '@modelcontextprotocol/server';
import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
import type { Request, Response } from 'express';
// Create an MCP server with implementation details
@ -11,7 +13,7 @@ const server = new McpServer({
});
// Store transports by session ID to send notifications
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const transports: { [sessionId: string]: NodeStreamableHTTPServerTransport } = {};
const addResource = (name: string, content: string) => {
const uri = `https://mcp-example.com/dynamic/${encodeURIComponent(name)}`;
@ -41,14 +43,14 @@ app.post('/mcp', async (req: Request, res: Response) => {
try {
// Check for existing session ID
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
let transport: NodeStreamableHTTPServerTransport;
if (sessionId && transports[sessionId]) {
// Reuse existing transport
transport = transports[sessionId];
} else if (!sessionId && isInitializeRequest(req.body)) {
// New initialization request
transport = new StreamableHTTPServerTransport({
transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: sessionId => {
// Store the transport by session ID when session is initialized