feat(mcp): Streamable HTTP transport + deploy; secure restoreRevision
- apps/mcp/src/http.ts: serve MCP over Streamable HTTP (MCP 2025-03-26) on :4021, stateless mode (sessionIdGenerator undefined), CORS on /mcp. Remote clients can now call the 6 tools + 4 resources without a stdio subprocess. - deploy/mcpedia-mcp.service: supervised systemd unit (MCP_PORT=4021). - Caddy: mcp.asepharyana.my.id -> 4021; wiki. domain now also routes /trpc/* to the API (was swallowed by web -> tRPC was unreachable on the domain). - apps/api: restoreRevision tRPC mutation now requires x-webhook-secret (the Web UI calls @mcpedia/core directly, so this only gates the open network endpoint). Threads the header into tRPC Context. Secures a state-changing action that was anonymously callable. Verified live: https://mcp.asepharyana.my.id/mcp initialize/tools/list/ resources/list all 200; restoreRevision no-secret -> unauthorized, with-secret -> handler; read-only tRPC reachable via domain.
This commit is contained in:
1 parent
ec0ab4dbb3
commit
b621923868
8 files changed
+137
-2
No files matched your search
@@ -0,0 +1,44 @@
|
||||
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import { createMcpServer } from "./index";
|
||||
|
||||
// Phase 3 follow-up: serve the MCPedia MCP server over Streamable HTTP
|
||||
// (MCP 2025-03-26 transport) so remote clients can use its tools/resources
|
||||
// without spawning a stdio subprocess. Stateless mode (sessionIdGenerator
|
||||
// undefined): one McpServer + transport per request, no session affinity, no
|
||||
// shared-transport connect race, no session-map memory leak. Re-registering
|
||||
// 6 tools + 4 resources per request is negligible for a KB-sized corpus.
|
||||
|
||||
const PORT = Number(process.env.MCP_PORT ?? 4021);
|
||||
const PATH = "/mcp";
|
||||
|
||||
const CORS: Record<string, string> = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers":
|
||||
"Content-Type, Accept, Authorization, MCP-Protocol-Version, mcp-session-id",
|
||||
};
|
||||
|
||||
const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
||||
for (const [k, v] of Object.entries(CORS)) res.setHeader(k, v);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(204).end();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((req.url ?? "").split("?")[0] !== PATH) {
|
||||
res.writeHead(404).end("Not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Stateless: fresh server + transport per request.
|
||||
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
||||
const server = createMcpServer();
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res);
|
||||
});
|
||||
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`MCPedia MCP server (Streamable HTTP) listening on http://localhost:${PORT}/mcp`);
|
||||
});
|
||||
Reference in new issue
Block a user