Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com> Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
99 lines
4 KiB
TypeScript
99 lines
4 KiB
TypeScript
/**
|
|
* Standalone GET stream + `notifications/resources/list_changed` (sessionful
|
|
* 2025).
|
|
*
|
|
* One `NodeStreamableHTTPServerTransport` + `McpServer` per session, the way
|
|
* you would deploy a sessionful 2025 server. The `add_resource` tool registers
|
|
* a new resource on the session's instance — `McpServer.registerResource` emits
|
|
* `notifications/resources/list_changed`, which on a sessionful transport
|
|
* travels over the **standalone GET** SSE stream the client opened. The client
|
|
* decides when to mutate (no timer race with the runner).
|
|
*
|
|
* **HTTP-only**, sessionful 2025 by definition — so the canonical
|
|
* `serveStdio` / `createMcpHandler` shape does not apply (per-request stateless
|
|
* has no GET stream).
|
|
*/
|
|
import { randomUUID } from 'node:crypto';
|
|
|
|
import { parseExampleArgs } from '@mcp-examples/shared';
|
|
import { createMcpExpressApp } from '@modelcontextprotocol/express';
|
|
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
|
|
import { isInitializeRequest, McpServer } from '@modelcontextprotocol/server';
|
|
import type { Request, Response } from 'express';
|
|
import * as z from 'zod/v4';
|
|
|
|
function buildServer(): McpServer {
|
|
const server = new McpServer(
|
|
{ name: 'standalone-get-example', version: '1.0.0' },
|
|
{ capabilities: { resources: { listChanged: true } } }
|
|
);
|
|
let nextId = 1;
|
|
const register = (name: string, content: string) =>
|
|
server.registerResource(
|
|
name,
|
|
`https://mcp-example.com/dynamic/${encodeURIComponent(name)}`,
|
|
{ mimeType: 'text/plain' },
|
|
async uri => ({
|
|
contents: [{ uri: uri.href, mimeType: 'text/plain', text: content }]
|
|
})
|
|
);
|
|
register('initial', 'Initial content');
|
|
|
|
server.registerTool(
|
|
'add_resource',
|
|
{
|
|
description:
|
|
'Register a new resource on this session — emits notifications/resources/list_changed over the standalone GET stream.',
|
|
inputSchema: z.object({ content: z.string() })
|
|
},
|
|
async ({ content }) => {
|
|
const name = `note-${nextId++}`;
|
|
register(name, content);
|
|
return { content: [{ type: 'text', text: `registered ${name}` }] };
|
|
}
|
|
);
|
|
return server;
|
|
}
|
|
|
|
const sessions = new Map<string, NodeStreamableHTTPServerTransport>();
|
|
const app = createMcpExpressApp();
|
|
|
|
app.post('/mcp', async (req: Request, res: Response) => {
|
|
const sid = req.headers['mcp-session-id'] as string | undefined;
|
|
if (sid && sessions.has(sid)) {
|
|
await sessions.get(sid)!.handleRequest(req, res, req.body);
|
|
} else if (!sid && isInitializeRequest(req.body)) {
|
|
const transport = new NodeStreamableHTTPServerTransport({
|
|
sessionIdGenerator: () => randomUUID(),
|
|
onsessioninitialized: id => {
|
|
sessions.set(id, transport);
|
|
}
|
|
});
|
|
transport.onclose = () => transport.sessionId && sessions.delete(transport.sessionId);
|
|
await buildServer().connect(transport);
|
|
await transport.handleRequest(req, res, req.body);
|
|
} else if (sid) {
|
|
res.status(404).json({ jsonrpc: '2.0', error: { code: -32_001, message: 'Session not found' }, id: null });
|
|
} else {
|
|
res.status(400).json({ jsonrpc: '2.0', error: { code: -32_000, message: 'Bad Request: Session ID required' }, id: null });
|
|
}
|
|
});
|
|
|
|
// The standalone GET stream (the point of this story) and DELETE (explicit
|
|
// session termination per the MCP spec) route to the session's transport.
|
|
const sessionVerb = async (req: Request, res: Response) => {
|
|
const sid = req.headers['mcp-session-id'] as string | undefined;
|
|
const t = sid ? sessions.get(sid) : undefined;
|
|
if (!t) {
|
|
res.status(sid ? 404 : 400).send(sid ? 'Session not found' : 'Missing session ID');
|
|
return;
|
|
}
|
|
await t.handleRequest(req, res);
|
|
};
|
|
app.get('/mcp', sessionVerb);
|
|
app.delete('/mcp', sessionVerb);
|
|
|
|
const { port } = parseExampleArgs();
|
|
app.listen(port, () => {
|
|
console.error(`[server] listening on http://127.0.0.1:${port}/mcp`);
|
|
});
|