Merge pull request #377 from modelcontextprotocol/ihrpr/example-clients

Streamable Http - fix server initialization validation in Stateless servers
This commit is contained in:
ihrpr 2025-04-22 10:47:17 +01:00 • committed by GitHub
commit 73f464f76a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 806 additions and 445 deletions

View file

@ -310,19 +310,23 @@ For simpler use cases where session management isn't needed:
const app = express();
app.use(express.json());
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // set to undefined for stateless servers
});
// Setup routes for the server
const setupServer = async () => {
await server.connect(transport);
};
app.post('/mcp', async (req: Request, res: Response) => {
console.log('Received MCP request:', req.body);
// 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 {
await transport.handleRequest(req, res, req.body);
const server = getServer();
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
res.on('close', () => {
console.log('Request closed');
transport.close();
server.close();
});
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
@ -362,15 +366,11 @@ app.delete('/mcp', async (req: Request, res: Response) => {
}));
});
// Start the server
const PORT = 3000;
setupServer().then(() => {
app.listen(PORT, () => {
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
});
}).catch(error => {
console.error('Failed to set up the server:', error);
process.exit(1);
app.listen(PORT, () => {
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
});
```

View file

@ -0,0 +1,160 @@
import { Client } from '../../client/index.js';
import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js';
import {
CallToolRequest,
CallToolResultSchema,
LoggingMessageNotificationSchema,
CallToolResult,
} from '../../types.js';
/**
* Multiple Clients MCP Example
*
* This client demonstrates how to:
* 1. Create multiple MCP clients in parallel
* 2. Each client calls a single tool
* 3. Track notifications from each client independently
*/
// Command line args processing
const args = process.argv.slice(2);
const serverUrl = args[0] || 'http://localhost:3000/mcp';
interface ClientConfig {
id: string;
name: string;
toolName: string;
toolArguments: Record<string, string | number | boolean>;
}
async function createAndRunClient(config: ClientConfig): Promise<{ id: string; result: CallToolResult }> {
console.log(`[${config.id}] Creating client: ${config.name}`);
const client = new Client({
name: config.name,
version: '1.0.0'
});
const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
// Set up client-specific error handler
client.onerror = (error) => {
console.error(`[${config.id}] Client error:`, error);
};
// Set up client-specific notification handler
client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
console.log(`[${config.id}] Notification: ${notification.params.data}`);
});
try {
// Connect to the server
await client.connect(transport);
console.log(`[${config.id}] Connected to MCP server`);
// Call the specified tool
console.log(`[${config.id}] Calling tool: ${config.toolName}`);
const toolRequest: CallToolRequest = {
method: 'tools/call',
params: {
name: config.toolName,
arguments: {
...config.toolArguments,
// Add client ID to arguments for identification in notifications
caller: config.id
}
}
};
const result = await client.request(toolRequest, CallToolResultSchema);
console.log(`[${config.id}] Tool call completed`);
// Keep the connection open for a bit to receive notifications
await new Promise(resolve => setTimeout(resolve, 5000));
// Disconnect
await transport.close();
console.log(`[${config.id}] Disconnected from MCP server`);
return { id: config.id, result };
} catch (error) {
console.error(`[${config.id}] Error:`, error);
throw error;
}
}
async function main(): Promise<void> {
console.log('MCP Multiple Clients Example');
console.log('============================');
console.log(`Server URL: ${serverUrl}`);
console.log('');
try {
// Define client configurations
const clientConfigs: ClientConfig[] = [
{
id: 'client1',
name: 'basic-client-1',
toolName: 'start-notification-stream',
toolArguments: {
interval: 3, // 1 second between notifications
count: 5 // Send 5 notifications
}
},
{
id: 'client2',
name: 'basic-client-2',
toolName: 'start-notification-stream',
toolArguments: {
interval: 2, // 2 seconds between notifications
count: 3 // Send 3 notifications
}
},
{
id: 'client3',
name: 'basic-client-3',
toolName: 'start-notification-stream',
toolArguments: {
interval: 1, // 0.5 second between notifications
count: 8 // Send 8 notifications
}
}
];
// Start all clients in parallel
console.log(`Starting ${clientConfigs.length} clients in parallel...`);
console.log('');
const clientPromises = clientConfigs.map(config => createAndRunClient(config));
const results = await Promise.all(clientPromises);
// Display results from all clients
console.log('\n=== Final Results ===');
results.forEach(({ id, result }) => {
console.log(`\n[${id}] Tool result:`);
if (Array.isArray(result.content)) {
result.content.forEach((item: { type: string; text?: string }) => {
if (item.type === 'text' && item.text) {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
});
} else {
console.log(` Unexpected result format:`, result);
}
});
console.log('\n=== All clients completed successfully ===');
} catch (error) {
console.error('Error running multiple clients:', error);
process.exit(1);
}
}
// Start the example
main().catch((error: unknown) => {
console.error('Error running MCP multiple clients example:', error);
process.exit(1);
});

View file

@ -0,0 +1,197 @@
import { Client } from '../../client/index.js';
import { StreamableHTTPClientTransport } from '../../client/streamableHttp.js';
import {
ListToolsRequest,
ListToolsResultSchema,
CallToolResultSchema,
LoggingMessageNotificationSchema,
CallToolResult,
} from '../../types.js';
/**
* Parallel Tool Calls MCP Client
*
* This client demonstrates how to:
* 1. Start multiple tool calls in parallel
* 2. Track notifications from each tool call using a caller parameter
*/
// Command line args processing
const args = process.argv.slice(2);
const serverUrl = args[0] || 'http://localhost:3000/mcp';
async function main(): Promise<void> {
console.log('MCP Parallel Tool Calls Client');
console.log('==============================');
console.log(`Connecting to server at: ${serverUrl}`);
let client: Client;
let transport: StreamableHTTPClientTransport;
try {
// Create client with streamable HTTP transport
client = new Client({
name: 'parallel-tool-calls-client',
version: '1.0.0'
});
client.onerror = (error) => {
console.error('Client error:', error);
};
// Connect to the server
transport = new StreamableHTTPClientTransport(new URL(serverUrl));
await client.connect(transport);
console.log('Successfully connected to MCP server');
// Set up notification handler with caller identification
client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
console.log(`Notification: ${notification.params.data}`);
});
console.log("List tools")
const toolsRequest = await listTools(client);
console.log("Tools: ", toolsRequest)
// 2. Start multiple notification tools in parallel
console.log('\n=== Starting Multiple Notification Streams in Parallel ===');
const toolResults = await startParallelNotificationTools(client);
// Log the results from each tool call
for (const [caller, result] of Object.entries(toolResults)) {
console.log(`\n=== Tool result for ${caller} ===`);
result.content.forEach((item: { type: string; text?: string; }) => {
if (item.type === 'text') {
console.log(` ${item.text}`);
} else {
console.log(` ${item.type} content:`, item);
}
});
}
// 3. Wait for all notifications (10 seconds)
console.log('\n=== Waiting for all notifications ===');
await new Promise(resolve => setTimeout(resolve, 10000));
// 4. Disconnect
console.log('\n=== Disconnecting ===');
await transport.close();
console.log('Disconnected from MCP server');
} catch (error) {
console.error('Error running client:', error);
process.exit(1);
}
}
/**
* List available tools on the server
*/
async function listTools(client: Client): Promise<void> {
try {
const toolsRequest: ListToolsRequest = {
method: 'tools/list',
params: {}
};
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
console.log('Available tools:');
if (toolsResult.tools.length === 0) {
console.log(' No tools available');
} else {
for (const tool of toolsResult.tools) {
console.log(` - ${tool.name}: ${tool.description}`);
}
}
} catch (error) {
console.log(`Tools not supported by this server: ${error}`);
}
}
/**
* Start multiple notification tools in parallel with different configurations
* Each tool call includes a caller parameter to identify its notifications
*/
async function startParallelNotificationTools(client: Client): Promise<Record<string, CallToolResult>> {
try {
// Define multiple tool calls with different configurations
const toolCalls = [
{
caller: 'fast-notifier',
request: {
method: 'tools/call',
params: {
name: 'start-notification-stream',
arguments: {
interval: 2, // 0.5 second between notifications
count: 10, // Send 10 notifications
caller: 'fast-notifier' // Identify this tool call
}
}
}
},
{
caller: 'slow-notifier',
request: {
method: 'tools/call',
params: {
name: 'start-notification-stream',
arguments: {
interval: 5, // 2 seconds between notifications
count: 5, // Send 5 notifications
caller: 'slow-notifier' // Identify this tool call
}
}
}
},
{
caller: 'burst-notifier',
request: {
method: 'tools/call',
params: {
name: 'start-notification-stream',
arguments: {
interval: 1, // 0.1 second between notifications
count: 3, // Send just 3 notifications
caller: 'burst-notifier' // Identify this tool call
}
}
}
}
];
console.log(`Starting ${toolCalls.length} notification tools in parallel...`);
// Start all tool calls in parallel
const toolPromises = toolCalls.map(({ caller, request }) => {
console.log(`Starting tool call for ${caller}...`);
return client.request(request, CallToolResultSchema)
.then(result => ({ caller, result }))
.catch(error => {
console.error(`Error in tool call for ${caller}:`, error);
throw error;
});
});
// Wait for all tool calls to complete
const results = await Promise.all(toolPromises);
// Organize results by caller
const resultsByTool: Record<string, CallToolResult> = {};
results.forEach(({ caller, result }) => {
resultsByTool[caller] = result;
});
return resultsByTool;
} catch (error) {
console.error(`Error starting parallel notification tools:`, error);
throw error;
}
}
// Start the client
main().catch((error: unknown) => {
console.error('Error running MCP client:', error);
process.exit(1);
});

View file

@ -5,74 +5,78 @@ import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import { z } from 'zod';
import { CallToolResult, isInitializeRequest } from '../../types.js';
// Create an MCP server with implementation details
const server = new McpServer({
name: 'json-response-streamable-http-server',
version: '1.0.0',
}, {
capabilities: {
logging: {},
}
});
const getServer = () => {
const server = new McpServer({
name: 'json-response-streamable-http-server',
version: '1.0.0',
}, {
capabilities: {
logging: {},
}
});
// Register a simple tool that returns a greeting
server.tool(
'greet',
'A simple greeting tool',
{
name: z.string().describe('Name to greet'),
},
async ({ name }): Promise<CallToolResult> => {
return {
content: [
{
type: 'text',
text: `Hello, ${name}!`,
},
],
};
}
);
// Register a simple tool that returns a greeting
server.tool(
'greet',
'A simple greeting tool',
{
name: z.string().describe('Name to greet'),
},
async ({ name }): Promise<CallToolResult> => {
return {
content: [
{
type: 'text',
text: `Hello, ${name}!`,
},
],
};
}
);
// Register a tool that sends multiple greetings with notifications
server.tool(
'multi-greet',
'A tool that sends different greetings with delays between them',
{
name: z.string().describe('Name to greet'),
},
async ({ name }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
// Register a tool that sends multiple greetings with notifications
server.tool(
'multi-greet',
'A tool that sends different greetings with delays between them',
{
name: z.string().describe('Name to greet'),
},
async ({ name }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
await sendNotification({
method: "notifications/message",
params: { level: "debug", data: `Starting multi-greet for ${name}` }
});
await sendNotification({
method: "notifications/message",
params: { level: "debug", data: `Starting multi-greet for ${name}` }
});
await sleep(1000); // Wait 1 second before first greeting
await sleep(1000); // Wait 1 second before first greeting
await sendNotification({
method: "notifications/message",
params: { level: "info", data: `Sending first greeting to ${name}` }
});
await sendNotification({
method: "notifications/message",
params: { level: "info", data: `Sending first greeting to ${name}` }
});
await sleep(1000); // Wait another second before second greeting
await sleep(1000); // Wait another second before second greeting
await sendNotification({
method: "notifications/message",
params: { level: "info", data: `Sending second greeting to ${name}` }
});
await sendNotification({
method: "notifications/message",
params: { level: "info", data: `Sending second greeting to ${name}` }
});
return {
content: [
{
type: 'text',
text: `Good morning, ${name}!`,
}
],
};
}
);
return {
content: [
{
type: 'text',
text: `Good morning, ${name}!`,
}
],
};
}
);
return server;
}
const app = express();
app.use(express.json());
@ -104,6 +108,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
});
// Connect the transport to the MCP server BEFORE handling the request
const server = getServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
return; // Already handled
@ -153,6 +158,5 @@ app.listen(PORT, () => {
// Handle server shutdown
process.on('SIGINT', async () => {
console.log('Shutting down server...');
await server.close();
process.exit(0);
});

View file

@ -15,60 +15,63 @@ import { CallToolResult } from '../../types.js';
*/
// Create an MCP server instance
const server = new McpServer({
name: 'simple-sse-server',
version: '1.0.0',
}, { capabilities: { logging: {} } });
const getServer = () => {
const server = new McpServer({
name: 'simple-sse-server',
version: '1.0.0',
}, { capabilities: { logging: {} } });
server.tool(
'start-notification-stream',
'Starts sending periodic notifications',
{
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 }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
server.tool(
'start-notification-stream',
'Starts sending periodic notifications',
{
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 }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
// Send the initial notification
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Starting notification stream with ${count} messages every ${interval}ms`
}
});
// Send periodic notifications
while (counter < count) {
counter++;
await sleep(interval);
try {
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Notification #${counter} at ${new Date().toISOString()}`
}
});
}
catch (error) {
console.error("Error sending notification:", error);
}
}
return {
content: [
{
type: 'text',
text: `Completed sending ${count} notifications every ${interval}ms`,
// Send the initial notification
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Starting notification stream with ${count} messages every ${interval}ms`
}
],
};
}
);
});
// Send periodic notifications
while (counter < count) {
counter++;
await sleep(interval);
try {
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Notification #${counter} at ${new Date().toISOString()}`
}
});
}
catch (error) {
console.error("Error sending notification:", error);
}
}
return {
content: [
{
type: 'text',
text: `Completed sending ${count} notifications every ${interval}ms`,
}
],
};
}
);
return server;
};
const app = express();
app.use(express.json());
@ -96,6 +99,7 @@ app.get('/mcp', async (req: Request, res: Response) => {
};
// Connect the transport to the MCP server
const server = getServer();
await server.connect(transport);
// Start the SSE transport to begin streaming
@ -163,7 +167,6 @@ process.on('SIGINT', async () => {
console.error(`Error closing transport for session ${sessionId}:`, error);
}
}
await server.close();
console.log('Server shutdown complete');
process.exit(0);
});

