Implement MCP 2026-07-28 (#2286)
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>
This commit is contained in:
parent
e4227d135c
commit
1823aae891
1 changed files with 69131 additions and 9001 deletions
99
examples/standalone-get/server.ts
Normal file
99
examples/standalone-get/server.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* 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`);
|
||||
});
|
||||
Loading…
Reference in a new issue