Skip to content

feat: MCP server — API versions as tools for agents - #20

Merged
dskripchenko merged 1 commit into
masterfrom
feature/mcp-transport
Oct 26, 2026
Merged

dskripchenko merged 1 commit into
masterfrom
feature/mcp-transport

Conversation

@dskripchenko

Copy link
Copy Markdown
Owner

Stage 2, step 2: the first non-HTTP transport, on top of Runtime\Dispatcher (#19).

What

With laravel-api.mcp.enabled, every served version is an MCP server at POST /{prefix}/mcp/{version} — Streamable HTTP with JSON answers, stateless (no Mcp-Session-Id; GET/DELETE → 405).

  • McpServer (JSON-RPC 2.0, no HTTP): initialize (protocol negotiation over 2025-11-25, 2025-06-18, 2025-03-26; serverInfo = mcp.name or the app name + the API version; instructions = the Api class docblock), ping, tools/list, tools/call. Notifications/responses → 202, no body. Batches → -32600; unknown method -32601; unknown tool / bad arguments -32602; bad JSON → 400 -32700.
  • McpToolset: one tool per action, controller.action (not the operationId: a_b+c and a+b_c collide). title = first line of the summary, description = summary + description (Deprecated. prefix when deprecated), inputSchema = InputDeclaration::schema() — templates moved under $defs with refs rewritten (#/components/schemas/… only resolves inside OpenAPI), a @Model body wrapped as {type: object, allOf: [$ref]}, empty input {type: object, properties: {}}. GET → annotations.readOnlyHint. 'mcp' => false on an action or controller leaves it out.
  • tools/call → Dispatcher (router, middleware cascade, validation; the MCP request's headers — e.g. Authorization — passed on) → content: [{type: text, text: <payload JSON>}], structuredContent when the payload is an object, isError = !success. A failed action is a tool result, not a protocol error, so the model can correct the call.
  • McpController: served versions (mcp.versions, default = all not hidden), Origin check (spec: against DNS rebinding) — no Origin passes, own origin passes, mcp.allowed_origins (* for any); mcp.middleware on the endpoint.
  • Config laravel-api.mcp (enabled / LARAVEL_API_MCP, versions, path, middleware, allowed_origins, name). Route api-mcp registered only when enabled (ApiServiceProvider::registerMcpRoutes()).

Performance (500 actions, OPcache)

tools/list, cold 29 ms
tools/list, from api:cache ~0 ms
tools/call lookup 0.25 ms (route map only — a first draft described all 500 actions to find one: 29 ms per call)

api:cache now stores the tool lists of every served version, keyed by version and Api class. Cache format 1 → 2: a file written by 6.1 is ignored (as if there were none) until the next api:cache. ApiCache's exportability check accepts stdClass (var_export writes it as (object) array(...), needed for {}).

Deliberately not offered

resources, prompts, sampling, SSE streaming, sessions, file uploads; outputSchema — @output isn't trusted to be exact enough for clients that validate structuredContent against it.

Docs

README (en/ru/zh/de) "MCP server" with a claude mcp add --transport http … example; config comments; CHANGELOG; CLAUDE.md.

Tests

623 passing; golden-master unchanged. New tests/Feature/Transports/McpTest.php (15): off by default; initialize + version negotiation + instructions; notification 202; ping {}; tools/list (names, readOnlyHint, middleware input in schema, title = first summary line); tools/call result shape; middleware cascade and header forwarding; JSON-RPC errors (unknown tool/method, batch, parse); 405s; served versions; Origin; 'mcp' => false; $defs (incl. a template referring to another); api:cache stores tools (and {} survives var_export, other class not served from it); tools/list served from the cache while calls still resolve.

🤖 Generated with Claude Code

With laravel-api.mcp.enabled, POST /{prefix}/mcp/{version} speaks MCP
(Streamable HTTP, JSON answers, stateless): initialize, ping, tools/list,
tools/call. One tool per action (controller.action), input schema from
the descriptor with templates under $defs, GET marked read-only. Calls go
through Runtime\Dispatcher as the caller of the MCP request. 'mcp' =>
false opts out; Origin is checked. api:cache caches the tool lists
(cache format 2); a call finds its action via the route map.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dskripchenko
dskripchenko merged commit 71014ab into master Oct 26, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant