Skip to main content

soma_auth/upstream/
config.rs

1//! Minimal, self-contained upstream configuration shape.
2//!
3//! soma-auth intentionally does not depend on any gateway/runtime crate for
4//! outbound upstream OAuth. Only the fields the OAuth runtime actually reads
5//! (`name`, `url`, `oauth`) are modeled here — a full gateway config schema
6//! (tool/resource exposure allowlists, proxy flags, priority, import
7//! provenance, ...) belongs to whatever consumer wires this runtime up, not
8//! to the auth crate itself.
9
10use serde::{Deserialize, Serialize};
11
12/// Upstream MCP server identity + outbound OAuth configuration, as needed by
13/// [`crate::upstream::manager::UpstreamOauthManager`].
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15pub struct UpstreamConfig {
16    /// Human-readable name for this upstream (used as the OAuth manager/cache key).
17    pub name: String,
18    /// URL of the upstream MCP server. Required when `oauth` is set.
19    #[serde(default)]
20    pub url: Option<String>,
21    /// Outbound OAuth configuration. `None` means no OAuth manager is built
22    /// for this upstream.
23    #[serde(default)]
24    pub oauth: Option<UpstreamOauthConfig>,
25}
26
27impl UpstreamConfig {
28    /// Canonicalized `url` per RFC 3986 §6.2.2 (scheme/host lowercase,
29    /// default port stripped, dot-segment removal, percent-encoding case
30    /// normalization). Trailing slashes are preserved — they are
31    /// semantically significant in HTTP paths.
32    ///
33    /// Returns `None` when `url` is unset, `Some(Err(_))` when it is set but
34    /// not a valid URL.
35    #[must_use]
36    pub fn canonical_url(&self) -> Option<Result<String, url::ParseError>> {
37        self.url
38            .as_deref()
39            .map(|raw| url::Url::parse(raw.trim()).map(|parsed| parsed.to_string()))
40    }
41}
42
43/// Outbound OAuth configuration for a single upstream.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct UpstreamOauthConfig {
46    pub mode: UpstreamOauthMode,
47    pub registration: UpstreamOauthRegistration,
48    #[serde(default)]
49    pub scopes: Option<Vec<String>>,
50    /// When `true`, always use the Client ID Metadata Document (CIMD)
51    /// strategy regardless of whether the upstream advertises a
52    /// `registration_endpoint`. When `false`, always use dynamic
53    /// registration (RFC 7591) when the upstream advertises a
54    /// `registration_endpoint`. `None` leaves the choice to the caller
55    /// wiring up [`UpstreamOauthRegistration::Dynamic`].
56    #[serde(default)]
57    pub prefer_client_metadata_document: Option<bool>,
58}
59
60/// Outbound OAuth mode. Currently only `authorization_code_pkce` is supported.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(rename_all = "snake_case")]
63pub enum UpstreamOauthMode {
64    AuthorizationCodePkce,
65}
66
67/// Outbound OAuth client-registration strategy.
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(tag = "strategy", rename_all = "snake_case")]
70pub enum UpstreamOauthRegistration {
71    ClientMetadataDocument {
72        url: String,
73    },
74    Preregistered {
75        client_id: String,
76        #[serde(default)]
77        client_secret_env: Option<String>,
78    },
79    Dynamic,
80}