Skip to content

Streamable Http - fix examples for streamable HTTP #363

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 18, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 58 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,35 +309,72 @@ app.listen(3000);
For simpler use cases where session management isn't needed:

```typescript
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const app = express();
app.use(express.json());

const server = new McpServer({
name: "stateless-server",
version: "1.0.0"
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // set to undefined for stateless servers
});

// ... set up server resources, tools, and prompts ...
// Setup routes for the server
const setupServer = async () => {
await server.connect(transport);
};

const app = express();
app.use(express.json());
app.post('/mcp', async (req: Request, res: Response) => {
console.log('Received MCP request:', req.body);
try {
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,
});
}
}
});

// Handle all MCP requests (GET, POST, DELETE) at a single endpoint
app.all('/mcp', async (req, res) => {
// Disable session tracking by setting sessionIdGenerator to undefined
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
req,
res
app.get('/mcp', async (req: Request, res: Response) => {
console.log('Received GET MCP request');
res.writeHead(405).end(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Method not allowed."
},
id: null
}));
});

app.delete('/mcp', async (req: Request, res: Response) => {
console.log('Received DELETE MCP request');
res.writeHead(405).end(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Method not allowed."
},
id: null
}));
});

// Start the server
const PORT = 3000;
setupServer().then(() => {
app.listen(PORT, () => {
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
});

// Connect to server and handle the request
await server.connect(transport);
await transport.handleRequest(req, res);
}).catch(error => {
console.error('Failed to set up the server:', error);
process.exit(1);
});

app.listen(3000);
```

This stateless approach is useful for:
Expand Down
172 changes: 172 additions & 0 deletions src/examples/server/simpleStatelessStreamableHttp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import express, { Request, Response } from 'express';
import { McpServer } from '../../server/mcp.js';
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: {} } });

// 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`,
}
],
};
}
);

// 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!',
},
],
};
}
);

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);
try {
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,
});
}
}
});

app.get('/mcp', async (req: Request, res: Response) => {
console.log('Received GET MCP request');
res.writeHead(405).end(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Method not allowed."
},
id: null
}));
});

app.delete('/mcp', async (req: Request, res: Response) => {
console.log('Received DELETE MCP request');
res.writeHead(405).end(JSON.stringify({
jsonrpc: "2.0",
error: {
code: -32000,
message: "Method not allowed."
},
id: null
}));
});

// 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);
});

// 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);
});
38 changes: 38 additions & 0 deletions src/integration-tests/stateManagementStreamableHttp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,44 @@ describe('Streamable HTTP Transport Session Management', () => {
server.close();
});

it('should support multiple client connections', async () => {
// Create and connect a client
const client1 = new Client({
name: 'test-client',
version: '1.0.0'
});

const transport1 = new StreamableHTTPClientTransport(baseUrl);
await client1.connect(transport1);

// Verify that no session ID was set
expect(transport1.sessionId).toBeUndefined();

// List available tools
await client1.request({
method: 'tools/list',
params: {}
}, ListToolsResultSchema);

const client2 = new Client({
name: 'test-client',
version: '1.0.0'
});

const transport2 = new StreamableHTTPClientTransport(baseUrl);
await client2.connect(transport2);

// Verify that no session ID was set
expect(transport2.sessionId).toBeUndefined();

// List available tools
await client1.request({
method: 'tools/list',
params: {}
}, ListToolsResultSchema);


});
it('should operate without session management', async () => {
// Create and connect a client
const client = new Client({
Expand Down
2 changes: 1 addition & 1 deletion src/server/streamableHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ export class StreamableHTTPServerTransport implements Transport {
if (isInitializationRequest) {
// If it's a server with session management and the session ID is already set we should reject the request
// to avoid re-initialization.
if (this._initialized) {
if (this._initialized && this.sessionId !== undefined) {
res.writeHead(400).end(JSON.stringify({
jsonrpc: "2.0",
error: {
Expand Down