Skip to main content

forge_sdk/mcp/
orchestration.rs

1//! Glue between `McpServer` instances and the `claude` binary.
2//!
3//! Semantics:
4//! - Keeps a `HashMap<server_name, McpServer>` for dispatch.
5//! - Produces the `--mcp-config` inline JSON argv for subprocess spawn.
6//! - Routes `mcp_message` `control_request`s to the right server.
7//!
8//! No transport ownership: servers are in-process, dispatch is a direct
9//! method call.
10
11use std::collections::HashMap;
12
13use crate::mcp::McpServer;
14use crate::mcp::protocol::{JsonRpcRequest, JsonRpcResponse};
15use forge_primitives::McpServerConfig;
16
17/// Outcome of routing an `mcp_message`. Kept three-way so an unroutable
18/// message cannot share a representation with a notification: the caller
19/// answers a notification with an id-less empty result, which for an unknown
20/// server would report success for something that never ran.
21pub(crate) enum Dispatched {
22    /// The server produced a response to send back.
23    Response(Box<JsonRpcResponse>),
24    /// A JSON-RPC notification - no response is owed.
25    Notification,
26    /// No server is registered under that name.
27    UnknownServer,
28}
29
30/// Registry of MCP servers (both in-process SDK and external stdio /
31/// SSE / HTTP). Keyed by the name under which the caller registered
32/// them via `OptionsBuilder::mcp_server` / `external_mcp_server`.
33#[derive(Clone, Debug, Default)]
34pub(crate) struct McpHosts {
35    servers: HashMap<String, McpServer>,
36    external: HashMap<String, McpServerConfig>,
37}
38
39impl McpHosts {
40    /// Build from the `Options.mcp_servers` vector of in-process servers
41    /// plus a `HashMap` of external configs. Either side may be empty.
42    pub(crate) fn new(
43        sdk_entries: Vec<(String, McpServer)>,
44        external: HashMap<String, McpServerConfig>,
45    ) -> Self {
46        let mut servers = HashMap::new();
47        for (name, server) in sdk_entries {
48            servers.insert(name, server);
49        }
50        Self { servers, external }
51    }
52
53    /// True when no servers of either flavour are registered.
54    pub(crate) fn is_empty(&self) -> bool {
55        self.servers.is_empty() && self.external.is_empty()
56    }
57
58    /// Build the `--mcp-config` inline JSON argument. SDK servers emit
59    /// `{"type":"sdk","name":<n>}`; external servers serialise via
60    /// [`McpServerConfig`]'s own serde impl. Returns `"{"mcpServers":{}}"`
61    /// when empty - caller should skip adding the flag in that case.
62    pub(crate) fn config_argv(&self) -> String {
63        let mut servers: serde_json::Map<String, serde_json::Value> = self
64            .servers
65            .keys()
66            .map(|name| (name.clone(), serde_json::json!({"type": "sdk", "name": name})))
67            .collect();
68        for (name, cfg) in &self.external {
69            match serde_json::to_value(cfg) {
70                Ok(v) => {
71                    servers.insert(name.clone(), v);
72                }
73                Err(e) => {
74                    tracing::warn!(
75                        %name,
76                        error = %e,
77                        "external MCP server config failed to serialise; omitting from --mcp-config"
78                    );
79                }
80            }
81        }
82        serde_json::json!({"mcpServers": servers}).to_string()
83    }
84
85    /// Route an incoming `mcp_message` to the named server.
86    pub(crate) async fn dispatch(&self, server_name: &str, req: &JsonRpcRequest) -> Dispatched {
87        let Some(server) = self.servers.get(server_name) else {
88            return Dispatched::UnknownServer;
89        };
90        match server.dispatch(req).await {
91            Some(r) => Dispatched::Response(Box::new(r)),
92            None => Dispatched::Notification,
93        }
94    }
95}