# NATIVE MCP

Direct plugin MCP implementation for Streamable HTTP/SSE at `/mcp`. This subtree owns protocol metadata, sessions, dynamic tool visibility, and translation into the bridge subsystem; it does not own editor action implementations.

## STRUCTURE
| Area | Responsibility |
|------|----------------|
| `DynamicTools/` (7) | Enabled state, categories, protected tools; `McpTaskResults` keeps the calls that answered "still running" for `manage_tools get_task_result` |
| `Execute/` (25, at cap) | Native execute pipeline: request parse, folded pins, schema validation, receipts, result pages (`McpNativeResultPages`); `Execute/Request/McpExecuteContractOptions` parses `options.expect`/`require` (judged by `Domains/Debug/Contracts/McpContracts`: require before the handler, expect in the reply funnel) |
| `Gateway/` (25, at cap) | The gateway: catalog, capability store, describe, search, guidance, folding (`McpNativeGatewayFolding`: legacy pairs, pins, `dispatchBy`) |
| `Generated/` (24) | **ALL GENERATED** capability shards (`npm run registry:generate`). Never hand-edit |
| `Primitives/` (7) | Prompts, completions, resource revision |
| `Protocol/` (14) | JSON-RPC parse/build helpers, MCP tool-result envelopes, and the 2026-07-28 request path (`McpModernRequest`, `McpModernConsent`, `McpNativeTransportModern.cpp`, and `McpNativeTransportListen.cpp` for subscriptions; transport members kept here because `Transport/` is full) |
| `Registry/` (5) | Canonical-name gate, `FMcpToolDefinition` (name/description/category), `McpSchemaBuilder`, cached schemas |
| `Resources/` (8) | `ue://` resource catalog and readers |
| `Routing/` (7) | Consolidated parent-tool action routing helpers |
| `Tools/` (1) | `McpGeneratedParentRegistry.cpp`: **GENERATED** from the capability records; registers one `FMcpToolDefinition` per parent |
| `Transport/` (25, at cap) | Bind/listen, HTTP parsing, sessions, SSE, pending requests, the keepalive thread (`McpNativeTransportKeepalive.cpp`), shutdown |

## CANONICAL SURFACE
`FMcpToolRegistry::Register()` accepts exactly these 23 names:

```text
manage_tools, manage_asset, manage_blueprint, control_actor, control_editor, manage_level
build_environment, animation_physics, system_control, manage_sequence, inspect
manage_audio, manage_geometry, manage_effect, manage_gas, manage_character, manage_combat
manage_ai, manage_inventory, manage_interaction, manage_networking, manage_level_structure, manage_pcg
```

- Registration is generated (`Tools/McpGeneratedParentRegistry.cpp`); `Registry/McpToolRegistry.cpp` accepts only canonical names and ignores duplicates.
- Adding a canonical registrar entry alone cannot expose a new parent tool. Update the canonical gate deliberately, keep TS/native parity, and justify context growth.
- `tools/list` filters accepted registry entries by dynamic enabled state; `tools/call` enforces the same state before dispatch.

## TOOL DEFINITIONS
- `FMcpToolDefinition` is pure data (name, description, category); every tool dispatches on its own name and the handler reads the concrete action from `action`.
- Execute mirrors `action` into `subAction` for handlers that still read the older field. Do not spread additional alias normalization.
- `manage_tools` is intercepted locally and returns a one-shot response; other tool calls queue through `UEngineWireSubsystem` and complete over SSE.

## DYNAMIC TOOLS
- Startup enables all accepted tools when `bLoadAllToolsOnStart` is true; otherwise it enables the `core` category.
- `manage_tools` and `inspect` are protected tools. The `core` category cannot be disabled.
- `DynamicTools` owns the internal tool visibility state (enabled tools/categories) consumed by `unreal.execute`. The public `tools/list` is permanently a single static `unreal` tool, so visibility never changes its shape: `OnToolsListChanged()` returns early and `notifications/tools/list_changed` is suppressed. Preserve locking around tool/category state and cached registry schemas. The `bEnableNativeGateway` setting and the legacy 23-tool direct listing were removed in the Task 30 cutover.

