diff --git a/src/command/dev/do_dev.rs b/src/command/dev/do_dev.rs index e5d05c47b5..fb7cd39a57 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_url_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_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 652c897c01..387029b11d 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_url_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_url_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_url_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_url_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_url_path(router_url_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_url_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_url_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_url_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..40f8676a9c 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_url_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_url_path(router_url_path) .and_mcp_config_path(mcp_config_path) .env(env) .build();