From 818bc8894349ea0dc5d6fa173ecdc4119a454972 Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Wed, 26 Aug 2026 10:46:44 +0000 Subject: [PATCH 1/7] Allow arbitrary MCP hostname --- src/routing/domain.rs | 18 ++++++++ src/routing/middleware/prerender.rs | 8 +++- src/routing/middleware/validate.rs | 26 +++++++++-- src/routing/mod.rs | 67 +++++++++++++---------------- 4 files changed, 76 insertions(+), 43 deletions(-) diff --git a/src/routing/domain.rs b/src/routing/domain.rs index 4e53dcf..abb413d 100644 --- a/src/routing/domain.rs +++ b/src/routing/domain.rs @@ -105,6 +105,24 @@ pub struct DomainLookup { pub flags: Option, } +impl DomainLookup { + pub fn new(domain: &Fqdn) -> Self { + Self { + domain: Domain { + name: domain.into(), + custom: false, + http: true, + api: true, + }, + canister_id: None, + timestamp: 0, + verify: true, + priority: 0, + flags: None, + } + } +} + /// Resolves hostname to a canister id pub trait ResolvesDomain: Send + Sync { fn resolve(&self, host: &Fqdn) -> Option; diff --git a/src/routing/middleware/prerender.rs b/src/routing/middleware/prerender.rs index 5071b27..f03b16f 100644 --- a/src/routing/middleware/prerender.rs +++ b/src/routing/middleware/prerender.rs @@ -481,7 +481,13 @@ mod tests { let svc = ServiceBuilder::new() .layer(from_fn_with_state( - ValidateState::new(Arc::new(resolver), false, false), + ValidateState::new( + Arc::new(resolver), + false, + false, + #[cfg(feature = "mcp")] + None, + ), validate::middleware, )) .layer(from_fn_with_state( diff --git a/src/routing/middleware/validate.rs b/src/routing/middleware/validate.rs index 968e7b3..c32f149 100644 --- a/src/routing/middleware/validate.rs +++ b/src/routing/middleware/validate.rs @@ -24,6 +24,8 @@ pub struct ValidateState { pub resolver: Arc, pub canister_id_from_query_params: bool, pub canister_id_from_referer: bool, + #[cfg(feature = "mcp")] + pub mcp_hostname: Option, } pub async fn middleware( @@ -43,10 +45,26 @@ pub async fn middleware( }); // Resolve the domain - let mut lookup = state - .resolver - .resolve(&authority) - .ok_or_else(|| ErrorCause::Client(ClientError::UnknownDomain(authority.clone())))?; + + // Check if it's an MCP hostname first, if configured + #[cfg(feature = "mcp")] + let lookup = if let Some(mcp_hostname) = &state.mcp_hostname + && &authority == mcp_hostname + { + use crate::routing::domain::DomainLookup; + + // Override request type + request.extensions_mut().insert(RequestType::Mcp); + + // Emit some stub lookup + Some(DomainLookup::new(mcp_hostname)) + } else { + // Otherwise, resolve the domain normally + state.resolver.resolve(&authority) + }; + + let mut lookup = + lookup.ok_or_else(|| ErrorCause::Client(ClientError::UnknownDomain(authority.clone())))?; if let Some(v) = lookup.flags { request.extensions_mut().insert(v); diff --git a/src/routing/mod.rs b/src/routing/mod.rs index 2b546e9..bfe6848 100644 --- a/src/routing/mod.rs +++ b/src/routing/mod.rs @@ -26,7 +26,6 @@ use ic_bn_lib::{ http::{ Client, ClientHttp, cache::{CacheBuilder, KeyExtractorUriRange}, - extract_authority, extract_host, middleware::{request_meta, waf::WafLayer}, shed::{ ShardedOptions, ShedResponse, TypeExtractor, @@ -51,7 +50,6 @@ use crate::{ cli::Cli, metrics::{self}, routing::{ - error_cause::ClientError, ic::routing_table_manager::LooksUpSubnetType, middleware::{ canister_match, cors, headers, @@ -106,6 +104,8 @@ pub enum RequestType { CustomDomains, #[strum(transparent)] Api(RequestTypeApi), + #[cfg(feature = "mcp")] + Mcp, #[default] Unknown, } @@ -122,6 +122,8 @@ impl FromStr for RequestType { "registrations" => Self::Registrations, "custom_domains" => Self::CustomDomains, "unknown" => Self::Unknown, + #[cfg(feature = "mcp")] + "mcp" => Self::Mcp, _ => Self::Api(RequestTypeApi::from_str(s).context("unable to parse API type")?), }) } @@ -231,6 +233,24 @@ pub async fn setup_router( cli.domain.domain_skip_authority_validation, )) as Arc; + #[cfg(feature = "mcp")] + let mcp = if let Some(v) = cli.mcp.mcp_ii_instance { + warn!( + "Starting MCP at {} (II {v})", + cli.mcp.mcp_public_url.as_ref().unwrap(), + ); + + let mcp_hostname = + FQDN::from_str(cli.mcp.mcp_public_url.clone().unwrap().host_str().unwrap()).unwrap(); + + let router = crate::mcp::setup_mcp(&cli.mcp, ic_agent.clone(), registry, &mut *tasks) + .context("unable to set up MCP")?; + + Some((router, mcp_hostname)) + } else { + None + }; + // Denylist let denylist_mw = option_layer( (cli.policy.policy_denylist_seed.is_some() || cli.policy.policy_denylist_url.is_some()) @@ -461,6 +481,8 @@ pub async fn setup_router( domain_resolver, cli.domain.domain_canister_id_from_query_params, cli.domain.domain_canister_id_from_referer, + #[cfg(feature = "mcp")] + mcp.as_ref().map(|x| x.1.clone()), ); // Request type state for alternate error domain configuration @@ -524,31 +546,7 @@ pub async fn setup_router( )) .layer(option_layer(prerender_mw)); - let api_hostname = cli.api.api_hostname.clone().map(|x| x.to_string()); - - #[cfg(feature = "mcp")] - let mcp = if let Some(v) = cli.mcp.mcp_ii_instance { - warn!( - "Starting MCP at {} (II {v})", - cli.mcp.mcp_public_url.as_ref().unwrap(), - ); - - let mcp_hostname = cli - .mcp - .mcp_public_url - .clone() - .unwrap() - .host_str() - .unwrap() - .to_string(); - - let router = crate::mcp::setup_mcp(&cli.mcp, ic_agent.clone(), registry, &mut *tasks) - .context("unable to set up MCP")?; - - Some((router, mcp_hostname)) - } else { - None - }; + let api_hostname = cli.api.api_hostname.clone(); let custom_domains_router = custom_domains_router.map(|x| { Router::new() @@ -570,23 +568,16 @@ pub async fn setup_router( .nest("/api/v4", router_api_v4) .fallback( |Extension(ctx): Extension>, request: Request| async move { - let Some(host) = extract_authority(&request) else { - return Ok(ErrorCause::Client(ClientError::NoAuthority).into_response()); - }; - // Check if MCP is enabled & the request's host matches MCP hostname #[cfg(feature = "mcp")] - if let (Some((mcp_router, mcp_hostname)), Some(host)) = (mcp, extract_host(host)) - && host.eq_ignore_ascii_case(&mcp_hostname) + if let Some((mcp_router, mcp_hostname)) = mcp + && mcp_hostname == ctx.authority { return mcp_router.oneshot(request).await; } - // Check if the request's host matches API hostname - if api_hostname - .zip(extract_host(host)) - .is_some_and(|(a, b)| a == b) - { + // Check if API is enabled & the request's host matches API hostname + if api_hostname.is_some_and(|x| x == ctx.authority) { return router_api.oneshot(request).await; } From 6d5c4afc6e2d7a681c72826f3c6f59b0803a9fef Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Wed, 26 Aug 2026 10:55:31 +0000 Subject: [PATCH 2/7] Update mcp test --- src/routing/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/routing/mod.rs b/src/routing/mod.rs index bfe6848..546daac 100644 --- a/src/routing/mod.rs +++ b/src/routing/mod.rs @@ -830,10 +830,12 @@ mod test { ACCESS_CONTROL_ALLOW_METHODS, AUTHORIZATION, CACHE_CONTROL, LOCATION, WWW_AUTHENTICATE, }; - const MCP_HOST: &str = "mcp.ic0.app"; - const ISSUER: &str = "https://mcp.ic0.app/mcp"; + // Deliberately doesn't overlap with any base domain (e.g. ic0.app) to make sure MCP + // works on an arbitrary hostname rather than base domain or under it. + const MCP_HOST: &str = "mcp.example.com"; + const ISSUER: &str = "https://mcp.example.com/mcp"; const PROTECTED_RESOURCE_URL: &str = - "https://mcp.ic0.app/.well-known/oauth-protected-resource/mcp"; + "https://mcp.example.com/.well-known/oauth-protected-resource/mcp"; fn request(method: Method, host: &str, path_and_query: &str) -> Request { let mut req = Request::new(Body::from("")); @@ -862,7 +864,7 @@ mod test { "--mcp-ii-instance", "prod", "--mcp-public-url", - "https://mcp.ic0.app", + "https://mcp.example.com", "--mcp-state-dir", "/tmp/ic-gateway-test-mcp-state", "--mcp-root-redirect", From 7177007968aae64537b89bf7477025d7c5c0c17d Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Wed, 26 Aug 2026 11:45:08 +0000 Subject: [PATCH 3/7] Rework how MCP calls are handled --- src/mcp.rs | 45 +++++++++++++++++++++++--- src/routing/domain.rs | 18 ----------- src/routing/middleware/prerender.rs | 8 +---- src/routing/middleware/validate.rs | 26 +++------------ src/routing/mod.rs | 49 ++++++++++------------------- 5 files changed, 63 insertions(+), 83 deletions(-) diff --git a/src/mcp.rs b/src/mcp.rs index 3a9525e..fa0060f 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -5,8 +5,16 @@ use std::{ use anyhow::{Context, Error, anyhow}; use async_trait::async_trait; -use axum::{Router, middleware::from_fn_with_state, response::Redirect}; -use ic_bn_lib::tasks::{Run, TaskManager}; +use axum::{ + Router, + extract::{Request, State}, + middleware::{Next, from_fn_with_state}, + response::{IntoResponse, Redirect, Response}, +}; +use ic_bn_lib::{ + http::extract_authority, + tasks::{Run, TaskManager}, +}; use imcp2::{ Agent, IiInstance, McpConfig, McpServer, SharedClients, auth_callbacks_router, metrics::{Metrics, write_request_metrics}, @@ -14,6 +22,7 @@ use imcp2::{ use prometheus::Registry; use strum::{Display, EnumString}; use tokio_util::sync::CancellationToken; +use tower::ServiceExt; #[derive(EnumString, Clone, Copy, Display)] #[strum(serialize_all = "snake_case")] @@ -36,13 +45,33 @@ impl Run for McpWrapper { } } +pub struct McpState { + hostname: String, + router: Router, +} + +pub async fn middleware( + State(state): State>, + request: Request, + next: Next, +) -> Response { + // If the request is for the MCP hostname, route it to the MCP router directly + if let Some(authority) = extract_authority(&request) + && authority == state.hostname + { + return state.router.clone().oneshot(request).await.into_response(); + } + + next.run(request).await.into_response() +} + /// Inject MCP routes into Router pub fn setup_mcp( cli: &McpCli, agent: Agent, registry: &Registry, tasks: &mut TaskManager, -) -> Result { +) -> Result { let ii_instance = match cli.mcp_ii_instance.as_ref().unwrap() { IiType::Beta => IiInstance::beta(), IiType::Prod => IiInstance::prod(), @@ -84,6 +113,14 @@ pub fn setup_mcp( .context("unable to create MCP metrics")?; let mcp_redirect_url = cli.mcp_root_redirect.to_string(); + let hostname = cli + .mcp_public_url + .clone() + .unwrap() + .host_str() + .unwrap() + .to_string(); + let router = Router::new() .nest_service(mcp.mcp_path(), mcp.mcp_router()) .merge(mcp.well_known_router()) @@ -94,5 +131,5 @@ pub fn setup_mcp( tasks.add("mcp", Arc::new(McpWrapper(mcp))); - Ok(router) + Ok(McpState { hostname, router }) } diff --git a/src/routing/domain.rs b/src/routing/domain.rs index abb413d..4e53dcf 100644 --- a/src/routing/domain.rs +++ b/src/routing/domain.rs @@ -105,24 +105,6 @@ pub struct DomainLookup { pub flags: Option, } -impl DomainLookup { - pub fn new(domain: &Fqdn) -> Self { - Self { - domain: Domain { - name: domain.into(), - custom: false, - http: true, - api: true, - }, - canister_id: None, - timestamp: 0, - verify: true, - priority: 0, - flags: None, - } - } -} - /// Resolves hostname to a canister id pub trait ResolvesDomain: Send + Sync { fn resolve(&self, host: &Fqdn) -> Option; diff --git a/src/routing/middleware/prerender.rs b/src/routing/middleware/prerender.rs index f03b16f..5071b27 100644 --- a/src/routing/middleware/prerender.rs +++ b/src/routing/middleware/prerender.rs @@ -481,13 +481,7 @@ mod tests { let svc = ServiceBuilder::new() .layer(from_fn_with_state( - ValidateState::new( - Arc::new(resolver), - false, - false, - #[cfg(feature = "mcp")] - None, - ), + ValidateState::new(Arc::new(resolver), false, false), validate::middleware, )) .layer(from_fn_with_state( diff --git a/src/routing/middleware/validate.rs b/src/routing/middleware/validate.rs index c32f149..968e7b3 100644 --- a/src/routing/middleware/validate.rs +++ b/src/routing/middleware/validate.rs @@ -24,8 +24,6 @@ pub struct ValidateState { pub resolver: Arc, pub canister_id_from_query_params: bool, pub canister_id_from_referer: bool, - #[cfg(feature = "mcp")] - pub mcp_hostname: Option, } pub async fn middleware( @@ -45,26 +43,10 @@ pub async fn middleware( }); // Resolve the domain - - // Check if it's an MCP hostname first, if configured - #[cfg(feature = "mcp")] - let lookup = if let Some(mcp_hostname) = &state.mcp_hostname - && &authority == mcp_hostname - { - use crate::routing::domain::DomainLookup; - - // Override request type - request.extensions_mut().insert(RequestType::Mcp); - - // Emit some stub lookup - Some(DomainLookup::new(mcp_hostname)) - } else { - // Otherwise, resolve the domain normally - state.resolver.resolve(&authority) - }; - - let mut lookup = - lookup.ok_or_else(|| ErrorCause::Client(ClientError::UnknownDomain(authority.clone())))?; + let mut lookup = state + .resolver + .resolve(&authority) + .ok_or_else(|| ErrorCause::Client(ClientError::UnknownDomain(authority.clone())))?; if let Some(v) = lookup.flags { request.extensions_mut().insert(v); diff --git a/src/routing/mod.rs b/src/routing/mod.rs index 546daac..5bafe7b 100644 --- a/src/routing/mod.rs +++ b/src/routing/mod.rs @@ -104,8 +104,6 @@ pub enum RequestType { CustomDomains, #[strum(transparent)] Api(RequestTypeApi), - #[cfg(feature = "mcp")] - Mcp, #[default] Unknown, } @@ -122,8 +120,6 @@ impl FromStr for RequestType { "registrations" => Self::Registrations, "custom_domains" => Self::CustomDomains, "unknown" => Self::Unknown, - #[cfg(feature = "mcp")] - "mcp" => Self::Mcp, _ => Self::Api(RequestTypeApi::from_str(s).context("unable to parse API type")?), }) } @@ -233,24 +229,6 @@ pub async fn setup_router( cli.domain.domain_skip_authority_validation, )) as Arc; - #[cfg(feature = "mcp")] - let mcp = if let Some(v) = cli.mcp.mcp_ii_instance { - warn!( - "Starting MCP at {} (II {v})", - cli.mcp.mcp_public_url.as_ref().unwrap(), - ); - - let mcp_hostname = - FQDN::from_str(cli.mcp.mcp_public_url.clone().unwrap().host_str().unwrap()).unwrap(); - - let router = crate::mcp::setup_mcp(&cli.mcp, ic_agent.clone(), registry, &mut *tasks) - .context("unable to set up MCP")?; - - Some((router, mcp_hostname)) - } else { - None - }; - // Denylist let denylist_mw = option_layer( (cli.policy.policy_denylist_seed.is_some() || cli.policy.policy_denylist_url.is_some()) @@ -481,8 +459,6 @@ pub async fn setup_router( domain_resolver, cli.domain.domain_canister_id_from_query_params, cli.domain.domain_canister_id_from_referer, - #[cfg(feature = "mcp")] - mcp.as_ref().map(|x| x.1.clone()), ); // Request type state for alternate error domain configuration @@ -568,14 +544,6 @@ pub async fn setup_router( .nest("/api/v4", router_api_v4) .fallback( |Extension(ctx): Extension>, request: Request| async move { - // Check if MCP is enabled & the request's host matches MCP hostname - #[cfg(feature = "mcp")] - if let Some((mcp_router, mcp_hostname)) = mcp - && mcp_hostname == ctx.authority - { - return mcp_router.oneshot(request).await; - } - // Check if API is enabled & the request's host matches API hostname if api_hostname.is_some_and(|x| x == ctx.authority) { return router_api.oneshot(request).await; @@ -617,6 +585,23 @@ pub async fn setup_router( ) .layer(common_layers); + #[cfg(feature = "mcp")] + if let Some(v) = cli.mcp.mcp_ii_instance { + use crate::mcp; + + warn!( + "Starting MCP at {} (II {v})", + cli.mcp.mcp_public_url.as_ref().unwrap(), + ); + + let state = mcp::setup_mcp(&cli.mcp, ic_agent.clone(), registry, &mut *tasks) + .context("unable to set up MCP")?; + + // Inject MCP middleware to the top of the chain that will intercept the calls + // to the MCP hostname and route them to the MCP router directly + router = router.layer(from_fn_with_state(Arc::new(state), mcp::middleware)); + } + #[cfg(all(target_os = "linux", feature = "sev-snp"))] if cli.sev_snp.sev_snp_enable { let router_sev_snp = Router::new().route( From 0a572932123b6f2ba5bd94ed2b1a94964e904a97 Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Wed, 26 Aug 2026 12:48:06 +0000 Subject: [PATCH 4/7] Add custom II instance support --- src/mcp.rs | 105 ++++++++++++++++++++++++++++++++++++++++++--- src/routing/mod.rs | 2 +- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/src/mcp.rs b/src/mcp.rs index fa0060f..679919e 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -1,4 +1,6 @@ use std::{ + fmt::Display, + str::FromStr, sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; @@ -11,6 +13,7 @@ use axum::{ middleware::{Next, from_fn_with_state}, response::{IntoResponse, Redirect, Response}, }; +use candid::Principal; use ic_bn_lib::{ http::extract_authority, tasks::{Run, TaskManager}, @@ -19,16 +22,57 @@ use imcp2::{ Agent, IiInstance, McpConfig, McpServer, SharedClients, auth_callbacks_router, metrics::{Metrics, write_request_metrics}, }; +use itertools::Itertools; use prometheus::Registry; -use strum::{Display, EnumString}; use tokio_util::sync::CancellationToken; use tower::ServiceExt; +use url::Url; -#[derive(EnumString, Clone, Copy, Display)] -#[strum(serialize_all = "snake_case")] +#[derive(Clone)] pub enum IiType { Prod, Beta, + Custom(IiInstance), +} + +impl Display for IiType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Prod => write!(f, "prod"), + Self::Beta => write!(f, "beta"), + Self::Custom(instance) => write!( + f, + "{}:{}:{}", + instance.name, instance.ii_canister, instance.ii_url + ), + } + } +} + +impl FromStr for IiType { + type Err = Error; + + fn from_str(s: &str) -> Result { + Ok(match s { + "prod" => Self::Prod, + "beta" => Self::Beta, + _ => { + let (canister_id, url) = s.splitn(2, ':').collect_tuple().ok_or_else(|| { + anyhow!("invalid custom II instance format, expected name:canister_id:url") + })?; + + let canister_id = + Principal::from_str(canister_id).context("invalid canister id")?; + let url = Url::parse(url).context("invalid URL")?; + + Self::Custom(IiInstance { + name: "custom", + ii_canister: canister_id, + ii_url: url.to_string(), + }) + } + }) + } } use crate::cli::McpCli; @@ -73,10 +117,10 @@ pub fn setup_mcp( tasks: &mut TaskManager, ) -> Result { let ii_instance = match cli.mcp_ii_instance.as_ref().unwrap() { - IiType::Beta => IiInstance::beta(), - IiType::Prod => IiInstance::prod(), - } - .map_err(Error::msg)?; + IiType::Beta => IiInstance::beta().map_err(Error::msg)?, + IiType::Prod => IiInstance::prod().map_err(Error::msg)?, + IiType::Custom(instance) => instance.clone(), + }; let state_dir = cli .mcp_state_dir @@ -133,3 +177,50 @@ pub fn setup_mcp( Ok(McpState { hostname, router }) } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_ii_type_from_str_prod() { + assert!(matches!(IiType::from_str("prod").unwrap(), IiType::Prod)); + } + + #[test] + fn test_ii_type_from_str_beta() { + assert!(matches!(IiType::from_str("beta").unwrap(), IiType::Beta)); + } + + #[test] + fn test_ii_type_from_str_custom() { + let s = "aaaaa-aa:https://example.com"; + let ii_type = IiType::from_str(s).unwrap(); + + let IiType::Custom(instance) = ii_type else { + panic!("expected IiType::Custom"); + }; + + assert_eq!(instance.name, "custom"); + assert_eq!( + instance.ii_canister, + Principal::from_str("aaaaa-aa").unwrap() + ); + assert_eq!(instance.ii_url, "https://example.com/"); + } + + #[test] + fn test_ii_type_from_str_custom_invalid_format() { + assert!(IiType::from_str("no-colon-here").is_err()); + } + + #[test] + fn test_ii_type_from_str_custom_invalid_canister_id() { + assert!(IiType::from_str("not-a-canister-id:https://example.com").is_err()); + } + + #[test] + fn test_ii_type_from_str_custom_invalid_url() { + assert!(IiType::from_str("aaaaa-aa:not a url").is_err()); + } +} diff --git a/src/routing/mod.rs b/src/routing/mod.rs index 5bafe7b..20436e2 100644 --- a/src/routing/mod.rs +++ b/src/routing/mod.rs @@ -586,7 +586,7 @@ pub async fn setup_router( .layer(common_layers); #[cfg(feature = "mcp")] - if let Some(v) = cli.mcp.mcp_ii_instance { + if let Some(v) = &cli.mcp.mcp_ii_instance { use crate::mcp; warn!( From 60ee6ece84981100a22fe6be1abb6ce215f199f1 Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Wed, 26 Aug 2026 13:33:24 +0000 Subject: [PATCH 5/7] Use case insensitive comparison --- src/mcp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp.rs b/src/mcp.rs index 679919e..2c7478c 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -101,7 +101,7 @@ pub async fn middleware( ) -> Response { // If the request is for the MCP hostname, route it to the MCP router directly if let Some(authority) = extract_authority(&request) - && authority == state.hostname + && authority.eq_ignore_ascii_case(&state.hostname) { return state.router.clone().oneshot(request).await.into_response(); } From a01e646649a13915f6a019f505938b4235a46c7f Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Wed, 26 Aug 2026 13:34:13 +0000 Subject: [PATCH 6/7] Fix typo --- src/mcp.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mcp.rs b/src/mcp.rs index 2c7478c..64cfb8a 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -58,7 +58,7 @@ impl FromStr for IiType { "beta" => Self::Beta, _ => { let (canister_id, url) = s.splitn(2, ':').collect_tuple().ok_or_else(|| { - anyhow!("invalid custom II instance format, expected name:canister_id:url") + anyhow!("invalid custom II instance format, expected canister_id:url") })?; let canister_id = From 8b09809bbb6d6e3984d3a6d5e03abe45cf818b09 Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Wed, 26 Aug 2026 13:50:16 +0000 Subject: [PATCH 7/7] Fix nit --- src/mcp.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mcp.rs b/src/mcp.rs index 64cfb8a..008e7e4 100644 --- a/src/mcp.rs +++ b/src/mcp.rs @@ -28,6 +28,8 @@ use tokio_util::sync::CancellationToken; use tower::ServiceExt; use url::Url; +use crate::cli::McpCli; + #[derive(Clone)] pub enum IiType { Prod, @@ -75,8 +77,6 @@ impl FromStr for IiType { } } -use crate::cli::McpCli; - struct McpWrapper(McpServer); #[async_trait] @@ -159,7 +159,7 @@ pub fn setup_mcp( let mcp_redirect_url = cli.mcp_root_redirect.to_string(); let hostname = cli .mcp_public_url - .clone() + .as_ref() .unwrap() .host_str() .unwrap()