## TRANSPORT LIFECYCLE
- `POST /mcp` handles JSON-RPC; `GET /mcp` opens the persistent notification SSE stream; `DELETE /mcp` terminates a session and its streams.
- `initialize` must carry an id and returns `Mcp-Session-Id`. All later requests and notification streams require a valid session header.
- Client notifications receive HTTP 202 after validation. `tools/call` owns its socket until the streamed result completes.
- Return JSON-RPC errors through `McpJsonRpc` and tool outcomes through MCP `content[]` plus `isError`; never leak raw handler JSON as the top-level response.
- Do not block socket threads on Unreal work. Shutdown intentionally pumps game-thread tasks while draining active connections and async writes.
- **Long calls answer before the client gives up**. The keepalive thread (`McpNativeTransportKeepalive.cpp`, never the game thread) pings every open `tools/call` after 8 s of silence with `EngineWire::DescribeEditorWork` (the handler in flight, its running time and last progress, the shader queue) and at 27 s writes a success receipt whose `task.state` is `running` or `queued`; the work goes on, and `bAnsweredRunning` keeps the entry until its completion settles the idempotency slot. `AnswerStillRunning` notes the task in `TaskResults` (with the sender's principal) BEFORE it writes the answer, `CompletePendingRequest` stores the compacted reply as its outcome, and `TryHandleLocalToolCall` answers `get_task_result` from that store (queued / running with live progress / done), only to the same principal. Nothing on that thread may take `AutomationRequestExecutionMutex` (a running handler holds it): no `CancelAutomationRequest` there. A handler that reports progress (`SendProgressUpdate`) is what makes these replies say how far it has got.

## SECURITY
- Empty/`localhost` listen hosts normalize to loopback. A disallowed non-loopback host falls back to `127.0.0.1`.
- **Fail-closed LAN coupling**: the native transport refuses to bind non-loopback unless `bRequireCapabilityToken` is also enabled (`SECURITY: refusing to bind native MCP to non-loopback` in `Transport/McpNativeTransportLifecycle.cpp`). A LAN-exposed surface can never start without auth.
- When capability auth is enabled, require `X-MCP-Capability-Token` before method dispatch.
- **Constant-time token checks**: `McpConstantTimeTokenEquals` (`Private/Foundation/McpSecureTokenCompare.h`) compares the token with no data-dependent early exit, so timing never leaks how much of a token matched.
- **Session-scoped bounded cancellation**: `notifications/cancelled` correlates only to the caller's in-flight request, keyed by the client JSON-RPC id and the owning session id, so one session cannot cancel another. The cancel-marker maps (`CancelledInternalRequestIds` + `CancelledMarkerOrder`) are capped by `MaxCancelledMarkers` with oldest-first eviction, and a late response for a cancelled request is suppressed (the SSE socket closes without a result). See `Transport/McpNativeTransportCancellation.cpp`.
- Browser Origin/CORS access is allowed only under capability-token protection; preserve origin rejection and preflight behavior.
- Keep request-size limits, session expiry, method/path checks, write serialization, and socket ownership accounting intact.

## PROTOCOL VERSION NEGOTIATION (intentional legacy asymmetry)
The native transport supports **exactly the three modern MCP versions**:
`2025-11-25` (latest), `2025-06-18`, and `2025-03-26` (see `McpSupportedProtocolVersions` in `Transport/McpNativeTransportPrivate.h`). At `initialize` it echoes the highest mutually supported version, or the latest for an unknown well-formed request. A post-initialize request that omits the `MCP-Protocol-Version` header is accepted; only a present-but-unsupported value is refused (HTTP 400).

- **`2026-07-28` is served per request, beside them** (`Protocol/McpNativeTransportModern.cpp`; it stays out of `McpSupportedProtocolVersions`, which is the initialize set). A request whose `_meta` names it skips initialize and sessions: `HandleModernRequest` checks the version (`-32022`, HTTP 400), the `MCP-Protocol-Version`/`Mcp-Method`/`Mcp-Name` headers against the body (`-32020`, HTTP 400, base64 sentinel decoded), then serves it on one implicit session per principal (salted id, never sent) so the rate limits, pending-call caps and principal checks apply unchanged. `McpModern::FScope` marks the transport thread for the synchronous part: `SendHttpResponse` stamps `resultType` and the `ttlMs`/`cacheScope` hints, `StreamToolCall` copies `bModern`/`bTasks` onto the connection for the streamed result. Unknown methods (ping, logging/setLevel, initialize included) answer 404 `-32601`.
- **Consent through the client** (`Protocol/McpModernConsent.cpp`): when the gate refuses only for a missing grant and the client declared form elicitation, the call answers `input_required` with a yes/no form and an HMAC-signed `requestState` (per-run key, principal, `McpCanonicalFingerprint` of the call, 10-minute expiry, nonce). A verified yes becomes the grant describe would mint, its nonce single-use through `FMcpConsentLedger`; a no refuses without the describe nextCall.
- **Subscriptions** (`Protocol/McpNativeTransportListen.cpp`, the same wire shapes as the TypeScript SDK): `subscriptions/listen` validates the filter (`-32602`), gates each `resourceSubscriptions` URI with `McpAuthorizePrimitiveRead` as a read would be, takes a notification-stream slot (`-32603` "Subscription limit reached" in band, never 429), and parks the socket as an SSE answer: `notifications/subscriptions/acknowledged` first (only resource subscriptions are honored: no list ever changes), then `notifications/resources/updated` for `ue://changes`, each stamped with `_meta["io.modelcontextprotocol/subscriptionId"]` = the listen id. Earlier versions send `resources/subscribe` (`ChangesSubscribedSessions`, guarded by `LogEventSubscriptionsMutex`) and hear on their GET stream. `SweepChangeNotifications` runs on the keepalive thread each second, comparing each stream's `ChangesSeq` with `McpChangeJournal::LatestSeq()`; shutdown writes each listen's `resultType: "complete"` result before closing it. `resources.subscribe` is advertised for both eras.
- **Tasks extension**: with `io.modelcontextprotocol/tasks` declared, a call still open at 27 s answers a `resultType: "task"` handle; `tasks/get` reads `TaskResults` (the stored tool result once done), `tasks/cancel` stops only a call still waiting in the queue (cancelled after the answer is sent), `tasks/update` refuses (no task waits for input).
- **The stdio proxy** (`src/proxy/native-proxy.ts`, the npm package) answers `initialize` itself with the same three versions, relays session requests on its own upstream session, and sends a `2026-07-28` request sessionless with the `MCP-Protocol-Version`/`Mcp-Method`/`Mcp-Name` headers `HandleModernRequest` checks. A client pinned to a legacy version (`2024-11-05`, `2024-10-07`) is answered with `2025-11-25`.

## GATEWAY DISCOVERY
The native surface permanently exposes the single `unreal` tool; it is the only gateway (the npm package relays to it). `describe` drills one level per call and never dumps the catalog:
- `describe {}` -> the domains, `{ domain }` -> its families, `{ domain?, family }` -> its capabilities (`McpNativeGatewayDescribeOverview.cpp`; 50 rows unless `limit` is sent).
- `describe { tool }` -> the parent summary: every action name unless `limit` is sent, and a `browse` call (search filtered to the tool).
- `describe { capability }` or `{ tool, action }` -> that capability's exact contract (`perActionSchemas` true: only the parameters it declares). `{ ..., param }` -> one parameter's schema (`scope: "parameter"`).
- `search` rows are built by `McpSearchRow` in `McpNativeGatewayCatalog.cpp`; the envelope echoes `filters` and `maxBytes`, pages with `c:<offset>` cursors and reports clamped integers as `coercions`. An unfiltered first page also lists the saved recipes the request names under `recipes`, for a session whose principal holds Read (project data: Write does not imply Read) (`McpRecipes::SearchRows`, Domains/Scripts: files and JSON only, since search answers on the socket thread).

Every view is sourced from the generated capability store (`McpNativeGatewayCapabilityStore.cpp` over the `Generated/` shards), generated from the TypeScript records.

## VALIDATION
```bash
npm run registry:check   # generated shards and parent registry match the records
npm run test:params
```

- Source contracts under `tests/unit/plugin/` read the C++ as text; behaviour is checked live through the `unreal` tool.
- Folded families: `McpNativeGatewayValidation.cpp` applies `McpApplyFoldedPins` before defaults and schema validation and `McpResolveDispatchAction` after them; `FindByParentAction` falls back to any legacy pair, so every former name still resolves. A consent grant may name the capability by its canonical id, an alias, or a folded `tool.action` pair (`FMcpCapabilityDemand::ConsentNames`).
