Skip to content

Commit b37a2e1

Browse files
committed
support for server returning json -- not stream
1 parent 0fa46aa commit b37a2e1

File tree

6 files changed

+431
-70
lines changed

6 files changed

+431
-70
lines changed

src/examples/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,34 @@ This directory contains example implementations of MCP clients and servers using
66

77
Multi node with stete management example will be added soon after we add support.
88

9+
### Server with JSON response mode (`server/jsonResponseStreamableHttp.ts`)
10+
11+
A simple MCP server that uses the Streamable HTTP transport with JSON response mode enabled, implemented with Express. The server provides a simple `greet` tool that returns a greeting for a name.
12+
13+
#### Running the server
14+
15+
```bash
16+
npx tsx src/examples/server/jsonResponseStreamableHttp.ts
17+
```
18+
19+
The server will start on port 3000. You can test the initialization and tool calling:
20+
21+
```bash
22+
# Initialize the server and get the session ID from headers
23+
SESSION_ID=$(curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" \
24+
-d '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":"1"}' \
25+
-i http://localhost:3000/mcp 2>&1 | grep -i "mcp-session-id" | cut -d' ' -f2 | tr -d '\r')
26+
echo "Session ID: $SESSION_ID"
27+
28+
# Call the greet tool using the saved session ID
29+
curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" \
30+
-H "mcp-session-id: $SESSION_ID" \
31+
-d '{"jsonrpc":"2.0","method":"mcp.call_tool","params":{"name":"greet","arguments":{"name":"World"}},"id":"2"}' \
32+
http://localhost:3000/mcp
33+
```
34+
35+
Note that in this example, we're using plain JSON response mode by setting `Accept: application/json` header.
36+
937
### Server (`server/simpleStreamableHttp.ts`)
1038

1139
A simple MCP server that uses the Streamable HTTP transport, implemented with Express. The server provides:

src/examples/client/simpleStreamableHttp.ts

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ import {
1515

1616
async function main(): Promise<void> {
1717
// Create a new client with streamable HTTP transport
18-
const client = new Client({
19-
name: 'example-client',
20-
version: '1.0.0'
18+
const client = new Client({
19+
name: 'example-client',
20+
version: '1.0.0'
2121
});
2222
const transport = new StreamableHTTPClientTransport(
2323
new URL('http://localhost:3000/mcp')
@@ -27,7 +27,6 @@ async function main(): Promise<void> {
2727
await client.connect(transport);
2828

2929
console.log('Connected to MCP server');
30-
3130
// List available tools
3231
const toolsRequest: ListToolsRequest = {
3332
method: 'tools/list',
@@ -48,32 +47,44 @@ async function main(): Promise<void> {
4847
console.log('Greeting result:', greetResult.content[0].text);
4948

5049
// List available prompts
51-
const promptsRequest: ListPromptsRequest = {
52-
method: 'prompts/list',
53-
params: {}
54-
};
55-
const promptsResult = await client.request(promptsRequest, ListPromptsResultSchema);
56-
console.log('Available prompts:', promptsResult.prompts);
50+
try {
51+
const promptsRequest: ListPromptsRequest = {
52+
method: 'prompts/list',
53+
params: {}
54+
};
55+
const promptsResult = await client.request(promptsRequest, ListPromptsResultSchema);
56+
console.log('Available prompts:', promptsResult.prompts);
57+
} catch (error) {
58+
console.log(`Prompts not supported by this server (${error})`);
59+
}
5760

5861
// Get a prompt
59-
const promptRequest: GetPromptRequest = {
60-
method: 'prompts/get',
61-
params: {
62-
name: 'greeting-template',
63-
arguments: { name: 'MCP User' }
64-
}
65-
};
66-
const promptResult = await client.request(promptRequest, GetPromptResultSchema);
67-
console.log('Prompt template:', promptResult.messages[0].content.text);
62+
try {
63+
const promptRequest: GetPromptRequest = {
64+
method: 'prompts/get',
65+
params: {
66+
name: 'greeting-template',
67+
arguments: { name: 'MCP User' }
68+
}
69+
};
70+
const promptResult = await client.request(promptRequest, GetPromptResultSchema);
71+
console.log('Prompt template:', promptResult.messages[0].content.text);
72+
} catch (error) {
73+
console.log(`Prompt retrieval not supported by this server (${error})`);
74+
}
6875

6976
// List available resources
70-
const resourcesRequest: ListResourcesRequest = {
71-
method: 'resources/list',
72-
params: {}
73-
};
74-
const resourcesResult = await client.request(resourcesRequest, ListResourcesResultSchema);
75-
console.log('Available resources:', resourcesResult.resources);
76-
77+
try {
78+
const resourcesRequest: ListResourcesRequest = {
79+
method: 'resources/list',
80+
params: {}
81+
};
82+
const resourcesResult = await client.request(resourcesRequest, ListResourcesResultSchema);
83+
console.log('Available resources:', resourcesResult.resources);
84+
} catch (error) {
85+
console.log(`Resources not supported by this server (${error})`);
86+
}
87+
7788
// Close the connection
7889
await client.close();
7990
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import express, { Request, Response } from 'express';
2+
import { randomUUID } from 'node:crypto';
3+
import { McpServer } from '../../server/mcp.js';
4+
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
5+
import { z } from 'zod';
6+
import { CallToolResult } from '../../types.js';
7+
8+
// Create an MCP server with implementation details
9+
const server = new McpServer({
10+
name: 'json-response-streamable-http-server',
11+
version: '1.0.0',
12+
});
13+
14+
// Register a simple tool that returns a greeting
15+
server.tool(
16+
'greet',
17+
'A simple greeting tool',
18+
{
19+
name: z.string().describe('Name to greet'),
20+
},
21+
async ({ name }): Promise<CallToolResult> => {
22+
return {
23+
content: [
24+
{
25+
type: 'text',
26+
text: `Hello, ${name}!`,
27+
},
28+
],
29+
};
30+
}
31+
);
32+
33+
const app = express();
34+
app.use(express.json());
35+
36+
// Map to store transports by session ID
37+
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
38+
39+
app.post('/mcp', async (req: Request, res: Response) => {
40+
console.log('Received MCP request:', req.body);
41+
try {
42+
// Check for existing session ID
43+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
44+
let transport: StreamableHTTPServerTransport;
45+
46+
if (sessionId && transports[sessionId]) {
47+
// Reuse existing transport
48+
transport = transports[sessionId];
49+
} else if (!sessionId && isInitializeRequest(req.body)) {
50+
// New initialization request - use JSON response mode
51+
transport = new StreamableHTTPServerTransport({
52+
sessionIdGenerator: () => randomUUID(),
53+
enableJsonResponse: true, // Enable JSON response mode
54+
});
55+
56+
// Connect the transport to the MCP server BEFORE handling the request
57+
await server.connect(transport);
58+
59+
// After handling the request, if we get a session ID back, store the transport
60+
await transport.handleRequest(req, res, req.body);
61+
62+
// Store the transport by session ID for future requests
63+
if (transport.sessionId) {
64+
transports[transport.sessionId] = transport;
65+
}
66+
return; // Already handled
67+
} else {
68+
// Invalid request - no session ID or not initialization request
69+
res.status(400).json({
70+
jsonrpc: '2.0',
71+
error: {
72+
code: -32000,
73+
message: 'Bad Request: No valid session ID provided',
74+
},
75+
id: null,
76+
});
77+
return;
78+
}
79+
80+
// Handle the request with existing transport - no need to reconnect
81+
await transport.handleRequest(req, res, req.body);
82+
} catch (error) {
83+
console.error('Error handling MCP request:', error);
84+
if (!res.headersSent) {
85+
res.status(500).json({
86+
jsonrpc: '2.0',
87+
error: {
88+
code: -32603,
89+
message: 'Internal server error',
90+
},
91+
id: null,
92+
});
93+
}
94+
}
95+
});
96+
97+
// Helper function to detect initialize requests
98+
function isInitializeRequest(body: unknown): boolean {
99+
if (Array.isArray(body)) {
100+
return body.some(msg => typeof msg === 'object' && msg !== null && 'method' in msg && msg.method === 'initialize');
101+
}
102+
return typeof body === 'object' && body !== null && 'method' in body && body.method === 'initialize';
103+
}
104+
105+
// Start the server
106+
const PORT = 3000;
107+
app.listen(PORT, () => {
108+
console.log(`MCP Streamable HTTP Server with JSON responses listening on port ${PORT}`);
109+
console.log(`Server is running. Press Ctrl+C to stop.`);
110+
console.log(`Initialize with: curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":"1"}' http://localhost:${PORT}/mcp`);
111+
console.log(`Then call tool with: curl -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "mcp-session-id: YOUR_SESSION_ID" -d '{"jsonrpc":"2.0","method":"mcp.call_tool","params":{"name":"greet","arguments":{"name":"World"}},"id":"2"}' http://localhost:${PORT}/mcp`);
112+
});
113+
114+
// Handle server shutdown
115+
process.on('SIGINT', async () => {
116+
console.log('Shutting down server...');
117+
await server.close();
118+
process.exit(0);
119+
});
16 KB
Binary file not shown.

0 commit comments

Comments
 (0)