View file

@ -4,108 +4,111 @@ import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import { z } from 'zod';
import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js';
// Create an MCP server with implementation details
const server = new McpServer({
name: 'stateless-streamable-http-server',
version: '1.0.0',
}, { capabilities: { logging: {} } });
const getServer = () => {
// Create an MCP server with implementation details
const server = new McpServer({
name: 'stateless-streamable-http-server',
version: '1.0.0',
}, { capabilities: { logging: {} } });
// Register a simple prompt
server.prompt(
'greeting-template',
'A simple greeting prompt template',
{
name: z.string().describe('Name to include in greeting'),
},
async ({ name }): Promise<GetPromptResult> => {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please greet ${name} in a friendly manner.`,
// Register a simple prompt
server.prompt(
'greeting-template',
'A simple greeting prompt template',
{
name: z.string().describe('Name to include in greeting'),
},
async ({ name }): Promise<GetPromptResult> => {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please greet ${name} in a friendly manner.`,
},
},
},
],
};
}
);
// Register a tool specifically for testing resumability
server.tool(
'start-notification-stream',
'Starts sending periodic notifications for testing resumability',
{
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
count: z.number().describe('Number of notifications to send (0 for 100)').default(10),
},
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
while (count === 0 || counter < count) {
counter++;
try {
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
}
});
}
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`,
// Register a tool specifically for testing resumability
server.tool(
'start-notification-stream',
'Starts sending periodic notifications for testing resumability',
{
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
count: z.number().describe('Number of notifications to send (0 for 100)').default(10),
},
async ({ interval, count }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
while (count === 0 || counter < count) {
counter++;
try {
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
}
});
}
],
};
}
);
catch (error) {
console.error("Error sending notification:", error);
}
// Wait for the specified interval
await sleep(interval);
}
// Create a simple resource at a fixed URI
server.resource(
'greeting-resource',
'https://example.com/greetings/default',
{ mimeType: 'text/plain' },
async (): Promise<ReadResourceResult> => {
return {
contents: [
{
uri: 'https://example.com/greetings/default',
text: 'Hello, world!',
},
],
};
}
);
return {
content: [
{
type: 'text',
text: `Started sending periodic notifications every ${interval}ms`,
}
],
};
}
);
// Create a simple resource at a fixed URI
server.resource(
'greeting-resource',
'https://example.com/greetings/default',
{ mimeType: 'text/plain' },
async (): Promise<ReadResourceResult> => {
return {
contents: [
{
uri: 'https://example.com/greetings/default',
text: 'Hello, world!',
},
],
};
}
);
return server;
}
const app = express();
app.use(express.json());
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
// Setup routes for the server
const setupServer = async () => {
await server.connect(transport);
};
app.post('/mcp', async (req: Request, res: Response) => {
console.log('Received MCP request:', req.body);
const server = getServer();
try {
await transport.handleRequest(req, res, req.body);
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
res.on('close', () => {
console.log('Request closed');
transport.close();
server.close();
});
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
@ -145,28 +148,15 @@ app.delete('/mcp', async (req: Request, res: Response) => {
}));
});
// Start the server
const PORT = 3000;
setupServer().then(() => {
app.listen(PORT, () => {
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
});
}).catch(error => {
console.error('Failed to set up the server:', error);
process.exit(1);
app.listen(PORT, () => {
console.log(`MCP Stateless Streamable HTTP Server listening on port ${PORT}`);
});
// Handle server shutdown
process.on('SIGINT', async () => {
console.log('Shutting down server...');
try {
console.log(`Closing transport`);
await transport.close();
} catch (error) {
console.error(`Error closing transport:`, error);
}
await server.close();
console.log('Server shutdown complete');
process.exit(0);
});

View file

@ -7,164 +7,167 @@ import { CallToolResult, GetPromptResult, isInitializeRequest, ReadResourceResul
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
// Create an MCP server with implementation details
const server = new McpServer({
name: 'simple-streamable-http-server',
version: '1.0.0',
}, { capabilities: { logging: {} } });
const getServer = () => {
const server = new McpServer({
name: 'simple-streamable-http-server',
version: '1.0.0',
}, { capabilities: { logging: {} } });
// Log the capability invocation details
server.onCapabilityChange((event) => {
switch (event.action) {
case 'invoked':
console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' started`);
break;
case 'completed':
console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' completed in ${event.durationMs}ms`);
break;
case 'error':
console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' failed in ${event.durationMs}ms: ${event.error}`);
break;
}
});
// Register a simple tool that returns a greeting
server.tool(
'greet',
'A simple greeting tool',
{
name: z.string().describe('Name to greet'),
},
async ({ name }): Promise<CallToolResult> => {
return {
content: [
{
type: 'text',
text: `Hello, ${name}!`,
},
],
};
}
);
// Register a tool that sends multiple greetings with notifications
server.tool(
'multi-greet',
'A tool that sends different greetings with delays between them',
{
name: z.string().describe('Name to greet'),
},
async ({ name }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
await sendNotification({
method: "notifications/message",
params: { level: "debug", data: `Starting multi-greet for ${name}` }
});
await sleep(1000); // Wait 1 second before first greeting
await sendNotification({
method: "notifications/message",
params: { level: "info", data: `Sending first greeting to ${name}` }
});
await sleep(1000); // Wait another second before second greeting
await sendNotification({
method: "notifications/message",
params: { level: "info", data: `Sending second greeting to ${name}` }
});
return {
content: [
{
type: 'text',
text: `Good morning, ${name}!`,
}
],
};
}
);
// Register a simple prompt
server.prompt(
'greeting-template',
'A simple greeting prompt template',
{
name: z.string().describe('Name to include in greeting'),
},
async ({ name }): Promise<GetPromptResult> => {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please greet ${name} in a friendly manner.`,
},
},
],
};
}
);
// Register a tool specifically for testing resumability
server.tool(
'start-notification-stream',
'Starts sending periodic notifications for testing resumability',
{
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 }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
while (count === 0 || counter < count) {
counter++;
try {
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
}
});
}
catch (error) {
console.error("Error sending notification:", error);
}
// Wait for the specified interval
await sleep(interval);
// Log the capability invocation details
server.onCapabilityChange((event) => {
switch (event.action) {
case 'invoked':
console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' started`);
break;
case 'completed':
console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' completed in ${event.durationMs}ms`);
break;
case 'error':
console.log(`${event.capabilityType} invocation ${event.invocationIndex}: '${event.capabilityName}' failed in ${event.durationMs}ms: ${event.error}`);
break;
}
});
return {
content: [
{
type: 'text',
text: `Started sending periodic notifications every ${interval}ms`,
// Register a simple tool that returns a greeting
server.tool(
'greet',
'A simple greeting tool',
{
name: z.string().describe('Name to greet'),
},
async ({ name }): Promise<CallToolResult> => {
return {
content: [
{
type: 'text',
text: `Hello, ${name}!`,
},
],
};
}
);
// Register a tool that sends multiple greetings with notifications
server.tool(
'multi-greet',
'A tool that sends different greetings with delays between them',
{
name: z.string().describe('Name to greet'),
},
async ({ name }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
await sendNotification({
method: "notifications/message",
params: { level: "debug", data: `Starting multi-greet for ${name}` }
});
await sleep(1000); // Wait 1 second before first greeting
await sendNotification({
method: "notifications/message",
params: { level: "info", data: `Sending first greeting to ${name}` }
});
await sleep(1000); // Wait another second before second greeting
await sendNotification({
method: "notifications/message",
params: { level: "info", data: `Sending second greeting to ${name}` }
});
return {
content: [
{
type: 'text',
text: `Good morning, ${name}!`,
}
],
};
}
);
// Register a simple prompt
server.prompt(
'greeting-template',
'A simple greeting prompt template',
{
name: z.string().describe('Name to include in greeting'),
},
async ({ name }): Promise<GetPromptResult> => {
return {
messages: [
{
role: 'user',
content: {
type: 'text',
text: `Please greet ${name} in a friendly manner.`,
},
},
],
};
}
);
// Register a tool specifically for testing resumability
server.tool(
'start-notification-stream',
'Starts sending periodic notifications for testing resumability',
{
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 }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
while (count === 0 || counter < count) {
counter++;
try {
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
}
});
}
],
};
}
);
catch (error) {
console.error("Error sending notification:", error);
}
// Wait for the specified interval
await sleep(interval);
}
// Create a simple resource at a fixed URI
server.resource(
'greeting-resource',
'https://example.com/greetings/default',
{ mimeType: 'text/plain' },
async (): Promise<ReadResourceResult> => {
return {
contents: [
{
uri: 'https://example.com/greetings/default',
text: 'Hello, world!',
},
],
};
}
);
return {
content: [
{
type: 'text',
text: `Started sending periodic notifications every ${interval}ms`,
}
],
};
}
);
// Create a simple resource at a fixed URI
server.resource(
'greeting-resource',
'https://example.com/greetings/default',
{ mimeType: 'text/plain' },
async (): Promise<ReadResourceResult> => {
return {
contents: [
{
uri: 'https://example.com/greetings/default',
text: 'Hello, world!',
},
],
};
}
);
return server;
};
const app = express();
app.use(express.json());
@ -207,6 +210,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
// Connect the transport to the MCP server BEFORE handling the request
// so responses can flow back through the same transport
const server = getServer();
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
@ -303,7 +307,6 @@ process.on('SIGINT', async () => {
console.error(`Error closing transport for session ${sessionId}:`, error);
}
}
await server.close();
console.log('Server shutdown complete');
process.exit(0);
});

