Engineering manualAPI route source
/api/mcp
Exact current route handler: hosted-app/app/api/mcp/route.js
Methods: DELETE, GET, POST. Input signals: query parameters. Literal response-status signals: 405.
Source: hosted-app/app/api/mcp/route.js. Imports: ../../../lib/bounded-http.mjs, ../../../lib/mcp-server.mjs, ../../../lib/mcp-transport.mjs, ../../../lib/supabase-rest.mjs, next/server. Exact source is shown for inspection; its presence does not prove authentication, validation or live behavior.
All API routes · API and execution contract
// /api/mcp — the Streamable HTTP MCP endpoint. Row `mcp_server` on
// src/local/upgrade-proposals.mjs: "Let an agent run a pull and read the
// result." See hosted-app/lib/mcp-server.mjs for the protocol itself; this
// file is the thinnest possible Next.js wrapper around it — parse the body,
// read the bearer header, hand both to handleMcpRequest, ship the response.
//
// GET/DELETE answer 405. This server keeps no session state (no
// Mcp-Session-Id, no SSE stream to resume), so there is nothing for either
// verb to do — a compliant Streamable HTTP client that tries session
// management against a server with no `Mcp-Session-Id` response header
// simply falls back to stateless POSTs, which is what this endpoint is.
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
// Reservations persist an approved queue job before returning. after() only
// signals the existing worker; no direct capture runs in this request.
export const maxDuration = 300;
import { NextResponse, after } from "next/server";
import { supabaseConfigFromEnv } from "../../../lib/supabase-rest.mjs";
import { bearerTokenFromHeader, JSONRPC_ERRORS } from "../../../lib/mcp-server.mjs";
import { serveMcpRequest } from "../../../lib/mcp-transport.mjs";
import { readBoundedJsonRequest } from "../../../lib/bounded-http.mjs";
export async function POST(request) {
const token = bearerTokenFromHeader(request.headers.get("authorization"));
const config = supabaseConfigFromEnv(process.env);
const origin = new URL(request.url).origin;
let rpcRequest;
try {
rpcRequest = await readBoundedJsonRequest(request, 131072);
} catch (error) {
const parseError = error?.code === "invalid_json" || error?.code === "readable_body_required";
return NextResponse.json(
{ jsonrpc: "2.0", id: null, error: {
code: parseError ? JSONRPC_ERRORS.PARSE_ERROR : JSONRPC_ERRORS.INVALID_REQUEST,
message: parseError ? "Request body is not valid JSON" : error?.code || "invalid_request_body"
} },
{ status: parseError ? 400 : error?.httpStatus || 400 }
);
}
return serveMcpRequest({
request,
rpcRequest,
token,
config,
origin,
// Signal only the armed, persisted job after this response. Queue leases,
// execution and continuation belong to the existing worker endpoint.
scheduleCapture: (fn) => after(fn)
});
}
const methodNotAllowed = () =>
NextResponse.json(
{ jsonrpc: "2.0", id: null, error: { code: JSONRPC_ERRORS.INVALID_REQUEST, message: "This endpoint is stateless Streamable HTTP: POST a JSON-RPC 2.0 request." } },
{ status: 405, headers: { allow: "POST" } }
);
export const GET = methodNotAllowed;
export const DELETE = methodNotAllowed;