MCP servers, tool pinning and A2A#
Agents act through two protocols, and MAQPNA governs both at the gateway:
- the Model Context Protocol (MCP), which agents use to call tools offered by MCP servers;
- agent-to-agent (A2A), which agents use to send tasks to other agents.
MCP servers#
An MCP server is registered with the kind MCPServer (short name mcps). Its name is a DNS label because it becomes both the gateway path and the scope: an agent calls POST {gateway}/mcp/<name> with a token holding tools:<name>. The gateway speaks MCP Streamable HTTP and supports protocol revisions 2025-03-26, 2025-06-18, 2025-11-25 and 2026-07-28 on the same endpoint.
| Field | Meaning |
|---|---|
url, protocolVersions, timeoutSeconds, caBundleRef |
Where and how to reach the server |
trust (trusted / untrusted, default untrusted) |
Untrusted servers taint the sessions that call them |
dataClass (public, internal, confidential, restricted) |
Confidential and restricted servers add a data-class:<class> taint |
jurisdiction |
Checked against the sovereignty policy |
auth |
The gateway's own credential: bearerSecret, headerSecret, oauthClientCredentials, tokenExchange or mtls; or perUser to inject the user's own token from the vault |
shared, allowedNamespaces |
Offer the server to other namespaces |
toolPins, toolLabels, dlpProfile |
Pinning, taint labels and DLP for this server's traffic |
The operator renders every MCPServer into the ConfigMap maqpna-upstreams and copies the referenced Secrets into a gateway-only Secret. Credentials never reach the sandbox, a ConfigMap, the ledger or the admin API. A namespace can only reach another namespace's server when the owner shares it, and a tenant's server is never offered to another tenant unless sharing allows it.
The gateway resolves /mcp/<name> for the verified caller in this order: the agent's own binding (serverRef), the caller namespace's server, exactly one server shared with the namespace (more than one is ambiguous_server), static upstreams, then the deprecated upstreamURL. Anything else is unknown_server.
Tool pinning#
A tool definition can change after you reviewed it: a server update, or a malicious "rug pull" that rewrites a tool's description to steer the model. Tool pinning records a hash of each tool's definition. A pin is that hash:
sha256:<hex SHA-256 of the canonical JSON of name, title, description, inputSchema, outputSchema, annotations>
Drift is a tool whose definition changed after it was pinned.
mode |
Effect |
|---|---|
off |
No pinning |
learn (default) |
Record first sightings and changes in the ledger (decision: observe); never block |
enforce |
A changed, unpinned or never-seen tool is handled per onChange |
onChange (enforce) |
Effect |
|---|---|
hide (default) |
Dropped from tools/list; a call is denied (tool_definition_changed, tool_unpinned, tool_definition_unverified) |
approve |
Visible; an otherwise allowed call needs approval |
alert |
Allowed; the record carries the pin status |
Pins come from ToolPolicy.spec.toolPins and MCPServer.spec.toolPins, merged strictest wins. The GitOps workflow: run in learn, review, export the observed hashes (maqpna mcp pin), commit them with mode: enforce. The gateway reports what it observed back onto the server's status (observedTools, driftedTools, condition Pinned: Verified, Drifted, NotPinned).
A2A#
A hosted agent exposes itself with Agent.spec.a2a.expose and an in-cluster endpoint; a remote agent is registered as an A2A peer (kind A2APeer, short name a2ap). Both are reachable through the gateway at /a2a/<namespace>/<name> in the JSON-RPC and HTTP+JSON bindings of A2A v1.0. Callers need the scope agents:<name> (or agents:<ns>/<name> across namespaces); tools:* never grants it.
The gateway governs an A2A call like a tool call (policy server a2a/<agent>, tool SendMessage or skill:<id>), plus:
- the target's
allowedCallers(empty means the target's own namespace); - delegation depth (default 5) and loops;
- the requested skill must be declared by the hosted agent, or listed in the verified card of the peer;
- a remote peer's Agent Card must verify against pinned keys (
requireSignedCard, default on), or calls fail closed (peer_card_unverified); - answers from untrusted targets (peers default to
untrusted) taint the caller's session; - both hops are audited: the caller's record (
ext.a2aHop=outbound) and, for hosted callees, the callee's record (inbound), correlated byext.a2aCall.
flowchart LR
A["Agent planner<br/>(sandbox)"] -- "/mcp/github<br/>scope tools:github" --> GW["Gateway"]
A -- "/a2a/team-a/coder<br/>scope agents:coder" --> GW
GW -- "credential injected,<br/>pins checked" --> M["MCPServer github"]
GW -- "Txn-Token,<br/>identity headers" --> B["Agent coder<br/>(hosted, spec.a2a.expose)"]
GW -- "signed card verified,<br/>no identity headers" --> P["A2APeer partner-agent<br/>(remote, untrusted)"]
What you see#
maqpna mcp list shows the registry, maqpna mcp tools a server's observed tools with their pin status, and maqpna mcp pin exports a toolPins: fragment. maqpna a2a routes prints TARGET KIND URL TRUST JURISDICTION CALLERS CARD ERROR. See maqpna mcp list, maqpna mcp tools, maqpna mcp pin and maqpna a2a routes.