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