Skip to main content

agentd/mcp/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// The MCP client now lives in the reusable `mcp` crate (`mcp::client`); re-export
3// so `crate::mcp::client::{McpClient, McpError}` keeps resolving. `from_spec`
4// below is the agentd integration (config + auth + identity) that stays here.
5pub use ::mcp::client;
6// Re-export the transport module so the agentd integration (and its tests) can
7// name the `RequestSigner` seam that credential providers (RFC 0031) plug into.
8pub use ::mcp::http;
9
10/// Build an MCP client from a declared [`crate::config::McpServerSpec`]: resolve
11/// its secret-free `{{secret:…}}` auth header templates (via [`auth`]) and connect
12/// to the spec's remote `endpoint`, stamping agentd's client identity. The
13/// config/auth-coupled counterpart of the crate's transport-only
14/// [`client::McpClient::connect`]. Call `initialize` on the result before use.
15pub fn from_spec(
16    spec: &crate::config::McpServerSpec,
17    timeout: std::time::Duration,
18) -> Result<client::McpClient, client::McpError> {
19    use client::{McpClient, McpError};
20    if spec.endpoint.trim().is_empty() {
21        return Err(McpError::Transport(format!(
22            "mcp server '{}' has no endpoint",
23            spec.name
24        )));
25    }
26    let headers = auth::resolve_headers(&spec.headers).map_err(McpError::Transport)?;
27    // AAuth (RFC 0023): sign requests to this server with the agent identity.
28    // Per-server opt-in — `spec.aauth == Some(false)` opts out; otherwise the
29    // global default is "sign all when an identity is configured". The signing
30    // path is absent entirely without `--features aauth`.
31    #[cfg(feature = "aauth")]
32    let aauth_signer = if spec.aauth == Some(false) {
33        None
34    } else {
35        let s = crate::aauth::signer();
36        // Learn the server's discovery metadata (content-digest requirement)
37        // once at connect (best-effort). Only when we will actually sign it.
38        if s.is_some()
39            && let Some(client) = crate::aauth::installed()
40        {
41            let authority = ::mcp::http::authority_of(&spec.endpoint);
42            client.discover(&authority, &spec.endpoint);
43        }
44        s
45    };
46    #[cfg(not(feature = "aauth"))]
47    let aauth_signer: Option<std::sync::Arc<dyn ::mcp::http::RequestSigner>> = None;
48
49    // Credential precedence (RFC 0031 §5): the unified `auth:` block wins (static
50    // / oauth2 device-login / client-credentials), then the legacy `oauth:`
51    // client-credentials shortcut, then per-server AAuth signing. An endpoint uses
52    // one mechanism. The `auth:`/`oauth:` paths are absent without `--features oauth`.
53    #[cfg(feature = "oauth")]
54    let signer: Option<std::sync::Arc<dyn ::mcp::http::RequestSigner>> = if let Some(a) = &spec.auth
55    {
56        crate::auth::device::signer_for(a, &format!("mcp:{}", spec.name), timeout)
57            .map_err(McpError::Transport)?
58    } else if let Some(o) = &spec.oauth {
59        Some(
60            std::sync::Arc::new(oauth::OAuthBearerSigner::new(o.clone(), timeout))
61                as std::sync::Arc<dyn ::mcp::http::RequestSigner>,
62        )
63    } else {
64        aauth_signer
65    };
66    #[cfg(not(feature = "oauth"))]
67    let signer = aauth_signer;
68    let client = McpClient::connect_signed(&spec.name, &spec.endpoint, headers, timeout, signer)?
69        .with_client_info(::mcp::wire::Implementation {
70            name: "agentd".into(),
71            version: crate::VERSION.into(),
72            title: None,
73        });
74    // SPIFFE X.509-SVID mTLS (RFC 0031 §9): set the transport client identity from
75    // the SPIRE-written cert + key when the server declares `auth: {kind: spiffe,
76    // svid: x509}`. Needs `--features tls` (mTLS); a JWT-SVID rides the signer seam.
77    #[cfg(feature = "tls")]
78    let client = match spiffe_x509_identity(spec)? {
79        Some(id) => client.with_identity(id),
80        None => client,
81    };
82    Ok(client)
83}
84
85/// Build a mutual-TLS client identity from a `kind: spiffe, svid: x509` auth
86/// block (the SPIRE-written cert + key files). `None` for any other auth.
87#[cfg(feature = "tls")]
88fn spiffe_x509_identity(
89    spec: &crate::config::McpServerSpec,
90) -> Result<Option<crate::net::tls::ClientIdentity>, client::McpError> {
91    use client::McpError;
92    let Some(a) = &spec.auth else {
93        return Ok(None);
94    };
95    if a.kind != "spiffe" || a.svid.as_deref() != Some("x509") {
96        return Ok(None);
97    }
98    let cert_path = a
99        .svid_file
100        .as_deref()
101        .ok_or_else(|| McpError::Transport("spiffe x509: svid_file is required".into()))?;
102    let key_path = a
103        .key_file
104        .as_deref()
105        .ok_or_else(|| McpError::Transport("spiffe x509: key_file is required".into()))?;
106    let cert = std::fs::read(cert_path)
107        .map_err(|e| McpError::Transport(format!("spiffe svid_file: {e}")))?;
108    let key = std::fs::read(key_path)
109        .map_err(|e| McpError::Transport(format!("spiffe key_file: {e}")))?;
110    crate::net::tls::ClientIdentity::from_pem(&cert, &key)
111        .map(Some)
112        .map_err(|e| McpError::Transport(format!("spiffe svid: {e}")))
113}
114
115// The Streamable HTTP client transport (RFC 0004) now lives in the reusable `mcp`
116// crate as `mcp::http`; `client` uses it directly (`::mcp::http`).
117// Auth material resolution for remote MCP endpoints (RFC 0012 §3.7): materialize
118// secret-free `{{secret:…}}` header templates into wire headers at connect time.
119pub mod auth;
120pub mod elicit;
121// OAuth 2.1 client-credentials (M2M) token source for endpoints behind an OAuth
122// gateway (RFC 0006 §auth). Feature-gated; dependency-free.
123#[cfg(feature = "oauth")]
124pub mod oauth;
125// Built-in Streamable HTTP mock MCP server (the hidden `--internal-mock-mcp-http`
126// mode, v2.0.0) for the test + conformance suites: serves a one-resource reactive
127// MCP over a unix socket, so the harness drives agentd's HTTP transport end to end.
128// In debug it's always present (so `cargo test` works with no flag); in release it
129// ships only under `internal-mocks`, so the production binary carries no test
130// scaffolding.
131#[cfg(any(feature = "internal-mocks", debug_assertions))]
132pub mod mock_http;
133
134// A2A client-side wire helpers (`TaskState` + request/response shaping) shared
135// with `a2a_client`. The v1 self-MCP server + v1 A2A server surfaces were removed
136// with the mode cut-over; the v2 A2A server is `runtime::a2a_server`.
137#[cfg(feature = "a2a")]
138// agentd-as-A2A-client: the remote-A2A-agent delegation backend (RFC 0020 §3).
139// Connects to a declared peer over HTTP(S) + the RFC 0004
140// JSON-RPC codec, runs `a2a.SendMessage` then polls `a2a.GetTask` to a terminal
141// state, and returns the distillate. Reuses the wire types from `a2a`; no deps.
142#[cfg(feature = "a2a")]
143pub mod a2a_client;