Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

47. Agent Protocols and Interoperability

Protocols standardize edges. They do not grant trust, authority, or shared truth.

On O-1001, Intake loads order facts through MCP tools. Resolution and Policy write proposals and verdicts to the ledger — not to each other over ad-hoc JSON-RPC. Someone proposes A2A between Resolution and Policy inside one deploy. That is coordination tax with no isolation benefit. Meanwhile Outreach nearly sends because an MCP tool server accepted a prompt-shaped argument and nothing in Policy checked send-window rules or required fields.

MCP → external tools | ledger → Intake/Resolution/Policy inside trust boundary | A2A → foreign org agents | Policy + authz on every consequential path

The failure mode is protocol substitution for authz: acronyms on the diagram, no permit/deny on the consequential path.

This chapter adds where each wire belongs — MCP for tools, ledger for in-boundary coordination, A2A for cross-org tasks — without confusing interoperability with authorization.

First principles

Keep three layers distinct:

LayerJobShopOps home
MCP (Model Context Protocol)Agent ↔ tools/resources: discover, call, typed argsOMS/CRM, balances, policy check APIs, channel gateways
A2A (Agent2Agent)Agent ↔ agent across org/trust boundaries: tasks, artifacts, capability cardsExternal fraud-check or carrier agent; sister-company logistics agent
LedgerShared facts / conflicts inside your trust boundaryResolution vs Policy beliefs on case O-1001

Rules of thumb:

  1. MCP for every system your worker calls as a tool.
  2. Ledger for coordination among your own modules/agents on one case.
  3. A2A when another organization’s agent must accept a task and return an artifact — and you still record that task on your ledger for audit.

Interoperability ≠ authorization. An MCP server that exposes send_email still needs your policy engine, authz, and idempotency (Ch 5–6, 27–28). A2A capability cards are advertisements, not proofs of safety.

Ch 35 (cost/latency) already covered KV/prefix cache — do not re-teach serving math here. This chapter is about wires and trust boundaries.

Concrete example

Case O-1001:

  1. Worker calls MCP tool get_order on the accounts server.
  2. Resolution and Policy write proposals/verdicts to the ledger (not MCP, not A2A).
  3. Address / fraud verification is outsourced: orchestrator opens an A2A task to an external carrier verification agent; result artifact returns; worker appends ADDRESS_VERIFIED to ledger.
  4. Outreach sends only after policy + approval — via MCP draft_email / send_email tools with scopes.

If you replaced step 2 with A2A between Resolution and Policy inside one process boundary, you paid coordination tax for no isolation benefit.

Diagram

flowchart TB
  subgraph trust_boundary [Your trust boundary]
    W[Case worker]
    L[(Ledger)]
    P[Policy / authz]
    W --> L
    W --> P
  end
  W -->|MCP tools| CRM[CRM MCP server]
  W -->|MCP tools| CH[Channels MCP server]
  W -->|A2A task| EXT[External bureau agent]
  EXT -->|artifact| W
  EXT -.->|must also land as| L

Caption: MCP down to tools; A2A out to foreign agents; ledger for shared truth inside. Protocols do not replace P.

Layer cake (ASCII)

┌─────────────────────────────────────────────┐
│  Orchestration / HITL / eval                │
├─────────────────────────────────────────────┤
│  Ledger (facts, conflicts, audit events)    │
├─────────────────────────────────────────────┤
│  A2A (cross-org tasks / artifacts)          │
├─────────────────────────────────────────────┤
│  MCP (tools, resources, prompts)            │
├─────────────────────────────────────────────┤
│  HTTP / DB / queues / model gateway         │
└─────────────────────────────────────────────┘

Implementation

Minimal MCP-shaped stub over ShopOps tools (stdio JSON-RPC teaching server — not a full SDK):

# manuscript/code/shopops/shopops/mcp_server.py
"""MCP-shaped JSON-RPC stub for Chapter 47.

This is a teaching surface: list tools + call tools.
It does NOT implement authn, sampling, or roots.
Policy/authz remain outside — call into your executor.
"""

from __future__ import annotations

import json
import sys
from typing import Any, Callable


ToolHandler = Callable[[dict[str, Any]], dict[str, Any]]


TOOLS: dict[str, dict[str, Any]] = {
    "get_order": {
        "name": "get_order",
        "description": "Return fictional order total for a case",
        "inputSchema": {
            "type": "object",
            "properties": {"case_id": {"type": "string"}},
            "required": ["case_id"],
        },
    },
    "draft_email": {
        "name": "draft_email",
        "description": "Draft email body; does not send",
        "inputSchema": {
            "type": "object",
            "properties": {
                "case_id": {"type": "string"},
                "body": {"type": "string"},
            },
            "required": ["case_id", "body"],
        },
    },
}


