1use std::{
20 collections::{BTreeMap, HashMap},
21 path::PathBuf,
22 sync::Arc,
23};
24
25use async_trait::async_trait;
26use chrono::Utc;
27use tokio::sync::RwLock;
28
29pub mod aud_check;
30pub mod capability_index;
31pub mod error;
32pub mod oauth;
33pub mod resilient;
34pub mod rmcp_host;
35pub mod types;
36
37pub use aud_check::{validate_token_aud, AudCheckOutcome};
38pub use capability_index::{InMemoryToolCapabilityIndex, ToolCapabilityIndex};
39pub use error::McpHostError;
40pub use oauth::{manager_from_vault, VaultCredentialStore};
41pub use resilient::{ResilienceConfig, ResilientMcpHost};
42pub use rmcp_host::RmcpHost;
43pub use types::{
44 CallOutcome, MountedServer, OAuthConfig, ServerConfig, ServerInfo, ServerStatus, ToolDescriptor,
45};
46
47pub const MCP_PROTOCOL_VERSION: &str = "2025-11-25";
49
50#[async_trait]
52pub trait MCPHost: Send + Sync {
53 async fn mount(&self, name: String, cfg: ServerConfig) -> Result<(), McpHostError>;
56
57 async fn unmount(&self, name: &str) -> Result<(), McpHostError>;
60
61 async fn list_servers(&self) -> Vec<ServerStatus>;
63
64 async fn list_all_tools(&self) -> Vec<ToolDescriptor>;
67
68 async fn call(
71 &self,
72 server: &str,
73 tool: &str,
74 args: serde_json::Value,
75 ) -> Result<CallOutcome, McpHostError>;
76}
77
78#[async_trait]
80pub trait MCPClient: Send + Sync {
81 async fn initialize(&self) -> Result<ServerInfo, McpHostError>;
82 async fn list_tools(&self) -> Result<Vec<ToolDescriptor>, McpHostError>;
83 async fn call_tool(
84 &self,
85 name: &str,
86 args: serde_json::Value,
87 ) -> Result<CallOutcome, McpHostError>;
88 async fn shutdown(&self) -> Result<(), McpHostError>;
89 fn server_info(&self) -> Option<ServerInfo>;
90}
91
92#[derive(Default)]
96pub struct InMemoryMcpHost {
97 mounted: RwLock<HashMap<String, MountedServer>>,
98}
99
100impl InMemoryMcpHost {
101 pub fn new() -> Self {
102 Self {
103 mounted: RwLock::new(HashMap::new()),
104 }
105 }
106
107 pub fn shared() -> Arc<dyn MCPHost> {
108 Arc::new(Self::new())
109 }
110}
111
112#[async_trait]
113impl MCPHost for InMemoryMcpHost {
114 async fn mount(&self, name: String, cfg: ServerConfig) -> Result<(), McpHostError> {
115 let mut guard = self.mounted.write().await;
116 if guard.contains_key(&name) {
117 return Err(McpHostError::AlreadyMounted(name));
118 }
119 guard.insert(
120 name.clone(),
121 MountedServer {
122 name,
123 config: cfg,
124 mounted_at: Utc::now(),
125 info: None,
126 tools: Vec::new(),
127 },
128 );
129 Ok(())
130 }
131
132 async fn unmount(&self, name: &str) -> Result<(), McpHostError> {
133 self.mounted
134 .write()
135 .await
136 .remove(name)
137 .map(|_| ())
138 .ok_or_else(|| McpHostError::NotMounted(name.to_string()))
139 }
140
141 async fn list_servers(&self) -> Vec<ServerStatus> {
142 self.mounted
143 .read()
144 .await
145 .values()
146 .map(|m| ServerStatus {
147 name: m.name.clone(),
148 mounted_at: m.mounted_at,
149 tool_count: m.tools.len(),
150 info: m.info.clone(),
151 })
152 .collect()
153 }
154
155 async fn list_all_tools(&self) -> Vec<ToolDescriptor> {
156 self.mounted
157 .read()
158 .await
159 .values()
160 .flat_map(|m| m.tools.clone())
161 .collect()
162 }
163
164 async fn call(
165 &self,
166 server: &str,
167 _tool: &str,
168 _args: serde_json::Value,
169 ) -> Result<CallOutcome, McpHostError> {
170 let guard = self.mounted.read().await;
171 if !guard.contains_key(server) {
172 return Err(McpHostError::NotMounted(server.to_string()));
173 }
174 Err(McpHostError::Transport(
178 "no transport configured for in-memory host".to_string(),
179 ))
180 }
181}
182
183pub fn empty_env() -> BTreeMap<String, String> {
185 BTreeMap::new()
186}
187
188pub fn stdio_cfg(command: impl Into<String>, args: Vec<String>) -> ServerConfig {
190 ServerConfig::Stdio {
191 command: command.into(),
192 args,
193 env: empty_env(),
194 cwd: None::<PathBuf>,
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[tokio::test]
203 async fn mount_and_list() {
204 let host = InMemoryMcpHost::new();
205 host.mount("fs".into(), stdio_cfg("mcp-fs", vec![]))
206 .await
207 .unwrap();
208 let servers = host.list_servers().await;
209 assert_eq!(servers.len(), 1);
210 assert_eq!(servers[0].name, "fs");
211 assert_eq!(servers[0].tool_count, 0);
212 }
213
214 #[tokio::test]
215 async fn double_mount_rejected() {
216 let host = InMemoryMcpHost::new();
217 host.mount("fs".into(), stdio_cfg("mcp-fs", vec![]))
218 .await
219 .unwrap();
220 let err = host
221 .mount("fs".into(), stdio_cfg("mcp-fs", vec![]))
222 .await
223 .unwrap_err();
224 assert!(matches!(err, McpHostError::AlreadyMounted(_)));
225 }
226
227 #[tokio::test]
228 async fn unmount_missing_errors() {
229 let host = InMemoryMcpHost::new();
230 let err = host.unmount("nope").await.unwrap_err();
231 assert!(matches!(err, McpHostError::NotMounted(_)));
232 }
233
234 #[tokio::test]
235 async fn call_without_transport_errors() {
236 let host = InMemoryMcpHost::new();
237 host.mount("fs".into(), stdio_cfg("mcp-fs", vec![]))
238 .await
239 .unwrap();
240 let err = host
241 .call("fs", "read_text_file", serde_json::json!({}))
242 .await
243 .unwrap_err();
244 assert!(matches!(err, McpHostError::Transport(_)));
245 }
246
247 #[test]
248 fn protocol_version_matches_spec() {
249 assert_eq!(MCP_PROTOCOL_VERSION, "2025-11-25");
250 }
251}