From 07a778adce5d304d3a608c18758195cd6b96d16c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 07:43:47 +0000 Subject: [PATCH 1/6] Make the CIMD switch a McpConfig field; the binary maps OAUTH_CIMD_ENABLED to it The library read OAUTH_CIMD_ENABLED itself, once per process through a OnceLock, which gave an embedding host no way to decide in code and left the setting out of the server's configuration. It is now McpConfig::cimd_enabled, handed to each AuthStore at construction. The imcp2 binary reads the variable (same on-values) and passes it to every instance, so the deploy path is unchanged; the parser and its test move there. Docs say which switch applies where. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xd7VT72Qt16qiAJynu9EKj --- README.md | 4 +- docs/anthropic-directory-submission.md | 3 +- docs/openai-directory-submission.md | 2 +- src/auth.rs | 78 +++++++------------------- src/e2e_handshake.rs | 1 + src/lib.rs | 12 ++++ src/main.rs | 36 ++++++++++++ src/metrics.rs | 1 + tests/routers.rs | 1 + 9 files changed, 78 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 792eb12..ad2725a 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ async fn main() -> anyhow::Result<()> { clients: SharedClients::load(&state_dir), state_dir, require_resource: true, // strict RFC 8707 (reject a missing `resource`) + cimd_enabled: false, // advertise Client ID Metadata Documents (URL client_ids) }); server.spawn_session_reaper(); let app = axum::Router::new() @@ -716,7 +717,8 @@ its AS issuer is `/mcp` and everything OAuth lives under it: once per process (however many instances the binary mounts), four per host, so one slow host cannot hold up the rest. The fetch connects directly, never through a proxy from the environment, so the address pin always binds. Claude and ChatGPT both select CIMD over DCR when it is - advertised — which it is only where `OAUTH_CIMD_ENABLED=1` is set (the deploy + advertised — which it is only where the deployment opts in: `McpConfig::cimd_enabled` + for an embedding host, `OAUTH_CIMD_ENABLED=1` for the `imcp2` binary (the deploy template takes it from the GitHub Environment's variable of that name, so a deploy never enables it by itself; to roll back, unset it and redeploy — the value is read once at start-up, so the variable alone changes nothing — and diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index 95b6d5e..4c0f2cd 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -81,7 +81,8 @@ Claude registers a new client on each fresh connection (the registration store is a bounded LRU of 10,000, which tolerates that churn); Anthropic recommends **CIMD** (Client ID Metadata Documents) for high-traffic directory listings, and the server implements it (PR #143's trust-policy-gated design) and -advertises it where `OAUTH_CIMD_ENABLED=1` is set, so there Claude selects CIMD +advertises it where the deployment opts in (`McpConfig::cimd_enabled`; +`OAUTH_CIMD_ENABLED=1` for the `imcp2` binary), so there Claude selects CIMD and registers nothing. ## Blockers to resolve before submitting diff --git a/docs/openai-directory-submission.md b/docs/openai-directory-submission.md index 5e4393c..411a0a1 100644 --- a/docs/openai-directory-submission.md +++ b/docs/openai-directory-submission.md @@ -64,7 +64,7 @@ add details not published in the docs. | Requirement | Status | |---|---| | OAuth 2.1 authorization-code + PKCE **S256**, per the MCP authorization spec | ✅ live; `code_challenge_methods_supported: ["S256"]` | -| Client registration: CIMD preferred; DCR (`registration_endpoint`) and predefined clients also accepted | ✅ both. CIMD implemented (trust-policy-gated per PR #143), advertised as `client_id_metadata_document_supported: true` where the deployment sets `OAUTH_CIMD_ENABLED=1` (off by default) — ChatGPT's live document (`https://chatgpt.com/oauth/client.json`) is a fixture of the parsing test ([`src/auth.rs`](../src/auth.rs)); it prefers `private_key_jwt` but lists `none`, which is what it uses against this AS — and RFC 7591 DCR live and verified | +| Client registration: CIMD preferred; DCR (`registration_endpoint`) and predefined clients also accepted | ✅ both. CIMD implemented (trust-policy-gated per PR #143), advertised as `client_id_metadata_document_supported: true` where the deployment opts in (`McpConfig::cimd_enabled`; `OAUTH_CIMD_ENABLED=1` for the `imcp2` binary; off by default) — ChatGPT's live document (`https://chatgpt.com/oauth/client.json`) is a fixture of the parsing test ([`src/auth.rs`](../src/auth.rs)); it prefers `private_key_jwt` but lists `none`, which is what it uses against this AS — and RFC 7591 DCR live and verified | | Discovery documents (RFC 8414 AS metadata + RFC 9728 protected-resource) | ✅ all live, path-scoped + root fallback | | Both of ChatGPT's callbacks accepted — `https://chatgpt.com/connector_platform_oauth_redirect` (the form it sends us) and `https://chatgpt.com/connector/oauth/{callback_id}` | ✅ the redirect allow-list pins both paths for `chatgpt.com` ([`src/auth.rs`](../src/auth.rs), `DEFAULT_ALLOWED_REDIRECTS`) | | No machine-to-machine grants (client credentials etc. unsupported by ChatGPT) | ✅ user-consent authorization-code flow only | diff --git a/src/auth.rs b/src/auth.rs index 817798e..8808f81 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -785,17 +785,15 @@ fn loopback_match(registered: &str, requested: &str) -> bool { // ones; were one added, the only attested fact is the HOST of the `client_id` // URL — never the self-asserted `client_name` or `logo_uri`. // -// OPT-IN per deployment: `OAUTH_CIMD_ENABLED=1` advertises the mechanism and -// accepts URL `client_id`s; unset, the metadata does not advertise it and a URL +// OPT-IN per deployment ([`crate::McpConfig::cimd_enabled`]): on, the metadata +// advertises the mechanism and URL `client_id`s are accepted; off, a URL // `client_id` is an unknown client. Claude and ChatGPT both switch to CIMD the // moment an AS advertises it, so a routine deploy must never switch them over -// by itself (the deploy template takes the variable from the GitHub -// Environment). The rollback, should a vendor's document turn out to be shaped -// in a way this implementation refuses, is to unset the variable AND redeploy: -// it is rendered into the unit at deploy time and read here once at start-up -// ([`cimd_enabled_by_env`]), so changing it alone changes nothing on the host. -// Once the process restarts without it, the clients re-read the metadata within -// minutes and fall back to DCR. +// by itself: an embedding host sets the field, and the `imcp2` binary takes it +// from `OAUTH_CIMD_ENABLED`, which the deploy template renders from the GitHub +// Environment's variable. The rollback, should a vendor's document turn out to +// be shaped in a way this implementation refuses, is to switch it off and +// restart; the clients re-read the metadata within minutes and fall back to DCR. /// Byte cap on a `client_id` URL before it is treated as CIMD at all: the URL /// becomes a key of the process-wide cache and single-flight map (and part of a @@ -844,29 +842,6 @@ const CIMD_CACHE_MAX_TTL: Duration = Duration::from_secs(24 * 60 * 60); /// out a real client; nor is a transient (a deadline, a 5xx), which is retried. const CIMD_NEGATIVE_TTL: Duration = Duration::from_secs(60); -/// Whether this process is deployed with CIMD on: `OAUTH_CIMD_ENABLED` set to -/// an on-value ([`cimd_enabled_by`]). Read once (the env is process-static), -/// like the allow-list's `OAUTH_ALLOWED_REDIRECT_PREFIXES`; each [`AuthStore`] -/// takes its own copy at construction, which tests set directly. -fn cimd_enabled_by_env() -> bool { - static ENABLED: std::sync::OnceLock = std::sync::OnceLock::new(); - *ENABLED.get_or_init(|| { - let enabled = cimd_enabled_by(std::env::var("OAUTH_CIMD_ENABLED").ok().as_deref()); - if enabled { - tracing::info!("OAUTH_CIMD_ENABLED is set: Client ID Metadata Documents are on"); - } - enabled - }) -} - -/// The opt-in's reading of `OAUTH_CIMD_ENABLED`: `1`, `true`, `yes` and `on` -/// (any case) switch CIMD on; unset, empty, or anything else leaves it off. -fn cimd_enabled_by(value: Option<&str>) -> bool { - value.is_some_and(|v| { - matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on") - }) -} - /// A validated Client ID Metadata Document: what this server needs from it. #[derive(Clone, Debug, PartialEq, Eq)] struct ClientMetadata { @@ -1476,8 +1451,8 @@ pub struct AuthStore { /// bounds — the PROCESS's ([`CimdState::shared`]), so every mounted instance /// draws on the same bounds; a test may give a store its own. cimd: Arc, - /// Whether URL `client_id`s are accepted and CIMD advertised: the process's - /// `OAUTH_CIMD_ENABLED` ([`cimd_enabled_by_env`]), or what a test set. + /// Whether URL `client_id`s are accepted and CIMD advertised + /// ([`crate::McpConfig::cimd_enabled`]). cimd_enabled: bool, } @@ -1587,6 +1562,7 @@ impl AuthStore { public_url: String, mcp_path: String, require_resource: bool, + cimd_enabled: bool, ) -> Self { Self { clients: clients.0, @@ -1598,12 +1574,12 @@ impl AuthStore { mcp_path, require_resource, cimd: CimdState::shared(), - cimd_enabled: cimd_enabled_by_env(), + cimd_enabled, } } - /// This store with CIMD switched on or off, whatever the environment says, - /// and with CIMD state of its own, so tests do not see each other's flights. + /// This store with CIMD switched on or off, and with CIMD state of its own, + /// so tests do not see each other's flights. #[cfg(test)] fn with_cimd(mut self, enabled: bool) -> Self { self.cimd_enabled = enabled; @@ -3015,9 +2991,8 @@ pub async fn authorization_server_metadata(State(store): State) -> Re // identify itself with the https URL of its Client ID Metadata Document // instead of registering (see `cimd_client_id`). Claude and ChatGPT both // select CIMD over DCR when this is advertised alongside `none` above — - // which is why it is advertised only where the deployment opts in with - // `OAUTH_CIMD_ENABLED`, and a redeploy without that withdraws it (no - // rebuild; the value is read once at start-up). + // which is why it is advertised only where the deployment opts in + // (`McpConfig::cimd_enabled`), and switching that off withdraws it. "client_id_metadata_document_supported": store.cimd_enabled, // RFC 9207: we emit `iss` on every authorization response, so we MUST // advertise it here (a client that sees this flag rejects any response @@ -3628,19 +3603,6 @@ mod tests { assert!(invalid(FetchError::NotUtf8("bytes".into()))); } - /// The deploy-time opt-in's reading of its variable: off unless it says on. - #[test] - fn cimd_opt_in_values() { - use super::cimd_enabled_by; - let off = [None, Some(""), Some(" "), Some("0"), Some("false"), Some("no"), Some("off")]; - for value in off.into_iter().chain([Some("enabled"), Some("2")]) { - assert!(!cimd_enabled_by(value), "{value:?} should leave CIMD off"); - } - for value in [Some("1"), Some("true"), Some("Yes"), Some("ON"), Some(" 1 ")] { - assert!(cimd_enabled_by(value), "{value:?} should turn CIMD on"); - } - } - /// PR #143's trust policy: a document is fetched only from a vetted vendor /// origin — a host on or under an allow-listed domain, on the default port. /// Any other URL `client_id` is refused before any fetch and told where to @@ -4365,13 +4327,13 @@ mod tests { } fn test_store_cfg(require_resource: bool) -> super::AuthStore { - // As deployed with `OAUTH_CIMD_ENABLED=1`, and with CIMD state of its own so - // tests do not see each other's flights; the off case sets this itself. + // As a deployment with CIMD on, and with CIMD state of its own so tests do + // not see each other's flights; the off case sets this itself. new_store(require_resource).with_cimd(true) } - /// A store exactly as [`super::AuthStore::new`] builds it: CIMD per the - /// environment (off under `cargo test`), CIMD state shared process-wide. + /// A store exactly as [`super::AuthStore::new`] builds it with CIMD off, CIMD + /// state shared process-wide. fn new_store(require_resource: bool) -> super::AuthStore { use candid::Principal; use imcp2_core::identities::{Identities, IiInstance}; @@ -4395,6 +4357,7 @@ mod tests { "https://mcp.test".into(), "/mcp".into(), require_resource, + false, ) } @@ -4639,6 +4602,7 @@ mod tests { "https://mcp.test".into(), mcp_path.into(), false, + false, ) }; // Both instances, so the allow-list covers each mount. diff --git a/src/e2e_handshake.rs b/src/e2e_handshake.rs index 846981d..d90113c 100644 --- a/src/e2e_handshake.rs +++ b/src/e2e_handshake.rs @@ -340,6 +340,7 @@ async fn registration_delegation_end_to_end() { state_dir: state_dir.clone(), // The handshake under test carries no `resource`; keep it lenient. require_resource: false, + cimd_enabled: false, }); let app = axum::Router::new() .nest_service(server.mcp_path(), server.mcp_router()) diff --git a/src/lib.rs b/src/lib.rs index 901a600..033fb2f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,6 +42,7 @@ //! clients: SharedClients::load(&state_dir), //! state_dir, //! require_resource: true, // strict RFC 8707 (reject a missing `resource`) +//! cimd_enabled: false, // advertise Client ID Metadata Documents (URL client_ids) //! }); //! server.spawn_session_reaper(); //! let app = axum::Router::new() @@ -149,6 +150,14 @@ pub struct McpConfig { /// of turning away any client predating RFC 8707. When `false`, a missing /// `resource` is tolerated (a present one must still match). pub require_resource: bool, + /// Client ID Metadata Documents: when `true`, the AS metadata advertises + /// `client_id_metadata_document_supported` and a URL `client_id` on a vetted + /// vendor origin is accepted by fetching its document; when `false`, a URL + /// `client_id` is an unknown client. Claude and ChatGPT switch to CIMD the + /// moment it is advertised, so it is off unless the deployment opts in. Set + /// by the embedding application; the `imcp2` binary sets it from + /// `$OAUTH_CIMD_ENABLED`. + pub cimd_enabled: bool, } /// One MCP server instance: the shared state behind [`Self::mcp_router`] and @@ -187,6 +196,7 @@ impl McpServer { public_url.clone(), mcp_path.clone(), config.require_resource, + config.cimd_enabled, ); Self { agent: config.agent, @@ -618,6 +628,7 @@ mod lib_tests { clients: SharedClients::load(&dir), state_dir: dir, require_resource: true, + cimd_enabled: false, }) } @@ -644,6 +655,7 @@ mod lib_tests { clients: SharedClients::load(std::env::temp_dir().join("dir-a")), state_dir: std::env::temp_dir().join("dir-b"), require_resource: true, + cimd_enabled: false, }); } diff --git a/src/main.rs b/src/main.rs index 916fc2c..3545f61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,6 +79,21 @@ fn serve_beta() -> bool { .unwrap_or(false) } +/// Whether to advertise and accept Client ID Metadata Documents, the +/// registration mode Claude and ChatGPT prefer over DCR. Off unless +/// `$OAUTH_CIMD_ENABLED` is truthy (`1`/`true`/`yes`/`on`): both switch to CIMD +/// the moment it is advertised, so a deploy must never turn it on by itself. +/// Read once and handed to every instance; to roll back, unset it and redeploy. +fn cimd_enabled() -> bool { + cimd_enabled_by(std::env::var("OAUTH_CIMD_ENABLED").ok().as_deref()) +} + +fn cimd_enabled_by(value: Option<&str>) -> bool { + value.is_some_and(|v| { + matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on") + }) +} + /// Whether to serve the Prometheus exposition at `/metrics`. Off unless /// `$MCP_SERVE_METRICS` is truthy (`1`/`true`/`yes`/`on`). /// @@ -291,6 +306,12 @@ async fn main() -> anyhow::Result<()> { // the operational directory. let clients = SharedClients::load(&state_dir); + // Client ID Metadata Documents: one deploy-time switch for every instance. + let cimd_on = cimd_enabled(); + if cimd_on { + tracing::info!("OAUTH_CIMD_ENABLED is set: Client ID Metadata Documents are on"); + } + // Production Internet Identity at `/mcp`: always served, and the origin's // default instance (it answers the plain-root discovery probes). A // self-contained McpServer whose sessions/tokens never cross instances. @@ -302,6 +323,7 @@ async fn main() -> anyhow::Result<()> { clients: clients.clone(), state_dir: state_dir.clone(), require_resource: require_resource(), + cimd_enabled: cimd_on, }); prod.spawn_session_reaper(); @@ -316,6 +338,7 @@ async fn main() -> anyhow::Result<()> { clients, state_dir, require_resource: require_resource(), + cimd_enabled: cimd_on, }); beta.spawn_session_reaper(); Some(beta) @@ -543,6 +566,19 @@ mod tests { landing_redirects_router, metrics_router, openai_apps_challenge_router, serve_metrics, site_metadata_router, }; + + /// `OAUTH_CIMD_ENABLED`'s reading: off unless it says on. + #[test] + fn cimd_opt_in_values() { + use super::cimd_enabled_by; + let off = [None, Some(""), Some(" "), Some("0"), Some("false"), Some("no"), Some("off")]; + for value in off.into_iter().chain([Some("enabled"), Some("2")]) { + assert!(!cimd_enabled_by(value), "{value:?} should leave CIMD off"); + } + for value in [Some("1"), Some("true"), Some("Yes"), Some("ON"), Some(" 1 ")] { + assert!(cimd_enabled_by(value), "{value:?} should turn CIMD on"); + } + } use axum::http::{Request, StatusCode}; use http_body_util::BodyExt; use tower::ServiceExt; diff --git a/src/metrics.rs b/src/metrics.rs index 782a7b0..1980217 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -329,6 +329,7 @@ mod tests { clients: crate::SharedClients::load(std::env::temp_dir()), state_dir: std::env::temp_dir(), require_resource: true, + cimd_enabled: false, }) } diff --git a/tests/routers.rs b/tests/routers.rs index a5af63c..d6236cc 100644 --- a/tests/routers.rs +++ b/tests/routers.rs @@ -44,6 +44,7 @@ fn server(instance: imcp2::IiInstance, mcp_path: &str) -> imcp2::McpServer { // Lenient: these router contract tests drive flows that don't carry a // `resource`; strict RFC 8707 is covered by the auth unit tests. require_resource: false, + cimd_enabled: false, }) } From e17a25c904e2d3d1f2a45d192d90cc4e75919024 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 07:44:16 +0000 Subject: [PATCH 2/6] Place the moved test after the module's imports Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xd7VT72Qt16qiAJynu9EKj --- src/main.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3545f61..0a7709a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -566,22 +566,22 @@ mod tests { landing_redirects_router, metrics_router, openai_apps_challenge_router, serve_metrics, site_metadata_router, }; - - /// `OAUTH_CIMD_ENABLED`'s reading: off unless it says on. - #[test] - fn cimd_opt_in_values() { - use super::cimd_enabled_by; - let off = [None, Some(""), Some(" "), Some("0"), Some("false"), Some("no"), Some("off")]; - for value in off.into_iter().chain([Some("enabled"), Some("2")]) { - assert!(!cimd_enabled_by(value), "{value:?} should leave CIMD off"); - } - for value in [Some("1"), Some("true"), Some("Yes"), Some("ON"), Some(" 1 ")] { - assert!(cimd_enabled_by(value), "{value:?} should turn CIMD on"); - } +} +use axum::http::{Request, StatusCode}; +use http_body_util::BodyExt; +use tower::ServiceExt; + +/// `OAUTH_CIMD_ENABLED`'s reading: off unless it says on. +#[test] +fn cimd_opt_in_values() { + use super::cimd_enabled_by; + let off = [None, Some(""), Some(" "), Some("0"), Some("false"), Some("no"), Some("off")]; + for value in off.into_iter().chain([Some("enabled"), Some("2")]) { + assert!(!cimd_enabled_by(value), "{value:?} should leave CIMD off"); + } + for value in [Some("1"), Some("true"), Some("Yes"), Some("ON"), Some(" 1 ")] { + assert!(cimd_enabled_by(value), "{value:?} should turn CIMD on"); } - use axum::http::{Request, StatusCode}; - use http_body_util::BodyExt; - use tower::ServiceExt; /// The exposition must be **off** unless asked for, and the ask must be /// explicit. This is the security-relevant half of the gate: the native host From 93431e8edc340a8d604d23a4589a8214278a7fd3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 07:45:36 +0000 Subject: [PATCH 3/6] Restore the test module the previous commit split in two The move of the parser's test cut the function one brace short, so the module closed early and the remaining tests landed at the top level; the commit went out on a chain that did not gate on the test run. Redone from the passing tree, and checked. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xd7VT72Qt16qiAJynu9EKj --- src/main.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/main.rs b/src/main.rs index 0a7709a..e7d8f38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -566,21 +566,22 @@ mod tests { landing_redirects_router, metrics_router, openai_apps_challenge_router, serve_metrics, site_metadata_router, }; -} -use axum::http::{Request, StatusCode}; -use http_body_util::BodyExt; -use tower::ServiceExt; - -/// `OAUTH_CIMD_ENABLED`'s reading: off unless it says on. -#[test] -fn cimd_opt_in_values() { - use super::cimd_enabled_by; - let off = [None, Some(""), Some(" "), Some("0"), Some("false"), Some("no"), Some("off")]; - for value in off.into_iter().chain([Some("enabled"), Some("2")]) { - assert!(!cimd_enabled_by(value), "{value:?} should leave CIMD off"); - } - for value in [Some("1"), Some("true"), Some("Yes"), Some("ON"), Some(" 1 ")] { - assert!(cimd_enabled_by(value), "{value:?} should turn CIMD on"); + + use axum::http::{Request, StatusCode}; + use http_body_util::BodyExt; + use tower::ServiceExt; + + /// `OAUTH_CIMD_ENABLED`'s reading: off unless it says on. + #[test] + fn cimd_opt_in_values() { + use super::cimd_enabled_by; + let off = [None, Some(""), Some(" "), Some("0"), Some("false"), Some("no"), Some("off")]; + for value in off.into_iter().chain([Some("enabled"), Some("2")]) { + assert!(!cimd_enabled_by(value), "{value:?} should leave CIMD off"); + } + for value in [Some("1"), Some("true"), Some("Yes"), Some("ON"), Some(" 1 ")] { + assert!(cimd_enabled_by(value), "{value:?} should turn CIMD on"); + } } /// The exposition must be **off** unless asked for, and the ask must be From 7655c0b3bb2752fa991f0c2009a0c817408a5688 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 24 Sep 2026 08:40:11 +0000 Subject: [PATCH 4/6] Say in the examples that false leaves CIMD off The inline comment on the example's cimd_enabled: false read as if false advertised the mechanism. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xd7VT72Qt16qiAJynu9EKj --- README.md | 2 +- src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ad2725a..b92543c 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ async fn main() -> anyhow::Result<()> { clients: SharedClients::load(&state_dir), state_dir, require_resource: true, // strict RFC 8707 (reject a missing `resource`) - cimd_enabled: false, // advertise Client ID Metadata Documents (URL client_ids) + cimd_enabled: false, // Client ID Metadata Documents (URL client_ids) off; true opts in }); server.spawn_session_reaper(); let app = axum::Router::new() diff --git a/src/lib.rs b/src/lib.rs index 033fb2f..7036542 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ //! clients: SharedClients::load(&state_dir), //! state_dir, //! require_resource: true, // strict RFC 8707 (reject a missing `resource`) -//! cimd_enabled: false, // advertise Client ID Metadata Documents (URL client_ids) +//! cimd_enabled: false, // Client ID Metadata Documents (URL client_ids) off; true opts in //! }); //! server.spawn_session_reaper(); //! let app = axum::Router::new() From 751ebde9a4b9c781fa82dddcd26653b3cc7fef56 Mon Sep 17 00:00:00 2001 From: aterga Date: Thu, 24 Sep 2026 10:14:01 +0000 Subject: [PATCH 5/6] Turn CIMD on by default; OAUTH_CIMD_ENABLED is now a falsy-only kill switch Client ID Metadata Documents are the registration mode Claude and ChatGPT prefer, and staging has run them end to end, so the binary no longer waits for an opt-in: `cimd_enabled()` is true unless `$OAUTH_CIMD_ENABLED` is `0`/`false`/`no`/`off`, mirroring `OAUTH_REQUIRE_RESOURCE`. The variable is kept only as a roll-out kill switch and marked for removal (TODO in `main.rs`); the start-up log now notes the off state instead of the on one, and the parser test asserts the inverted table. `McpConfig::cimd_enabled` stays a required field for an embedding host; its doc and the crate/README examples now show `true`. The auth.rs section comment, the unit template, deploy.sh, the reusable workflow, the deploy README and the two directory-submission docs say the same, and the README's list of the binary's variables gains the switch. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xd7VT72Qt16qiAJynu9EKj --- .github/workflows/deploy-native.yml | 9 ++++--- README.md | 18 +++++++------ deploy/native/README.md | 8 +++--- deploy/native/deploy.sh | 6 ++--- deploy/native/imcp2.service | 14 +++++------ docs/anthropic-directory-submission.md | 7 +++--- docs/openai-directory-submission.md | 2 +- src/auth.rs | 16 ++++++------ src/lib.rs | 10 ++++---- src/main.rs | 35 ++++++++++++++------------ 10 files changed, 66 insertions(+), 59 deletions(-) diff --git a/.github/workflows/deploy-native.yml b/.github/workflows/deploy-native.yml index 06980db..c13fa0f 100644 --- a/.github/workflows/deploy-native.yml +++ b/.github/workflows/deploy-native.yml @@ -249,10 +249,11 @@ jobs: # Unset -> endpoint 404s. An optional secret that neither caller # passes arrives here as the empty string. OPENAI_APPS_CHALLENGE_TOKEN: ${{ secrets.OPENAI_APPS_CHALLENGE_TOKEN }} - # Client ID Metadata Documents: advertised only where this GitHub - # Environment defines the OAUTH_CIMD_ENABLED variable as `1` (this job - # runs in that environment, so its variables resolve here). Unset - # arrives as the empty string, which the server reads as off. + # Client ID Metadata Documents: on, unless this GitHub Environment + # defines the OAUTH_CIMD_ENABLED variable as `0` (this job runs in that + # environment, so its variables resolve here). Unset arrives as the + # empty string, which the server reads as on. A roll-out kill switch + # only, to be removed. OAUTH_CIMD_ENABLED: ${{ vars.OAUTH_CIMD_ENABLED }} run: deploy/native/deploy.sh diff --git a/README.md b/README.md index b92543c..525c150 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ async fn main() -> anyhow::Result<()> { clients: SharedClients::load(&state_dir), state_dir, require_resource: true, // strict RFC 8707 (reject a missing `resource`) - cimd_enabled: false, // Client ID Metadata Documents (URL client_ids) off; true opts in + cimd_enabled: true, // Client ID Metadata Documents (URL client_ids); false turns them off }); server.spawn_session_reaper(); let app = axum::Router::new() @@ -600,6 +600,10 @@ when hosting: falsey value (`0`/`false`/`no`/`off`) only if you must serve a client too old to send `resource`; that reopens the confused-deputy path for such clients, so prefer updating the client. +- **`OAUTH_CIMD_ENABLED`** — Client ID Metadata Documents, **on by default**. A + falsey value (`0`/`false`/`no`/`off`) switches them off, in which case Claude + and ChatGPT register through DCR instead. A kill switch for the roll-out only, + to be removed once CIMD has run in production for a while. A `Dockerfile` is included (works on Render / Fly / Cloud Run / Koyeb). The reference deployment (`deploy/native/`, see its README) instead runs the binary @@ -717,12 +721,12 @@ its AS issuer is `/mcp` and everything OAuth lives under it: once per process (however many instances the binary mounts), four per host, so one slow host cannot hold up the rest. The fetch connects directly, never through a proxy from the environment, so the address pin always binds. Claude and ChatGPT both select CIMD over DCR when it is - advertised — which it is only where the deployment opts in: `McpConfig::cimd_enabled` - for an embedding host, `OAUTH_CIMD_ENABLED=1` for the `imcp2` binary (the deploy - template takes it from the GitHub Environment's variable of that name, so a - deploy never enables it by itself; to roll back, unset it and redeploy — the - value is read once at start-up, so the variable alone changes nothing — and - clients re-read the metadata within minutes and fall back to DCR). Only a + advertised — which it is by default: `McpConfig::cimd_enabled` for an embedding + host, and the `imcp2` binary has it on unless `OAUTH_CIMD_ENABLED` is falsey + (a roll-out kill switch, to be removed; the deploy template takes it from the + GitHub Environment's variable of that name and the value is read once at + start-up, so switching means setting it and redeploying — clients then re-read + the metadata within minutes and fall back to DCR). Only a document on a vetted vendor origin is fetched at all — a host on or under an allow-listed domain, default port (the trust policy of PR #143); any other URL `client_id` is refused before any request and pointed at the allow-listing diff --git a/deploy/native/README.md b/deploy/native/README.md index bd2c7b1..3087309 100644 --- a/deploy/native/README.md +++ b/deploy/native/README.md @@ -313,13 +313,13 @@ addresses, since the ship job runs inside the VPN. | `DEPLOY_KNOWN_HOSTS` | `PROD_DEPLOY_KNOWN_HOSTS` | *(optional)* output of `ssh-keyscan `; pin it to avoid trust-on-first-use | | `OPENAI_APPS_CHALLENGE_TOKEN` | *(same name)* | *(optional)* OpenAI Apps domain-verification token, served at `/.well-known/openai-apps-challenge` (404 while unset). Submission-specific rather than host-specific, so one repository-level secret feeds both environments | -One setting is a GitHub Environment **variable** rather than a secret (**Settings → -Environments → *staging* / *production* → Variables**); `deploy.sh` renders it into -the unit like the secrets above: +One optional setting is a GitHub Environment **variable** rather than a secret +(**Settings → Environments → *staging* / *production* → Variables**); `deploy.sh` +renders it into the unit like the secrets above: | Variable | Value | |---|---| -| `OAUTH_CIMD_ENABLED` | `1` to advertise Client ID Metadata Documents — the registration mode Claude and ChatGPT prefer over DCR — on that environment; unset or empty is off. Off by default so a routine deploy never switches the directory clients over by itself: enable it on staging first, then production. The value is rendered into the unit at deploy time and read once at start-up, so to roll back, unset (or clear) the variable **and redeploy** — `workflow_dispatch` with the same ref is enough, no rebuild; changing the variable alone changes nothing on the host | +| `OAUTH_CIMD_ENABLED` | Client ID Metadata Documents — the registration mode Claude and ChatGPT prefer over DCR — are **on** unless this is a falsey value (`0`/`false`/`no`/`off`); unset or empty is on. A roll-out kill switch, to be removed once CIMD has run in production for a while. The value is rendered into the unit at deploy time and read once at start-up, so to switch CIMD off, set the variable to `0` **and redeploy** — `workflow_dispatch` with the same ref is enough, no rebuild; changing the variable alone changes nothing on the host | > **Set these as repository-level secrets** (**Settings → Secrets and variables → > Actions**). The callers pass them into the reusable workflow, and a job that calls diff --git a/deploy/native/deploy.sh b/deploy/native/deploy.sh index 56e91cd..610f107 100755 --- a/deploy/native/deploy.sh +++ b/deploy/native/deploy.sh @@ -80,9 +80,9 @@ tar -C "$repo_root" -cf - monitoring | $SSH "tar -C $REMOTE_DIR -xf -" echo ">> rendering + installing units and Caddyfile, then (re)starting services" # MCP_SERVE_BETA is set (to "1") only for the staging deployment, so /mcp-beta # is exposed there and not in production; it defaults to empty (off) otherwise. -# OAUTH_CIMD_ENABLED ("1" to advertise Client ID Metadata Documents) comes from -# the GitHub Environment's variable of that name and defaults to empty (off), -# so enabling CIMD is a per-environment decision, never a side effect of a deploy. +# OAUTH_CIMD_ENABLED comes from the GitHub Environment's variable of that name: +# Client ID Metadata Documents are on unless it is a falsey value (0/false/no/off); +# empty (unset) is on. A roll-out kill switch only, to be removed. unit_mcp="$(sed -e "s#__PUBLIC_URL__#https://$DOMAIN#g" -e "s#__MCP_SERVE_BETA__#${MCP_SERVE_BETA:-}#g" -e "s#__OPENAI_APPS_CHALLENGE_TOKEN__#${OPENAI_APPS_CHALLENGE_TOKEN:-}#g" -e "s#__OAUTH_CIMD_ENABLED__#${OAUTH_CIMD_ENABLED:-}#g" "$here/imcp2.service")" # SERVE_STATUS likewise is set (to "1") only for the staging deployment. Staging # keeps the Caddyfile's marked /status/ block (minus the marker lines) so the diff --git a/deploy/native/imcp2.service b/deploy/native/imcp2.service index 9c838b9..754698f 100644 --- a/deploy/native/imcp2.service +++ b/deploy/native/imcp2.service @@ -32,13 +32,13 @@ Environment=MCP_SERVE_METRICS=1 # deploy inputs). Environment=OPENAI_APPS_CHALLENGE_TOKEN=__OPENAI_APPS_CHALLENGE_TOKEN__ # Client ID Metadata Documents (CIMD), the registration mode Claude and ChatGPT -# prefer over DCR: OFF unless this is `1`. deploy.sh substitutes -# __OAUTH_CIMD_ENABLED__ from the OAUTH_CIMD_ENABLED variable of the GitHub -# Environment being deployed (empty when unset), so a routine deploy never -# switches the directory clients over by itself: enable it on staging first, -# then production, each deliberately. Rendered at deploy time and read once at -# start-up, so to roll back, unset the variable AND redeploy (the same ref will -# do — no rebuild); changing the variable alone changes nothing on the host. +# prefer over DCR: ON unless this is a falsey value (0/false/no/off). deploy.sh +# substitutes __OAUTH_CIMD_ENABLED__ from the OAUTH_CIMD_ENABLED variable of the +# GitHub Environment being deployed (empty when unset, which is on). A roll-out +# kill switch only, to be removed once CIMD has run in production for a while. +# Rendered at deploy time and read once at start-up, so to switch CIMD off, set +# the variable to 0 AND redeploy (the same ref will do — no rebuild); changing +# the variable alone changes nothing on the host. Environment=OAUTH_CIMD_ENABLED=__OAUTH_CIMD_ENABLED__ # imcp2 creates its operational files (today: the dynamic-client-registration # store, so OAuth clients that cached their client_id keep working across diff --git a/docs/anthropic-directory-submission.md b/docs/anthropic-directory-submission.md index 4c0f2cd..a199422 100644 --- a/docs/anthropic-directory-submission.md +++ b/docs/anthropic-directory-submission.md @@ -59,7 +59,7 @@ submission — and match a live scan of a deployed instance of that build | HTTPS remote server, Streamable HTTP transport | ✅ `rmcp` streamable-HTTP, stateless, JSON responses ([`src/lib.rs`](../src/lib.rs)) | | OAuth 2.0, authorization-code + PKCE **S256**, advertised in metadata | ✅ `code_challenge_methods_supported: ["S256"]` in the live RFC 8414 document | | Dynamic Client Registration (RFC 7591) — the out-of-the-box `oauth_dcr` mode | ✅ live probe: `POST /mcp/oauth/register` with the claude.ai callback → `201` | -| Client ID Metadata Documents — the `oauth_cimd` mode Anthropic recommends over DCR for directory listings | ✅ implemented, trust-policy-gated per the scoping in PR #143; advertised as `client_id_metadata_document_supported: true` alongside `"none"` in `token_endpoint_auth_methods_supported` — the two flags Claude requires to select CIMD — only where the deployment sets `OAUTH_CIMD_ENABLED=1` (off by default; enable per environment). Claude Code's live document (`https://claude.ai/oauth/claude-code-client-metadata`) is a fixture of the parsing test ([`src/auth.rs`](../src/auth.rs), `cimd_client_id` / `parse_client_metadata`) | +| Client ID Metadata Documents — the `oauth_cimd` mode Anthropic recommends over DCR for directory listings | ✅ implemented, trust-policy-gated per the scoping in PR #143; advertised as `client_id_metadata_document_supported: true` alongside `"none"` in `token_endpoint_auth_methods_supported` — the two flags Claude requires to select CIMD — by default (`McpConfig::cimd_enabled`; the `imcp2` binary has it on unless `OAUTH_CIMD_ENABLED=0`). Claude Code's live document (`https://claude.ai/oauth/claude-code-client-metadata`) is a fixture of the parsing test ([`src/auth.rs`](../src/auth.rs), `cimd_client_id` / `parse_client_metadata`) | | Claude's hosted callback `https://claude.ai/api/mcp/auth_callback` accepted | ✅ seeded in the redirect allow-list ([`src/auth.rs`](../src/auth.rs), `DEFAULT_ALLOWED_REDIRECTS`) | | Claude Code loopback redirects (RFC 8252) | ✅ loopback redirects are exempt from the hosted allow-list | | Discovery documents (RFC 8414 + RFC 9728, path-scoped + root fallback) | ✅ all four live, `WWW-Authenticate` on the 401 points at the resource metadata | @@ -81,9 +81,8 @@ Claude registers a new client on each fresh connection (the registration store is a bounded LRU of 10,000, which tolerates that churn); Anthropic recommends **CIMD** (Client ID Metadata Documents) for high-traffic directory listings, and the server implements it (PR #143's trust-policy-gated design) and -advertises it where the deployment opts in (`McpConfig::cimd_enabled`; -`OAUTH_CIMD_ENABLED=1` for the `imcp2` binary), so there Claude selects CIMD -and registers nothing. +advertises it by default (`McpConfig::cimd_enabled`; the `imcp2` binary has it +on unless `OAUTH_CIMD_ENABLED=0`), so Claude selects CIMD and registers nothing. ## Blockers to resolve before submitting diff --git a/docs/openai-directory-submission.md b/docs/openai-directory-submission.md index 411a0a1..a6edebb 100644 --- a/docs/openai-directory-submission.md +++ b/docs/openai-directory-submission.md @@ -64,7 +64,7 @@ add details not published in the docs. | Requirement | Status | |---|---| | OAuth 2.1 authorization-code + PKCE **S256**, per the MCP authorization spec | ✅ live; `code_challenge_methods_supported: ["S256"]` | -| Client registration: CIMD preferred; DCR (`registration_endpoint`) and predefined clients also accepted | ✅ both. CIMD implemented (trust-policy-gated per PR #143), advertised as `client_id_metadata_document_supported: true` where the deployment opts in (`McpConfig::cimd_enabled`; `OAUTH_CIMD_ENABLED=1` for the `imcp2` binary; off by default) — ChatGPT's live document (`https://chatgpt.com/oauth/client.json`) is a fixture of the parsing test ([`src/auth.rs`](../src/auth.rs)); it prefers `private_key_jwt` but lists `none`, which is what it uses against this AS — and RFC 7591 DCR live and verified | +| Client registration: CIMD preferred; DCR (`registration_endpoint`) and predefined clients also accepted | ✅ both. CIMD implemented (trust-policy-gated per PR #143), advertised as `client_id_metadata_document_supported: true` by default (`McpConfig::cimd_enabled`; the `imcp2` binary has it on unless `OAUTH_CIMD_ENABLED=0`) — ChatGPT's live document (`https://chatgpt.com/oauth/client.json`) is a fixture of the parsing test ([`src/auth.rs`](../src/auth.rs)); it prefers `private_key_jwt` but lists `none`, which is what it uses against this AS — and RFC 7591 DCR live and verified | | Discovery documents (RFC 8414 AS metadata + RFC 9728 protected-resource) | ✅ all live, path-scoped + root fallback | | Both of ChatGPT's callbacks accepted — `https://chatgpt.com/connector_platform_oauth_redirect` (the form it sends us) and `https://chatgpt.com/connector/oauth/{callback_id}` | ✅ the redirect allow-list pins both paths for `chatgpt.com` ([`src/auth.rs`](../src/auth.rs), `DEFAULT_ALLOWED_REDIRECTS`) | | No machine-to-machine grants (client credentials etc. unsupported by ChatGPT) | ✅ user-consent authorization-code flow only | diff --git a/src/auth.rs b/src/auth.rs index 8808f81..5a033e3 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -785,15 +785,15 @@ fn loopback_match(registered: &str, requested: &str) -> bool { // ones; were one added, the only attested fact is the HOST of the `client_id` // URL — never the self-asserted `client_name` or `logo_uri`. // -// OPT-IN per deployment ([`crate::McpConfig::cimd_enabled`]): on, the metadata +// Per deployment ([`crate::McpConfig::cimd_enabled`]): on, the metadata // advertises the mechanism and URL `client_id`s are accepted; off, a URL -// `client_id` is an unknown client. Claude and ChatGPT both switch to CIMD the -// moment an AS advertises it, so a routine deploy must never switch them over -// by itself: an embedding host sets the field, and the `imcp2` binary takes it -// from `OAUTH_CIMD_ENABLED`, which the deploy template renders from the GitHub -// Environment's variable. The rollback, should a vendor's document turn out to -// be shaped in a way this implementation refuses, is to switch it off and -// restart; the clients re-read the metadata within minutes and fall back to DCR. +// `client_id` is an unknown client. Claude and ChatGPT both select CIMD the +// moment an AS advertises it, so it is normally on: an embedding host sets the +// field, and the `imcp2` binary has it on unless `OAUTH_CIMD_ENABLED`, a +// roll-out kill switch slated for removal, says off. The rollback, should a +// vendor's document turn out to be shaped in a way this implementation refuses, +// is to switch it off and restart; the clients re-read the metadata within +// minutes and fall back to DCR. /// Byte cap on a `client_id` URL before it is treated as CIMD at all: the URL /// becomes a key of the process-wide cache and single-flight map (and part of a diff --git a/src/lib.rs b/src/lib.rs index 7036542..48bb3d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ //! clients: SharedClients::load(&state_dir), //! state_dir, //! require_resource: true, // strict RFC 8707 (reject a missing `resource`) -//! cimd_enabled: false, // Client ID Metadata Documents (URL client_ids) off; true opts in +//! cimd_enabled: true, // Client ID Metadata Documents (URL client_ids); false turns them off //! }); //! server.spawn_session_reaper(); //! let app = axum::Router::new() @@ -153,10 +153,10 @@ pub struct McpConfig { /// Client ID Metadata Documents: when `true`, the AS metadata advertises /// `client_id_metadata_document_supported` and a URL `client_id` on a vetted /// vendor origin is accepted by fetching its document; when `false`, a URL - /// `client_id` is an unknown client. Claude and ChatGPT switch to CIMD the - /// moment it is advertised, so it is off unless the deployment opts in. Set - /// by the embedding application; the `imcp2` binary sets it from - /// `$OAUTH_CIMD_ENABLED`. + /// `client_id` is an unknown client. Claude and ChatGPT select CIMD the + /// moment it is advertised, so a deployment normally sets `true`. Set by the + /// embedding application; the `imcp2` binary has it on unless + /// `$OAUTH_CIMD_ENABLED` switches it off. pub cimd_enabled: bool, } diff --git a/src/main.rs b/src/main.rs index e7d8f38..d2418bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -80,17 +80,20 @@ fn serve_beta() -> bool { } /// Whether to advertise and accept Client ID Metadata Documents, the -/// registration mode Claude and ChatGPT prefer over DCR. Off unless -/// `$OAUTH_CIMD_ENABLED` is truthy (`1`/`true`/`yes`/`on`): both switch to CIMD -/// the moment it is advertised, so a deploy must never turn it on by itself. -/// Read once and handed to every instance; to roll back, unset it and redeploy. +/// registration mode Claude and ChatGPT prefer over DCR. **On by default**; a +/// falsey `$OAUTH_CIMD_ENABLED` (`0`/`false`/`no`/`off`) switches it off. Read +/// once and handed to every instance, so switching means setting it and +/// redeploying. +/// +/// TODO: remove the variable (pass `true` to every instance) once CIMD has run +/// in production for a while; it exists only as a kill switch for the roll-out. fn cimd_enabled() -> bool { cimd_enabled_by(std::env::var("OAUTH_CIMD_ENABLED").ok().as_deref()) } fn cimd_enabled_by(value: Option<&str>) -> bool { - value.is_some_and(|v| { - matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on") + !value.is_some_and(|v| { + matches!(v.trim().to_ascii_lowercase().as_str(), "0" | "false" | "no" | "off") }) } @@ -306,10 +309,10 @@ async fn main() -> anyhow::Result<()> { // the operational directory. let clients = SharedClients::load(&state_dir); - // Client ID Metadata Documents: one deploy-time switch for every instance. + // Client ID Metadata Documents: on for every instance unless the deploy says no. let cimd_on = cimd_enabled(); - if cimd_on { - tracing::info!("OAUTH_CIMD_ENABLED is set: Client ID Metadata Documents are on"); + if !cimd_on { + tracing::warn!("OAUTH_CIMD_ENABLED is off: Client ID Metadata Documents are disabled"); } // Production Internet Identity at `/mcp`: always served, and the origin's @@ -571,16 +574,16 @@ mod tests { use http_body_util::BodyExt; use tower::ServiceExt; - /// `OAUTH_CIMD_ENABLED`'s reading: off unless it says on. + /// `OAUTH_CIMD_ENABLED`'s reading: on unless it says off. #[test] - fn cimd_opt_in_values() { + fn cimd_opt_out_values() { use super::cimd_enabled_by; - let off = [None, Some(""), Some(" "), Some("0"), Some("false"), Some("no"), Some("off")]; - for value in off.into_iter().chain([Some("enabled"), Some("2")]) { - assert!(!cimd_enabled_by(value), "{value:?} should leave CIMD off"); + let on = [None, Some(""), Some(" "), Some("1"), Some("true"), Some("yes"), Some("on")]; + for value in on.into_iter().chain([Some("disabled"), Some("2")]) { + assert!(cimd_enabled_by(value), "{value:?} should leave CIMD on"); } - for value in [Some("1"), Some("true"), Some("Yes"), Some("ON"), Some(" 1 ")] { - assert!(cimd_enabled_by(value), "{value:?} should turn CIMD on"); + for value in [Some("0"), Some("false"), Some("No"), Some("OFF"), Some(" 0 ")] { + assert!(!cimd_enabled_by(value), "{value:?} should switch CIMD off"); } } From ebe902ae5e525d845474a4b8b295806e3963a5b4 Mon Sep 17 00:00:00 2001 From: aterga Date: Thu, 24 Sep 2026 10:16:47 +0000 Subject: [PATCH 6/6] Dashboard: describe CIMD as on by default The status dashboard's description of the AS-metadata check still said Client ID Metadata Documents are on only with OAUTH_CIMD_ENABLED=1; it now says on by default, off only where the variable is falsey, matching the binary. Found by Copilot's review. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Xd7VT72Qt16qiAJynu9EKj --- monitoring/mcp-status/checks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monitoring/mcp-status/checks.js b/monitoring/mcp-status/checks.js index bf39b28..d2a3054 100644 --- a/monitoring/mcp-status/checks.js +++ b/monitoring/mcp-status/checks.js @@ -415,7 +415,7 @@ export const checkMcpEndpoints = async ( id: "as-metadata", label: "OAuth Authorization Server Metadata", description: - "Verifies the RFC 8414 metadata advertising the authorize/token/registration endpoints and PKCE support that clients need to log in, and reports whether Client ID Metadata Documents are advertised (the registration mode Claude and ChatGPT prefer over DCR; on only where the server runs with OAUTH_CIMD_ENABLED=1).", + "Verifies the RFC 8414 metadata advertising the authorize/token/registration endpoints and PKCE support that clients need to log in, and reports whether Client ID Metadata Documents are advertised (the registration mode Claude and ChatGPT prefer over DCR; on by default, off only where the server runs with OAUTH_CIMD_ENABLED set to a falsey value).", target: `GET ${url}`, expected: "200 JSON with issuer + authorize/token/register endpoints", status: pass ? "pass" : "fail",