def _get_order(args: dict[str, Any]) -> dict[str, Any]:
    # Fictional accounts only
    return {"case_id": args["case_id"], "order_total_cents": 54000, "currency": "USD"}


def _draft_email(args: dict[str, Any]) -> dict[str, Any]:
    return {"case_id": args["case_id"], "draft": args["body"], "status": "drafted"}


HANDLERS: dict[str, ToolHandler] = {
    "get_order": _get_order,
    "draft_email": _draft_email,
}


def handle(msg: dict[str, Any]) -> dict[str, Any]:
    mid = msg.get("id")
    method = msg.get("method")
    params = msg.get("params") or {}

    if method == "initialize":
        return {
            "jsonrpc": "2.0",
            "id": mid,
            "result": {
                "protocolVersion": "2024-11-05",
                "serverInfo": {"name": "shopops-mcp-stub", "version": "0.1.0"},
                "capabilities": {"tools": {}},
            },
        }

    if method == "tools/list":
        return {
            "jsonrpc": "2.0",
            "id": mid,
            "result": {"tools": list(TOOLS.values())},
        }

    if method == "tools/call":
        name = params.get("name")
        args = params.get("arguments") or {}
        if name not in HANDLERS:
            return {
                "jsonrpc": "2.0",
                "id": mid,
                "error": {"code": -32601, "message": f"unknown tool: {name}", "data": {"known": list(HANDLERS)}},
            }
        # NOTE: production must validate schema + authz + policy before handler
        result = HANDLERS[name](args)
        return {
            "jsonrpc": "2.0",
            "id": mid,
            "result": {"content": [{"type": "text", "text": json.dumps(result)}], "isError": False},
        }

    return {
        "jsonrpc": "2.0",
        "id": mid,
        "error": {"code": -32601, "message": f"method not found: {method}"},
    }


def main() -> None:
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        msg = json.loads(line)
        sys.stdout.write(json.dumps(handle(msg)) + "\n")
        sys.stdout.flush()


if __name__ == "__main__":
    main()

A2A mapping (conceptual): only org-boundary tasks — e.g. VerifyIncomeTask{case_id, evidence_request_id} → artifact IncomeArtifact{verified: bool, provider_ref}. On receipt, append ledger event; never let the foreign agent call your send_email tool directly.

Failure modes

FailureSymptomFix
MCP as securityTools callable without authzSidecar policy; least-privilege tokens
A2A inside monolithChatty task theaterUse ledger / function calls
Ledger over HTTP toolsEverything becomes an eventTools for I/O; ledger for beliefs
Schema driftSilent arg coercionVersion tools; fail closed
Sampling-in-toolNested unbudgeted agentDisable or treat as full agent
Trusting capability cardsForeign agent over-scopedSeparate credentials; allowlists

Production considerations

  • One MCP server per trust tier (read CRM ≠ send channel).
  • Pin protocol and tool schema versions in traces.
  • Treat tool output as untrusted content (Ch 26).
  • Adopt MCP first; add A2A when a second independent org agent exists — not before.
  • Official docs evolve quickly — verify field names against current specs before production. [VERIFY]

Chapter summary

  • MCP standardizes tool edges; A2A standardizes cross-agent tasks; ledger stores shared facts inside your boundary.
  • Interoperability does not include trust, authz, or policy.
  • Prefer ledger over A2A for co-owned agents on one case.
  • Teaching MCP stub lists/calls tools; policy stays in your executor.
  • Record foreign A2A artifacts on the ledger or audit stops at the protocol edge.

Exercises

  1. Mechanical: Pipe an initialize + tools/list + tools/call get_order for O-1001 into mcp_server.py.
  2. Boundary: Redesign a mistaken A2A link between Resolution and Policy as ledger events.
  3. Security: Add a fake send_email tool to the stub; show where authz must block before HANDLERS.
  4. Design: Write a one-page integration guide for a bureau A2A task including ledger event types.

References

  • Model Context Protocol — official documentation. https://modelcontextprotocol.io [VERIFY]
  • Agent2Agent (A2A) — official specification / docs. [VERIFY URL]
  • Lamport, 1978 — event ordering intuition for ledgers.
  • Kleppmann — logs as systems of record.
  • Cross-links: Ch 4–5 (tools), Ch 16–17 (ledger/conflicts), Ch 26–28 (trust/policy), Ch 35 (serving — separate concern).