protocol-droid 0.5.1

A Model Context Protocol toolkit that speaks every published spec revision, with one canonical IR behind many per-revision codecs.
Documentation
[package]
name = "protocol-droid"
description = "A Model Context Protocol toolkit that speaks every published spec revision, with one canonical IR behind many per-revision codecs."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
documentation.workspace = true
readme.workspace = true
keywords.workspace = true
# Both HTTP halves are first-class here — every transport ships a server and a
# client. `web-programming::http-server` and `::http-client` are the slugs
# crates.io actually carries; there is no bare `web-programming::http` leaf.
categories = [
    "network-programming",
    "parser-implementations",
    "web-programming::http-server",
    "web-programming::http-client",
]
# docs.rs builds with the crate's DEFAULT features, which are serde + stdio —
# so the HTTP server and client, the OAuth client and resource server, and the
# A2A and ACP modules are all absent from the published documentation.
# `all-features` is what makes the crate's actual API visible, and it is what
# the `rustdoc` CI job checks so the two cannot disagree.
[package.metadata.docs.rs]
all-features = true

# No `[features]` table yet. The feature gates for each transport, the legacy
# SSE path, and OAuth are introduced by the tasks that implement them (T13-T16),
# not declared empty in advance.

[features]
# The envelope decoder defaults to `serde_json`, which is portable and has no
# target-specific code. `simd` swaps in a vectorised parser behind a flag so the
# two can be benchmarked against each other rather than argued about; it is off
# by default because a `default-features = false` build has to stay lean and
# because a decoder that is only fast on some targets is a portability trap.
default = ["serde", "stdio"]
# serde_json is the portable default and is always compiled — the generated
# wire types depend on it, so it is data rather than an optional integration.
serde = []
# T13. The stdio transport is the only adapter in the crate that names a
# runtime, so it is the one adapter that is gated. `default-features = false`
# drops the executor, the `AsyncRead`/`AsyncWrite` plumbing and the diagnostic
# channel with it, leaving the domain, the IR and every codec — which is the
# shape a server that speaks only HTTP, or a tool that only speaks the IR, wants.
stdio = ["dep:tokio"]
# T14. The Streamable HTTP server adapter, which is the other half of the same
# story: `stdio` covers the process-spawning client and this covers the
# network server, and the two disagree about nearly everything. `http` is
# **not** a default because the two transports are alternatives, not a set —
# a `stdio` server has no use for a `Router`, and a `default-features = false`
# build is how a consumer says "I am linking this as a library, not running a
# server". Turning it on adds axum, the `net` half of tokio, and the SSE
# responder; everything protocol-shaped — the session store, the event ring,
# the idempotency port, the routing headers, negotiation — is already compiled
# without it, so a consumer that only wants the IR and the codecs pays for none
# of this.
http = ["dep:axum", "dep:base64", "dep:futures-core", "dep:futures-util", "dep:reqwest", "dep:tower", "dep:tokio", "tokio/net", "tokio/rt", "tokio/sync", "tokio/macros"]
# T15. The deprecated 2024-11-05 HTTP+SSE transport, which `2025-03-26`
# replaced and which is the only transport that revision speaks. It needs the
# HTTP stack, so it implies it rather than listing it again — and it is a
# separate gate from `http` because the two are alternatives, not a set: a
# server that speaks Streamable HTTP has no use for a second endpoint and an
# `endpoint` event. `TransportKind::LegacySse` stays ungated in the domain,
# because describing a transport costs nothing and only speaking it does.
legacy-sse = ["http"]
# T16. OAuth 2.1 authorization. It implies `http` because both token sources
# reach an authorization server that is somewhere else, and it adds the one
# dependency the rest of the crate has no use for: a JWS signature check.
#
# Only the *adapters* are gated. `domain/mcp/authorization` and
# `application/oauth/token` stay compiled in a lean build for the same reason
# the session store and the routing headers do — they are protocol-shaped and
# pure, and gating them would cost a consumer the authorization model to save
# nothing.
oauth = ["http", "dep:jsonwebtoken", "dep:base64", "dep:url"]
# Session ids that do not enumerate. `IdSource` is a port every server must
# satisfy and this crate shipped no implementation of it, so the only code a
# consumer could copy was a `fetch_add` counter.
random-ids = ["dep:getrandom"]
# T27. The A2A (Agent2Agent) protocol module — per-revision method table,
# per-revision wire codec, and the application handlers behind them. Gated
# on its own feature because (a) the dependency footprint is independent of
# MCP and a consumer that never speaks A2A should not pay it, and (b) the
# feature gate is the seam that lets a future `a2a-grpc` / `a2a-rest` build
# on top of this without re-touching MCP. `domain/a2a` and `application/a2a`
# stay compiled in a lean build for the same reason the MCP domain does —
# they are pure value objects, so gating them would cost the version model to
# save nothing. The `url` dep is here (not in the unconditional list) because
# only the SSRF filter needs it, and the SSRF filter is A2A-specific.
a2a = ["http", "dep:url", "url/serde"]
# T29. The ACP (Agent Client Protocol) module — Zed's editor↔agent
# JSON-RPC binding. The feature is `acp-experimental` (not `acp`)
# because the spec is pre-1.0 (`schema-v1.25.0` is the pin) and the
# task explicitly excludes it from semver guarantees. The gate is
# what keeps the experimental module out of a consumer's lean
# build; the schema is still compiled because the type names live
# in the protocol-neutral IR.
#
# Pulls in `tokio` (with `process`, `io-std`, `io-util`, `rt`,
# `sync`, `time`) because the stdio transport is the only ACP
# adapter that runs an executor. The `acp-stdio` sub-module is
# gated on the same flag so a consumer that pulls in
# `acp-experimental` for the typed `initialize` shape but never
# spawns a child process pays for the typed envelope and not the
# runtime. The feature interaction with the crate-wide `stdio`
# flag is `acp-experimental` *adds* the deps; turning the crate
# `stdio` on does not turn `acp-experimental` on. A consumer that
# wants both the typed envelope and the stdio peer enables both
# flags.
acp-experimental = [
    "dep:futures-core",
    "dep:tokio",
    "tokio/io-std",
    "tokio/io-util",
    "tokio/process",
    "tokio/rt",
    "tokio/sync",
    "tokio/time",
    "tokio/macros",
]

