MCP · 11. The MCP surface

The Crux daemon speaks MCP Streamable HTTP on http://127.0.0.1:14801/mcp, advertises protocol version 2024-11-05, implements exactly four JSON-RPC methods, and serves a catalogue of 118 tools (119 with one feature flag on). Everything else in the MCP specification, resources, prompts, completion, logging, roots, ping, is not implemented and returns JSON-RPC -32601 inside an HTTP 200.

This chapter is reference. It is the map. The complete per-tool contract for all 118 tools is in chapter 12 and chapter 13. The free/hosted boundary is chapter 14. Where our own documentation and our code disagree is chapter 15.

Extension tools (ext.*) are covered in the daemon developer guide, chapter 10, MCP surface; this chapter states only how they interact with the catalogue and does not repeat that material.

In plain English. MCP, the Model Context Protocol, is a standard way for an AI assistant to discover and call tools that live outside itself. The assistant asks a server "what can you do", gets back a list of tools with their parameters, and can then call them by name. The value of a standard here is that you do not write an adapter per assistant: anything that speaks MCP can use anything that serves MCP. The daemon serves MCP on its own port, separately from its HTTP API, and what it offers over that port is a catalogue of 118 tools for storing facts, recalling them, managing sessions and the rest.

The distinction that matters most on arrival is that this is a second listener, not a route on the HTTP API. If you are looking for /mcp on port 14800 you will not find it. The two surfaces overlap in what they can do and differ in who they are for: HTTP is for code you write, MCP is for agents you run.

You will use this chapter when you are wiring an agent to the daemon for the first time and need to know what the connection looks like, and when a tool call behaved unexpectedly and you need to know whether the problem is the protocol, the catalogue or the tool. The complete per-tool contract lives in chapters 12 and 13; this one is the map.

The thing people get wrong is expecting the whole MCP specification to be there. It is not, and the way it is absent is unusual enough to trip clients: exactly four JSON-RPC methods are implemented, and everything else, resources, prompts, completion, logging, roots and ping, returns a JSON-RPC error inside an HTTP 200. A client that checks the HTTP status and assumes success will read that error as a valid response. Check the JSON-RPC body, not the status line. The other common surprise is retrieval-shaped rather than protocol-shaped: omitting token_budget on a query returns unbounded results, which is the fastest way to fill an agent's context window without meaning to.

11.0 Four things that surprise integrators

  • Omitting token_budget on query, query_scan or query_facts does not apply a default. It applies no bound at all (query.rs:120). See 11.11.
  • resources/list and friends return -32601 inside an HTTP 200. A client that treats a non-200 as "unsupported" will hang on the body instead. See 11.4.
  • An anonymous caller sees the whole catalogue, not a reduced one. Anonymity does not shrink tools/list; an absent capability token means the authz filter is skipped entirely. See 11.8.
  • A tool hidden from tools/list by surface shaping is still callable by name. Shaping is advertisement, not authorisation (surface.rs:11).

11.1 The listener

MCP runs as a separate axum server on its own port. It is not mounted under the main HTTP router on 14800. It is not stdio, a stdio bridge exists, as a separate subcommand (11.3).

SettingEnv varYAML keyDefaultSource
MCP hostCORECRUXD_MCP_HOSTdaemon.listen_addr127.0.0.1config.rs:817
MCP portCORECRUXD_MCP_PORTdaemon.mcp_port14801config.rs:822
MCP enabledCORECRUXD_MCP_ENABLEDdaemon.mcp_enabledtrueconfig.rs:827

Precedence is env, then file config, then default, for all three. The context is built once and only when mcp_enabled (main.rs:1116), and the router is mounted at main.rs:1176.

The canonical endpoint is http://127.0.0.1:14801/mcp. For contrast, HTTP is 14800 and gRPC is 4007, see the daemon guide, chapter 1, Architecture.

Bind-posture guard

MCP may not bind a non-loopback address without agent tokens. validate_mcp_bind_posture (main.rs:2050, called at main.rs:342) aborts startup unless CRUX_AGENT_TOKEN or CRUX_AGENT_TOKENS is set, or CORECRUXD_ALLOW_INSECURE_DEV_AUTH_BIND=1 is set explicitly.

Routes on the MCP port

Router builder, server.rs:48.

