Skip to content

Overview

protocol-droid is built hexagonal-collapsed, with a domain layer that holds no I/O dependencies, application handlers that declare the port traits they consume, and adapters that implement those traits and live on the outside. The shape is what makes “speak every MCP revision plus A* protocols through one model” tractable.

Layer model

The collapsed layout is src/{domain,application,adapters/{primary,secondary}}:

LayerHoldsDependencies
domain/Value objects, aggregates, pure functionsnone
application/One module per use case; each declares its portdomain
adapters/primary/Driving adapters (servers, CLI)application, domain
adapters/secondary/Driven adapters (clients, clocks, filesystem)application, domain

The arrow points inward: adapters depend on the layers they serve; domain depends on nothing in the workspace. A consumer who needs to swap an adapter for a test fake only writes the trait, which lives in application/ next to the handler that calls it.

The protocol-neutral core

The JSON-RPC envelope, the session engine, and every transport live in protocol-agnostic modules (domain/jsonrpc, application/session, adapters/_/transport). Nothing in those paths imports an MCP type. MCP-specific code sits in mcp/ submodules within each layer; A2A sits in a2a/, and ACP — when it lands — sits in acp/.

Three rules

  1. Domain has no traits, no I/O, no third-party error types. A handler that needs to log, sleep, or read a clock takes the port; the adapter is the only place tracing, tokio, or reqwest appears. Domain functions are pure.
  2. The port trait returns a DOMAIN error. An axum handler that bubbles axum::Error into application/ is a bug, not a convenience. The boundary is where the third-party type disappears.
  3. All schema revisions are always compiled. They are data, not features. A --no-default-features build drops transports and OAuth, never a spec revision.

Negotiation, the IR, and the codecs

A request arrives, gets routed by the single-pass envelope router (jsonrpc, id, method lifted from a borrowed &RawValue), negotiated against the peer’s capabilities, and lifted into the IR. The IR is a superset of every revision’s surface; the codec is the only thing that knows the wire shape of one revision, and the only thing that needs to. The application code speaks the IR; the wire version is whatever the negotiation step chose.

Why one IR walks through the claim with a concrete tools/call example. FeatureProfile and gating walks through the runtime feature model that sits on top of it.

Transports

Every transport the specification has defined is an adapter on the same session engine: stdio, the 2024-11-05 HTTP+SSE pair, Streamable HTTP, and the 2026-07-28 stateless lifecycle. A server mounts Streamable HTTP and the legacy routes on one listener (HttpBuilder::legacy_sse()), and both answer from the same registry, so a tool registered at runtime is announced on whichever sessions negotiated list_changed. Authorization is a client-side use case too: an OAuthAuthorizer in application/oauth/ owns the decisions and asks an OAuthEnvironment port for the HTTP and the user.

The bridge

The bridge is the only piece of the library that knows the wire shape of two revisions at once. A 2024-11-05 client’s request arrives, gets lifted into the IR, and gets lowered to a 2026-07-28 server’s wire. The reverse direction works the same way. The consumer never has to think about the wire version the peer speaks; they drive the library through a single API and the bridge translates.

Bridging two peers walks through the bridge surface, including the A*-protocol cross-bridge configuration.

What’s in the codebase

CrateRole
protocol-droid-schemaVendored official schemas, build-time generator, per-revision wire types
protocol-droidThe library: IR, codecs, session engine, transports
protocol-droid-macrosDerive macros. Proc-macro crates must be separate
protocol-droid-cliThe protocol-droid binary: bridge, probe, inspect, record, replay, conformance
xtaskDeveloper tooling. Not published

The full architectural write-up — including the conformance-suite verification, the differential interop against rmcp and the TypeScript SDK, the security audit, and the fuzz harness — is in docs/ARCHITECTURE.md in the repository.