Merge pull request #351 from modelcontextprotocol/ihrpr/clean-up-examples
StreamableHttp - Update examples and README
This commit is contained in:
commit
64653f54bd
4 changed files with 565 additions and 381 deletions
|
|
@ -148,28 +148,6 @@ app.get('/mcp', async (req: Request, res: Response) => {
|
|||
const PORT = 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
|
||||
console.log(`Initialize session with the command below id you are using curl for testing:
|
||||
-----------------------------
|
||||
SESSION_ID=$(curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Accept: text/event-stream" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"capabilities": {},
|
||||
"protocolVersion": "2025-03-26",
|
||||
"clientInfo": {
|
||||
"name": "test",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
"id": "1"
|
||||
}' \
|
||||
-i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\\r')
|
||||
echo "Session ID: $SESSION_ID"
|
||||
-----------------------------`);
|
||||
});
|
||||
|
||||
// Handle server shutdown
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { CallToolResult } from '../../types.js';
|
|||
* (protocol version 2024-11-05). It mainly used for testing backward compatible clients.
|
||||
*
|
||||
* The server exposes two endpoints:
|
||||
* - /sse: For establishing the SSE stream (GET)
|
||||
* - /mcp: For establishing the SSE stream (GET)
|
||||
* - /messages: For receiving client messages (POST)
|
||||
*
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,84 +1,10 @@
|
|||
import express, { Request, Response } from 'express';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { McpServer } from '../../server/mcp.js';
|
||||
import { EventStore, StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
|
||||
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
|
||||
import { z } from 'zod';
|
||||
import { CallToolResult, GetPromptResult, isInitializeRequest, JSONRPCMessage, ReadResourceResult } from '../../types.js';
|
||||
|
||||
// Create a simple in-memory EventStore for resumability
|
||||
class InMemoryEventStore implements EventStore {
|
||||
private events: Map<string, { streamId: string, message: JSONRPCMessage }> = new Map();
|
||||
|
||||
/**
|
||||
* Generates a unique event ID for a given stream ID
|
||||
*/
|
||||
private generateEventId(streamId: string): string {
|
||||
return `${streamId}_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
||||
}
|
||||
|
||||
private getStreamIdFromEventId(eventId: string): string {
|
||||
const parts = eventId.split('_');
|
||||
return parts.length > 0 ? parts[0] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores an event with a generated event ID
|
||||
* Implements EventStore.storeEvent
|
||||
*/
|
||||
async storeEvent(streamId: string, message: JSONRPCMessage): Promise<string> {
|
||||
const eventId = this.generateEventId(streamId);
|
||||
console.log(`Storing event ${eventId} for stream ${streamId}`);
|
||||
this.events.set(eventId, { streamId, message });
|
||||
return eventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays events that occurred after a specific event ID
|
||||
* Implements EventStore.replayEventsAfter
|
||||
*/
|
||||
async replayEventsAfter(lastEventId: string,
|
||||
{ send }: { send: (eventId: string, message: JSONRPCMessage) => Promise<void> }
|
||||
): Promise<string> {
|
||||
if (!lastEventId || !this.events.has(lastEventId)) {
|
||||
console.log(`No events found for lastEventId: ${lastEventId}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
// Extract the stream ID from the event ID
|
||||
const streamId = this.getStreamIdFromEventId(lastEventId);
|
||||
if (!streamId) {
|
||||
console.log(`Could not extract streamId from lastEventId: ${lastEventId}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
let foundLastEvent = false;
|
||||
let eventCount = 0;
|
||||
|
||||
// Sort events by eventId for chronological ordering
|
||||
const sortedEvents = [...this.events.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
||||
|
||||
for (const [eventId, { streamId: eventStreamId, message }] of sortedEvents) {
|
||||
// Only include events from the same stream
|
||||
if (eventStreamId !== streamId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start sending events after we find the lastEventId
|
||||
if (eventId === lastEventId) {
|
||||
foundLastEvent = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (foundLastEvent) {
|
||||
await send(eventId, message);
|
||||
eventCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Replayed ${eventCount} events after ${lastEventId} for stream ${streamId}`);
|
||||
return streamId;
|
||||
}
|
||||
}
|
||||
import { CallToolResult, GetPromptResult, isInitializeRequest, ReadResourceResult } from '../../types.js';
|
||||
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
|
||||
|
||||
// Create an MCP server with implementation details
|
||||
const server = new McpServer({
|
||||
|
|
@ -346,28 +272,6 @@ app.delete('/mcp', async (req: Request, res: Response) => {
|
|||
const PORT = 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
|
||||
console.log(`Initialize session with the command below id you are using curl for testing:
|
||||
-----------------------------
|
||||
SESSION_ID=$(curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Accept: text/event-stream" \
|
||||
-d '{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"capabilities": {},
|
||||
"protocolVersion": "2025-03-26",
|
||||
"clientInfo": {
|
||||
"name": "test",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
"id": "1"
|
||||
}' \
|
||||
-i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\\r')
|
||||
echo "Session ID: $SESSION_ID"
|
||||
-----------------------------`);
|
||||
});
|
||||
|
||||
// Handle server shutdown
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import express, { Request, Response } from 'express';
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { McpServer } from '../../server/mcp.js';
|
||||
import { EventStore, StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
|
||||
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
|
||||
import { SSEServerTransport } from '../../server/sse.js';
|
||||
import { z } from 'zod';
|
||||
import { CallToolResult, isInitializeRequest, JSONRPCMessage } from '../../types.js';
|
||||
import { CallToolResult, isInitializeRequest } from '../../types.js';
|
||||
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
|
||||
|
||||
/**
|
||||
* This example server demonstrates backwards compatibility with both:
|
||||
|
|
@ -17,81 +18,7 @@ import { CallToolResult, isInitializeRequest, JSONRPCMessage } from '../../types
|
|||
* - /messages: The deprecated POST endpoint for older clients (POST to send messages)
|
||||
*/
|
||||
|
||||
// Simple in-memory event store for resumability
|
||||
class InMemoryEventStore implements EventStore {
|
||||
private events: Map<string, { streamId: string, message: JSONRPCMessage }> = new Map();
|
||||
|
||||
/**
|
||||
* Generates a unique event ID for a given stream ID
|
||||
*/
|
||||
private generateEventId(streamId: string): string {
|
||||
return `${streamId}_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
||||
}
|
||||
|
||||
private getStreamIdFromEventId(eventId: string): string {
|
||||
const parts = eventId.split('_');
|
||||
return parts.length > 0 ? parts[0] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores an event with a generated event ID
|
||||
* Implements EventStore.storeEvent
|
||||
*/
|
||||
async storeEvent(streamId: string, message: JSONRPCMessage): Promise<string> {
|
||||
const eventId = this.generateEventId(streamId);
|
||||
console.log(`Storing event ${eventId} for stream ${streamId}`);
|
||||
this.events.set(eventId, { streamId, message });
|
||||
return eventId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays events that occurred after a specific event ID
|
||||
* Implements EventStore.replayEventsAfter
|
||||
*/
|
||||
async replayEventsAfter(lastEventId: string,
|
||||
{ send }: { send: (eventId: string, message: JSONRPCMessage) => Promise<void> }
|
||||
): Promise<string> {
|
||||
if (!lastEventId || !this.events.has(lastEventId)) {
|
||||
console.log(`No events found for lastEventId: ${lastEventId}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
const streamId = this.getStreamIdFromEventId(lastEventId);
|
||||
if (!streamId) {
|
||||
console.log(`Could not extract streamId from lastEventId: ${lastEventId}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
let foundLastEvent = false;
|
||||
let eventCount = 0;
|
||||
|
||||
// Sort events by eventId for chronological ordering
|
||||
const sortedEvents = [...this.events.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
||||
|
||||
for (const [eventId, { streamId: eventStreamId, message }] of sortedEvents) {
|
||||
// Only include events from the same stream
|
||||
if (eventStreamId !== streamId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Start sending events after we find the lastEventId
|
||||
if (eventId === lastEventId) {
|
||||
foundLastEvent = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (foundLastEvent) {
|
||||
await send(eventId, message);
|
||||
eventCount++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Replayed ${eventCount} events after ${lastEventId} for stream ${streamId}`);
|
||||
return streamId;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a shared MCP server instance
|
||||
const server = new McpServer({
|
||||
name: 'backwards-compatible-server',
|
||||
version: '1.0.0',
|
||||
|
|
|
|||
Loading…
Reference in a new issue