RouteMethodPurposeSource
/mcpPOSTJSON-RPC 2.0 request and responseserver.rs:51, handler :94
/mcpGETSSE stream when Accept: text/event-stream; otherwise a static discovery blob of protocolVersion and serverInfoserver.rs:52, handler :227
/.well-known/oauth-protected-resourceGETRFC 9728 metadata. 404 unless CRUX_MCP_RESOURCE_URL is setserver.rs:53, handler :65
/.well-known/agent-cardGETA2A discovery card, filtered to the 16-tool core floorserver.rs:57, filter agent_card.rs:106

The SSE stream is server-to-client push only and carries exactly one notification: notifications/tools/list_changed (sse.rs:27, pushed by notify_list_changed at sse.rs:209). It fires when an intent-bearing cuecrux_session call arrives on a session that has a registered stream (server.rs:208).

Session header

Mcp-Session-Id, maximum 128 characters, charset [A-Za-z0-9._:-] (server.rs:28, validated at :405). Minted as a simple UUID on initialize (server.rs:201) or on SSE open (:252).

SSE and auth environment variables

Env varDefaultSource
CRUX_MCP_SSE_MAX_SESSIONS1024; 0 means unlimitedsse.rs:29
CRUX_MCP_SSE_MAX_SESSIONS_PER_OWNER64; 0 means unlimitedsse.rs:30
CRUX_AGENT_TOKEN and CRUX_AGENT_TOKENSunset, so anonymous access is allowedserver.rs:389
CRUX_AGENT_CARDon; =0 disables the agent cardserver.rs:87
CRUX_MCP_RESOURCE_URLunset, so the well-known route 404soauth.rs:51
CRUX_MCP_AUTH_SERVERunsetoauth.rs:55
CRUX_MCP_INTROSPECT_URL, _CLIENT_ID, _CLIENT_SECRETunsetoauth.rs:118

Exceeding the SSE session limit returns 429 with {"error":"sse_session_limit","scope","limit"} (server.rs:450). Reusing another owner's session id returns 403 sse_session_owner_mismatch (server.rs:439). The owner key is agent:<name>, ip:<addr> or anonymous (server.rs:425).

11.2 The handshake

PROTOCOL_VERSION = "2024-11-05" (dispatch.rs:385), SERVER_NAME = "crux" (:388), server version is the crate version (:394).

{
  "protocolVersion": "2024-11-05",
  "capabilities": { "tools": { "listChanged": true } },
  "serverInfo": { "name": "crux", "version": "<crate version>" },
  "_welcome": {
    "hint": "…",
    "quickstart": ["…"],
    "docs": "https://github.com/CueCrux/Crux/blob/main/docs/agent-guide.md"
  }
}

Only the tools capability is advertised (dispatch.rs:404). There is no resources capability, no prompts, no logging. A spec-compliant client reading this response will not call them, which is the intended contract, see 11.4 for what happens if a client calls them anyway.

11.3 The four JSON-RPC methods

There is a single dispatch site, dispatch() at dispatch.rs:397, matching on the method string. Four methods are accepted. That is the complete set.

MethodBehaviourSource
initializeHandshake, as in 11.2dispatch.rs:400
notifications/initializedresult: null. Defensive only, the HTTP layer short-circuits every id-less request to 202 before dispatch is reacheddispatch.rs:434
tools/listlist_tools_json_for_context(ctx, now); optionally emits an agent.tools_offered.v1 ledger event under CORECRUXD_FEATURE_TOOL_LEDGERdispatch.rs:437
tools/calldispatch_tool_call(id, &params, ctx)dispatch.rs:457
anything elsewarn! plus -32601 "method not found: {other}"dispatch.rs:460

Notifications, requests where id is absent, receive a bare 202 Accepted with no body (server.rs:158). The comment at server.rs:149 records why: Codex's rmcp transport rejects the older {"id":null,"result":null} shape.

The stdio bridge

corecruxd mcp-stdio (mcp_stdio.rs) reads line-delimited JSON-RPC on stdin and relays it to POST $CRUX_MCP_URL, defaulting to http://127.0.0.1:14801/mcp (mcp_stdio.rs:36). Auth is forwarded from CRUX_AGENT_TOKEN (:22). Upstream failures surface as JSON-RPC -32000 (:39), a code that exists nowhere else in the surface.

11.4 What is not implemented, and how it fails

