/** * MCP Streamable HTTP endpoint (protocol revision 2026-07-28, dual-era). * * Exposes an MCP server at /_emdash/api/mcp. Modern clients send per-request * `_meta` (protocol version, capabilities) with the `Mcp-Method`/`Mcp-Name` * headers and get plain JSON back; `server/discover` advertises what this * endpoint supports. 2025-era clients that still open with `initialize` are * served statelessly by the same factory. A fresh server is built for every * request; nothing is kept between requests. * * Authentication is the EmDash auth middleware's: a session or a Bearer API * token (Settings → API tokens). Each tool enforces its own scope. * * POST /_emdash/api/mcp — JSON-RPC (modern envelope or legacy handshake) * GET / DELETE — not part of the stateless surface (405) */ import { createMcpHandler } from "@modelcontextprotocol/server"; import type { APIRoute } from "astro"; import { apiError } from "#api/error.js"; import { createMcpServer } from "#mcp/server.js"; export const prerender = false; export const POST: APIRoute = async ({ request, locals }) => { const { emdash, user } = locals; if (!emdash) { return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500); } if (!user) { return apiError("UNAUTHORIZED", "Authentication required", 401); } const pluginTools = await emdash.getEnabledPluginMcpTools(); const handler = createMcpHandler(() => createMcpServer(pluginTools, request), { legacy: "stateless", responseMode: "json", onerror: (error) => console.error("[MCP]", error), }); try { return await handler.fetch(request, { authInfo: { token: "", clientId: "emdash-admin", scopes: [], extra: { emdash, user, userId: user.id, userRole: user.role, tokenAuth: locals.tokenAuth === true, }, }, }); } catch (error) { console.error("[MCP]", error); return new Response( JSON.stringify({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }), { status: 500, headers: { "Content-Type": "application/json", "Cache-Control": "private, no-store" } }, ); } finally { await handler.close().catch(() => {}); } }; const notAllowed = (message: string) => new Response(JSON.stringify({ jsonrpc: "2.0", error: { code: -32000, message }, id: null }), { status: 405, headers: { "Content-Type": "application/json", Allow: "POST" }, }); /** GET — no server-to-client stream in stateless serving; subscriptions use `subscriptions/listen` over POST. */ export const GET: APIRoute = async () => notAllowed("Method not allowed. This is a stateless MCP endpoint — use POST."); /** DELETE — there are no sessions to close. */ export const DELETE: APIRoute = async () => notAllowed("Method not allowed. This is a stateless MCP endpoint.");