oxicode_agent/mcp/auth.rs
1//! MCP credential provider (v2.1).
2//!
3//! Abstracts how an MCP transport obtains the credentials it needs to
4//! authenticate against a server (API key, OAuth bearer token, ...).
5//! Implementations live outside `oxicode-agent` so that the storage backend
6//! (e.g. `oxicode-cli`'s auth store) can be swapped without changing the
7//! transport code.
8//!
9//! The trait is deliberately narrow — two methods, both infallible from
10//! the transport's point of view (they return `Option<Credential>` and
11//! the transport simply omits the `Authorization` header when no
12//! credential is available). It is **not** promoted to a full SDK port
13//! (see `docs/designs/2026-06-19-mcp-v2-conformance-transports.md` §D11
14//! / §5.2): MCP is an agent feature, not infrastructure, and most
15//! products are happy with the noop default plus a per-product
16//! implementation injected via [`crate::mcp::McpManager::set_credential_provider`].
17
18use async_trait::async_trait;
19
20/// A single credential materialising an MCP server's authentication.
21///
22/// The transport treats this as opaque and only consumes [`Self::access_token`]
23/// to populate the `Authorization: Bearer …` header. Refresh material
24/// (e.g. OAuth refresh token) is handled by [`McpCredentialProvider::refresh`]
25/// and is never read by the transport itself.
26#[derive(Debug, Clone)]
27pub struct Credential {
28 /// OAuth bearer token (or equivalent) sent in `Authorization: Bearer`.
29 pub access_token: String,
30}
31
32/// Source of authentication material for an MCP transport.
33///
34/// `server` is the configured server name (e.g. `"github"`) and `url`
35/// is the MCP endpoint the transport is connecting to. Providers may
36/// key storage on either or both. Implementations should be cheap to
37/// call — `access_token` is consulted on every connect, `refresh` is
38/// called at most once per request on a `401`/`403` response.
39#[async_trait]
40pub trait McpCredentialProvider: Send + Sync {
41 /// Return a credential for `server`/`url` if one is known.
42 /// Returning `None` means "no auth" — the transport connects
43 /// without an `Authorization` header.
44 async fn access_token(&self, server: &str, url: &str) -> Option<Credential>;
45
46 /// Refresh the credential for `server`/`url` and return the new
47 /// value. Called by the HTTP transport after a `401`/`403`
48 /// response. Returning `None` means refresh failed; the transport
49 /// surfaces the original error to the caller.
50 async fn refresh(&self, server: &str, url: &str) -> Option<Credential>;
51}
52
53/// Default no-op provider. Returns `None` for every lookup, which tells
54/// transports to connect without authentication. Used by
55/// [`crate::mcp::McpManager`] when no real provider has been injected.
56#[derive(Debug, Default, Clone, Copy)]
57pub struct NoopCredentialProvider;
58
59#[async_trait]
60impl McpCredentialProvider for NoopCredentialProvider {
61 async fn access_token(&self, _server: &str, _url: &str) -> Option<Credential> {
62 None
63 }
64
65 async fn refresh(&self, _server: &str, _url: &str) -> Option<Credential> {
66 None
67 }
68}