Preamble#
An agentic platform that communicates through untyped strings, ad hoc HTTP endpoints, and free-form prompt concatenation is not an architecture—it is a fragility surface. The moment multiple agents coordinate, tools multiply, memory layers diversify, and external applications integrate, every boundary without a typed contract becomes a latent failure mode. This chapter establishes the formal protocol stack that transforms an agentic system from prompt glue into a deterministic, observable, versionable, and production-hardened distributed system.
The thesis is precise: three protocol layers, each selected for the mechanical properties its boundary demands, unified through a common IDL strategy and cross-protocol gateway fabric. JSON-RPC 2.0 governs the application boundary where human-facing clients and external integrators require simplicity, schema transparency, and transport neutrality. gRPC/Protobuf governs internal agent-to-agent and agent-to-service execution where latency, streaming, and type safety are non-negotiable. The Model Context Protocol (MCP) governs tool, resource, and prompt surface discovery where dynamic registration, capability negotiation, and bidirectional context exchange define the interaction pattern. Together, they form a closed, typed, observable execution envelope around every agentic operation.
4.1 The Three-Layer Protocol Thesis: Boundary (JSON-RPC), Internal (gRPC), Discovery (MCP)#
4.1.1 Architectural Motivation#
Agentic systems exhibit three fundamentally distinct communication regimes, each imposing different constraints on serialization format, transport, latency tolerance, schema evolution, and discoverability. Collapsing these into a single protocol yields either excessive complexity at the boundary (forcing external clients to manage Protobuf compilation) or insufficient performance internally (routing all inter-agent traffic through JSON parsing). The three-layer thesis resolves this by assigning each protocol to its mechanical optimum.
Layer Classification:
| Layer | Protocol | Boundary Type | Primary Constraint | Serialization | Transport |
|---|---|---|---|---|---|
| Boundary | JSON-RPC 2.0 | Application ↔ Platform | Simplicity, universality, human readability | JSON | HTTP/1.1, WebSocket |
| Internal | gRPC/Protobuf | Agent ↔ Agent, Agent ↔ Service | Latency, streaming, type safety, backpressure | Protobuf (binary) | HTTP/2 |
| Discovery | MCP | Agent ↔ Tool/Resource/Prompt | Dynamic registration, capability negotiation, context exchange | JSON (MCP schema) | stdio, SSE/HTTP |
4.1.2 Formal Layer Separation Invariants#
Let denote the protocol set. For any message traversing the system, define the protocol assignment function:
where is the universe of all messages. The following invariants must hold:
- Boundary Exclusivity: Every message originating from or destined to an external client must transit :
- Internal Optimization: Every message between co-located agents or services within the platform trust boundary must transit :
- Discovery Isolation: Every capability query, tool registration, resource enumeration, or prompt surface negotiation must transit :
- No Protocol Leakage: No internal Protobuf types shall appear in boundary responses; no MCP-specific discovery semantics shall leak into gRPC service definitions without explicit gateway translation.
4.1.3 Cross-Cutting Concerns#
All three layers share cross-cutting requirements that are enforced uniformly:
- Distributed Tracing: Every request carries an OpenTelemetry
trace_idandspan_id, propagated across protocol boundaries via gateway headers or metadata. - Deadline Propagation: Absolute deadlines (not relative timeouts) are set at the boundary and decremented at each hop.
- Error Classification: All protocols map errors into a unified taxonomy (§4.2.3) before surfacing to the caller.
- Versioned Contracts: Every schema element carries a semantic version; breaking changes are detected mechanically (§4.5).
- Authentication Context: Caller identity and authorization scope propagate as typed metadata, never as ambient state (§4.8).
4.1.4 Pseudo-Algorithm: Protocol Layer Router#
ALGORITHM ProtocolLayerRouter
INPUT: message m, source_context src, destination_context dst
OUTPUT: routed_message with protocol-appropriate envelope
1. CLASSIFY message origin:
IF src.trust_zone = EXTERNAL:
assigned_protocol ← JSON_RPC_2_0
ELSE IF src.trust_zone = INTERNAL AND dst.trust_zone = INTERNAL:
assigned_protocol ← GRPC_PROTOBUF
ELSE IF m.type ∈ {TOOL_DISCOVERY, RESOURCE_ENUM, PROMPT_SURFACE, CAPABILITY_QUERY}:
assigned_protocol ← MCP
ELSE:
assigned_protocol ← GRPC_PROTOBUF // default internal
2. EXTRACT cross-cutting metadata from m:
trace_context ← extract_or_generate_trace(m)
deadline ← compute_remaining_deadline(m.original_deadline, elapsed_time)
auth_context ← extract_caller_credentials(src)
schema_version ← resolve_contract_version(m.method, dst.service_id)
3. IF deadline ≤ 0:
RETURN error(DEADLINE_EXCEEDED, trace_context)
4. CONSTRUCT protocol envelope:
SWITCH assigned_protocol:
CASE JSON_RPC_2_0:
envelope ← build_jsonrpc_request(m, trace_context, auth_context, schema_version)
CASE GRPC_PROTOBUF:
envelope ← serialize_protobuf(m, trace_context, deadline, auth_context)
CASE MCP:
envelope ← build_mcp_message(m, trace_context, auth_context)
5. VALIDATE envelope against registered schema for (m.method, schema_version):
IF validation fails:
RETURN error(SCHEMA_VIOLATION, trace_context, validation_errors)
6. DISPATCH envelope to transport layer for assigned_protocol
7. RECORD span(trace_context, assigned_protocol, m.method, dst.service_id, deadline)
8. RETURN dispatch_handle4.1.5 Token-Budget Implications#
The protocol choice directly affects the context engineering budget for the agent. When an agent receives tool descriptions via MCP, those descriptions consume tokens in the active context window. When tool invocation results return via gRPC, the binary-to-JSON transcoding step determines how many tokens the result occupies. The protocol stack must therefore be conscious of the agent's token budget :
where is directly influenced by how many tool schemas are loaded (MCP lazy loading minimizes this), and depends on the serialization efficiency of returned evidence (Protobuf-to-summary transcoding reduces this).
4.2 JSON-RPC 2.0 at the Application Boundary: Schema Design, Batch Requests, Error Taxonomy#
4.2.1 Why JSON-RPC 2.0#
JSON-RPC 2.0 is selected for the boundary layer based on a precise set of mechanical properties:
- Transport Neutrality: JSON-RPC is agnostic to transport—it operates identically over HTTP, WebSocket, stdin/stdout, or message queues.
- Simplicity of Integration: External clients (web applications, mobile SDKs, CLI tools, third-party platforms) require no code generation, no Protobuf compiler, no schema registry client—only a JSON parser.
- Batch Semantics: The specification natively supports batched requests, enabling clients to submit multiple agent operations atomically.
- Deterministic Structure: Every request and response follows an invariant structure (
jsonrpc,method,params,id,result,error), enabling mechanical validation without protocol-specific parsers. - Notification Support: Fire-and-forget messages (no
idfield) support event-driven patterns without requiring response tracking.
4.2.2 Schema Design Principles#
Every JSON-RPC method exposed at the boundary must satisfy:
- Typed
params: Defined by a JSON Schema document registered in the schema registry under a semantic version. - Typed
result: The success payload schema is equally specified; no method returns untyped JSON objects. - Idempotency Key: State-mutating methods accept an optional
idempotency_keyin params to enable safe retries. - Pagination Tokens: List-returning methods use opaque cursor-based pagination, never offset-based.
- Deadline Header: The HTTP header
X-Deadline-Msor adeadline_msfield inparamspropagates the caller's absolute deadline.
Canonical Request Structure:
{
"jsonrpc": "2.0",
"method": "agent.submitTask",
"params": {
"task_id": "uuid-v7",
"objective": "...",
"context_refs": ["mem://session/abc", "retrieval://query/xyz"],
"constraints": { "max_steps": 12, "budget_tokens": 8192 },
"idempotency_key": "client-generated-uuid",
"deadline_ms": 1719500000000
},
"id": "req-001"
}Canonical Success Response:
{
"jsonrpc": "2.0",
"result": {
"task_id": "uuid-v7",
"status": "ACCEPTED",
"estimated_completion_ms": 4500,
"trace_id": "otel-trace-abc123"
},
"id": "req-001"
}4.2.3 Error Taxonomy#
A rigorous error taxonomy is essential for deterministic client-side handling. The JSON-RPC 2.0 specification defines error codes in the range for protocol-level errors. The agentic platform extends this with application-level codes.
Unified Error Code Space:
| Code Range | Category | Examples |
|---|---|---|
| Parse Error | Malformed JSON | |
| Invalid Request | Missing jsonrpc field, wrong version | |
| Method Not Found | Unknown method name | |
| Invalid Params | Schema validation failure on params | |
| Internal Error | Unclassified server fault | |
| to | Server Errors (reserved) | Server overloaded, shutting down |
| – | Agent Execution Errors | Step limit exceeded, recursion bound hit, tool failure |
| – | Retrieval Errors | No evidence found, source timeout, provenance unverifiable |
| – | Memory Errors | Write policy violation, dedup conflict, expiry rejection |
| – | Authorization Errors | Insufficient scope, credential expired, approval required |
| – | Rate/Quota Errors | Rate limit exceeded, token budget exhausted, cost cap hit |
Canonical Error Response:
{
"jsonrpc": "2.0",
"error": {
"code": 1003,
"message": "Agent recursion depth exceeded",
"data": {
"max_depth": 8,
"current_depth": 9,
"agent_id": "planner-v2",
"trace_id": "otel-trace-abc123",
"recovery_hint": "REDUCE_DECOMPOSITION_GRANULARITY"
}
},
"id": "req-001"
}4.2.4 Batch Request Semantics#
JSON-RPC 2.0 natively supports batch requests as JSON arrays. The agentic platform enforces additional constraints:
- Batch Size Limit: where is configurable per client tier (e.g., 50 for standard, 200 for enterprise).
- Atomic Deadline: The batch inherits a single deadline; individual requests within the batch cannot extend it.
- Independent Execution: Each request in the batch is processed independently—no implicit ordering or transactionality. Clients requiring ordering must use explicit dependency chains via
depends_onfields in params. - Partial Success: The response array may contain a mix of success results and error objects. Clients must handle partial success.
Formal Batch Throughput Model:
Let be batch size, the mean processing time per request, and the concurrency limit for batch execution. The expected batch latency:
where accounts for batch parsing, response assembly, and serialization. This model informs the gateway's concurrency pool sizing.
4.2.5 Pseudo-Algorithm: JSON-RPC Boundary Gateway#
ALGORITHM JsonRpcBoundaryGateway
INPUT: raw_http_request
OUTPUT: http_response containing JSON-RPC result(s)
1. PARSE raw_http_request.body as JSON:
IF parse fails:
RETURN jsonrpc_error(-32700, "Parse error")
2. DETERMINE if payload is batch (JSON array) or single (JSON object):
IF batch:
requests ← payload
IF |requests| > B_max:
RETURN jsonrpc_error(-32600, "Batch size exceeds limit")
ELSE:
requests ← [payload]
3. EXTRACT deadline:
deadline ← MIN(header["X-Deadline-Ms"], NOW() + default_timeout_ms)
4. FOR EACH request r IN requests DO IN PARALLEL (bounded by concurrency c):
a. VALIDATE r against JSON-RPC 2.0 structure:
REQUIRE fields: jsonrpc="2.0", method (string)
IF r has "id": it is a call; ELSE: it is a notification
b. RESOLVE method → handler mapping from method_registry:
IF not found: EMIT error(-32601) for this request
c. VALIDATE r.params against registered JSON Schema for (r.method, version):
IF invalid: EMIT error(-32602, validation_details)
d. AUTHENTICATE caller from request headers:
auth_ctx ← verify_token(header["Authorization"])
IF auth fails: EMIT error(4001, "Authentication failed")
e. AUTHORIZE caller against method's required scopes:
IF insufficient: EMIT error(4002, "Insufficient scope")
f. CHECK rate limit for (auth_ctx.client_id, r.method):
IF exceeded: EMIT error(5001, "Rate limit exceeded", retry_after)
g. CHECK idempotency:
IF r.params.idempotency_key EXISTS in idempotency_store:
RETURN cached_response for that key
h. PROPAGATE trace context:
span ← start_span("jsonrpc." + r.method, parent=extract_trace(headers))
i. TRANSLATE r → internal gRPC request via gateway codec:
internal_req ← transcode_jsonrpc_to_grpc(r, auth_ctx, deadline, span)
j. DISPATCH internal_req to appropriate gRPC service with remaining deadline
k. AWAIT response or deadline expiry:
IF deadline exceeded: EMIT error(-32000, "Deadline exceeded")
l. TRANSCODE gRPC response → JSON-RPC result
m. IF r.params.idempotency_key EXISTS AND response is success:
STORE (idempotency_key → response) with TTL
n. RECORD span metrics and close span
5. ASSEMBLE responses array (preserving order, matching by id)
6. IF original was single request: RETURN responses[0]
ELSE: RETURN responses as JSON array
7. EMIT gateway metrics: batch_size, latency_p50/p99, error_rate, auth_failures4.3 gRPC/Protobuf for Internal Agent-to-Agent and Agent-to-Service Communication#
4.3.1 Proto3 Schema Design for Agent Messages, Tool Invocations, and Memory Operations#
Design Rationale#
Internal communication between agents, between agents and the retrieval engine, between agents and memory services, and between agents and tool execution backends requires:
- Binary efficiency: Protobuf serialization is more compact than JSON, reducing network bandwidth and deserialization cost.
- Strong typing: Proto3 schemas enforce field types, enumerations, and nested message structures at compile time.
- Code generation: Language-specific stubs are auto-generated, eliminating hand-written serialization.
- Schema evolution: Proto3's field numbering and backward-compatibility rules enable non-breaking schema evolution.
- Streaming: gRPC natively supports unary, server-streaming, client-streaming, and bidirectional streaming RPCs.
Core Proto3 Message Taxonomy#
The agentic platform's Protobuf schema is organized into four domains:
- Agent Execution Domain (
agent.proto) - Tool Invocation Domain (
tool.proto) - Memory Operations Domain (
memory.proto) - Retrieval Domain (
retrieval.proto)
Agent Execution Messages — Pseudo-Schema:
message AgentTask {
string task_id = 1; // UUID v7
string parent_task_id = 2; // for decomposed subtasks
string agent_type = 3; // e.g., "planner", "coder", "verifier"
TaskObjective objective = 4;
ExecutionConstraints constraints = 5;
repeated ContextReference context_refs = 6;
TraceContext trace = 7;
AuthContext auth = 8;
google.protobuf.Timestamp deadline = 9;
uint32 schema_version = 10;
}
message TaskObjective {
string natural_language = 1;
repeated StructuredGoal goals = 2;
repeated string success_criteria = 3;
}
message ExecutionConstraints {
uint32 max_steps = 1;
uint32 max_recursion_depth = 2;
uint32 token_budget = 3;
uint32 cost_budget_microdollars = 4;
repeated string permitted_tools = 5;
repeated string prohibited_actions = 6;
}
message AgentStepResult {
string task_id = 1;
uint32 step_number = 2;
StepType type = 3; // PLAN, RETRIEVE, ACT, VERIFY, CRITIQUE, REPAIR, COMMIT
StepOutcome outcome = 4; // SUCCESS, PARTIAL, FAILURE, NEEDS_REPAIR
bytes payload = 5; // type-specific serialized content
repeated ToolInvocationRecord tool_calls = 6;
ResourceUsage usage = 7;
TraceContext trace = 8;
}Tool Invocation Messages — Pseudo-Schema:
message ToolInvocationRequest {
string invocation_id = 1; // UUID v7, idempotency key
string tool_name = 2; // fully qualified: "mcp://server/tool_name"
string tool_version = 3;
google.protobuf.Struct input_params = 4; // validated against tool's input schema
AuthContext caller_auth = 5;
google.protobuf.Timestamp deadline = 6;
TraceContext trace = 7;
MutationClass mutation_class = 8; // READ_ONLY, IDEMPOTENT_WRITE, NON_IDEMPOTENT_WRITE
bool requires_human_approval = 9;
}
message ToolInvocationResponse {
string invocation_id = 1;
ToolResultStatus status = 2; // SUCCESS, ERROR, TIMEOUT, APPROVAL_PENDING
google.protobuf.Struct output = 3;
string error_message = 4;
uint32 error_code = 5;
ResourceUsage usage = 6;
TraceContext trace = 7;
ProvenanceRecord provenance = 8;
}
enum MutationClass {
READ_ONLY = 0;
IDEMPOTENT_WRITE = 1;
NON_IDEMPOTENT_WRITE = 2;
DESTRUCTIVE = 3;
}Memory Operations Messages — Pseudo-Schema:
message MemoryWriteRequest {
string memory_id = 1;
MemoryLayer target_layer = 2; // WORKING, SESSION, EPISODIC, SEMANTIC, PROCEDURAL
string content = 3;
map<string, string> metadata = 4;
ProvenanceRecord provenance = 5;
WritePolicy policy = 6;
google.protobuf.Timestamp expiry = 7;
string dedup_key = 8;
TraceContext trace = 9;
AuthContext auth = 10;
}
message WritePolicy {
bool require_validation = 1;
bool require_dedup_check = 2;
bool require_provenance = 3;
PromotionCriteria promotion = 4;
ConflictResolution on_conflict = 5; // REJECT, MERGE, OVERWRITE, VERSION
}
enum MemoryLayer {
WORKING = 0;
SESSION = 1;
EPISODIC = 2;
SEMANTIC = 3;
PROCEDURAL = 4;
}Schema Design Invariants#
- Every field has an explicit number: Field numbers are immutable once published; removed fields are marked
reserved. - No
requiredfields: Proto3 does not supportrequired; application-level validation enforces mandatory fields. - Enums start at 0 (UNKNOWN/UNSPECIFIED): The zero value always represents "unset" to distinguish missing from default.
google.protobuf.Structfor dynamic payloads: Tool inputs/outputs useStructwhen schemas vary per tool; validation occurs against the tool's registered JSON Schema, not at the Protobuf level.- Timestamps use
google.protobuf.Timestamp: Never raw integers for temporal values.
4.3.2 Bidirectional Streaming for Real-Time Agent Coordination#
Streaming RPC Taxonomy#
| RPC Type | Use Case | Cardinality |
|---|---|---|
| Unary | Single tool invocation, memory read/write | |
| Server-streaming | Agent step-by-step progress reporting, log streaming | |
| Client-streaming | Batch memory ingestion, multi-file upload | |
| Bidirectional streaming | Real-time multi-agent coordination, interactive debugging |
Bidirectional Streaming for Agent Coordination#
The canonical use case is the orchestrator ↔ specialist agent coordination loop. The orchestrator streams subtask assignments; the specialist agent streams back step results, intermediate findings, and resource requests in real time. This avoids the latency overhead of repeated unary RPCs.
Service Definition — Pseudo-Schema:
service AgentCoordination {
// Orchestrator ↔ Agent bidirectional stream
rpc CoordinateExecution(stream OrchestratorMessage)
returns (stream AgentMessage);
// Agent → Orchestrator: step-by-step progress
rpc ReportProgress(stream AgentStepResult)
returns (ProgressAck);
// Orchestrator → multiple agents: broadcast context update
rpc BroadcastContextUpdate(ContextUpdate)
returns (stream AgentAck);
}
message OrchestratorMessage {
oneof payload {
AgentTask new_task = 1;
ContextUpdate context_update = 2;
CancellationSignal cancel = 3;
ResourceGrant resource_grant = 4;
DeadlineExtension deadline_ext = 5;
}
}
message AgentMessage {
oneof payload {
AgentStepResult step_result = 1;
ResourceRequest resource_request = 2;
EscalationRequest escalation = 3;
CompletionReport completion = 4;
HealthHeartbeat heartbeat = 5;
}
}Stream Lifecycle Management#
Formal Stream State Machine:
Define the stream state .
At any state, a deadline expiry or error transitions to , triggering cleanup.
Heartbeat Requirement: For long-lived streams, agents must emit heartbeat messages at interval . If no message (heartbeat or data) arrives within (where is typically 3), the stream is considered dead and cleaned up.
4.3.3 Deadline Propagation, Cancellation Semantics, and Backpressure#
Deadline Propagation Model#
Every request entering the system at the boundary carries an absolute deadline . As the request traverses internal services, the remaining budget decreases:
where is the processing time at hop and is the network transit time. At each hop, the service checks:
where is the minimum useful remaining time (e.g., 50ms, below which no meaningful work can complete).
Pseudo-Algorithm: Deadline-Aware Dispatch
ALGORITHM DeadlineAwareDispatch
INPUT: request req, absolute_deadline D_0
OUTPUT: response or DEADLINE_EXCEEDED error
1. t_now ← CURRENT_TIME()
2. D_remaining ← D_0 - t_now
3. IF D_remaining ≤ ε_min:
RETURN error(DEADLINE_EXCEEDED, D_remaining)
4. // Reserve time for response serialization and network return
D_downstream ← D_remaining - t_reserve_response
5. IF D_downstream ≤ ε_min:
RETURN error(DEADLINE_EXCEEDED, D_downstream)
6. SET gRPC deadline on outbound call to D_downstream
7. DISPATCH req with deadline
8. AWAIT response WITH timeout = D_downstream:
IF timeout: RETURN error(DEADLINE_EXCEEDED)
IF response.error AND error is retryable AND D_remaining > 2 * ε_min:
// One retry with remaining budget
D_retry ← D_0 - CURRENT_TIME() - t_reserve_response
IF D_retry > ε_min:
DISPATCH req with deadline D_retry
AWAIT response WITH timeout = D_retry
9. RETURN responseCancellation Semantics#
gRPC provides native cancellation propagation. When a client cancels a call, all downstream RPCs spawned within that call's context are automatically cancelled. The agentic platform extends this with semantic cancellation:
- Hard Cancel: Immediate abort; partial results discarded. Used when the user abandons a task.
- Soft Cancel: Complete current step, then stop. Used when replanning makes the current subtask obsolete.
- Preempt: Pause current execution, yield resources, resume later. Used under resource contention.
Cancellation Propagation Rule: When an orchestrator cancels a parent task, all child tasks must receive cancellation within , enforced by the coordination stream.
Backpressure Mechanisms#
Backpressure prevents fast producers from overwhelming slow consumers. The platform implements backpressure at three levels:
- gRPC Flow Control: HTTP/2 window-based flow control limits how much data can be in flight per stream.
- Application-Level Backpressure: The agent coordination service monitors per-agent queue depth . When :
- Circuit Breaker: If a downstream service's error rate exceeds threshold over a sliding window of seconds:
During open state, requests are fast-failed. After , a half-open probe tests recovery before closing the circuit.
Pseudo-Algorithm: Backpressure Controller
ALGORITHM BackpressureController
INPUT: incoming_request req, target_service svc
OUTPUT: dispatch decision {ALLOW, THROTTLE, REJECT, CIRCUIT_OPEN}
1. // Circuit breaker check
cb_state ← circuit_breaker[svc.id].state
IF cb_state = OPEN:
IF CURRENT_TIME() - cb_state.opened_at > t_recovery:
cb_state ← HALF_OPEN
ELSE:
RETURN CIRCUIT_OPEN
2. // Queue depth check
Q ← svc.pending_queue.size()
IF Q > 2 * Q_max:
RETURN REJECT
IF Q > Q_max:
APPLY throttle: SLEEP(backoff_ms * (Q - Q_max) / Q_max)
// Re-check after throttle
IF svc.pending_queue.size() > 2 * Q_max:
RETURN REJECT
3. // Concurrency check
active ← svc.active_requests.count()
IF active ≥ svc.max_concurrency:
RETURN THROTTLE (enqueue with bounded wait)
4. // Dispatch
result ← DISPATCH req to svc
5. // Update circuit breaker
IF result.is_error:
circuit_breaker[svc.id].record_failure()
IF circuit_breaker[svc.id].error_rate() > θ_e:
circuit_breaker[svc.id].state ← OPEN
circuit_breaker[svc.id].opened_at ← CURRENT_TIME()
ELSE:
circuit_breaker[svc.id].record_success()
IF cb_state = HALF_OPEN:
circuit_breaker[svc.id].state ← CLOSED
6. RETURN result4.4 Model Context Protocol (MCP) — Deep Technical Specification#
4.4.1 MCP Server Architecture: Tool Servers, Resource Servers, Prompt Surface Servers#
Architectural Role of MCP#
MCP occupies the discovery and interoperability layer of the protocol stack. Where gRPC enforces static, compile-time-known service contracts, MCP enables runtime-dynamic capability surfaces. An agent does not need to know at compile time which tools, resources, or prompt templates are available; it discovers them through MCP's capability negotiation protocol.
MCP defines three primary server archetypes:
1. Tool Servers
Tool servers expose executable capabilities—functions the agent can invoke to affect the environment or retrieve computed results. Each tool is described by a typed schema (name, description, input JSON Schema, output JSON Schema, mutation class, required permissions).
- Examples: code execution sandboxes, database query engines, web search APIs, file system operators, CI/CD pipeline triggers.
- Key Property: Tools are invoked by the agent through
tools/call; the result is returned synchronously or via a completion callback. - Lazy Loading: Tool descriptions are loaded into the agent's context window only when the agent's planner selects them as relevant, minimizing .
2. Resource Servers
Resource servers expose data that the agent can read—files, database records, API responses, configuration, documentation. Resources are identified by URI and may have MIME types.
- Examples: repository file trees, documentation indices, configuration registries, log archives.
- Key Property: Resources are read by the agent through
resources/read; they are not executable. - Distinction from Tools: A tool does something; a resource provides information. This distinction governs authorization (resources require read scope; tools may require write scope).
3. Prompt Surface Servers
Prompt surface servers expose reusable prompt templates with typed argument slots. These are not free-text prompts but structured templates that the agent (or its orchestrator) can instantiate with specific parameters.
- Examples: code review templates, incident analysis frameworks, summarization patterns, compliance check protocols.
- Key Property: Prompt surfaces are retrieved and instantiated via
prompts/get, returning an assembled prompt with arguments filled.
MCP Server Composition Model#
A single MCP server process may expose any combination of tools, resources, and prompt surfaces. The capability advertisement (§4.4.2) declares which categories the server supports.
Formal Server Capability Declaration:
Let be an MCP server. Its capability set is:
where each is either present (with optional sub-capabilities like listChanged for change notifications) or absent. The client (agent runtime) adjusts its interaction pattern based on the declared capabilities.
4.4.2 Capability Discovery, Schema Negotiation, and Dynamic Tool Registration#
Initialization Handshake#
MCP uses a strict initialization protocol before any substantive communication:
Pseudo-Algorithm: MCP Initialization Handshake
ALGORITHM McpInitializationHandshake
PARTICIPANTS: client (agent runtime), server (MCP server)
1. CLIENT sends `initialize` request:
{
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {
roots: { listChanged: true },
sampling: {}
},
clientInfo: { name: "agent-runtime", version: "3.2.1" }
}
}
2. SERVER responds with its capabilities:
{
result: {
protocolVersion: "2025-03-26",
capabilities: {
tools: { listChanged: true },
resources: { subscribe: true, listChanged: true },
prompts: { listChanged: true }
},
serverInfo: { name: "code-tools", version: "1.4.0" }
}
}
3. VERSION NEGOTIATION:
negotiated_version ← MIN(client.protocolVersion, server.protocolVersion)
IF no compatible version exists:
ABORT connection with VERSION_MISMATCH error
4. CLIENT sends `initialized` notification:
{ method: "notifications/initialized" }
5. CONNECTION is now ACTIVE.
Client may now call tools/list, resources/list, prompts/list.
6. RECORD capability matrix:
FOR EACH capability c IN server.capabilities:
capability_registry.register(server.id, c, c.sub_capabilities)Dynamic Tool Registration#
Tools can be registered and deregistered at runtime. When a server's tool set changes, it emits a notifications/tools/list_changed notification. The client must then re-fetch the tool list via tools/list.
Tool Registration Lifecycle:
Schema Negotiation Rules:
- Every tool's
inputSchemamust be a valid JSON Schema Draft 2020-12 document. - The client validates the schema is well-formed upon discovery.
- Before invocation, the client validates the actual input parameters against the tool's
inputSchema. - If a tool's schema changes (detected via
list_changed), all cached schema references are invalidated.
Tool Relevance Scoring for Context Loading#
Not all discovered tools should be loaded into the agent's context window simultaneously. The platform computes a tool relevance score to determine which tool descriptions to include in the prefill:
where:
- is the semantic similarity between the tool description and the current task objective,
- is the historical invocation frequency of tool by the agent type handling task ,
- is a decay function favoring recently successful tools,
- is the tool's historical success rate,
- are tunable weights.
Only tools with are included in the context prefill, enforcing the token budget .
4.4.3 Local (stdio) vs. Remote (SSE/HTTP) Transport Modes#
MCP supports two transport modes, selected based on deployment topology:
Local Transport (stdio)#
- Mechanism: The MCP server runs as a child process of the client. Communication occurs over stdin/stdout using newline-delimited JSON-RPC messages.
- Use Case: Local development, sandboxed tool execution, co-located integrations where network overhead is unnecessary.
- Security Model: Process isolation; the server inherits the client's environment unless explicitly sandboxed.
- Lifecycle: The client manages the server process lifecycle (spawn, monitor, restart, kill).
Process Management Invariants:
- If the server process exits unexpectedly, the client must detect exit within 1 heartbeat interval and trigger restart or failover.
- Stderr from the server is captured for diagnostics but never parsed as protocol messages.
- The client must not write to stdout if the server expects exclusive stdin; multiplexing requires explicit framing.
Remote Transport (SSE/HTTP)#
- Mechanism: The MCP server runs as a network service. The client connects via HTTP using Server-Sent Events (SSE) for server-to-client streaming and HTTP POST for client-to-server requests.
- Use Case: Shared tool servers serving multiple agents, cloud-hosted integrations, cross-network tool access.
- Security Model: TLS required; authentication via bearer tokens or mutual TLS.
- Lifecycle: The server runs independently; clients connect and disconnect without affecting server state.
Transport Selection Decision:
Formal Latency Comparison:
For a single tool invocation:
Typically for co-located scenarios (sub-millisecond pipe transit vs. milliseconds of TLS + network), justifying stdio for latency-critical local tools.
4.4.4 Pagination, Change Notifications, and Subscription Semantics#
Pagination#
MCP methods that return lists (tools/list, resources/list, prompts/list, resources/templates/list) support cursor-based pagination:
- The response includes a
nextCursorfield if more results exist. - The client passes
cursorin subsequent requests to fetch the next page. - Cursors are opaque strings; the client must not parse, modify, or persist them across sessions.
- Page size is server-determined; the client may request a preferred size but the server is not obligated to honor it.
Pagination Invariant: For a complete enumeration of items with page size :
The total latency for full enumeration:
This motivates caching the full list locally and refreshing only upon list_changed notifications.
Change Notifications#
When a server's capability set changes (tools added/removed, resources updated, prompt surfaces modified), it emits a notification:
notifications/tools/list_changednotifications/resources/list_changednotifications/prompts/list_changed
These are fire-and-forget notifications (no id field, no response expected). The client is responsible for re-fetching the affected list.
Change Notification Handling:
ALGORITHM HandleMcpChangeNotification
INPUT: notification n from MCP server S
1. PARSE n.method:
SWITCH n.method:
CASE "notifications/tools/list_changed":
affected_cache ← tool_cache[S.id]
CASE "notifications/resources/list_changed":
affected_cache ← resource_cache[S.id]
CASE "notifications/prompts/list_changed":
affected_cache ← prompt_cache[S.id]
2. INVALIDATE affected_cache
3. SCHEDULE async refresh:
new_list ← CALL S.list_method() with pagination
VALIDATE each item's schema
UPDATE affected_cache with new_list
COMPUTE delta (added, removed, modified items)
4. IF delta affects currently active agent contexts:
FOR EACH affected_agent a:
IF a.state = EXECUTING:
a.context.invalidate_tool_cache()
a.planner.notify_capability_change(delta)
// Do NOT abort execution; let planner adapt at next step boundary
5. EMIT metric: mcp_capability_change{server=S.id, type=n.method, delta_size}Resource Subscriptions#
MCP supports resource subscriptions via resources/subscribe and resources/unsubscribe. When a subscribed resource changes, the server emits notifications/resources/updated with the resource URI. The client can then re-read the resource.
Subscription Lifecycle:
4.4.5 MCP Roots, Sampling, and Bidirectional Context Exchange#
Roots#
Roots provide the server with visibility into the client's workspace context. The client declares a set of root URIs (e.g., file system paths, repository URLs) that define the boundaries of what the server should consider when operating.
- The server uses roots to scope operations (e.g., a code analysis tool only examines files within declared roots).
- Roots are declared during initialization and can be updated dynamically via
notifications/roots/list_changed. - Roots do not grant the server direct access; they inform the server's operational scope.
Security Implication: Roots define the principle of least privilege for server operations. A server that receives a root of /project/src should not attempt to read /project/secrets.
Sampling#
Sampling is MCP's mechanism for server-initiated LLM inference. A server can request the client (which has access to an LLM) to perform a completion on the server's behalf. This enables the "human-in-the-loop" pattern: the server prepares context, the client's LLM generates, and the server uses the result.
Sampling Flow:
ALGORITHM McpSamplingFlow
PARTICIPANTS: server S, client C (has LLM access)
1. SERVER constructs a sampling request:
{
method: "sampling/createMessage",
params: {
messages: [
{ role: "user", content: { type: "text", text: "Analyze this code..." } }
],
modelPreferences: {
hints: [{ name: "claude-sonnet-4-20250514" }],
costPriority: 0.3,
speedPriority: 0.5,
intelligencePriority: 0.8
},
systemPrompt: "You are a code analysis expert...",
maxTokens: 2048,
temperature: 0.2
}
}
2. CLIENT receives request and applies POLICY CHECKS:
a. Is sampling permitted by client configuration?
b. Does the request comply with token budget constraints?
c. Does the model preference match available models?
d. HUMAN-IN-THE-LOOP: optionally present to user for approval
3. CLIENT dispatches to LLM with appropriate model, returns result:
{
result: {
model: "claude-sonnet-4-20250514",
role: "assistant",
content: { type: "text", text: "The analysis reveals..." },
stopReason: "endTurn"
}
}
4. SERVER uses result to continue its operationBidirectional Context Exchange Model:
MCP's architecture enables a bidirectional flow of context that is unique among tool protocols:
- Client → Server (via Roots): "Here is my workspace scope."
- Server → Client (via Resources): "Here is data you may need."
- Server → Client (via Sampling): "Please reason about this for me."
- Client → Server (via Tool Calls): "Please execute this action."
This forms a closed loop:
4.5 Versioned Contracts: Semantic Versioning for Agent Interfaces, Breaking Change Detection#
4.5.1 Versioning Model#
Every interface in the protocol stack carries a semantic version where is the major version, is the minor version, and is the patch version, following standard Semantic Versioning (SemVer) with agent-specific interpretations:
| Version Component | Increment When | Agent-Specific Meaning |
|---|---|---|
| Major () | Breaking change | Tool input/output schema incompatible; agent behavior changes semantically |
| Minor () | Backward-compatible addition | New optional tool parameter, new resource type, new prompt surface |
| Patch () | Bug fix, documentation | Schema unchanged; implementation-only fix |
4.5.2 Compatibility Matrix#
Define the compatibility predicate :
- true: Full compatibility; client can use all features up to .
- false: Breaking incompatibility; connection must be refused or routed to a compatible version.
- degraded: Client expects features the server doesn't have; the client must gracefully handle missing capabilities.
4.5.3 Breaking Change Detection#
Breaking changes are detected mechanically in CI/CD by comparing the current schema against the previous release schema.
Pseudo-Algorithm: Breaking Change Detector
ALGORITHM BreakingChangeDetector
INPUT: schema_old S_old (version v_old), schema_new S_new (version v_new)
OUTPUT: list of breaking changes, or PASS
1. COMPUTE diff D = structural_diff(S_old, S_new)
2. FOR EACH change c IN D:
CLASSIFY c:
- FIELD_REMOVED: breaking (clients may depend on it)
- FIELD_TYPE_CHANGED: breaking (serialization incompatible)
- REQUIRED_FIELD_ADDED: breaking (old clients won't send it)
- ENUM_VALUE_REMOVED: breaking (old clients may send it)
- FIELD_NUMBER_CHANGED (Protobuf): breaking (binary incompatible)
- FIELD_ADDED_OPTIONAL: non-breaking
- ENUM_VALUE_ADDED: non-breaking (with UNSPECIFIED default)
- DESCRIPTION_CHANGED: non-breaking
3. breaking_changes ← FILTER D WHERE classification = breaking
4. IF |breaking_changes| > 0:
IF v_new.major = v_old.major:
FAIL CI: "Breaking changes detected without major version bump"
REPORT breaking_changes with field paths and classifications
ELSE:
WARN: "Breaking changes in major version bump — ensure migration guide exists"
REQUIRE: migration_guide document exists for v_old → v_new
REQUIRE: deprecation notice was published ≥ 2 release cycles ago
5. RETURN breaking_changes or PASS4.5.4 Multi-Version Serving#
The platform supports serving multiple major versions simultaneously via version-prefixed routing:
- JSON-RPC: Method names include version prefix (e.g.,
v2.agent.submitTask) - gRPC: Service definitions are versioned in package names (e.g.,
package agent.v2;) - MCP: Protocol version is negotiated during initialization handshake
Sunset Policy: A major version is supported for at least release cycles (configurable, typically 3–6 months) after the successor is released. After sunset, requests to the old version receive error code DEPRECATED_VERSION with a migration URI.
4.6 Interface Definition Language (IDL) Strategy: Protobuf, JSON Schema, OpenAPI, MCP Schema Unification#
4.6.1 The IDL Fragmentation Problem#
The three-layer protocol stack introduces three distinct schema languages:
- Protobuf (Proto3): For gRPC service definitions and internal message types.
- JSON Schema (Draft 2020-12): For MCP tool input/output schemas and JSON-RPC parameter validation.
- OpenAPI 3.1: For HTTP API documentation consumed by external integrators.
Without a unification strategy, schema definitions drift, validators diverge, and breaking changes in one layer go undetected in others.
4.6.2 Canonical Source Strategy#
The platform adopts a single canonical source for each domain type, with automated generation of derivative schemas:
Decision Matrix for Canonical Source:
| Domain | Canonical IDL | Rationale | Generated Artifacts |
|---|---|---|---|
| Agent messages, memory ops | Protobuf | Internal perf-critical, strong typing | → JSON Schema (for boundary validation), → OpenAPI (for docs) |
| Tool schemas | JSON Schema | MCP native format, dynamic registration | → Protobuf Struct wrappers (for internal transport) |
| Boundary API surface | OpenAPI 3.1 | Developer portal, SDK generation | → JSON Schema (params validation), → Protobuf (gateway transcoding) |
4.6.3 Schema Unification Pipeline#
Pseudo-Algorithm: Schema Unification Pipeline
ALGORITHM SchemaUnificationPipeline
INPUT: canonical schema files, target formats
OUTPUT: unified, validated schema artifacts
1. DISCOVER all canonical schema files by convention:
proto_files ← glob("schemas/proto/**/*.proto")
json_schemas ← glob("schemas/json/**/*.schema.json")
openapi_specs ← glob("schemas/openapi/**/*.yaml")
2. FOR EACH proto file p IN proto_files:
a. COMPILE p with protoc → validate syntax
b. GENERATE json_schema from p using proto-to-jsonschema converter
c. GENERATE openapi fragment from p using protoc-gen-openapi
d. STORE generated artifacts with provenance: {source: p, generator_version, timestamp}
3. FOR EACH json_schema j IN json_schemas:
a. VALIDATE j against JSON Schema meta-schema
b. GENERATE protobuf Struct wrapper message for j
c. REGISTER j in MCP tool schema registry
4. FOR EACH openapi spec o IN openapi_specs:
a. VALIDATE o with openapi-validator
b. EXTRACT params schemas → validate against corresponding json_schemas
c. GENERATE SDK client stubs
5. CROSS-VALIDATE:
FOR EACH type T that exists in multiple IDLs:
proto_schema ← lookup(T, proto_files)
json_schema ← lookup(T, json_schemas)
openapi_schema ← lookup(T, openapi_specs)
field_diff ← structural_compare(proto_schema, json_schema, openapi_schema)
IF field_diff contains SEMANTIC_MISMATCH:
FAIL: "Schema drift detected for type {T}: {field_diff}"
6. PUBLISH unified schema registry with version metadata
7. EMIT metrics: schema_count, drift_violations, generation_duration4.6.4 Schema Registry Architecture#
The schema registry is a versioned, queryable store of all schema artifacts:
- Storage: Git-backed (for auditability) with a query API layered on top.
- Indexing: By type name, version, domain, protocol, and provenance.
- Queries: "Give me the JSON Schema for
ToolInvocationRequestat version 2.3.x" → returns the exact schema with provenance. - Validation Service: Any service can submit a payload + type name + version and receive a validation result (pass/fail with error paths).
4.7 Cross-Protocol Gateway Design: JSON-RPC ↔ gRPC ↔ MCP Translation Layers#
4.7.1 Gateway Architecture#
The cross-protocol gateway is the critical integration point where the three protocol layers meet. It is not a monolithic translator but a composable codec pipeline with distinct stages.
Gateway Topology:
External Client
│
▼
┌─────────────────────────┐
│ JSON-RPC Ingress │ ← TLS termination, auth, rate limiting
│ (HTTP/WebSocket) │
└─────────┬───────────────┘
│
▼
┌─────────────────────────┐
│ Protocol Codec Layer │ ← JSON-RPC ↔ gRPC transcoding
│ (Bidirectional) │ ← MCP ↔ gRPC bridging
└─────────┬───────────────┘
│
▼
┌─────────────────────────┐
│ gRPC Internal Mesh │ ← Agent services, memory, retrieval
└─────────┬───────────────┘
│
▼
┌─────────────────────────┐
│ MCP Connector Manager │ ← Tool servers, resource servers
│ (stdio / SSE adapters) │
└─────────────────────────┘4.7.2 Transcoding Rules#
JSON-RPC → gRPC Transcoding:
Define the transcoding function :
The transcoding applies the following transformations:
- Method Mapping: JSON-RPC method
"agent.submitTask"→ gRPC serviceAgentService, methodSubmitTask. - Params → Protobuf: JSON
paramsobject is deserialized into the corresponding Protobuf message using the schema registry. Type coercion follows strict rules (no implicit string-to-int conversion). - Metadata Injection: Trace context, auth context, and deadline are extracted from HTTP headers and injected as gRPC metadata.
- Response Mapping: gRPC response Protobuf → JSON
resultobject; gRPC status codes → JSON-RPC error codes.
gRPC → MCP Bridging:
When an agent (operating within the gRPC mesh) needs to invoke an MCP tool:
- Tool Resolution: The
tool_namefield is resolved against the MCP tool registry to identify the target server and transport mode. - Params Extraction: The
input_params(google.protobuf.Struct) is serialized to JSON for the MCPargumentsfield. - Auth Propagation: Caller-scoped credentials are mapped to MCP-compatible auth headers or tokens.
- Result Transcoding: MCP
contentresults (text, image, resource references) are packed back intoToolInvocationResponse.output.
4.7.3 Pseudo-Algorithm: Cross-Protocol Gateway#
ALGORITHM CrossProtocolGateway
INPUT: request from any protocol layer
OUTPUT: response in the originating protocol format
1. IDENTIFY source protocol:
source ← detect_protocol(request) // JSON_RPC, GRPC, MCP
2. IDENTIFY target service and its native protocol:
target_service, target_protocol ← route(request.method)
3. IF source = target_protocol:
// No transcoding needed; pass through
DISPATCH request directly
RETURN response
4. // Transcoding required
SWITCH (source → target_protocol):
CASE JSON_RPC → GRPC:
a. LOOKUP proto message type for request.method from schema_registry
b. DESERIALIZE request.params → proto message (validate against schema)
c. EXTRACT auth, trace, deadline from HTTP headers → gRPC metadata
d. DISPATCH gRPC call with deadline
e. AWAIT gRPC response
f. SERIALIZE proto response → JSON result
g. MAP gRPC status → JSON-RPC error code (if error)
h. RETURN JSON-RPC response
CASE GRPC → MCP:
a. RESOLVE tool_name → MCP server connection (stdio or SSE)
b. SERIALIZE proto Struct → JSON arguments
c. CONSTRUCT MCP tools/call request
d. SET timeout from gRPC deadline remaining
e. DISPATCH to MCP server
f. AWAIT MCP response
g. PARSE MCP content → proto Struct
h. CONSTRUCT ToolInvocationResponse proto
i. RETURN gRPC response
CASE MCP → GRPC:
a. MAP MCP sampling/createMessage → internal LLM service gRPC call
b. TRANSLATE message format (MCP messages → proto LlmRequest)
c. DISPATCH, AWAIT, TRANSLATE response back to MCP format
d. RETURN MCP response
CASE JSON_RPC → MCP:
// Two-hop: JSON_RPC → GRPC → MCP (agent mediates)
// Direct JSON_RPC → MCP is prohibited (violates layer isolation)
RETURN error(PROTOCOL_VIOLATION, "Direct boundary→MCP not permitted")
5. LOG transcoding event with latency, source, target, trace_id
6. INCREMENT counter: gateway_transcoding_total{source, target, method, status}4.7.4 Gateway Performance Model#
The gateway introduces transcoding latency . For a request traversing protocol boundaries:
Minimizing is a key design goal; ideally for any end-to-end path (boundary → internal → tool). The gateway caches compiled transcoding plans (method → proto type mapping, field mappings) to minimize per-request schema lookups, achieving for typical messages.
4.8 Authentication, Authorization, and Caller-Scoped Credential Propagation Across Protocol Boundaries#
4.8.1 Trust Model#
The agentic platform operates under a zero-trust, caller-scoped security model. Key principles:
- No Ambient Authority: Agents do not possess their own credentials. They operate under the authority of the originating caller (user, service account, or upstream agent).
- Caller-Scoped Credentials: The original caller's identity and permission scope propagate through every protocol boundary, from JSON-RPC ingress through gRPC mesh to MCP tool invocations.
- Least Privilege for Tools: Each tool invocation is authorized against the caller's scope, not the agent's. An agent cannot escalate privileges beyond what the caller granted.
- Human-in-the-Loop for Mutations: State-changing operations classified as
NON_IDEMPOTENT_WRITEorDESTRUCTIVEmay require explicit human approval before execution.
4.8.2 Credential Propagation Chain#
Define the credential propagation chain for a request originating from user with scope set :
At each boundary, the scope may be narrowed but never widened:
This is enforced by the gateway's scope-filtering middleware.
4.8.3 Authentication Mechanisms by Protocol Layer#
| Layer | Mechanism | Token Format | Validation |
|---|---|---|---|
| JSON-RPC (Boundary) | Bearer token in Authorization header | JWT (RS256) or opaque reference token | Token introspection or local JWT validation with JWKS |
| gRPC (Internal) | Metadata key authorization | Platform-issued short-lived JWT | Local validation against platform JWKS; mTLS for service identity |
| MCP (Tool) | Bearer token in HTTP header (SSE) or env variable (stdio) | Scoped access token | Tool server validates against platform token endpoint |
4.8.4 Pseudo-Algorithm: Authorization Gate#
ALGORITHM AuthorizationGate
INPUT: request req, caller_identity id, required_scopes R, mutation_class mc
OUTPUT: ALLOW or DENY with reason
1. EXTRACT caller scopes S from id.token:
S ← decode_and_validate_token(id.token)
IF token invalid or expired:
RETURN DENY("Invalid or expired credentials")
2. CHECK scope coverage:
missing ← R \ S // set difference
IF |missing| > 0:
RETURN DENY("Missing required scopes: " + missing)
3. CHECK mutation class:
IF mc = NON_IDEMPOTENT_WRITE OR mc = DESTRUCTIVE:
IF policy_requires_approval(req.method, id.role):
approval ← request_human_approval(req, id, timeout=approval_timeout)
IF approval = DENIED OR approval = TIMEOUT:
RETURN DENY("Human approval not granted")
RECORD approval_audit_entry(req, id, approval.approver, approval.timestamp)
4. CHECK resource-level authorization:
IF req targets specific resource R:
IF NOT has_access(id, R, req.action):
RETURN DENY("No access to resource " + R)
5. ISSUE scoped downstream token:
downstream_scopes ← S ∩ R // narrow to only needed scopes
downstream_token ← mint_token(id, downstream_scopes, ttl=req.remaining_deadline)
6. ATTACH downstream_token to req.auth_context
7. RETURN ALLOW4.8.5 Credential Rotation and Revocation#
- Short-Lived Tokens: Internal tokens have TTL ≤ 15 minutes; they are never persisted.
- Revocation: A revocation event (user deactivation, scope change, security incident) propagates to all active sessions within via a revocation list broadcast.
- Rotation: JWKS keys rotate on a configurable schedule (default: 24 hours) with overlap periods ensuring no validation gaps.
4.9 Observability Integration: Distributed Tracing (OpenTelemetry) Across All Protocol Layers#
4.9.1 Observability as a First-Class Protocol Concern#
An agentic system that cannot be observed cannot be debugged, optimized, or trusted. Observability is not an afterthought bolted onto the protocol stack—it is a first-class cross-cutting concern woven into every protocol boundary, every gateway transcoding, and every agent execution step.
The platform adopts OpenTelemetry (OTel) as the observability framework, providing unified tracing, metrics, and logging across all three protocol layers.
4.9.2 Trace Context Propagation#
Every request carries a W3C Trace Context consisting of:
trace-id: 128-bit globally unique trace identifier.span-id: 64-bit identifier for the current span.trace-flags: Sampling flags.tracestate: Vendor-specific key-value pairs.
Propagation by Protocol Layer:
| Layer | Propagation Mechanism |
|---|---|
| JSON-RPC (HTTP) | traceparent and tracestate HTTP headers |
| gRPC | grpc-trace-bin binary metadata key (or traceparent text metadata) |
| MCP (SSE/HTTP) | traceparent HTTP header on POST requests; embedded in SSE event metadata |
| MCP (stdio) | _trace field in JSON-RPC params or a dedicated trace envelope |
4.9.3 Span Hierarchy for Agentic Execution#
The trace hierarchy mirrors the agentic execution structure:
[Root Span: user_request]
├── [Span: jsonrpc_gateway]
│ ├── [Span: auth_validation]
│ ├── [Span: schema_validation]
│ └── [Span: grpc_transcode]
├── [Span: agent_orchestrator]
│ ├── [Span: plan]
│ ├── [Span: decompose]
│ │ ├── [Span: subtask_1]
│ │ └── [Span: subtask_2]
│ ├── [Span: retrieve]
│ │ ├── [Span: semantic_search]
│ │ ├── [Span: exact_match]
│ │ └── [Span: rank_fuse]
│ ├── [Span: act]
│ │ └── [Span: tool_call_via_mcp]
│ │ ├── [Span: mcp_gateway_transcode]
│ │ └── [Span: tool_execution]
│ ├── [Span: verify]
│ ├── [Span: critique]
│ └── [Span: commit]
└── [Span: response_synthesis]4.9.4 Metrics at Protocol Boundaries#
Each protocol boundary emits standardized metrics:
Counter Metrics:
protocol_requests_total{protocol, method, status, version}— Total requests by method and outcome.protocol_errors_total{protocol, error_code, method}— Error counts by code.gateway_transcoding_total{source_protocol, target_protocol, method}— Transcoding volume.
Histogram Metrics:
protocol_request_duration_seconds{protocol, method}— Latency distribution.gateway_transcoding_duration_seconds{source, target}— Transcoding latency.mcp_tool_call_duration_seconds{tool_name, server_id}— Per-tool invocation latency.
Gauge Metrics:
grpc_active_streams{service, method}— Currently active streaming RPCs.mcp_connected_servers{transport}— Active MCP server connections.circuit_breaker_state{service}— 0=closed, 1=half-open, 2=open.
4.9.5 Pseudo-Algorithm: Observability Middleware#
ALGORITHM ObservabilityMiddleware
INPUT: request req, protocol_layer, handler_fn
OUTPUT: response with trace and metrics recorded
1. EXTRACT OR GENERATE trace context:
parent_ctx ← extract_trace_context(req, protocol_layer)
IF parent_ctx = NULL:
trace_id ← generate_trace_id()
parent_ctx ← new_root_context(trace_id)
2. START span:
span ← tracer.start_span(
name = protocol_layer + "." + req.method,
parent = parent_ctx,
attributes = {
"protocol": protocol_layer,
"method": req.method,
"version": req.schema_version,
"caller_id": req.auth.client_id
}
)
3. START timer:
t_start ← CURRENT_TIME_NS()
4. EXECUTE handler:
TRY:
response ← handler_fn(req, span.context)
span.set_status(OK)
status_label ← "success"
CATCH error e:
span.set_status(ERROR)
span.record_exception(e)
status_label ← "error"
response ← construct_error_response(e)
5. RECORD metrics:
t_duration ← CURRENT_TIME_NS() - t_start
metrics.counter("protocol_requests_total",
labels={protocol_layer, req.method, status_label, req.schema_version}).increment()
metrics.histogram("protocol_request_duration_seconds",
labels={protocol_layer, req.method}).observe(t_duration / 1e9)
IF status_label = "error":
metrics.counter("protocol_errors_total",
labels={protocol_layer, response.error.code, req.method}).increment()
6. INJECT trace context into response:
inject_trace_context(response, span.context, protocol_layer)
7. END span:
span.end()
8. RETURN response4.9.6 Log Correlation#
All structured log entries include:
trace_id: For correlation with distributed traces.span_id: For correlation with specific spans within a trace.agent_id: The agent instance emitting the log.task_id: The task being executed.level: Severity (DEBUG, INFO, WARN, ERROR, FATAL).
Logs are emitted via OpenTelemetry's log bridge, ensuring they appear in the same trace viewer as spans and metrics.
4.10 Protocol Compliance Testing, Fuzzing, and Contract Verification in CI/CD#
4.10.1 Testing Taxonomy#
Protocol compliance testing for the agentic platform covers four layers:
| Test Category | What It Validates | When It Runs |
|---|---|---|
| Schema Conformance | Messages match their IDL definitions | Every commit (CI) |
| Contract Compatibility | New schemas are backward-compatible with previous versions | Every commit (CI) |
| Protocol Compliance | Implementations conform to JSON-RPC 2.0, gRPC semantics, MCP spec | Nightly + pre-release |
| Fuzzing | Robustness against malformed, unexpected, or adversarial inputs | Continuous (dedicated fuzzing infra) |
| Integration Compliance | End-to-end cross-protocol flows produce correct results | Pre-merge + staging |
4.10.2 Schema Conformance Testing#
Pseudo-Algorithm: Schema Conformance Test Suite
ALGORITHM SchemaConformanceTests
INPUT: all proto files, json schemas, openapi specs, test corpus
OUTPUT: PASS/FAIL with detailed report
1. FOR EACH proto file p:
a. COMPILE p with protoc --strict mode
b. GENERATE test message instances using proto-gen-fuzz
c. ROUND-TRIP test: serialize → deserialize → assert equality
d. VALIDATE default values match specification
2. FOR EACH json schema j:
a. VALIDATE j against meta-schema (Draft 2020-12)
b. FOR EACH positive example e+ IN test_corpus[j]:
ASSERT validate(e+, j) = PASS
c. FOR EACH negative example e- IN test_corpus[j]:
ASSERT validate(e-, j) = FAIL
ASSERT error message identifies correct field
3. FOR EACH openapi spec o:
a. VALIDATE o with openapi-spec-validator
b. GENERATE request/response examples from spec
c. VALIDATE examples against corresponding json schemas
4. CROSS-VALIDATE:
FOR EACH type T with representations in multiple IDLs:
proto_instance ← generate_random_instance(T, proto)
json_transcoded ← proto_to_json(proto_instance)
VALIDATE json_transcoded against json_schema[T]
proto_round_tripped ← json_to_proto(json_transcoded)
ASSERT proto_instance = proto_round_tripped (field-by-field)
5. REPORT: total tests, pass count, fail count, coverage percentage4.10.3 Contract Compatibility Testing#
This validates that schema changes do not break existing clients (§4.5.3). The breaking change detector runs on every pull request that modifies any schema file.
CI Pipeline Integration:
ALGORITHM ContractCompatibilityCI
INPUT: pull_request PR modifying schema files
OUTPUT: CI verdict (PASS, WARN, FAIL)
1. IDENTIFY modified schema files in PR diff
2. FOR EACH modified schema file f:
a. FETCH previous released version f_old from schema registry
b. RUN BreakingChangeDetector(f_old, f_new)
c. IF breaking changes detected AND major version not bumped:
FAIL CI with detailed breaking change report
d. IF breaking changes detected AND major version bumped:
REQUIRE migration guide document in PR
REQUIRE deprecation notice for old version
WARN with change summary
3. RUN SchemaUnificationPipeline to ensure derived schemas are consistent
4. RUN SchemaConformanceTests on new schemas
5. IF all pass: APPROVE schema changes4.10.4 Protocol Fuzzing#
Fuzzing validates robustness against adversarial or malformed inputs. The platform fuzzes at three levels:
Level 1: Serialization Fuzzing
- Feed randomly mutated byte sequences to Protobuf deserializers.
- Feed randomly generated JSON to JSON-RPC parsers.
- Verify: no crashes, no unbounded memory allocation, proper error responses.
Level 2: Semantic Fuzzing
- Generate structurally valid but semantically nonsensical messages (e.g., negative token budgets, impossible deadlines, tool names with injection characters).
- Verify: proper validation errors, no state corruption, no information leakage in error messages.
Level 3: Protocol State Fuzzing
- Send messages out of order (e.g.,
tools/callbeforeinitialize; gRPC data frames before HEADERS). - Send concurrent conflicting requests (e.g., simultaneous writes to the same memory key).
- Verify: proper state machine enforcement, no deadlocks, no data races.
Pseudo-Algorithm: Protocol Fuzzer
ALGORITHM ProtocolFuzzer
INPUT: target_endpoint, protocol_type, corpus_seed, max_iterations N
OUTPUT: list of discovered violations
1. INITIALIZE fuzzer:
corpus ← load_seed_corpus(corpus_seed)
violations ← []
coverage_tracker ← new CoverageTracker()
2. FOR i = 1 TO N:
a. SELECT base input from corpus (weighted by coverage novelty)
b. MUTATE base input using strategy based on protocol_type:
SWITCH protocol_type:
CASE JSON_RPC:
mutation ← random_choice([
flip_json_type, // string ↔ number ↔ bool ↔ null
remove_required_field,
inject_extra_field,
overflow_string_length,
inject_unicode_edge_cases,
duplicate_id_field,
malform_json_syntax
])
CASE GRPC:
mutation ← random_choice([
corrupt_protobuf_varint,
truncate_message,
set_unknown_field_number,
overflow_repeated_field,
send_wrong_message_type
])
CASE MCP:
mutation ← random_choice([
send_before_initialize,
call_nonexistent_tool,
exceed_sampling_token_limit,
inject_malformed_cursor,
send_notification_with_id
])
c. SEND mutated input to target_endpoint
d. CAPTURE response and system state:
response_time, response_code, response_body, memory_usage, error_logs
e. CHECK invariants:
- No crash (process still alive)
- Response time < timeout_limit
- Memory usage < memory_limit
- Response is valid protocol message (even if error)
- No sensitive data in error response
- No state corruption (verified by health check)
f. IF any invariant violated:
violations.append({
input: mutated_input,
violation_type: failed_invariant,
response: captured_response,
iteration: i
})
ADD mutated_input to corpus (for further exploration)
g. UPDATE coverage_tracker with execution path information
3. REPORT violations with reproducible test cases
4. EMIT metrics: total_iterations, violations_found, coverage_percentage4.10.5 Integration Compliance Testing#
End-to-end tests validate that the complete protocol stack (JSON-RPC → gateway → gRPC → MCP → tool → response) produces correct, observable, and secure results.
Test Scenario Categories:
- Happy Path: Submit task → agent plans → retrieves → calls tool → returns result. Validate trace completeness, response schema, and latency bounds.
- Deadline Propagation: Set a tight deadline at the boundary; verify it propagates to MCP tool calls and triggers DEADLINE_EXCEEDED at the correct layer.
- Auth Propagation: Submit request with limited scopes; verify tool calls are rejected when tool requires scope not in caller's set.
- Batch Semantics: Submit batch JSON-RPC request; verify independent execution, partial success handling, and response ordering.
- Stream Lifecycle: Open bidirectional gRPC stream; verify heartbeat enforcement, graceful half-close, and error propagation.
- Version Mismatch: Connect with old client version to new server; verify graceful degradation or informative rejection.
- Circuit Breaker: Simulate tool server failure; verify circuit opens after threshold, fast-fails during open state, and recovers during half-open.
Pseudo-Algorithm: End-to-End Compliance Test Runner
ALGORITHM EndToEndComplianceTestRunner
INPUT: test_suite TS, environment env (staging/CI)
OUTPUT: test report with pass/fail/skip per scenario
1. PROVISION test environment:
- Deploy gateway, agent services, mock MCP servers
- Configure known tool schemas, auth tokens, rate limits
- Initialize OpenTelemetry collector for trace capture
2. FOR EACH test scenario s IN TS:
a. SETUP:
- Reset state (clear caches, memories, idempotency stores)
- Configure mocks for s.expected_behavior
b. EXECUTE:
- Send request(s) per s.protocol and s.input
- Capture response(s), latency, traces, metrics, logs
c. ASSERT response correctness:
- Response schema matches expected schema
- Response values match expected values (or acceptable ranges)
- Error codes match expected error codes (for failure scenarios)
d. ASSERT observability:
- Trace exists with expected span hierarchy
- All spans have required attributes (protocol, method, caller_id)
- Metrics counters incremented correctly
- Logs contain trace_id correlation
e. ASSERT security:
- Auth context propagated correctly through trace
- Unauthorized requests rejected at correct layer
- No credential leakage in error responses or logs
f. ASSERT performance:
- Latency within SLO bounds for the scenario
- No memory leaks (compare pre/post memory)
g. RECORD result: {scenario: s.name, status: PASS/FAIL, details, duration}
3. GENERATE test report:
- Total: pass/fail/skip counts
- Coverage: protocol paths exercised
- Regressions: newly failing tests (compared to last run)
- Performance: latency percentiles per scenario
4. IF any FAIL AND env = CI:
BLOCK merge with failure report4.10.6 Continuous Compliance Enforcement#
Compliance testing is not a one-time activity; it is a continuous enforcement mechanism integrated into the development lifecycle:
| Trigger | Tests Run | Gate |
|---|---|---|
| Every commit | Schema conformance, contract compatibility | Merge blocked on failure |
| Every PR with schema changes | Breaking change detection, cross-IDL validation | Merge blocked on failure |
| Nightly | Full protocol compliance suite, integration tests | Alert on regression |
| Continuous (background) | Fuzzing (all levels) | Ticket created on violation |
| Pre-release | Full suite + performance benchmarks + canary deployment | Release blocked on failure |
| Post-deployment | Synthetic probes (golden signal tests) | Rollback on SLO violation |
Quality Gate Formula:
A release candidate passes the protocol compliance gate if:
where is the minimum pass rate (typically ), counts failures in security, auth propagation, or data integrity tests, and is the 99th percentile latency.
Chapter Summary#
This chapter established the typed protocol stack that transforms an agentic platform from ad hoc prompt orchestration into a deterministic, observable, versionable distributed system. The key architectural decisions and their justifications:
| Decision | Justification |
|---|---|
| JSON-RPC 2.0 at the boundary | Universal accessibility, transport neutrality, batch semantics, minimal integration burden |
| gRPC/Protobuf internally | Binary efficiency ( compression), streaming, strong typing, deadline propagation, native cancellation |
| MCP for discovery | Runtime-dynamic tool/resource/prompt registration, capability negotiation, bidirectional context exchange, lazy loading for token budget optimization |
| Cross-protocol gateway with codec pipeline | Clean layer isolation while enabling end-to-end flows; cached transcoding plans for sub-millisecond translation |
| Caller-scoped credential propagation | Zero-trust security model; scope narrowing across boundaries (); human-in-the-loop for destructive mutations |
| OpenTelemetry across all layers | Unified trace context propagation; span hierarchy mirrors agent execution structure; mechanical correlation of logs, metrics, and traces |
| Continuous contract verification | Breaking changes detected in CI; fuzzing discovers robustness gaps; integration tests validate end-to-end correctness; quality gates enforce release criteria |
The protocol stack is not merely a communication layer—it is the immune system of the agentic platform. Every typed contract prevents a class of runtime errors. Every versioned schema prevents a class of deployment failures. Every propagated deadline prevents a class of resource leaks. Every scoped credential prevents a class of security violations. Every trace span prevents a class of debugging nightmares. Together, they establish the mechanical foundation upon which reliable, scalable, and auditable agentic systems are built.
End of Chapter 4.