ADR-0060: MCP as Agent Authoring Surface¶
- Status: Accepted (2026-04-19)
- Date: 2026-04-18
- Supersedes: —
- Superseded by: —
- Extends: ADR-0008 (MCP primary, HTTP secondary)
- Depends on: ADR-0005 (Rich capability manifest), ADR-0008 (MCP primary transport), ADR-0046 (MCP KG resources), ADR-0055 (Meta-capability surface), ADR-0056 (ORM validation ergonomics), ADR-0057 (Versioned migrations), ADR-0059 (Store-agnostic validation)
Context¶
Today the MCP server is an invocation surface: MCP projects every
@capability to tools/* and exposes five built-in resources
(trails://schema, trails://kg/types, trails://stats,
trails://doctor, trails://prov/latest) plus two dynamic URI
patterns (trails://kg/{Type} and trails://kg/{Type}/{id}) with
subscriptions on the live ones. See docs/mcp-surface.md:46-64 for the
resource catalogue and docs/mcp-surface.md:170-182 for the JSON-RPC
method map. The surface works; agents can call capabilities and read
a handful of introspection URIs.
What the surface is not yet is a place where an agent can author and operate a Trails application end-to-end. Concretely:
- Framework-authoring operations are invisible to MCP. ADR-0055
introduced
@meta_capabilityfor scaffolders, schema ops, and migrations, and landedtrails.meta.ping(commit3b28512, Phase 1). Thetrails_meta_<name>projector is wired atpython/src/trails/mcp_server.py:193-239(list_toolsextends withtrails.meta.mcp_tool_schemas();trails_meta_*names route throughtrails.meta.mcp_invoke). Catalogue size: one. - KG instances are a coarse read-only view.
trails://kg/typeslists node types with counts andtrails://kg/{Type}/{id}fetches one instance (python/src/trails/mcp_resources.py:643-668), but there is no typed list endpoint, no filter grammar, no explicitapplication/ld+jsonMIME, and no documented subscription cost. An agent that wants "allRequirementwherestatus = proposed" must fall through to a handwritten capability — exactly the per-project glue the framework should absorb. - The tool
inputSchemais runtime-derived and silently lossy.capability_to_tool_schemaatpython/src/trails/mcp_server.py:99-154mapsstr|int|float|booldirectly and falls every other hint to"string"(mcp_server.py:144). SHACL-grade constraints declared viapredicate(..., pattern=..., one_of=..., min_length=...)inpython/src/trails/shapes.py:77-175never reach the MCP schema. No snapshot test exists (docs/mcp-surface.md:278-296), so a type hint change silently alters the agent contract. - Validation failures become opaque tool errors. ADR-0056 shipped
FieldError(field, code, message, value, constraint), but MCPtools/callwraps any handler exception in{"isError": true, "content": [...]}atpython/src/trails/mcp_server.py:200-212. AValidationErrorcarrying threeFieldErrors flattens to a stringified message. - No authentication, no scope. MCP stdio has no auth; SSE
inherits whatever HTTP provides. ADR-0055's scope model
(
runtime/builder/dev) is enforced at dispatch, but nothing on the MCP boundary reads a bearer token or rejects abuilder-scoped call from an unauthenticated client.
The feature-inventory claim that MCP is "more polished than HTTP"
(docs/feature-inventory.md:113-126) is half-right: the JSON-RPC
core, subscriptions, and list-changed notifications are clean
(docs/mcp-surface.md:140-163); the schema contract, validation
plumbing, and auth story are not (docs/mcp-surface.md:329-344).
This ADR's job is to decide what agent authoring surface means on top of the primitives ADR-0055 / ADR-0056 / ADR-0057 / ADR-0059 give us, specifically for MCP. It does not re-decide ADR-0008 (MCP-primary) nor ADR-0055's scope model; it commits to a concrete projection.
Decision¶
Treat the Trails MCP server as an authoring surface, not just an invocation surface. The agent must be able to (a) discover every registered capability AND every meta-capability that their scope permits, (b) read, filter, and subscribe to typed KG instances, and © receive structured, actionable errors when validation fails — all without leaving MCP.
Three bundles:
- Bundle 1 — Auto-project every
@meta_capabilityto MCP, with scope gating at the MCP boundary and a discriminator on the tool descriptor so clients can separate meta from runtime. - Bundle 2 — Every
@node_typeclass becomes a typed KG resource namespace with a bounded filter grammar. - Bundle 3 — Derive tool
inputSchemafrom@shape/predicate()constraints, surfaceFieldErrors as structured JSON-RPC error data, and lock the schema contract behind golden-file snapshot tests.
Bundles are independent at the implementation level (Bundles 2 and 3 are unblocked today; Bundle 1 depends on ADR-0055 Phase 2) but they share one contract: nothing on the MCP side extends the MCP specification. We project, we do not invent wire format.
Bundle 1 — Meta-capability auto-projection to MCP¶
Every @meta_capability registers a matching MCP tool. Phase 2 of
ADR-0055 is the enabling work; this ADR locks the MCP-specific
concerns so the MCP projector does not re-decide them per op.
Tool naming¶
- MCP tool name:
trails_meta_<namespace>_<name>. Single-segment namespaces (trails.meta.ping) project astrails_meta_ping; nested ones (trails.meta.gen.shape) project astrails_meta_gen_shape. The convention is already live (python/src/trails/meta/_projector.py:18-21;mcp_tool_namein the same module is the single source of truth). - Dots in the canonical id become underscores in the MCP name;
underscores in an op name are forbidden to keep the mapping
reversible. The projector raises
TrailsErrorat decoration time on_in an op name. (Phase 2 lint.)
Tool descriptor shape¶
Each tool descriptor in the tools/list response carries a
Trails-reserved extension field:
{
"name": "trails_meta_gen_shape",
"description": "Scaffold a @shape skeleton; returns a FileDiff.",
"inputSchema": { ... },
"trails": {
"kind": "meta",
"scope": "dev",
"namespace": "gen",
"op": "shape",
"idempotent": false
}
}
Runtime capabilities get:
{
"name": "research.search",
"description": "...",
"inputSchema": { ... },
"trails": {
"kind": "runtime"
}
}
The namespaced trails key is the discriminator. It is not a new
MCP spec field; the MCP spec permits additional properties on tool
objects, and the trails.* prefix keeps the extension self-identified.
(Open question: whether MCP has since adopted a standard _meta
field — see §Open questions.) Clients that do not know the extension
see a normal tool; clients that do get filtering.
Scope gating and auth on the MCP boundary¶
The server process starts with a scope set (the union of scopes it will surface):
- stdio: env
TRAILS_MCP_SCOPES(comma list; defaultruntime) plus envTRAILS_MCP_TOKENfor a single bearer token (optional; if present, the client passes it ininitialize.clientInfo.authorization.bearer). Mismatched token → JSON-RPC-32001 Authentication failedoninitialize. stdio is single-client by construction (python/src/trails/mcp_server.py:179-186). - SSE: same env plus
Authorization: Bearer …onGET /sseandPOST /messages; per-connection narrowing via token claims (scopes: [...]). Validation reuses the HTTP auth middleware (not yet wired for SSE — seedocs/mcp-surface.md:223-233).
tools/list filters by the effective scope set (process default
intersected with connection claims). tools/call re-checks scope
immediately before dispatch — the list filter is advisory, the
call-site check is authoritative. Mirrors @meta_capability's
"projector rechecks scope" rule (ADR-0055 §"Security — the error
surface when scope check fails"). The broader DID/VC identity story
(ADR-0011 / ADR-0030) is orthogonal; this ADR only requires the MCP
boundary to have a place to put a token that the scope gate can read.
Scope-denied error shape¶
When scope gating rejects a tools/call invocation, the response is a
JSON-RPC error (not a tool error):
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32004,
"message": "scope denied: trails_meta_gen_shape requires dev",
"data": {
"tool": "trails_meta_gen_shape",
"required_scope": "dev",
"caller_scopes": ["runtime"]
}
}
}
Code -32004 is chosen from the application-error range
(-32000..-32099); it mirrors ADR-0055's cross-surface error
contract. JSON-RPC errors — rather than {"isError": true, ...} tool
errors — are used because the op did not run. This matches ADR-0055
§"Security — the error surface when scope check fails".
Argument schema derivation + tools/listChanged¶
Meta schemas derive from the handler signature via the same path as
runtime capabilities (Bundle 3 below): mcp_tool_schemas() feeds its
descriptors through capability_to_tool_schema (generalised to read
MetaDescriptor alongside capability handler metadata). When a
plugin (ADR-0049) registers a new @meta_capability at runtime, the
server emits notifications/tools/list_changed — the primitive
exists for resources (python/src/trails/mcp_resources.py:148-159)
and prompts; extending it to tools is one callback each on
_handlers mutation and on the meta registry.
Bundle 2 — @node_type instances as MCP resources¶
Every registered @node_type class becomes a typed resource
namespace. The three URI patterns:
| URI | Returns | MIME | Subscriptions |
|---|---|---|---|
trails://kg/<Type> |
Paginated list of {iri, label} objects |
application/json |
Watch: any create / update / delete of any instance of the type |
trails://kg/<Type>/<id> |
Full property dict for a single instance (JSON-LD-shaped) | application/ld+json |
Watch: create / update / delete of that one instance |
trails://kg/<Type>?where=<field>=<value> |
Paginated filtered list (equality on one scalar field) | application/json |
Not subscribable in v0.2.0 |
The first two already exist
(python/src/trails/mcp_resources.py:643-668) but are undocumented
in the MCP spec sense — they have no entry in resources/list, so
agents have to guess URIs. This bundle commits to:
- Listing:
resources/listreturns one umbrella resource per registered@node_typeattrails://kg/<Type>with description ("List all Requirement instances") anduriTemplatetrails://kg/<Type>{?where,limit,offset}. Per-instance URIs are not enumerated (an org with 10k instances does not want a 10k-entryresources/list); the existing dynamic resolver handles them. - List body: array of
{iri, label}objects. Paginated via?limit=/?offset=(defaultslimit=100, hard caplimit=1000). This is a narrowing of the current 100-hardcoded limit atmcp_resources.py:643-668. - Single-instance body: JSON-LD with
@id,@type(the node-type IRI), and one property per declared field. Property names are the registered predicate IRIs. Makes theapplication/ld+jsonMIME correct by construction.
Filter grammar for ?where=¶
v0.2.0 ships the minimum-viable subset — enough for the 80% case,
small enough to reject ambiguity. Exactly one filter clause per URI,
exactly one operator (=), value is either a scalar literal or a
bareword mapped to an xsd datatype per the node-type's registered
PredicateInfo:
trails://kg/Requirement?where=status=proposed
trails://kg/Requirement?where=priority=5
trails://kg/Requirement?where=status=proposed&limit=50
Explicitly not supported in v0.2.0:
- Comparison operators (
<,>,<=,>=,!=). - Conjunction / disjunction (
AND,OR). - Nested field paths (
owner.email=...). - Free-text search.
Each of those is a deliberate deferral. The grammar is not "ORM's
where() semantics shipped via URL"; it is "equality on one scalar
field, because that's what an agent needs to scope a read without
learning SPARQL." When an agent needs more, it calls a capability.
SPARQL injection — parameterised binding¶
Filter values are never interpolated. The grammar produces a
parameterised query using predicate_datatype_map[field] (derived
from PredicateInfo at python/src/trails/shapes.py:165-175) to
pick the literal datatype, then binds via the existing typed-param
path (sparql() / sparql_update() at
python/src/trails/context.py:350-421, commit f6ca4c8). A value
that cannot be parsed under the field's datatype returns -32602
Invalid params with data = {"field": ..., "reason":
"type_mismatch", "expected_datatype": "xsd:integer"}. Characters
that would terminate a string literal (", backslash, newline) are
rejected at URL parse time too — belt-and-braces on top of the
binder.
Read-only, subscriptions, PROV cross-reference¶
The MCP resource surface is read-only for KG data. There is no
resources/write. Every write goes through tools/call on a
@capability (or a builder-scoped meta-capability), which writes
through ValidatedStore (ADR-0059 §"Surface contract"). The
invariant "violations cannot land" holds end-to-end; MCP has no
bypass of the validation gate.
trails://kg/<Type>/<id> already participates in the per-URI
subscription pipeline
(python/src/trails/mcp_resources.py:177-181 and :205-263). This
bundle makes trails://kg/<Type> (umbrella) subscribable too: any
mutation to an instance of <Type> emits
notifications/resources/updated for the umbrella URI. Filter-URIs
are not subscribable in v0.2.0 (see Open questions). The
umbrella description carries an advisory subscription-cost note:
"Watching the full type fires one notification per instance
mutation. For types with >1000 instances, subscribe to a specific
<id> where possible." Documentation gate, not runtime cap — a
cap would be an ADR-0012 envelope decision.
PROV activities are not projected as per-Type resources. The
existing trails://prov/latest remains the canonical entry point. A
future ADR may add trails://prov?type=MetaActivity&since=... once
the PROV read ergonomics mature; out of scope here.
Bundle 3 — Schema-aware tools + FieldError-shaped errors + stability tests¶
Schema derivation from predicate() constraints¶
The runtime-derived fallback path in
python/src/trails/mcp_server.py:99-154 gets replaced with a
richer mapper. The input schema for a capability with a
@shape-typed or @node_type-typed parameter reflects every SHACL
constraint for which a JSON Schema keyword exists:
predicate() arg |
JSON Schema keyword | Notes |
|---|---|---|
required=True / min_count=1 |
enters the required array |
|
min_length=N |
minLength (string only) |
|
max_length=N |
maxLength (string only) |
|
pattern=r"..." |
pattern (SHACL uses XSD regex, JSON Schema uses ECMA; Trails validates with Python's re, so re.compile-able is authoritative) |
|
one_of=[...] |
enum (string + numeric literals) |
|
min_value=N |
minimum (integer / number) |
|
max_value=N |
maximum (integer / number) |
|
many=True / max_count=None |
type: "array" with scalar items |
Documented gaps:
sh:hasValue— deferred; noconstemission in v0.2.0.sh:classon reference fields — target is{"type": "string", "format": "iri"}plus atrails:node_type: "<TypeName>"annotation.- Compound uniqueness (M11 Phase 2) — not expressible in JSON
Schema; fields get a top-level
descriptionannotation listing them. Compound checks still run server-side.
FieldError on the wire¶
When a capability raises ValidationError carrying one or more
FieldErrors (ADR-0056 §"FieldError shape"), MCP tools/call
returns a JSON-RPC error rather than a tool error:
{
"jsonrpc": "2.0",
"id": 17,
"error": {
"code": -32602,
"message": "validation failed on 2 field(s)",
"data": {
"fields": [
{
"field": "status",
"code": "one_of",
"message": "status must be one of ['proposed','accepted','rejected']",
"value": "draft",
"constraint": ["proposed","accepted","rejected"]
},
{
"field": "req_id",
"code": "pattern",
"message": "req_id must match ^REQ-\\d+$",
"value": "R1",
"constraint": "^REQ-\\d+$"
}
]
}
}
}
-32602 Invalid params is the canonical JSON-RPC code for "your args
are wrong"; validation failures are the MCP-native form of "wrong
args". data.fields is a list, not a dict — matching ADR-0056's
flat-list choice (§Alt C there).
Other exception classes keep current behaviour: PermissionError
from @policy → -32002; BudgetError → -32003; uncategorised
handler exceptions → -32603 Internal error with the message and
data.trace_id.
Golden-file schema snapshot tests¶
New test file: python/tests/test_mcp_schema_stability.py. For each
fixture capability (initial set: meta.ping, research.search, one
shaped capability with the full constraint matrix, one node-type-typed
parameter), the test builds tools/list, extracts the descriptor, and
snapshots name + description + inputSchema + trails.* extension to
python/tests/fixtures/mcp_schemas/<capability_id>.json — one file
per tool, byte-exact JSON with sorted keys. A schema change requires
re-running with TRAILS_UPDATE_MCP_SNAPSHOTS=1 to rewrite the golden
file — a deliberate action visible in git diff. Closes the gap at
docs/mcp-surface.md:283-284. Matrix: at least one fixture per
mapped JSON Schema keyword and one per trails.* extension key.
Non-goals¶
- Not a reinvention of MCP. We implement the spec; we do not
extend it. The
trails.*tool-descriptor extension lives in the namespaced slot the spec permits for additional properties. If a later MCP revision standardises a field we already emit, we rename without breaking semantics. - Not multi-tenant. One server, one graph, one scope set. Hosting multiple Trails projects under one MCP process is a separate design problem (see ADR-0055 §Non-goals).
- Not a replacement for HTTP. HTTP keeps
/invoke,/admin, andPOSTergonomics for humans, batch tools, and webhooks. Bundle 1's HTTP projector is owned by ADR-0055; this ADR does not re-decide the HTTP shape. - Not auto-generated UI. The
@node_type-to-resource projection is read/filter/subscribe for agents. A human-facing auto-UI (Rails-scaffold-style) is a separate ADR at v0.3.0. - Not MCP Sampling / bidirectional calls. Trails stays a pure server; it does not call back into the client for LLM sampling.
- Not SPARQL over MCP. The
?where=grammar is a deliberate minimum; agents that need SPARQL callctx.kg.sparql(...)through a@capability. - Not retroactive. Resources reflect current-state data. Time-travel queries (ADR-0035 temporal KG, planned) get their own URI scheme.
Alternatives considered¶
Alt A — HTTP-first, MCP later¶
Ship the same authoring features on HTTP admin first and bring MCP to parity later. Rejected. MCP is the agent transport by design (ADR-0008); the authoring surface should live where the agent already lives. The two converge through ADR-0055's projector, not through an HTTP-primary redesign.
Alt B — Capability-only MCP¶
Keep MCP as tools/*-for-capabilities only; no meta projection, no
@node_type-as-resource. Rejected — status quo. Boxes Trails into
"function-call server" and leaves docs/mcp-surface.md:344's polish
thesis unrealised.
Alt C — A bespoke wire format¶
Design a Trails-specific JSON-RPC (or gRPC, or custom) for the
authoring surface. Rejected. The MCP ecosystem exists (Claude
Desktop, Continue, MCP-Hub-style registries); swimming against the
current is strictly worse. The trails.* descriptor extension is
the only Trails-specific concession.
Alt D — REST-over-MCP for resources¶
Let the agent call a kg.list / kg.read / kg.filter capability
instead of modelling KG instances as resources. Rejected — the
status-quo trap. Every project ships its own glue; modelling
@node_type instances as resources absorbs it once.
Alt E — Rich JSON Schema draft 2020-12¶
Emit $ref-linked schemas with allOf / oneOf / if/then/else
for compound constraints. Rejected for v0.2.0: complexity is high
and MCP client compatibility is narrow. Keep the schema linear and
draft-07-shaped; richer constraints live server-side.
Rollout / phases¶
Phases are gated independently; Bundles 2 and 3 are unblocked today and can proceed in parallel, Bundle 1 waits on ADR-0055 Phase 2.
| Phase | What lands | Depends on |
|---|---|---|
| Phase 1 (shipped) | Meta-surface Phase 1 done (trails_meta_ping routed via python/src/trails/mcp_server.py:193-239). KG resources beyond the current 100-hard-limit list: not done. Schema stability tests: not done. |
— |
| Phase 2 | Bundle 2 (KG resources — typed umbrella + ?where= + JSON-LD MIME + umbrella subscriptions + pagination) and Bundle 3 (schema derivation from predicate() + FieldError JSON-RPC errors + golden-file snapshot tests). Parallel agent work; no hard dep between bundles. |
ADR-0056 (already landed), ADR-0059 Phase 1 (for the validation-error path). |
| Phase 3 | Bundle 1 (every @meta_capability projected, generator port complete, tools/listChanged on dynamic registration). |
ADR-0055 Phase 2 (generator port + grant loader). |
| Phase 4 | Auth + scope enforcement on the MCP boundary (bearer tokens on stdio and SSE, per-connection scope narrowing, scope-denial error code -32004 wired). |
ADR-0055 Phase 3 (grant and scope plumbing). |
Each phase has one integration test in python/tests/test_mcp_*.py
that exercises the new surface end-to-end against an in-memory store
and a stdio transport, matching the existing test idiom
(python/tests/test_mcp_server.py:100+).
Relationship to other ADRs¶
| ADR | Impact |
|---|---|
| ADR-0008 (MCP primary transport) | Extends, does not replace. ADR-0008 made MCP the canonical agent transport; this ADR narrows what "agent transport" means to "agent authoring transport" — capabilities plus meta plus typed KG plus structured errors. The MCP-primary principle holds; this ADR fills in what "primary" projects. |
| ADR-0005 (Rich capability manifest) | Honoured. The canonical capability descriptor is unchanged; the MCP projection reads more of it (the predicate() constraints that were already authoritative but unread). No new descriptor fields. |
| ADR-0046 (MCP KG resources) | Subsumed for the live-instance case. ADR-0046 established the trails://kg/... namespace; this ADR commits to a stable filter grammar, pagination bounds, MIME correctness, and subscription-cost documentation on top of it. Where the two disagree, this ADR wins (v0.2.0+). |
| ADR-0055 (Meta-capability surface) | Complementary. ADR-0055 owns the registry and the projector machinery; this ADR specifies the MCP-side concerns (tool name grammar, trails.* extension, scope-denial error mapping, auth gate). If ADR-0055's projector changes, the contract in this ADR is what the MCP projector must still satisfy. |
| ADR-0056 (ORM validation ergonomics) | Reused. FieldError is the wire format for validation errors. No new validation plumbing — we surface what ADR-0056 already produces. |
| ADR-0057 (Versioned migrations) | Downstream. Once trails.meta.migrate.* registrations exist, they auto-appear on MCP via Bundle 1 — no migration-specific MCP code. The "agent proposes a migration via MCP" story in ADR-0057 resolves through Bundle 1 plus the builder scope gate. |
| ADR-0059 (Store-agnostic validation) | Upstream. Every write through an MCP tools/call funnels through ValidatedStore exactly like any other caller; MCP has no bypass. raw_sparql(skip_validation=True) is not projectable onto MCP in v0.2.0 (would require [validation].allow_bypass = true plus bearer-token bypass-grant, out of scope). |
| ADR-0009 (Provenance always on) | Honoured. Every tools/call emits PROV as today; trails:MetaActivity is written for meta invocations per ADR-0055. |
| ADR-0049 (Plugin system) | Complementary. Plugins that register @meta_capability at load time appear via tools/listChanged (Bundle 1 Phase 3). |
| ADR-0050 (Server environment modes) | Honoured. dev-scoped tools are hidden from tools/list when config.project.env == "production", same gate as ADR-0055 §"Scope model". |
Success criteria¶
tools/listreturns every runtime capability and every@meta_capabilitythe caller's scope set permits, with a machine-readabletrails.kinddiscriminator.tools/call trails_meta_gen_shapefrom an agent withdevscope produces the sameFileDiffthe CLI produces, byte-for-byte.resources/read trails://kg/Requirement?where=status=proposedreturns a paginated list matchingRequirement.objects(ctx).where(status="proposed").- A capability that raises
ValidationError([FieldError(...), FieldError(...)])surfaces as JSON-RPC error-32602withdata.fieldsa structured list, not as a tool-levelisErrorenvelope. python/tests/test_mcp_schema_stability.pyguards every mapped constraint; changing apredicate()kwarg on a fixture capability fails the snapshot until the golden file is deliberately updated.- MCP has no validation bypass: a
tools/callthat tries to write a shape-violating triple fails with the same-32602as the ORMsave()would. - stdio MCP with no
TRAILS_MCP_TOKENconfigured still accepts unauthenticatedruntime-scope calls (back-compat for the current developer inner loop), but anybuilderordevscope tool is hidden fromtools/listand rejected attools/callwith-32001.
Open questions¶
- Resource-URI grammar:
?where=field=valuevs?filter=field:valuevs a separate read-with-body. v0.2.0 picks?where=(reads like mini-SQL, matches the ORM method name). An MCP client survey may reveal a more conformant convention. Owner call before Phase 2. - SPARQL injection beyond parameterised binding. Proposal:
scalar-equality-only, parameterised via
predicate_datatype_map, plus URL-layer rejection of literal-terminator characters. Is the double-defence excessive? Tune after security review. - Tool-kind discriminator:
trails.*or standardised_meta? The MCP revision Trails targets is2024-11-05(python/src/trails/mcp_server.py:94-96). If_metais standardised since, migrate in Phase 2 and aliastrails.*for one cycle. subscribecost ontrails://kg/<Type>. 10k instances × one notification per mutation is a worst case. v0.2.0 documents a recommended max subscriber count per type; should we also enforce a hard cap (ADR-0012-envelope-shaped) on umbrella subscriptions per session? Punted to owner.- Schema snapshot file location.
python/tests/fixtures/mcp_schemas/<capability_id>.json(one file per tool) vs inline (bad — diffs hard to read) vs singleschemas.json(bad — merge-conflicty). Owner confirm. trails.kindon a capability that is both@capabilityand@meta_capability. ADR-0055's two-registry model forbids this by construction. If a plugin registers both, whichkindwins? Proposal: explicit error at plugin load — track in ADR-0049.- Back-pressure on
notifications/resources/updated. The per-sessionasyncio.Queueinmcp_transport.pyis unbounded. Umbrella subscriptions make this worse in the worst case. A separate ADR may decide a bounded-queue / drop-oldest policy.
Acceptance gate¶
This ADR is Proposed until:
- The open questions on grammar (§1) and discriminator (§3) receive owner sign-off.
- A Phase 2 prototype lands with: (a) the
trails://kg/<Type>umbrella resource for one@node_type, (b) a working?where=field=valuepath, © the schema-derivation mapper with at least threepredicate()kwargs lifted to JSON Schema keywords, and (d) one golden-file snapshot test.
Once the prototype passes its integration test in
python/tests/test_mcp_*.py, this ADR flips to Accepted and
Phase 3 (Bundle 1) becomes the next gate.