diff --git a/crates/ed-mcp/Cargo.toml b/crates/ed-mcp/Cargo.toml index 550b68a..f2234e2 100644 --- a/crates/ed-mcp/Cargo.toml +++ b/crates/ed-mcp/Cargo.toml @@ -16,7 +16,7 @@ agent-client-protocol = { workspace = true, optional = true } ed-agent = { workspace = true, optional = true } async-trait = { workspace = true, optional = true } http = "1" -rmcp = { workspace = true, features = ["client", "transport-child-process", "transport-streamable-http-client-reqwest", "reqwest"] } +rmcp = { workspace = true, features = ["client", "transport-streamable-http-client-reqwest", "reqwest"] } tokio = { workspace = true, features = ["sync", "time", "process", "macros", "io-std", "io-util", "rt"] } tokio-util.workspace = true futures.workspace = true @@ -24,6 +24,12 @@ serde_json.workspace = true anyhow.workspace = true tracing.workspace = true +# Spawning an MCP server as a child process needs `exec`, which Apple hides on tvOS (`libc` +# has no `execv` there, so `nix` under `process-wrap` doesn't compile). The stdio transport +# is everywhere else. +[target.'cfg(not(target_os = "tvos"))'.dependencies] +rmcp = { workspace = true, features = ["transport-child-process"] } + [features] # Map ACP `session/new` MCP server lists (`McpToolset::connect`, `ServerSpec::from_acp`). # Pulls in `agent-client-protocol`; `ed-acp` enables it. diff --git a/crates/ed-mcp/src/lib.rs b/crates/ed-mcp/src/lib.rs index 1ca0396..110c0c2 100644 --- a/crates/ed-mcp/src/lib.rs +++ b/crates/ed-mcp/src/lib.rs @@ -34,8 +34,10 @@ use rmcp::model::{ Implementation, ProgressNotificationParam, Request, ServerResult, Tool, }; use rmcp::service::{NotificationContext, PeerRequestOptions, RequestContext, RunningService}; +use rmcp::transport::StreamableHttpClientTransport; +#[cfg(not(target_os = "tvos"))] +use rmcp::transport::TokioChildProcess; use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig; -use rmcp::transport::{StreamableHttpClientTransport, TokioChildProcess}; use rmcp::{ClientHandler, ErrorData as McpError, RoleClient, ServiceExt}; use serde_json::{Map, Value, json}; use tokio::sync::Mutex; @@ -140,6 +142,14 @@ impl ClientInfo { #[derive(Clone)] pub enum Transport { /// Start `command args…` with extra environment variables and talk over its stdio. + /// + /// Stdio is only available on platforms that can spawn a child process. + /// On `target_os = "tvos"` the OS does not allow apps to create other + /// processes, so this variant is **not** supported on tvOS builds: the + /// matching arm in [`Connection::connect_with`] returns an error at + /// runtime instead of compiling out the variant. Use [`Transport::Http`] + /// for tvOS; if you only know the transport at runtime, branch on the + /// current `target_os` before constructing a `Stdio` spec. Stdio { command: PathBuf, args: Vec, @@ -313,12 +323,30 @@ pub struct Connection { impl Connection { /// Connect to `spec`, run the handshake and list its tools, each within `timeout`. + /// + /// # Errors + /// + /// - On tvOS, connecting a `Transport::Stdio` spec always fails with a + /// "this platform can't run MCP servers as processes" error — tvOS does + /// not allow child processes. Use [`Transport::Http`] on that target. + /// - On every platform, spawning the child, completing the handshake and + /// listing tools are each independently bounded by `timeout` and may + /// fail or time out. pub async fn connect(spec: &ServerSpec, info: &ClientInfo, timeout: Duration) -> Result { Self::connect_with(spec, info, None, timeout).await } /// Connect to `spec`, with an optional [`Elicitation`] hook for server-initiated /// `elicitation/create` requests. [`Connection::connect`] is this with no hook. + /// + /// # Errors + /// + /// - On tvOS, connecting a `Transport::Stdio` spec always fails with a + /// "this platform can't run MCP servers as processes" error — tvOS does + /// not allow child processes. Use [`Transport::Http`] on that target. + /// - On every platform, spawning the child, completing the handshake and + /// listing tools are each independently bounded by `timeout` and may + /// fail or time out. pub async fn connect_with( spec: &ServerSpec, info: &ClientInfo, @@ -332,6 +360,7 @@ impl Connection { elicitation, }; let handshake = match &spec.transport { + #[cfg(not(target_os = "tvos"))] Transport::Stdio { command, args, env } => { let mut cmd = tokio::process::Command::new(command); cmd.args(args).envs(env.iter().map(|(k, v)| (k, v))); @@ -339,6 +368,14 @@ impl Connection { .with_context(|| format!("starting {}", command.display()))?; tokio::time::timeout(timeout, handler.serve(transport)).await } + // tvOS can't start a process, so there is no stdio transport to offer. + #[cfg(target_os = "tvos")] + Transport::Stdio { command, .. } => { + bail!( + "can't start {}: this platform can't run MCP servers as processes", + command.display() + ); + } Transport::Http { url, bearer,