1use std::borrow::Cow;
11use std::sync::Arc;
12
13use rmcp::model::*;
14use rmcp::{ErrorData as McpError, ServerHandler};
15
16use crate::agents::AgentRegistry;
17use crate::config::Config;
18use crate::traits::{ToolContext, ToolRegistry};
19
20#[derive(Clone)]
25pub struct McpBridge {
26 config: Arc<Config>,
27 tools: Arc<ToolRegistry>,
28 extra_tools: Arc<ToolRegistry>,
29 agents: Arc<AgentRegistry>,
30 extra_agents: Arc<AgentRegistry>,
31}
32
33impl McpBridge {
34 pub fn new(
35 config: Arc<Config>,
36 tools: Arc<ToolRegistry>,
37 extra_tools: Arc<ToolRegistry>,
38 agents: Arc<AgentRegistry>,
39 extra_agents: Arc<AgentRegistry>,
40 ) -> Self {
41 Self {
42 config,
43 tools,
44 extra_tools,
45 agents,
46 extra_agents,
47 }
48 }
49
50 fn find_tool(&self, name: &str) -> Option<&dyn crate::traits::Tool> {
51 self.tools
52 .find(name)
53 .or_else(|| self.extra_tools.find(name))
54 }
55
56 fn find_agent(&self, name: &str) -> Option<&dyn crate::agents::Agent> {
57 self.agents
58 .find(name)
59 .or_else(|| self.extra_agents.find(name))
60 }
61
62 fn to_mcp_tool(tool: &dyn crate::traits::Tool) -> Tool {
64 let schema_value = tool.parameters_schema();
65 let input_schema: Arc<serde_json::Map<String, serde_json::Value>> = match schema_value {
66 serde_json::Value::Object(map) => Arc::new(map),
67 _ => Arc::new(serde_json::Map::new()),
68 };
69
70 Tool {
71 name: Cow::Owned(tool.name().to_string()),
72 title: None,
73 description: Some(Cow::Owned(tool.description().to_string())),
74 input_schema,
75 output_schema: None,
76 annotations: Some(ToolAnnotations::new().read_only(true)),
77 execution: None,
78 icons: None,
79 meta: None,
80 }
81 }
82
83 fn to_mcp_prompt(agent: &dyn crate::agents::Agent) -> Prompt {
85 let arguments: Option<Vec<PromptArgument>> = {
86 let args = agent.arguments();
87 if args.is_empty() {
88 None
89 } else {
90 Some(
91 args.into_iter()
92 .map(|a| PromptArgument {
93 name: a.name,
94 title: None,
95 description: Some(a.description),
96 required: Some(a.required),
97 })
98 .collect(),
99 )
100 }
101 };
102
103 Prompt {
104 name: agent.name().to_string(),
105 title: None,
106 description: Some(agent.description().to_string()),
107 arguments,
108 icons: None,
109 meta: None,
110 }
111 }
112}
113
114impl ServerHandler for McpBridge {
115 fn get_info(&self) -> ServerInfo {
116 ServerInfo {
117 protocol_version: ProtocolVersion::LATEST,
118 capabilities: ServerCapabilities::builder()
119 .enable_tools()
120 .enable_prompts()
121 .build(),
122 server_info: Implementation {
123 name: "context-harness".to_string(),
124 title: Some("Context Harness".to_string()),
125 version: env!("CARGO_PKG_VERSION").to_string(),
126 description: None,
127 icons: None,
128 website_url: None,
129 },
130 instructions: Some(
131 "Context Harness — local-first context ingestion and retrieval for AI tools. \
132 Use the search tool to find relevant documents, get to retrieve a specific \
133 document by ID, and sources to list connector status. \
134 Agents are available as prompts — use list_prompts to discover them."
135 .to_string(),
136 ),
137 }
138 }
139
140 fn list_tools(
143 &self,
144 _request: Option<PaginatedRequestParams>,
145 _context: rmcp::service::RequestContext<rmcp::RoleServer>,
146 ) -> impl std::future::Future<Output = Result<ListToolsResult, McpError>> + Send + '_ {
147 let mut tools: Vec<Tool> = self
148 .tools
149 .tools()
150 .iter()
151 .map(|t| Self::to_mcp_tool(t.as_ref()))
152 .collect();
153 for t in self.extra_tools.tools() {
154 tools.push(Self::to_mcp_tool(t.as_ref()));
155 }
156 std::future::ready(Ok(ListToolsResult::with_all_items(tools)))
157 }
158
159 fn get_tool(&self, name: &str) -> Option<Tool> {
160 self.find_tool(name).map(Self::to_mcp_tool)
161 }
162
163 async fn call_tool(
164 &self,
165 request: CallToolRequestParams,
166 _context: rmcp::service::RequestContext<rmcp::RoleServer>,
167 ) -> Result<CallToolResult, McpError> {
168 let tool = self.find_tool(&request.name).ok_or_else(|| {
169 McpError::new(
170 ErrorCode::METHOD_NOT_FOUND,
171 format!("no tool registered with name: {}", request.name),
172 None,
173 )
174 })?;
175
176 let params = request
177 .arguments
178 .map(serde_json::Value::Object)
179 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
180
181 let ctx = ToolContext::new(self.config.clone());
182 match tool.execute(params, &ctx).await {
183 Ok(result) => {
184 let text = serde_json::to_string_pretty(&result).unwrap_or_default();
185 Ok(CallToolResult::success(vec![Content::text(text)]))
186 }
187 Err(e) => Ok(CallToolResult::error(vec![Content::text(e.to_string())])),
188 }
189 }
190
191 fn list_prompts(
194 &self,
195 _request: Option<PaginatedRequestParams>,
196 _context: rmcp::service::RequestContext<rmcp::RoleServer>,
197 ) -> impl std::future::Future<Output = Result<ListPromptsResult, McpError>> + Send + '_ {
198 let mut prompts: Vec<Prompt> = self
199 .agents
200 .agents()
201 .iter()
202 .map(|a| Self::to_mcp_prompt(a.as_ref()))
203 .collect();
204 for a in self.extra_agents.agents() {
205 prompts.push(Self::to_mcp_prompt(a.as_ref()));
206 }
207 std::future::ready(Ok(ListPromptsResult::with_all_items(prompts)))
208 }
209
210 async fn get_prompt(
211 &self,
212 request: GetPromptRequestParams,
213 _context: rmcp::service::RequestContext<rmcp::RoleServer>,
214 ) -> Result<GetPromptResult, McpError> {
215 let agent = self.find_agent(&request.name).ok_or_else(|| {
216 McpError::new(
217 ErrorCode::METHOD_NOT_FOUND,
218 format!("no agent registered with name: {}", request.name),
219 None,
220 )
221 })?;
222
223 let args = request
224 .arguments
225 .map(serde_json::Value::Object)
226 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
227
228 let ctx = ToolContext::new(self.config.clone());
229 let resolved = agent.resolve(args, &ctx).await.map_err(|e| {
230 McpError::new(
231 ErrorCode::INTERNAL_ERROR,
232 format!("agent '{}': {}", request.name, e),
233 None,
234 )
235 })?;
236
237 let mut messages: Vec<PromptMessage> = Vec::new();
238
239 if !resolved.system.is_empty() {
242 messages.push(PromptMessage::new_text(
243 PromptMessageRole::User,
244 &resolved.system,
245 ));
246 }
247
248 for msg in &resolved.messages {
249 let role = match msg.role.as_str() {
250 "assistant" => PromptMessageRole::Assistant,
251 _ => PromptMessageRole::User,
252 };
253 messages.push(PromptMessage::new_text(role, &msg.content));
254 }
255
256 Ok(GetPromptResult {
257 description: Some(agent.description().to_string()),
258 messages,
259 })
260 }
261}