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}