Skip to content

feat: Add RawHttpServerAdapter for HTTP frameworks #474

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Running Your Server](#running-your-server)
- [stdio](#stdio)
- [Streamable HTTP](#streamable-http)
- [Using with Node.js HTTP Frameworks (Fastify, Express, etc.)](#using-with-nodejs-http-frameworks-fastify-express-etc)
- [Testing and Debugging](#testing-and-debugging)
- [Examples](#examples)
- [Echo Server](#echo-server)
Expand Down Expand Up @@ -386,6 +387,55 @@ This stateless approach is useful for:
- RESTful scenarios where each request is independent
- Horizontally scaled deployments without shared session state

### Using with Node.js HTTP Frameworks (Fastify, Express, etc.)

The `StreamableHTTPServerTransport` works directly with Node.js's native `IncomingMessage` and `ServerResponse` objects. To simplify integration with common Node.js web frameworks like Fastify or Express, the SDK provides the `RawHttpServerAdapter`. This adapter wraps `StreamableHTTPServerTransport` and exposes a `handleNodeRequest` method.

The `RawHttpServerAdapter` expects your framework's request and response objects to provide access to the underlying Node.js `request.raw` and `response.raw` objects. It also expects that your framework can provide the pre-parsed request body (e.g., `request.body`).

**Key features:**
- Implements the `Transport` interface, so it can be directly passed to `McpServer.connect()`.
- Delegates to an internal `StreamableHTTPServerTransport` instance.
- Simplifies request/response handling by working with the raw Node.js objects exposed by many frameworks.

**Example with a Fastify-like setup:**

```typescript
import { McpServer, StreamableHTTPServerTransportOptions } from "@modelcontextprotocol/sdk/server"; // Or specific paths
import { RawHttpServerAdapter } from "@modelcontextprotocol/sdk/server/raw-http-adapter"; // Adjust path as needed
import { randomUUID } from "node:crypto";
// import YourFramework from 'your-framework'; // e.g., Fastify or Express

// const frameworkApp = YourFramework();
// const mcpServer = new McpServer({ name: "MyFrameworkMCPServer", version: "1.0.0" });

const mcpAdapterOptions: StreamableHTTPServerTransportOptions = {
sessionIdGenerator: () => randomUUID(),
// enableJsonResponse: true, // Useful for POSTs if SSE is not desired for responses
};
const mcpAdapter = new RawHttpServerAdapter(mcpAdapterOptions);

await mcpServer.connect(mcpAdapter);

// In your framework's route handler for the MCP endpoint (e.g., /mcp)
// frameworkApp.all('/mcp', async (request, reply) => {
// try {
// await mcpAdapter.handleNodeRequest(
// { raw: request.raw, body: request.body }, // From your framework
// { raw: reply.raw } // From your framework
// );
// // IMPORTANT for SSE: Do not let your framework automatically end the response here.
// // The mcpAdapter will manage the response stream (reply.raw).
// } catch (error) {
// // Handle error
// if (!reply.raw.headersSent) { /* send error response via reply.raw */ }
// }
// });

// frameworkApp.listen({ port: 3000 });
```
Refer to the example in `src/examples/server/simpleFastifyServer.ts` for a runnable demonstration with Fastify.

### Testing and Debugging

To test your server, you can use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). See its README for more information.
Expand Down
211 changes: 211 additions & 0 deletions src/examples/server/simpleFastifyServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import Fastify from "fastify";
import { McpServer } from "../../server/mcp.js";
import { StreamableHTTPServerTransportOptions } from "../../server/streamableHttp.js";
import { RawHttpServerAdapter } from "../../server/raw-http-adapter.js";
import { randomUUID } from "node:crypto";
import { z } from "zod";
import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js';

async function runFastifyMCPServer() {
const fastify = Fastify({ logger: true });

const mcpAdapterOptions: StreamableHTTPServerTransportOptions = {
sessionIdGenerator: () => randomUUID(),
// For true JSON request/response (non-SSE) for POSTs, uncomment the next line.
// Otherwise, POST requests that expect a response will use an SSE stream.
// enableJsonResponse: true,
};
const mcpAdapter = new RawHttpServerAdapter(mcpAdapterOptions);

const mcpServer = new McpServer({
name: "simple-streamable-http-server",
version: "1.0.0",
}, { capabilities: { logging: {} } });

// === Tools, Resource, and Prompt from simpleStreamableHttp.ts ===

// Tool: greet
mcpServer.tool(
'greet',
'A simple greeting tool',
{
name: z.string().describe('Name to greet'),
},
async ({ name }): Promise<CallToolResult> => {
return {
content: [
{
type: 'text',
text: `Hello, ${name}!`,
},
],
};
}
);

// Tool: multi-greet
mcpServer.tool(
'multi-greet',
'A tool that sends different greetings with delays between them',
{
name: z.string().describe('Name to greet'),
},
{
title: 'Multiple Greeting Tool',
readOnlyHint: true,
openWorldHint: false
},
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}!`,
}
],
};
}
);

// Tool: start-notification-stream
mcpServer.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);
}
await sleep(interval);
}

return {
content: [
{
type: 'text',
text: `Started sending periodic notifications every ${interval}ms`,
}
],
};
}
);

// Resource: greeting-resource
mcpServer.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!',
},
],
};
}
);

// Prompt: greeting-template
mcpServer.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.`,
},
},
],
};
}
);

// === End of copied tools, resource, and prompt ===

await mcpServer.connect(mcpAdapter);

// Register a catch-all route for the /mcp endpoint
fastify.all("/mcp", async (request, reply) => {
try {
await mcpAdapter.handleNodeRequest(
{ raw: request.raw, body: request.body },
{ raw: reply.raw }
);
// IMPORTANT for SSE: Do not let Fastify automatically end the response here.
// The mcpAdapter (via StreamableHTTPServerTransport) will manage the response stream (reply.raw).
} catch (error) {
fastify.log.error(error, "Error in MCP request handler");
if (!reply.raw.headersSent) {
reply.raw.writeHead(500, { "Content-Type": "application/json" });
reply.raw.end(
JSON.stringify({
jsonrpc: "2.0",
error: { code: -32000, message: "Internal Server Error" },
id: null,
})
);
}
}
});

try {
const address = await fastify.listen({ port: 3000, host: "0.0.0.0" });
fastify.log.info(`MCP Server (Fastify with tools from simpleStreamableHttp) listening on ${address}`);
fastify.log.info(`MCP endpoint available at POST ${address}/mcp`);
fastify.log.info(`Available tools: greet, multi-greet, start-notification-stream`);
fastify.log.info(`Available resource: GET https://example.com/greetings/default (via MCP readResource)`);
fastify.log.info(`Available prompt: greeting-template`);
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
}

runFastifyMCPServer();
Loading