From 5ff76dcb11cb7e865e341d6f42be05d74abc0f0b Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Fri, 14 Nov 2025 17:08:49 -0500 Subject: [PATCH 1/2] fix: auto-configure MCP server endpoint with router's custom path --- src/command/dev/do_dev.rs | 3 + src/command/dev/mcp/binary.rs | 123 ++++++++++++++++++++++++++++++++-- src/command/dev/mcp/run.rs | 2 + 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src/command/dev/do_dev.rs b/src/command/dev/do_dev.rs index e5d05c47b5..8438c966f1 100644 --- a/src/command/dev/do_dev.rs +++ b/src/command/dev/do_dev.rs @@ -265,6 +265,8 @@ impl Dev { // the overrides for the temporary config we set for hot-reloading the router, but also as // a message to the user for where to find their router let router_address = *run_router.state.config.address(); + // Extract the router's listen path from the config to construct the full endpoint URL for MCP + let router_path = run_router.state.config.listen_path(); let hot_reload_overrides = HotReloadConfigOverrides::builder() .address(router_address) .build(); @@ -313,6 +315,7 @@ impl Dev { TokioSpawn::default(), run_router.state.hot_reload_schema_path.clone(), router_address, + router_path, config.clone(), run_router.state.env.clone(), ) diff --git a/src/command/dev/mcp/binary.rs b/src/command/dev/mcp/binary.rs index 652c897c01..91df6a9ca7 100644 --- a/src/command/dev/mcp/binary.rs +++ b/src/command/dev/mcp/binary.rs @@ -78,6 +78,7 @@ pub struct RunMcpServerBinary { supergraph_schema_path: Utf8PathBuf, spawn: Spawn, router_address: RouterAddress, + router_path: Option, mcp_config_path: Option, env: HashMap, } @@ -87,6 +88,13 @@ impl RunMcpServerBinary { // understood by the MCP server. // TODO: Magic strings are not fun to debug later. fn opts_into_env(self) -> HashMap { + // Build the endpoint URL with the optional path from router config + let endpoint = if let Some(path) = &self.router_path { + format!("{}{}", self.router_address.pretty_string(), path) + } else { + self.router_address.pretty_string() + }; + let overlaid = HashMap::from([ // Configure the schema to be a local file ("APOLLO_MCP_SCHEMA__SOURCE".to_string(), "local".to_string()), @@ -95,10 +103,7 @@ impl RunMcpServerBinary { self.supergraph_schema_path.to_string(), ), // Configure the endpoint from the running router instance - ( - "APOLLO_MCP_ENDPOINT".to_string(), - self.router_address.pretty_string(), - ), + ("APOLLO_MCP_ENDPOINT".to_string(), endpoint), ( "APOLLO_MCP_TRANSPORT__TYPE".to_string(), "streamable_http".to_string(), @@ -248,3 +253,113 @@ where }); } } + +#[cfg(test)] +mod tests { + use std::{collections::HashMap, net::IpAddr}; + + use camino::Utf8PathBuf; + use semver::Version; + + use super::*; + use crate::command::dev::router::config::{RouterAddress, RouterHost, RouterPort}; + + struct MockSpawn; + + #[test] + fn test_mcp_endpoint_without_router_path() { + let router_address = RouterAddress::new( + Some(RouterHost::Default(IpAddr::V4(std::net::Ipv4Addr::new( + 127, 0, 0, 1, + )))), + Some(RouterPort::Default(4000)), + ); + + let binary = McpServerBinary::new( + Utf8PathBuf::from("/fake/path"), + Version::parse("1.0.0").unwrap(), + ); + + let router_path: Option = None; + let mcp_config_path: Option = None; + + let runner = RunMcpServerBinary::::builder() + .mcp_server_binary(binary) + .supergraph_schema_path(Utf8PathBuf::from("/fake/schema.graphql")) + .spawn(MockSpawn) + .router_address(router_address) + .and_router_path(router_path) + .and_mcp_config_path(mcp_config_path) + .env(HashMap::new()) + .build(); + + let env = runner.opts_into_env(); + let endpoint = env.get("APOLLO_MCP_ENDPOINT").unwrap(); + + assert_eq!(endpoint, "http://localhost:4000"); + } + + #[test] + fn test_mcp_endpoint_with_router_path() { + let router_address = RouterAddress::new( + Some(RouterHost::Default(IpAddr::V4(std::net::Ipv4Addr::new( + 127, 0, 0, 1, + )))), + Some(RouterPort::Default(4000)), + ); + + let binary = McpServerBinary::new( + Utf8PathBuf::from("/fake/path"), + Version::parse("1.0.0").unwrap(), + ); + + let mcp_config_path: Option = None; + + let runner = RunMcpServerBinary::::builder() + .mcp_server_binary(binary) + .supergraph_schema_path(Utf8PathBuf::from("/fake/schema.graphql")) + .spawn(MockSpawn) + .router_address(router_address) + .and_router_path(Some("/graphql".to_string())) + .and_mcp_config_path(mcp_config_path) + .env(HashMap::new()) + .build(); + + let env = runner.opts_into_env(); + let endpoint = env.get("APOLLO_MCP_ENDPOINT").unwrap(); + + assert_eq!(endpoint, "http://localhost:4000/graphql"); + } + + #[test] + fn test_mcp_endpoint_with_custom_path() { + let router_address = RouterAddress::new( + Some(RouterHost::Default(IpAddr::V4(std::net::Ipv4Addr::new( + 127, 0, 0, 1, + )))), + Some(RouterPort::Default(4000)), + ); + + let binary = McpServerBinary::new( + Utf8PathBuf::from("/fake/path"), + Version::parse("1.0.0").unwrap(), + ); + + let mcp_config_path: Option = None; + + let runner = RunMcpServerBinary::::builder() + .mcp_server_binary(binary) + .supergraph_schema_path(Utf8PathBuf::from("/fake/schema.graphql")) + .spawn(MockSpawn) + .router_address(router_address) + .and_router_path(Some("/custom-path".to_string())) + .and_mcp_config_path(mcp_config_path) + .env(HashMap::new()) + .build(); + + let env = runner.opts_into_env(); + let endpoint = env.get("APOLLO_MCP_ENDPOINT").unwrap(); + + assert_eq!(endpoint, "http://localhost:4000/custom-path"); + } +} diff --git a/src/command/dev/mcp/run.rs b/src/command/dev/mcp/run.rs index 3b84051fe1..a2733ff9cc 100644 --- a/src/command/dev/mcp/run.rs +++ b/src/command/dev/mcp/run.rs @@ -57,6 +57,7 @@ impl RunMcpServer { spawn: Spawn, supergraph_schema_path: Utf8PathBuf, router_address: RouterAddress, + router_path: Option, mcp_config_path: Option, env: HashMap, ) -> Result, RunMcpServerBinaryError> @@ -70,6 +71,7 @@ impl RunMcpServer { .supergraph_schema_path(supergraph_schema_path.clone()) .spawn(spawn) .router_address(router_address) + .and_router_path(router_path) .and_mcp_config_path(mcp_config_path) .env(env) .build(); From 7d01844cc63e9a600d33ada21709d5102025a8fa Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Tue, 18 Nov 2025 10:40:14 -0500 Subject: [PATCH 2/2] refactor: rename router_path to router_url_path --- src/command/dev/do_dev.rs | 4 ++-- src/command/dev/mcp/binary.rs | 16 ++++++++-------- src/command/dev/mcp/run.rs | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/command/dev/do_dev.rs b/src/command/dev/do_dev.rs index 8438c966f1..fb7cd39a57 100644 --- a/src/command/dev/do_dev.rs +++ b/src/command/dev/do_dev.rs @@ -266,7 +266,7 @@ impl Dev { // a message to the user for where to find their router let router_address = *run_router.state.config.address(); // Extract the router's listen path from the config to construct the full endpoint URL for MCP - let router_path = run_router.state.config.listen_path(); + let router_url_path = run_router.state.config.listen_path(); let hot_reload_overrides = HotReloadConfigOverrides::builder() .address(router_address) .build(); @@ -315,7 +315,7 @@ impl Dev { TokioSpawn::default(), run_router.state.hot_reload_schema_path.clone(), router_address, - router_path, + router_url_path, config.clone(), run_router.state.env.clone(), ) diff --git a/src/command/dev/mcp/binary.rs b/src/command/dev/mcp/binary.rs index 91df6a9ca7..387029b11d 100644 --- a/src/command/dev/mcp/binary.rs +++ b/src/command/dev/mcp/binary.rs @@ -78,7 +78,7 @@ pub struct RunMcpServerBinary { supergraph_schema_path: Utf8PathBuf, spawn: Spawn, router_address: RouterAddress, - router_path: Option, + router_url_path: Option, mcp_config_path: Option, env: HashMap, } @@ -89,7 +89,7 @@ impl RunMcpServerBinary { // TODO: Magic strings are not fun to debug later. fn opts_into_env(self) -> HashMap { // Build the endpoint URL with the optional path from router config - let endpoint = if let Some(path) = &self.router_path { + let endpoint = if let Some(path) = &self.router_url_path { format!("{}{}", self.router_address.pretty_string(), path) } else { self.router_address.pretty_string() @@ -267,7 +267,7 @@ mod tests { struct MockSpawn; #[test] - fn test_mcp_endpoint_without_router_path() { + fn test_mcp_endpoint_without_router_url_path() { let router_address = RouterAddress::new( Some(RouterHost::Default(IpAddr::V4(std::net::Ipv4Addr::new( 127, 0, 0, 1, @@ -280,7 +280,7 @@ mod tests { Version::parse("1.0.0").unwrap(), ); - let router_path: Option = None; + let router_url_path: Option = None; let mcp_config_path: Option = None; let runner = RunMcpServerBinary::::builder() @@ -288,7 +288,7 @@ mod tests { .supergraph_schema_path(Utf8PathBuf::from("/fake/schema.graphql")) .spawn(MockSpawn) .router_address(router_address) - .and_router_path(router_path) + .and_router_url_path(router_url_path) .and_mcp_config_path(mcp_config_path) .env(HashMap::new()) .build(); @@ -300,7 +300,7 @@ mod tests { } #[test] - fn test_mcp_endpoint_with_router_path() { + fn test_mcp_endpoint_with_router_url_path() { let router_address = RouterAddress::new( Some(RouterHost::Default(IpAddr::V4(std::net::Ipv4Addr::new( 127, 0, 0, 1, @@ -320,7 +320,7 @@ mod tests { .supergraph_schema_path(Utf8PathBuf::from("/fake/schema.graphql")) .spawn(MockSpawn) .router_address(router_address) - .and_router_path(Some("/graphql".to_string())) + .and_router_url_path(Some("/graphql".to_string())) .and_mcp_config_path(mcp_config_path) .env(HashMap::new()) .build(); @@ -352,7 +352,7 @@ mod tests { .supergraph_schema_path(Utf8PathBuf::from("/fake/schema.graphql")) .spawn(MockSpawn) .router_address(router_address) - .and_router_path(Some("/custom-path".to_string())) + .and_router_url_path(Some("/custom-path".to_string())) .and_mcp_config_path(mcp_config_path) .env(HashMap::new()) .build(); diff --git a/src/command/dev/mcp/run.rs b/src/command/dev/mcp/run.rs index a2733ff9cc..40f8676a9c 100644 --- a/src/command/dev/mcp/run.rs +++ b/src/command/dev/mcp/run.rs @@ -57,7 +57,7 @@ impl RunMcpServer { spawn: Spawn, supergraph_schema_path: Utf8PathBuf, router_address: RouterAddress, - router_path: Option, + router_url_path: Option, mcp_config_path: Option, env: HashMap, ) -> Result, RunMcpServerBinaryError> @@ -71,7 +71,7 @@ impl RunMcpServer { .supergraph_schema_path(supergraph_schema_path.clone()) .spawn(spawn) .router_address(router_address) - .and_router_path(router_path) + .and_router_url_path(router_url_path) .and_mcp_config_path(mcp_config_path) .env(env) .build();