There is no resources/list, resources/read, resources/templates/list, prompts/list, prompts/get, completion/complete, logging/setLevel, roots/* or ping handler anywhere in the crate. Every one of them reaches the dispatch fallback at dispatch.rs:460.

What an integrator must know about that path:

  • The JSON-RPC error code is -32601 (protocol.rs:19) and the message is "method not found: resources/list" or equivalent.
  • The HTTP status is 200 OK. The error rides in the body (server.rs:471). Client code that branches on HTTP status will conclude the call succeeded.
  • There is no data field on the error object (protocol.rs:66); nothing to introspect beyond the message string.
  • The behaviour is pinned by unknown_method_returns_error (dispatch.rs:836).

One internal inconsistency, disclosed because it will mislead you. The OAuth read-only method allowlist OAUTH_READ_METHODS (oauth.rs:352) permits ping, resources/list, resources/read and resources/templates/list. Those four pass the read-only gate at server.rs:173 and then hit the dispatch fallback anyway. The allowlist is aspirational. The methods do not exist.

11.5 Error codes

CodeConstantSource
-32700PARSE_ERRORprotocol.rs:13
-32600INVALID_REQUESTprotocol.rs:16
-32601METHOD_NOT_FOUNDprotocol.rs:19
-32602INVALID_PARAMSprotocol.rs:22
-32603INTERNAL_ERRORprotocol.rs:25
-32030CAPABILITY_DENIED, Crux-specificdispatch.rs:27
-32000UPSTREAM_ERROR, stdio bridge onlymcp_stdio.rs:39

Pre-dispatch HTTP gates

These happen before dispatch() is reached at all.

GateResultSource
Auth failure401 plus WWW-Authenticate (RFC 9728) when configuredserver.rs:95
Bad Mcp-Session-Id400 invalid_mcp_session_idserver.rs:101
Unparseable body200 plus -32700; the error text is scrubbed through crux_observe::redactserver.rs:136
id absent202 Accepted, empty bodyserver.rs:158
OAuth read-only violation200 plus -32601, message '<x>' is not available to read-only OAuth callers (mcp:read scope)server.rs:173

11.6 The tools/call pipeline

dispatch_tool_call, dispatch.rs:468. Every call runs this sequence.

StepBehaviourSource
Missing name-32602, tools/call requires a "name" parameterdispatch.rs:473
RCX capability gateenforce_rcx_tool_capability; denial is -32030 carrying reason_code, mode, token_id, token_hash, stamp, refusal_receipt, upgrade_hintdispatch.rs:480, impl :641
Revocation gateA revoked passport is refused every tool outside a small read-only allowlistmod.rs:2876
OTel spanrecord_tool_span_startdispatch.rs:491
Executetools::call_tool(name, &args, ctx)dispatch.rs:494
Token accountingPer-passport in and out estimates plus the declared token_budgetdispatch.rs:510
Metrics and ledgerAn unknown tool label collapses to "unknown"; agent.tool_invocation.v1 is emitted when the ledger flag is ondispatch.rs:528
Trace ringrecord_dispatch_metered with the canonical signature and predicted effectsdispatch.rs:552
SuccessOptional audit-envelope wrap, then normalize_result_shape which guarantees a top-level result.contentdispatch.rs:564
Tool errorJsonRpcResponse::error(id, code, message); an unknown tool name is -32601 "unknown tool: {name}"mod.rs:3021

Every response is wrapped in the standard MCP {"content": [{"type":"text", …}]} envelope. The per-tool output shapes in chapters 12 and 13 describe what is inside that envelope.

11.7 The verified counts

QuantityValueEvidence
Tool modules under crates/crux-mcp/src/tools/52 .rs files, 51 tool modules plus mod.rsdirectory listing at commit 93b41a7
Tools in the base catalogue, both passport flags off118const TOOL_COUNT: usize = 118, mod.rs:3108, asserted against list_tools() at mod.rs:3156
Tools with CORECRUXD_FEATURE_PASSPORT_MINT_REQUESTS=1119, adds request_passport_mintfilter mod.rs:2440; assertion mod.rs:3366
ToolDefinition literals in list_tools_with_flags119, one of which is conditionally filteredmod.rs:163 onward
Extension (ext.*) toolsunbounded, and not counted in TOOL_COUNTinjected post-catalogue at mod.rs:2497
Distinct handler modules reached by call_tool45mod.rs:2881 onward

list_tools() (mod.rs:131) is the flags-off convenience wrapper over list_tools_with_flags(false, false) (mod.rs:156).

Exactly two flags change the catalogue itself:

  • CORECRUXD_AGENT_PASSPORTS, default off (config.rs:960). Changes issue_passport's surface marker from [hosted] to [local], not its membership (mod.rs:2446).
  • CORECRUXD_FEATURE_PASSPORT_MINT_REQUESTS, default off (config.rs:961). Changes membership: request_passport_mint is filtered out entirely while off (mod.rs:2440), and dispatch fails closed with -32601 (mint_request.rs:22).

A third flag, CORECRUXD_TOOL_SURFACE, changes what is advertised without changing what exists, see 11.10.

Four tests hold the count line: list_tools_returns_expected_count (mod.rs:3156), tool_names_unique (:3195), list_tools_json_has_tools_array (:3204) and tool_output_docs_covers_all_tools (:3270).

11.8 How tools/list filtering works

list_tools_json_for_context (mod.rs:2474) reads the surface mode from the environment and delegates to list_tools_json_for_context_with_mode (mod.rs:2480).

StepWhat happensSource
1auth is derived from the RCX router's token, or is None when there is no tokenmod.rs:2485
2Base catalogue = list_tools_with_flags(agent_passports_enabled, passport_mint_requests_enabled)mod.rs:2489
3Authz filter. No router means unfiltered. A router means filter_tools_for_rcx_routermod.rs:2490
4Extension tools are appendedmod.rs:2497
5Extension tools are separately RCX-filtered under capability crux-extension.<tool>mod.rs:2498
6tools.extend(extension_tools); there is no dedupmod.rs:2509
7Surface shaping runs last, after authz and after the extension merge, so it can only narrow and never widenmod.rs:2516, rationale :2510
8tools_to_json(tools, auth)mod.rs:2536

Capability naming: the grant-to-tool mapping

rcx_capability_for_tool (mod.rs:2713):

Tool patternCapability string
ext.*crux-extension.<tool_name>
query, query_scan, query_expandcorecrux.query.local, all three share one capability
everything elsecrux-mcp.<tool_name>

rcx_mcp_tool_capability (mod.rs:2661) additionally sets the backend. Hosted-gated tools get backend_id = "hosted.vaultcrux.com" (tool_surface.rs:21); everything else gets backend "local" with egress [None].

The operator's rule: a passport's token must contain a backend whose id matches (local or hosted.vaultcrux.com) and which lists the exact capability string. A crux-mcp.sync_pull grant attached to the local backend surfaces nothing.

The denial gates behind the filter

filter_tools_for_rcx_router (mod.rs:2648) runs a full decide() per tool (crux-router/src/lib.rs:291) with estimated_credit_cost: 0 and backend_reachable: true. In order (crux-router/src/lib.rs:197 onward):

GateReason codeSource
Token requires contextual verificationTokenInvalidlib.rs:198
Issuer signature invalidTokenInvalidlib.rs:201
validate_basic fails for a non-expiry reasonTokenInvalidlib.rs:204
No backend both matches preferred_backend and permits the capabilityCapabilityNotPermittedlib.rs:212
Capability absent from that backend's permitted listCapabilityNotPermittedlib.rs:217
Requested egress class not permittedEgressNotPermittedlib.rs:225
Required attestation absentAttestationMissinglib.rs:233
Token expired, so fallbackTokenExpiredlib.rs:241
Replay receipt class on a debitable callReceiptClassSideEffectDeniedlib.rs:260
Insufficient creditInsufficientCreditlib.rs:263

What gets attached to each tool in the response

tools_to_json (mod.rs:2558):

FieldWhenSource
name, description, inputSchemaalwaysmod.rs:2606
inputSchema["x-crux-token-ref"] = {token_id, token_hash}auth presentmod.rs:2564
inputSchema["x-crux-receipt-class"] and ["x-crux-tier"]auth presentmod.rs:2573
_meta.crux.consequence_metadata: {schema, domain, reversibility, materiality, idempotency_class, blast_radius, compensating_tool, pro_enricher_available}always, including for unauthenticated callersmod.rs:2577; source action_enrichment.rs:183
_meta.crux.filtered_by = "rcx-capability-token"auth presentmod.rs:2581
_meta.crux.token_ref, receipt_class, tierauth presentmod.rs:2582
_meta.crux.upgrade = {platform_available, requires, docs}description starts [hosted]mod.rs:2594
x-crux-output-schema = {$ref, contract:"crc-v1", kind, when}tool has a CRC-v1 kindmod.rs:2604; builder crc_v1.rs:381

ToolAuthMetadata (mod.rs:2539) is a four-string provenance stamp, token_id, token_hash, receipt_class, tier. It performs no filtering. It exists to record which token shaped the response.

The unauthenticated caller

Two layers must not be conflated.

Transport. authenticate_agent (server.rs:336): no Authorization header plus an empty agent registry gives Anonymous; an empty header against a non-empty registry gives 401. A valid hosted OAuth bearer carrying mcp:read is read-only, enforced pre-dispatch (server.rs:173).

Catalogue. Anonymity does not shrink tools/list. An anonymous caller normally has no RCX router, so:

  • No _meta.crux.filtered_by, no token_ref, no x-crux-* schema keys, no top-level _meta.
  • The base catalogue is returned unfiltered (mod.rs:2494), all 118 tools, including the three marked [hosted].
  • Zero extension tools, because the calling passport fingerprint is None (extensions.rs:40).
  • Under dynamic surface mode, all anonymous callers share one global intent slot keyed __anon__ (mod.rs:2521).

11.9 Extension tools are appended after the catalogue

ext.* tools are injected by list_extension_tools(ctx) (extensions.rs:39) at step 4 of the pipeline above, after list_tools_with_flags has produced the static catalogue. Three consequences follow directly.

  • They are not counted in TOOL_COUNT. The 118 figure is the static catalogue only. A daemon with extensions installed advertises more than 118 tools and no assertion covers the difference.
  • They are per-caller. Resolution requires a passport fingerprint, so a caller without one gets an empty list (extensions.rs:40). Two agents on the same daemon see different lists.
  • The merge does not dedup (mod.rs:2509). An extension declaring a name that collides with a built-in appears twice in tools/list, and every call routes to the built-in handler, because the built-in match arms precede the extension arm (mod.rs:2882 onward, extension guard at :3020). Nothing rejects this at install time.

The ext. prefix is load-bearing and unvalidated: is_extension_tool_name (extensions.rs:179) is a plain starts_with("ext."), while IntegrationManifest::validate (crux-integrations/src/lib.rs:481) checks only that a tool's name and description are non-empty. A manifest tool named without the prefix is advertised, gets the wrong capability string, and is not dispatchable. The full mechanism, including trust tiers and the loopback dispatch path, is in the daemon guide, chapter 10.

11.10 Intent-based surface narrowing

The full surface serialises to roughly 27.8k tokens, re-sent every turn (surface.rs:10). CORECRUXD_TOOL_SURFACE (surface.rs:96) trades catalogue size against discoverability.

ValueModeResult
unset, or any unrecognised valueFull (the default)Identity. Byte-for-byte the unshaped catalogue
minimalMinimalThe 16-tool core floor only (surface.rs:131)
dynamicDynamicFloor plus the top 12 scored tools (surface.rs:295)

Parsing is case-insensitive and trimmed, and any unrecognised value falls back to Full (surface.rs:106), a typo can never silently shrink a production surface. DYNAMIC_TOP_N = 12 (surface.rs:42); INTENT_TTL_SECONDS = 3600 (surface.rs:46).

The 16-tool core floor (surface.rs:56), with cuecrux_session pinned first:

GroupTools
discoverycuecrux_session
retrievequery, query_scan, query_expand
rememberstore_fact, query_facts, get_bootstrap, memory_view
session continuitysave_session, get_session
self-identifyget_agent_identity, get_passport
verifyreceipt_verify
ops posturesync_status
coordinationcreate_handoff, accept_handoff

Intent capture. cuecrux_session(intent=…) records a per-passport intent (cuecrux_session.rs:87) into a process-global map (surface.rs:146). A blank intent clears it; expired records are evicted on read. Because stateless HTTP cannot push, the intent is read on the next tools/list, unless the client holds an open SSE stream, in which case the list_changed notification fires immediately (server.rs:208).

There are exactly five valid intents (crux-session/src/intent.rs:45). Anything else scores zero bias and yields the floor only.

IntentAffinity biasesBeyond-floor tools advertised
audit_reviewaudit 30, proof 20, retrieval 10list_observations, get_observation, verify_observation, record_decision, declare_constraint, get_constraints, check_constraints, audit_config, check_config_audit, audit_export_bundle, tool_trace_recent, learn
compliance_exportaudit 30, proof 25, economy 10The identical 12, audit at 30 saturates the slots
document_ingestmemory 30, journal 20, retrieval 10delete_fact, list_entities, fact_history, memory_acknowledge_use, memory_forget, memory_forget_dry_run, memory_edit, memory_pin, memory_history, memory_freshness, memory_sweep_candidates, memory_set_horizon
session_reviewsession 30, memory 20, journal 10list_sessions, delete_session, archive_session, unarchive_session, get_workspace_storyline, register_repo, list_repos, then delete_fact, list_entities, fact_history, memory_acknowledge_use, memory_forget
knowledge_queryretrieval 30, memory 20, session 5get_gaps, then delete_fact, list_entities, fact_history, memory_acknowledge_use, memory_forget, memory_forget_dry_run, memory_edit, memory_pin, memory_history, memory_freshness, memory_sweep_candidates
none, or unrecognised-Floor only, identical to minimal

Scoring is intent_bias(tool_affinity(tool)) + trace_boost(tool), sorted descending and stable on catalogue index, keeping only positive scores (surface.rs:295). It never pads with irrelevant tools (surface.rs:258). Trace boosts are capped at 12 (surface.rs:269), deliberately below the maximum intent bias of 30, so a declared intent dominates recent habit.

Three limits of the affinity table, stated because they change what you will see:

  • Roughly 60 real tools have no affinity at all and can never be surfaced by intent alone, the whole coordination, orchestrator, punchcard, substrate, features, GitHub, approvals and passport-lifecycle planes. Under dynamic they appear only via trace boosts.
  • No ext.* name has an affinity, so extension tools are invisible under minimal and, absent trace boosts, under dynamic.
  • proof_verify appears in tool_affinity (surface.rs:248) but is not a tool; the journal and economy affinities are referenced by the intent table but no tool maps to either. Unlike the core floor, which is guarded by core_floor_names_exist_in_full_surface (surface.rs:349), tool_affinity has no existence test, so these dead entries are invisible to CI.

Shaping is advertisement, not authorisation. A shaped-out tool remains callable by name; dispatch is a match on the name gated only by the RCX capability check (dispatch.rs:457).

11.11 token_budget: what is actually mandatory

The house convention says token_budget is mandatory on every retrieval call. That is a house convention, not a code contract. Of the 25 tools that accept token_budget, 7 reject a call that omits it. There is no server-side maximum or clamp anywhere in the MCP surface, and no environment variable that sets a default.

The hazard, stated plainly

Omitting token_budget on query, query_scan or query_facts does not fall back to a default. It applies no bound. The load-bearing line is query.rs:120:

None => result.hits,

The result is bounded only by limit, which defaults to 10 for query, 20 for query_scan and top_k 10 for query_facts. That is survivable for a small corpus and a real cost problem at scale, especially with a raised limit. Pass token_budget explicitly on every retrieval call. Nothing in the daemon will do it for you.

Group A: hard-required; the call fails with -32602 if omitted

ToolIn schema required[]EnforcementMinimum
session_checkpointmod.rs:900sessions.rs:79 plus an explicit greater-than-zero checkgreater than 0
execplan_gatemod.rs:986same require_u64 pathgreater than 0
audit_export_bundlemod.rs:1446audit_export.rs:118at least 1
passport_splitmod.rs:1573identity.rs:841
passport_mergemod.rs:1621identity.rs:841
passport_link_devicemod.rs:1661identity.rs:841
approval_requestmod.rs:2242approvals.rs:279, a presence check only, so token_budget: 0 passes. It runs before the feature flag check at :317, so a call against a disabled feature still errors on the missing budget firstnone

Group B: optional, with a silent hardcoded default

No environment variable overrides any of these.

ToolDefaultApplied atDeclared in the schema?
memory_view2000memory.rs:170Description only (mod.rs:553); there is no "default" key
memory_freshness500freshness.rs:105Yes, mod.rs:638
memory_sweep_candidates500freshness.rs:190Yes, mod.rs:661
memory_contradictions500consolidation.rs:96Yes, mod.rs:684
tool_trace_recent2000tools/traces.rs:78Yes, tools/traces.rs:55
activity_recent500tools/activity.rs:116, an explicit 0 also falls back to 500Description says "Defaults to 500" (mod.rs:2206); not in required[], despite the prose at mod.rs:2189 claiming "Required: session_id, token_budget"

Group C: accepted, and it does not bound the result

ToolBehaviour when omittedSource
queryReturns all of limit, default 10. No budget applied.query.rs:120
query_scanNo trim; all of limit, default 20. The parameter is read by the handler but is not in the schemaquery.rs:202
query_factsThe whole top_k, default 10, returned untrimmedfacts.rs:787
memory_forgetThe resolver touches everything in scopeforget.rs:448
memory_forget_dry_runSameforget.rs:337
artefact_put, artefact_get, artefact_listDescriptions say "Mandatory output-token cap (QC.2)". It is never in required[] and never validatedmod.rs:778, :798, :819
output_attestWhen present it is an admission gate, not a trimmer, oversize content is rejected with -32602output_attest.rs:285
autonomy_contractFull matrix, no trimautonomy.rs:218
context_custody_auditAdvisory only, never read by the handler; the scorecard is fixed-sizecontext_custody_audit.rs:63
session_token_usageAccepted "for QC.2 conformance" and discarded, the handler signature ignores its argumentstoken_usage.rs:32

get_bootstrap declares no token_budget at all (mod.rs:378); its handler hardcodes top_k: 100, token_budget: None (facts.rs:844). Passing one is not an error; it is ignored.

No clamp, and non-uniform reporting

An agent may pass token_budget: 4_000_000_000 and it is honoured verbatim. The only clamp in the repository is on a non-MCP HTTP route, body.token_budget.clamp(128, 128_000) (workbench.rs:304).

Budget-usage reporting is not uniform, and nothing anywhere emits budget_remaining.

ToolReported fields
querytotal_candidates; under CRC-v1 also cost_estimate. No tokens_used, no truncation flag
query_scantokens_returned, budget_truncated
query_factsUnder CRC-v1, cost_estimate plus total_candidates. Legacy shape reports nothing
memory_viewtotal_tokens, returned
activity_recenttoken_budget echoed, returned, truncated
autonomy_contractsummary.truncated_by_token_budget: a count, not a boolean
session_token_usageused, limit, pct, tokens_in, tokens_out, calls
session_checkpoint, get_session, get_bootstrap, get_gapstotal_tokens

Truncation is uniformly instrumented, but as a Prometheus counter rather than a response field: corecrux_tool_response_truncated_total{tool, reason} (ledger.rs:265). Every dispatch also records the declared budget into the ledger event as token_budget_in (dispatch.rs:521).

CORECRUXD_SESSION_TOKEN_BUDGET is report-only. It is read solely by session_token_usage (token_usage.rs:47). Exceeding it neither fails nor truncates anything; it only makes pct exceed 100.

11.12 The bootstrap surface

get_bootstrap is the daemon's runtime knowledge surface, and it is a thin filter over the fact store, not a separate content system. It queries facts whose entity begins __bootstrap__:: (facts.rs:822).

topic is normalised by normalize_bootstrap_topic (facts.rs:882) and used as an entity prefix.

Caller passesNormalises toPrefix queried
doc, docsdoc__bootstrap__::doc:
pattern, patternspattern__bootstrap__::pattern:
error, errors, resolution, resolutionsresolution__bootstrap__::resolution:
tool, tool-output, tool-outputstool-output__bootstrap__::tool-output: plus synthesized entries
anything elsepasses through verbatim__bootstrap__::<verbatim>:
omitted-__bootstrap__::; everything

The taxonomy is open. An unrecognised topic is not an error; it is a prefix that probably matches nothing, and the tool returns no bootstrap knowledge for topic '<t>' (facts.rs:869).

Content arrives three ways: seeded unconditionally and idempotently at daemon startup (bootstrap.rs:65, called at main.rs:992, with newly embedded facts backfilled into already-seeded stores at bootstrap.rs:107); written by operators or agents as any store_fact under a __bootstrap__:: entity; and synthesized on demand for topic="tool-output" (crc_v1.rs:395).

The reserved __bootstrap__:: prefix is filtered out of memory_view, memory_freshness, memory_sweep_candidates, the audit envelope, traces and the activity log, bootstrap content never leaks into consumer memory surfaces.

The output-contract document. The MCP specification has no outputSchema field, so tool_output_docs() (mod.rs:2728) substitutes for it: a canonical per-tool output contract covering all 119 tools, reachable at runtime via get_bootstrap(topic="tool-output") and CI-guarded by tool_output_docs_covers_all_tools (mod.rs:3270). The "Output" rows in chapters 12 and 13 reproduce it.

Sources

All line references were verified at commit 93b41a7.