Skip to content

Commit 111586c

Browse files
committed
support sse though get
1 parent 75ec9de commit 111586c

File tree

4 files changed

+403
-47
lines changed

4 files changed

+403
-47
lines changed

src/examples/client/simpleStreamableHttp.ts

Lines changed: 68 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
GetPromptResultSchema,
1212
ListResourcesRequest,
1313
ListResourcesResultSchema,
14-
LoggingMessageNotificationSchema
14+
LoggingMessageNotificationSchema,
15+
ResourceListChangedNotificationSchema
1516
} from '../../types.js';
1617

1718
async function main(): Promise<void> {
@@ -24,50 +25,79 @@ async function main(): Promise<void> {
2425
const transport = new StreamableHTTPClientTransport(
2526
new URL('http://localhost:3000/mcp')
2627
);
28+
const supportsStandaloneSse = false;
2729

2830
// Connect the client using the transport and initialize the server
2931
await client.connect(transport);
32+
console.log('Connected to MCP server');
33+
// Open a standalone SSE stream to receive server-initiated messages
34+
console.log('Opening SSE stream to receive server notifications...');
35+
try {
36+
await transport.openSseStream();
37+
const supportsStandaloneSse = false;
38+
console.log('SSE stream established successfully. Waiting for notifications...');
39+
}
40+
catch (error) {
41+
console.error('Failed to open SSE stream:', error);
42+
}
43+
44+
// Set up notification handlers for server-initiated messages
3045
client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => {
3146
console.log(`Notification received: ${notification.params.level} - ${notification.params.data}`);
3247
});
48+
client.setNotificationHandler(ResourceListChangedNotificationSchema, async (_) => {
49+
console.log(`Resource list changed notification received!`);
50+
const resourcesRequest: ListResourcesRequest = {
51+
method: 'resources/list',
52+
params: {}
53+
};
54+
const resourcesResult = await client.request(resourcesRequest, ListResourcesResultSchema);
55+
console.log('Available resources count:', resourcesResult.resources.length);
56+
});
3357

34-
35-
console.log('Connected to MCP server');
3658
// List available tools
37-
const toolsRequest: ListToolsRequest = {
38-
method: 'tools/list',
39-
params: {}
40-
};
41-
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
42-
console.log('Available tools:', toolsResult.tools);
59+
try {
60+
const toolsRequest: ListToolsRequest = {
61+
method: 'tools/list',
62+
params: {}
63+
};
64+
const toolsResult = await client.request(toolsRequest, ListToolsResultSchema);
65+
console.log('Available tools:', toolsResult.tools);
4366

44-
// Call the 'greet' tool
45-
const greetRequest: CallToolRequest = {
46-
method: 'tools/call',
47-
params: {
48-
name: 'greet',
49-
arguments: { name: 'MCP User' }
50-
}
51-
};
52-
const greetResult = await client.request(greetRequest, CallToolResultSchema);
53-
console.log('Greeting result:', greetResult.content[0].text);
67+
if (toolsResult.tools.length === 0) {
68+
console.log('No tools available from the server');
69+
} else {
70+
// Call the 'greet' tool
71+
const greetRequest: CallToolRequest = {
72+
method: 'tools/call',
73+
params: {
74+
name: 'greet',
75+
arguments: { name: 'MCP User' }
76+
}
77+
};
78+
const greetResult = await client.request(greetRequest, CallToolResultSchema);
79+
console.log('Greeting result:', greetResult.content[0].text);
5480

55-
// Call the new 'multi-greet' tool
56-
console.log('\nCalling multi-greet tool (with notifications)...');
57-
const multiGreetRequest: CallToolRequest = {
58-
method: 'tools/call',
59-
params: {
60-
name: 'multi-greet',
61-
arguments: { name: 'MCP User' }
81+
// Call the new 'multi-greet' tool
82+
console.log('\nCalling multi-greet tool (with notifications)...');
83+
const multiGreetRequest: CallToolRequest = {
84+
method: 'tools/call',
85+
params: {
86+
name: 'multi-greet',
87+
arguments: { name: 'MCP User' }
88+
}
89+
};
90+
const multiGreetResult = await client.request(multiGreetRequest, CallToolResultSchema);
91+
console.log('Multi-greet results:');
92+
multiGreetResult.content.forEach(item => {
93+
if (item.type === 'text') {
94+
console.log(`- ${item.text}`);
95+
}
96+
});
6297
}
63-
};
64-
const multiGreetResult = await client.request(multiGreetRequest, CallToolResultSchema);
65-
console.log('Multi-greet results:');
66-
multiGreetResult.content.forEach(item => {
67-
if (item.type === 'text') {
68-
console.log(`- ${item.text}`);
69-
}
70-
});
98+
} catch (error) {
99+
console.log(`Tools not supported by this server (${error})`);
100+
}
71101

72102
// List available prompts
73103
try {
@@ -107,9 +137,11 @@ async function main(): Promise<void> {
107137
} catch (error) {
108138
console.log(`Resources not supported by this server (${error})`);
109139
}
140+
if (supportsStandaloneSse) {
141+
// Instead of closing immediately, keep the connection open to receive notifications
142+
console.log('\nKeeping connection open to receive notifications. Press Ctrl+C to exit.');
143+
}
110144

111-
// Close the connection
112-
await client.close();
113145
}
114146

115147
main().catch((error: unknown) => {
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
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 { ReadResourceResult } from '../../types.js';
6+
7+
// Create an MCP server with implementation details
8+
const server = new McpServer({
9+
name: 'resource-list-changed-notification-server',
10+
version: '1.0.0',
11+
}, {
12+
capabilities: {
13+
resources: {
14+
listChanged: true, // Support notifications for resource list changes
15+
},
16+
}
17+
});
18+
19+
// Store transports by session ID to send notifications
20+
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
21+
22+
const addResource = (name: string, content: string) => {
23+
const uri = `https://mcp-example.com/dynamic/${encodeURIComponent(name)}`;
24+
server.resource(
25+
name,
26+
uri,
27+
{ mimeType: 'text/plain', description: `Dynamic resource: ${name}` },
28+
async (): Promise<ReadResourceResult> => {
29+
return {
30+
contents: [{ uri, text: content }],
31+
};
32+
}
33+
);
34+
35+
};
36+
37+
addResource('example-resource', 'Initial content for example-resource');
38+
39+
const resourceChangeInterval = setInterval(() => {
40+
const name = randomUUID();
41+
addResource(name, `Content for ${name}`);
42+
}, 5000); // Change resources every 5 seconds for testing
43+
44+
const app = express();
45+
app.use(express.json());
46+
47+
app.post('/mcp', async (req: Request, res: Response) => {
48+
console.log('Received MCP request:', req.body);
49+
try {
50+
// Check for existing session ID
51+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
52+
let transport: StreamableHTTPServerTransport;
53+
54+
if (sessionId && transports[sessionId]) {
55+
// Reuse existing transport
56+
transport = transports[sessionId];
57+
} else if (!sessionId && isInitializeRequest(req.body)) {
58+
// New initialization request
59+
transport = new StreamableHTTPServerTransport({
60+
sessionIdGenerator: () => randomUUID(),
61+
});
62+
63+
// Connect the transport to the MCP server
64+
await server.connect(transport);
65+
66+
await transport.handleRequest(req, res, req.body);
67+
68+
// Store the transport by session ID for future requests
69+
if (transport.sessionId) {
70+
transports[transport.sessionId] = transport;
71+
}
72+
return; // Already handled
73+
} else {
74+
// Invalid request - no session ID or not initialization request
75+
res.status(400).json({
76+
jsonrpc: '2.0',
77+
error: {
78+
code: -32000,
79+
message: 'Bad Request: No valid session ID provided',
80+
},
81+
id: null,
82+
});
83+
return;
84+
}
85+
86+
// Handle the request with existing transport
87+
await transport.handleRequest(req, res, req.body);
88+
} catch (error) {
89+
console.error('Error handling MCP request:', error);
90+
if (!res.headersSent) {
91+
res.status(500).json({
92+
jsonrpc: '2.0',
93+
error: {
94+
code: -32603,
95+
message: 'Internal server error',
96+
},
97+
id: null,
98+
});
99+
}
100+
}
101+
});
102+
103+
// Handle GET requests for SSE streams (now using built-in support from StreamableHTTP)
104+
app.get('/mcp', async (req: Request, res: Response) => {
105+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
106+
if (!sessionId || !transports[sessionId]) {
107+
res.status(400).send('Invalid or missing session ID');
108+
return;
109+
}
110+
111+
console.log(`Establishing SSE stream for session ${sessionId}`);
112+
const transport = transports[sessionId];
113+
await transport.handleRequest(req, res);
114+
});
115+
116+
// Helper function to detect initialize requests
117+
function isInitializeRequest(body: unknown): boolean {
118+
if (Array.isArray(body)) {
119+
return body.some(msg => typeof msg === 'object' && msg !== null && 'method' in msg && msg.method === 'initialize');
120+
}
121+
return typeof body === 'object' && body !== null && 'method' in body && body.method === 'initialize';
122+
}
123+
124+
// Start the server
125+
const PORT = 3000;
126+
app.listen(PORT, () => {
127+
console.log(`Server listening on port ${PORT}`);
128+
});
129+
130+
// Handle server shutdown
131+
process.on('SIGINT', async () => {
132+
console.log('Shutting down server...');
133+
clearInterval(resourceChangeInterval);
134+
await server.close();
135+
process.exit(0);
136+
});

0 commit comments

Comments
 (0)