[dependencies]
# The registry publishes a new immutable snapshot by swapping the `Arc`. Not
# optional: the capability's requirement is that reads are lock-free, and an
# `RwLock` would make `tools/list` contend with a publish.
arc-swap = { workspace = true }
# `version` inherits from [workspace.package] like every other sibling dep. It
# used to be hardcoded at "0.1.0", which is how a workspace version bump
# becomes a build failure here instead of a version bump — the pin has to be
# the one place that changes, or it is the one place that stops you changing it.
protocol-droid-macros = { workspace = true }
protocol-droid-schema = { workspace = true }
schemars = "1.2.2"
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
# Only the `stdio` adapter needs an executor. `io-std` for the `AsyncRead`/
# `AsyncWrite` helpers and `tokio::io::Stderr`, `io-util` for the `AsyncReadExt`
# and `AsyncWriteExt` traits, and `process` for the child a client spawns.
#
# `process` does NOT need the public `signal` feature. On a Unix target tokio
# compiles its `SIGCHLD` reaper for `process` regardless, because
# `cfg_signal_internal!` in tokio's `macros/cfg.rs` is
# `any(feature = "signal", all(unix, feature = "process"))` — so enabling
# `signal` would add the public signal *API* and buy nothing, and a client that
# never registers a signal handler has no use for it. `rt` and `sync` come from
# the workspace declaration above and are not repeated.
tokio = { workspace = true, optional = true, features = [
    "io-std",
    "io-util",
    "process",
] }
# T14. The Streamable HTTP server. `http` turns these on; nothing else does,
# and a `--no-default-features` build resolves without them at all.
axum = { workspace = true, optional = true }
# T17. `limit` for the concurrency ceiling and `timeout` for the request
# deadline. Both are service wrappers rather than logic — re-implementing a
# semaphore-guarded service or a `tokio::time::timeout` wrapper by hand is the
# kind of "obviously simple" that is subtly wrong about cancellation, and
# `tower` is already in the workspace so this adds no new resolution.
tower = { workspace = true, optional = true, features = ["limit", "timeout", "util"] }
futures-core = { workspace = true, optional = true }
futures-util = { workspace = true, optional = true }
# T14. The Streamable HTTP client, gated by the same `http` feature as the
# server above: a consumer that does not run an HTTP server has no use for an
# HTTP client, and the two are alternatives rather than a set.
reqwest = { workspace = true, optional = true }
# T16. JWS signature verification for the JWKS token source. See the workspace
# declaration for why `use_pem` and `aws_lc_rs` are both off.
jsonwebtoken = { workspace = true, optional = true }
# T16. The OS CSPRNG behind `RandomIds`, so a server can mint session ids that
# do not enumerate. `getrandom` rather than `rand`: this is one `u64` per
# session opened, not a stream of draws, and it wants no seeding, no thread-local
# state and no `Default`.
getrandom = { workspace = true, optional = true }
# T16 stage 7. PKCE. `sha2` for the S256 derivation and `base64` for its
# base64url encoding — both are pulled by `jsonwebtoken` already, and both are
# declared here because the client half needs them directly rather than through
# somebody else's re-export. The `base64` line repeats the dev-dependency below
# rather than hoisting it to `[workspace.dependencies]`: one crate uses it, and
# a workspace table entry would invite a second one.
sha2 = { workspace = true }
# SEP-2322. A server hands the client an opaque `requestState` and must be able
# to tell a tampered one from its own: HMAC-SHA256 under a per-server key. Not
# feature-gated, because the multi round-trip pattern is core protocol at
# `2026-07-28` rather than an integration a consumer opts in to.
hmac = { workspace = true }
base64 = { version = "0.22.1", default-features = false, features = [
    "alloc",
], optional = true }
# T16 stage 7. Building the authorization redirect. `url` is already in the
# graph through `reqwest`, and appending query parameters to an endpoint that
# may already carry some is the case hand-rolled percent-encoding gets wrong:
# the endpoint comes from a discovery document this crate does not control.
url = { version = "2.5.8", optional = true }

