Status: RFC / request for input
Area: cpex-core (CMF phases + hooks), apl-core/apl-cpex (route scope), policy filter
Problem / gap
CPEX policy today fires at invocation time — tool_pre_invoke,
prompt_pre_invoke, resource_pre_fetch — and the payload it reasons about is
the call arguments. But a meaningful class of policy applies before any call
exists: when a server advertises its capabilities (tools/list,
prompts/list, resources/list, and the notifications/*/list_changed
follow-ups). At that point the relevant data isn't the args — it's the
capability definition: name, description, inputSchema, annotations.
Requested by an adopter who wants to run validation at registration/discovery
time. It doesn't map onto the invoke-args hooks — but not because it's
"payload-less"; it has a different payload (the definition, which is exactly
what you'd validate).
Proposed model: a new CMF phase, not a new model
CMF is already typed on entity {tool, prompt, resource} × phase
{pre_invoke, post_invoke} (+ resource fetch). Registration is those same
entities at a new phase — register / advertise:
|
entity |
phase |
payload |
direction |
| today |
tool/prompt/resource |
pre_invoke / post_invoke |
call args / result |
request/response |
| new |
tool/prompt/resource |
register |
definition (name, description, schema, annotations) |
response (*/list) + notification (list_changed) |
So we reuse the entity taxonomy, hook dispatch, and scoping — we add a phase
whose payload is the definition. The things that make it feel distinct:
- It fires on the response side of
*/list (and on unsolicited
list_changed notifications), server → client — closer to the result.* /
post-phase machinery than the request path.
- It is naturally catalog-scoped (the set of capabilities), though per-entity
rules also make sense.
Triggers
tools/list / prompts/list / resources/list responses — the full
advertised catalog (validate/filter the set).
notifications/tools/list_changed (+ prompt/resource) — server-initiated
catalog updates (re-validate; catch a definition that changed after initial
registration).
Scope
- Global / tag for catalog-wide policy: allow/deny-list which tools an agent
may even see; apply a scanner to every advertised description.
- Per-entity for specific rules:
on_register: on a route keyed by name
(delete_all must never be advertised; transfer_funds must carry a schema
hash). Same scoping shape as invoke policy.
The composition that motivates it: enrich-at-register, enforce-at-invoke
Registration policy enriches; invoke policy enforces:
- A register-time classifier tags a tool
risk: high → invoke-time policy
require_approval for risk: high tools. (Risk assessment done once, at
discovery, not on every call.)
- Register-time pins
hash(inputSchema) → invoke-time verifies the live schema
hash matches. Anti-rug-pull: a server that swaps a benign tool for a
malicious one after approval is caught.
This two-phase enrich→enforce split is the strongest argument for a first-class
registration phase (vs. ad-hoc filtering).
Use cases
- Allow/deny-listing which capabilities are exposed to the agent.
- Schema hygiene — reject under-specified or dangerous parameter shapes.
- Tool-description / prompt-injection scanning — poisoned descriptions that
manipulate the agent (a live threat class; MCP descriptions are model-visible).
- Risk tagging — attach metadata consumed by invoke-time policy.
- Rug-pull detection — definition drift between register and invoke.
Custom plugins / hook family
A registration.validate (or capability.register) hook family that plugins
register for — e.g. an allowlist plugin, a JSON-Schema linter, a
description-injection scanner. A plugin's registration for this hook is its
declaration that it operates on definitions, not args (mirrors how the header/body
phase split infers a plugin's body-need from its hook — see related issue).
Result shape parallels the invoke hooks: continue (advertise as-is),
modify (filter/annotate the entry — strip a field, attach a tag), or deny
(drop the capability from the advertised set / reject the catalog).
Design questions (input wanted)
- Filter vs annotate vs reject — should a denied capability be dropped from
the advertised list transparently, or should the whole */list response be
rejected? (Dropping is friendlier to the agent; rejecting is stricter.)
- Where enrichment lives — a register-time tag needs to persist to invoke
time. Session-scoped store keyed by (server, entity)? Part of the catalog
snapshot? This is the load-bearing plumbing question.
- Notification handling —
list_changed is unsolicited server→client; does
the gateway re-run registration policy and reconcile, or only validate on the
next explicit */list?
- APL surface — does this get DSL verbs (e.g.
deny_registration(...),
tag(risk, high) on an on_register: block), or is it plugin-only at first?
- Interaction with the header/body phase split — registration is also a
non-invoke-args phase; should both be modeled under one "policy phases"
framework, or kept as independent lifecycle hooks?
Status: RFC / request for input
Area: cpex-core (CMF phases + hooks), apl-core/apl-cpex (route scope), policy filter
Problem / gap
CPEX policy today fires at invocation time —
tool_pre_invoke,prompt_pre_invoke,resource_pre_fetch— and the payload it reasons about isthe call arguments. But a meaningful class of policy applies before any call
exists: when a server advertises its capabilities (
tools/list,prompts/list,resources/list, and thenotifications/*/list_changedfollow-ups). At that point the relevant data isn't the args — it's the
capability definition: name, description,
inputSchema, annotations.Requested by an adopter who wants to run validation at registration/discovery
time. It doesn't map onto the invoke-args hooks — but not because it's
"payload-less"; it has a different payload (the definition, which is exactly
what you'd validate).
Proposed model: a new CMF phase, not a new model
CMF is already typed on entity
{tool, prompt, resource}× phase{pre_invoke, post_invoke}(+ resource fetch). Registration is those sameentities at a new phase —
register/advertise:*/list) + notification (list_changed)So we reuse the entity taxonomy, hook dispatch, and scoping — we add a phase
whose payload is the definition. The things that make it feel distinct:
*/list(and on unsolicitedlist_changednotifications), server → client — closer to theresult.*/post-phase machinery than the request path.
rules also make sense.
Triggers
tools/list/prompts/list/resources/listresponses — the fulladvertised catalog (validate/filter the set).
notifications/tools/list_changed(+ prompt/resource) — server-initiatedcatalog updates (re-validate; catch a definition that changed after initial
registration).
Scope
may even see; apply a scanner to every advertised description.
on_register:on a route keyed by name(
delete_allmust never be advertised;transfer_fundsmust carry a schemahash). Same scoping shape as invoke policy.
The composition that motivates it: enrich-at-register, enforce-at-invoke
Registration policy enriches; invoke policy enforces:
risk: high→ invoke-time policyrequire_approvalforrisk: hightools. (Risk assessment done once, atdiscovery, not on every call.)
hash(inputSchema)→ invoke-time verifies the live schemahash matches. Anti-rug-pull: a server that swaps a benign tool for a
malicious one after approval is caught.
This two-phase enrich→enforce split is the strongest argument for a first-class
registration phase (vs. ad-hoc filtering).
Use cases
manipulate the agent (a live threat class; MCP descriptions are model-visible).
Custom plugins / hook family
A
registration.validate(orcapability.register) hook family that pluginsregister for — e.g. an allowlist plugin, a JSON-Schema linter, a
description-injection scanner. A plugin's registration for this hook is its
declaration that it operates on definitions, not args (mirrors how the header/body
phase split infers a plugin's body-need from its hook — see related issue).
Result shape parallels the invoke hooks:
continue(advertise as-is),modify(filter/annotate the entry — strip a field, attach a tag), ordeny(drop the capability from the advertised set / reject the catalog).
Design questions (input wanted)
the advertised list transparently, or should the whole
*/listresponse berejected? (Dropping is friendlier to the agent; rejecting is stricter.)
time. Session-scoped store keyed by (server, entity)? Part of the catalog
snapshot? This is the load-bearing plumbing question.
list_changedis unsolicited server→client; doesthe gateway re-run registration policy and reconcile, or only validate on the
next explicit
*/list?deny_registration(...),tag(risk, high)on anon_register:block), or is it plugin-only at first?non-invoke-args phase; should both be modeled under one "policy phases"
framework, or kept as independent lifecycle hooks?