1use crate::protocol::{InitializeParams, PROTOCOL_VERSION, Request, Response, ServerInfo, codes};
2use crate::{AiPermissions, Tool};
3use origin_platform::{ConfirmationDecision, ConfirmationRequest, ConfirmationService};
4use serde_json::{Value, json};
5use std::collections::BTreeMap;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::{Arc, Mutex};
8
9static SESSION_COUNTER: AtomicU64 = AtomicU64::new(1);
11
12fn next_session_id() -> String {
13 let number = SESSION_COUNTER.fetch_add(1, Ordering::Relaxed);
14 format!("mcp-session-{number}")
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18enum Lifecycle {
19 Uninitialized,
20 AwaitingInitialized,
21 Ready,
22}
23
24#[derive(Debug)]
30pub struct McpServer {
31 info: ServerInfo,
32 session_id: String,
35 tools: BTreeMap<String, Arc<dyn Tool>>,
36 permissions: AiPermissions,
37 confirmation: Option<Arc<dyn ConfirmationService>>,
41 lifecycle: Mutex<Lifecycle>,
42}
43
44impl Clone for McpServer {
45 fn clone(&self) -> Self {
46 Self {
47 info: self.info.clone(),
48 session_id: next_session_id(),
49 tools: self.tools.clone(),
50 permissions: self.permissions.clone(),
51 confirmation: self.confirmation.clone(),
52 lifecycle: Mutex::new(Lifecycle::Uninitialized),
55 }
56 }
57}
58
59impl McpServer {
60 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
61 Self {
62 info: ServerInfo {
63 name: name.into(),
64 version: version.into(),
65 },
66 session_id: next_session_id(),
67 tools: BTreeMap::new(),
68 permissions: AiPermissions::none(),
69 confirmation: None,
70 lifecycle: Mutex::new(Lifecycle::Uninitialized),
71 }
72 }
73
74 pub fn with_permissions(mut self, permissions: AiPermissions) -> Self {
76 self.permissions = permissions;
77 self
78 }
79
80 pub fn with_confirmation(mut self, confirmation: Arc<dyn ConfirmationService>) -> Self {
86 self.confirmation = Some(confirmation);
87 self
88 }
89
90 pub fn session_id(&self) -> &str {
92 &self.session_id
93 }
94
95 pub fn with_tool(mut self, tool: Arc<dyn Tool>) -> Self {
96 self.tools.insert(tool.descriptor().name, tool);
97 self
98 }
99
100 fn available(&self) -> impl Iterator<Item = &Arc<dyn Tool>> {
106 self.tools
107 .values()
108 .filter(|tool| self.permissions.allows(tool.descriptor().permission))
109 }
110
111 pub async fn handle(&self, request: Request) -> Option<Response> {
113 if request.jsonrpc != "2.0" {
114 return Some(Response::error(
115 request.id.unwrap_or(Value::Null),
116 codes::INVALID_REQUEST,
117 "jsonrpc must be `2.0`",
118 ));
119 }
120
121 let Some(id) = request.id.clone() else {
122 if request.method == "notifications/initialized" {
123 let mut lifecycle = self.lifecycle();
124 if *lifecycle == Lifecycle::AwaitingInitialized {
125 *lifecycle = Lifecycle::Ready;
126 } else {
127 tracing::warn!("unexpected MCP initialized notification");
128 }
129 }
130 tracing::debug!(method = %request.method, "mcp notification");
131 return None;
132 };
133
134 let response = match request.method.as_str() {
135 "initialize" => self.initialize(id, request.params),
136 "ping" => Response::result(id, json!({})),
137 _ if *self.lifecycle() != Lifecycle::Ready => {
138 Response::error(id, codes::INVALID_REQUEST, "MCP session is not initialized")
139 }
140 "tools/list" => Response::result(id, self.list_tools()),
141 "tools/call" => self.call_tool(id, request.params).await,
142 other => Response::error(
143 id,
144 codes::METHOD_NOT_FOUND,
145 format!("unsupported method `{other}`"),
146 ),
147 };
148
149 Some(response)
150 }
151
152 fn initialize(&self, id: Value, params: Value) -> Response {
153 let params: InitializeParams = match serde_json::from_value(params) {
154 Ok(params) => params,
155 Err(error) => {
156 return Response::error(
157 id,
158 codes::INVALID_PARAMS,
159 format!("invalid initialize parameters: {error}"),
160 );
161 }
162 };
163 if !params.capabilities.is_object() {
164 return Response::error(
165 id,
166 codes::INVALID_PARAMS,
167 "initialize capabilities must be an object",
168 );
169 }
170
171 let mut lifecycle = self.lifecycle();
172 if *lifecycle != Lifecycle::Uninitialized {
173 return Response::error(
174 id,
175 codes::INVALID_REQUEST,
176 "MCP session is already initialized",
177 );
178 }
179 *lifecycle = Lifecycle::AwaitingInitialized;
180 drop(lifecycle);
181
182 tracing::debug!(
183 client = %params.client_info.name,
184 client_version = %params.client_info.version,
185 requested_protocol = %params.protocol_version,
186 "MCP session initialized"
187 );
188
189 Response::result(
190 id,
191 json!({
192 "protocolVersion": PROTOCOL_VERSION,
193 "serverInfo": self.info,
194 "capabilities": { "tools": {} }
195 }),
196 )
197 }
198
199 fn list_tools(&self) -> Value {
200 let tools: Vec<Value> = self
201 .available()
202 .map(|tool| {
203 let descriptor = tool.descriptor();
204 json!({
205 "name": descriptor.name,
206 "title": descriptor.title,
207 "description": descriptor.description,
208 "inputSchema": descriptor.input_schema,
209 })
210 })
211 .collect();
212
213 json!({ "tools": tools })
214 }
215
216 async fn call_tool(&self, id: Value, params: Value) -> Response {
217 let Some(name) = params.get("name").and_then(Value::as_str) else {
218 return Response::error(id, codes::INVALID_PARAMS, "missing tool name");
219 };
220
221 let Some(tool) = self.tools.get(name) else {
222 return Response::error(id, codes::INVALID_PARAMS, format!("unknown tool `{name}`"));
223 };
224
225 let descriptor = tool.descriptor();
226 if !self.permissions.allows(descriptor.permission) {
227 tracing::warn!(
230 tool = name,
231 permission = descriptor.permission.as_str(),
232 "mcp tool call refused: permission not granted"
233 );
234 return Response::error(
235 id,
236 codes::INVALID_REQUEST,
237 format!(
238 "`{name}` needs the `{}` permission, which this application does not grant",
239 descriptor.permission.as_str()
240 ),
241 );
242 }
243
244 let arguments = params
245 .get("arguments")
246 .cloned()
247 .unwrap_or_else(|| json!({}));
248
249 if descriptor.permission.is_mutating() {
253 match &self.confirmation {
254 Some(service) => {
255 let request = ConfirmationRequest::new(
256 format!("Allow `{name}`?"),
257 format!(
258 "An external AI wants to run `{name}`, which {}data.\n\n{}\n\n\
259 Approve only if you asked for this.",
260 if descriptor.permission == crate::AiPermission::Delete {
261 "deletes "
262 } else {
263 "changes "
264 },
265 descriptor.description,
266 ),
267 );
268
269 match service.confirm(request).await {
270 Ok(ConfirmationDecision::Approved) => {}
271 Ok(ConfirmationDecision::Denied) => {
272 tracing::warn!(tool = name, "mcp mutating tool denied by the human");
273 return Response::result(
274 id,
275 json!({
276 "content": [{
277 "type": "text",
278 "text": format!(
279 "The user did not approve running \"{name}\". \
280 Nothing was changed."
281 ),
282 }],
283 "isError": true,
284 }),
285 );
286 }
287 Err(error) => {
288 tracing::warn!(tool = name, %error, "confirmation service failed; denying tool call");
290 return Response::result(
291 id,
292 json!({
293 "content": [{
294 "type": "text",
295 "text": format!(
296 "Confirmation failed for \"{name}\". Nothing \
297 was changed. ({error})"
298 ),
299 }],
300 "isError": true,
301 }),
302 );
303 }
304 }
305 }
306
307 None => {
310 tracing::warn!(
311 tool = name,
312 permission = descriptor.permission.as_str(),
313 "mcp mutating tool denied: no confirmation service configured"
314 );
315 return Response::result(
316 id,
317 json!({
318 "content": [{
319 "type": "text",
320 "text": format!(
321 "Running \"{name}\" requires a human, and the application \
322 has no confirmation service configured. Grant \
323 `AiPermission::Commit` only when a confirmer is wired."
324 ),
325 }],
326 "isError": true,
327 }),
328 );
329 }
330 }
331 }
332
333 tracing::info!(
334 mcp_session_id = %self.session_id,
335 tool = name,
336 permission = descriptor.permission.as_str(),
337 "mcp tool call"
338 );
339
340 match tool.call(arguments).await {
341 Ok(output) => {
342 let mut result = json!({
343 "content": [{ "type": "text", "text": output.text }],
344 "isError": false,
345 });
346 if let Some(structured) = output.structured {
347 result["structuredContent"] = structured;
348 }
349 Response::result(id, result)
350 }
351
352 Err(error) => {
356 tracing::warn!(tool = name, %error, "mcp tool call failed");
357 Response::result(
358 id,
359 json!({
360 "content": [{
361 "type": "text",
362 "text": error.to_contract().message,
363 }],
364 "isError": true,
365 }),
366 )
367 }
368 }
369 }
370
371 pub async fn handle_line(&self, line: &str) -> Option<Response> {
373 match serde_json::from_str::<Request>(line) {
374 Ok(request) => self.handle(request).await,
375 Err(error) => {
376 let code = if error.is_syntax() || error.is_eof() {
377 codes::PARSE_ERROR
378 } else {
379 codes::INVALID_REQUEST
380 };
381 Some(Response::error(
382 Value::Null,
383 code,
384 format!("malformed request: {error}"),
385 ))
386 }
387 }
388 }
389
390 fn lifecycle(&self) -> std::sync::MutexGuard<'_, Lifecycle> {
391 self.lifecycle
392 .lock()
393 .unwrap_or_else(|error| error.into_inner())
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400 use crate::{AiPermission, AiPermissions, Tool, ToolDescriptor, ToolOutput};
401 use async_trait::async_trait;
402 use origin_domain::Result;
403 use origin_platform::{ConfirmationDecision, ConfirmationRequest, ConfirmationService};
404 use std::sync::Arc;
405
406 #[derive(Debug)]
408 struct TestMutatingTool {
409 descriptor: ToolDescriptor,
410 called: std::sync::Mutex<bool>,
411 }
412
413 impl TestMutatingTool {
414 fn new() -> Self {
415 Self {
416 descriptor: ToolDescriptor::new(
417 "test.mutate",
418 "Test mutation",
419 "A mutating test tool.",
420 AiPermission::Commit,
421 ),
422 called: std::sync::Mutex::new(false),
423 }
424 }
425 }
426
427 #[async_trait]
428 impl Tool for TestMutatingTool {
429 fn descriptor(&self) -> ToolDescriptor {
430 self.descriptor.clone()
431 }
432
433 async fn call(&self, _arguments: serde_json::Value) -> Result<ToolOutput> {
434 *self.called.lock().unwrap() = true;
435 Ok(ToolOutput::text("done"))
436 }
437 }
438
439 #[derive(Debug, Default)]
441 struct AlwaysApproving {
442 requests: std::sync::Mutex<Vec<ConfirmationRequest>>,
443 }
444
445 #[async_trait]
446 impl ConfirmationService for AlwaysApproving {
447 async fn confirm(&self, request: ConfirmationRequest) -> Result<ConfirmationDecision> {
448 self.requests.lock().unwrap().push(request.clone());
449 Ok(ConfirmationDecision::Approved)
450 }
451 }
452
453 async fn initialized_server() -> McpServer {
454 let server = McpServer::new("test", "0.1.0")
455 .with_permissions(AiPermissions::from([
456 AiPermission::Read,
457 AiPermission::Commit,
458 ]))
459 .with_tool(Arc::new(TestMutatingTool::new()));
460
461 let request = Request {
463 jsonrpc: "2.0".to_owned(),
464 id: Some(serde_json::Value::String("1".to_owned())),
465 method: "initialize".to_owned(),
466 params: serde_json::json!({
467 "protocolVersion": "2024-11-05",
468 "capabilities": {},
469 "clientInfo": { "name": "test", "version": "0.1" }
470 }),
471 };
472 let _ = server.handle(request).await;
473 let initialized = Request {
474 jsonrpc: "2.0".to_owned(),
475 id: None,
476 method: "notifications/initialized".to_owned(),
477 params: serde_json::Value::Null,
478 };
479 let _ = server.handle(initialized).await;
480
481 server
482 }
483
484 #[tokio::test]
485 async fn a_mutating_tool_is_denied_when_no_confirmer_is_wired() {
486 let server = initialized_server().await;
487
488 let response = server
489 .handle_line(r#"{"jsonrpc":"2.0","id":"t1","method":"tools/call","params":{"name":"test.mutate","arguments":{}}}"#)
490 .await
491 .expect("must produce a response");
492
493 assert!(response.error.is_none());
494 assert!(
495 response.result.is_some(),
496 "a tool-level error still returns a result with isError:true"
497 );
498 }
499
500 #[tokio::test]
501 async fn a_mutating_tool_is_allowed_when_the_confirmer_approves() {
502 let confirmer = Arc::new(AlwaysApproving::default());
503 let server = initialized_server()
504 .await
505 .with_confirmation(confirmer.clone());
506
507 let response = server
508 .handle_line(
509 r#"{"jsonrpc":"2.0","id":"t2","method":"tools/call","params":{"name":"test.mutate","arguments":{}}}"#,
510 )
511 .await
512 .expect("must produce a response");
513
514 assert!(response.error.is_none());
515 assert_eq!(confirmer.requests.lock().unwrap().len(), 1);
516 }
517}