View file

@ -18,52 +18,54 @@ import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
* - /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: {} } });
const server = new McpServer({
name: 'backwards-compatible-server',
version: '1.0.0',
}, { capabilities: { logging: {} } });
// Register a simple tool that sends notifications over time
server.tool(
'start-notification-stream',
'Starts sending periodic notifications for testing resumability',
{
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 }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
// Register a simple tool that sends notifications over time
server.tool(
'start-notification-stream',
'Starts sending periodic notifications for testing resumability',
{
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 }, { sendNotification }): Promise<CallToolResult> => {
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
while (count === 0 || counter < count) {
counter++;
try {
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
}
});
}
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`,
while (count === 0 || counter < count) {
counter++;
try {
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
}
});
}
],
};
}
);
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 = express();
@ -125,6 +127,7 @@ app.all('/mcp', async (req: Request, res: Response) => {
};
// Connect the transport to the MCP server
const server = getServer();
await server.connect(transport);
} else {
// Invalid request - no session ID or not initialization request
@ -167,6 +170,7 @@ app.get('/sse', async (req: Request, res: Response) => {
res.on("close", () => {
delete transports[transport.sessionId];
});
const server = getServer();
await server.connect(transport);
});
@ -237,7 +241,6 @@ process.on('SIGINT', async () => {
console.error(`Error closing transport for session ${sessionId}:`, error);
}
}
await server.close();
console.log('Server shutdown complete');
process.exit(0);
});

View file

@ -463,6 +463,11 @@ export class StreamableHTTPServerTransport implements Transport {
* Returns true if the session is valid, false otherwise
*/
private validateSession(req: IncomingMessage, res: ServerResponse): boolean {
if (this.sessionIdGenerator === undefined) {
// If the sessionIdGenerator ID is not set, the session management is disabled
// and we don't need to validate the session ID
return true;
}
if (!this._initialized) {
// If the server has not been initialized yet, reject all requests
res.writeHead(400).end(JSON.stringify({
@ -475,11 +480,7 @@ export class StreamableHTTPServerTransport implements Transport {
}));
return false;
}
if (this.sessionId === undefined) {
// If the session ID is not set, the session management is disabled
// and we don't need to validate the session ID
return true;
}
const sessionId = req.headers["mcp-session-id"];
if (!sessionId) {