[dev-dependencies]
# The envelope path is the project's one perf-critical path, so its decode,
# route, and params-decode costs are measured rather than estimated. CI gates a
# >10% regression against the committed baseline.
divan = { workspace = true }
assert_matches = { workspace = true }
# The schema-validation property tests in `tests/acp_schema_validation.rs`
# validate the typed `InitializeRequest` shape against the vendored ACP
# v1.25.0 schema. `jsonschema` is a dev-only dep; the runtime is too
# expensive to ship in a library that already has its own typed
# validation.
jsonschema = { workspace = true }
# The golden feature profiles are snapshots a human reviews, so a matrix change
# shows up as a diff to read rather than as a failure to debug.
insta = { workspace = true }
# The lowering properties are the crate's central claim — that a value which
# fits survives a round trip, and that the gate is exact — so they are generated
# rather than hand-picked. A hand-picked value proves one case was thought of.
proptest = { workspace = true }
# `StreamExt::next` over a response body. Dev-only: nothing in the adapter
# consumes a `Stream` through these helpers, but a test reading the replayed
# frames out of an open SSE response has no other way to stop before the
# stream parks forever waiting for a push that is not coming.
futures-util = { version = "0.3.31", default-features = false, features = [
    "std",
] }
# The capability table is checked against the vendored `schema.json` files, so
# the domain's model is verified against the bytes it describes rather than
# against a second copy of the same assumptions.
serde_json = { workspace = true }
# The cancellation race test needs a real scheduler: a single-poll harness can
# prove the waker fires, but not that a handler awaiting the signal is resumed
# and then suppressed from answering. `time::pause` makes the deadline part of
# that test instantaneous and deterministic.
tokio = { workspace = true, features = ["rt", "time"] }
trybuild = "1.0.121"
# RFC 7515 base64url, for the one JWT no encoder will produce: an unsecured
# `alg: none` token. `jsonwebtoken` depends on it too, so this is a dev-only
# path to a crate already in the graph rather than a new one.
base64 = { version = "0.22.1", default-features = false, features = ["alloc"] }
# Drives a `Router` to a response without binding a socket, which is what lets
# the security-header test read headers off a real response rather than
# asserting that a middleware value is `Some`.
tower = { workspace = true, features = ["util"] }

[lints]
workspace = true

# divan is its own harness; libtest would only add a wrapper around a binary
# that is meant to be run directly.
[[bench]]
name = "jsonrpc_hot_path"
harness = false

[[bench]]
name = "registry"
harness = false

[[bench]]
name = "a2a_serving"
harness = false
# Same reason as the `examples/` below: this bench imports
# `adapters::secondary::a2a`, which is gated on `a2a`. Without this the default
# build — `cargo check --all-targets` with no features — fails to compile it.
required-features = ["a2a"]

# Cargo builds every file in `examples/` and `benches/` for every feature
# configuration, so one that speaks a gated transport must declare what it
# needs or `cargo check --all-targets` with no features at all — the plain
# default build — fails to compile it. The target is skipped rather than built
# lean; a stripped-down stdio server would demonstrate nothing, and a bench
# measuring the A2A table without the A2A table would measure nothing.
[[example]]
name = "stdio_server"
required-features = ["stdio"]

[[example]]
name = "mcp_client"
required-features = ["stdio"]

[[example]]
name = "server"
required-features = ["http"]

[[example]]
name = "http_server"
required-features = ["http"]

[[example]]
name = "http_client"
required-features = ["http"]

[[example]]
name = "conformance_reference_server"
required-features = ["http", "random-ids"]

[[example]]
name = "conformance_reference_client"
required-features = ["http", "oauth", "random-ids"]

# The single source for the resource-server assembly in the security guide.
# `oauth` for the authenticator, `http` for the router and `ServerConfig`.
[[example]]
name = "oauth_resource_server"
required-features = ["http", "oauth"]

[[example]]
name = "a2a_client"
required-features = ["a2a", "http"]

[[example]]
name = "a2a_minimal_server"
required-features = ["a2a", "http"]

[[example]]
name = "a2a_server"
required-features = ["a2a", "http"]

[[example]]
name = "a2a_typed_client"
required-features = ["a2a", "http"]

# The RFC 9728 discovery document. `http` only — the document is served over
# any axum app and needs neither an OAuth token source nor the A2A module.
[[example]]
name = "protected_resource_server"
required-features = ["http"]