1use crate::{
34 sigil_envelope::{SigilEnvelope, SigilKeypair, Verdict},
35 AuditEvent, AuditEventType, AuditLogger, SensitivityScanner, TrustLevel,
36};
37use serde::{Deserialize, Serialize};
38use std::collections::HashMap;
39use std::future::Future;
40use std::pin::Pin;
41use std::sync::Arc;
42
43#[derive(Debug, Deserialize, Default)]
47pub struct InboundSigil {
48 pub identity: Option<String>,
49 pub verdict: Option<String>,
50 pub signature: Option<String>,
51 pub nonce: Option<String>,
52 pub timestamp: Option<String>,
53}
54
55#[derive(Debug, Deserialize)]
58pub struct JsonRpcRequest {
59 pub jsonrpc: String,
60 pub id: Option<serde_json::Value>,
61 pub method: String,
62 #[serde(default)]
63 pub params: serde_json::Value,
64}
65
66#[derive(Debug, Serialize)]
67pub struct JsonRpcResponse {
68 pub jsonrpc: String,
69 pub id: serde_json::Value,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 pub result: Option<serde_json::Value>,
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub error: Option<JsonRpcError>,
74}
75
76#[derive(Debug, Serialize)]
77pub struct JsonRpcError {
78 pub code: i32,
79 pub message: String,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub data: Option<serde_json::Value>,
82}
83
84pub type ToolHandler = Box<
88 dyn Fn(serde_json::Value) -> Pin<Box<dyn Future<Output = anyhow::Result<serde_json::Value>> + Send>>
89 + Send
90 + Sync,
91>;
92
93pub struct ToolDef {
95 pub name: String,
96 pub description: String,
97 pub parameters_schema: serde_json::Value,
98 pub handler: ToolHandler,
99}
100
101pub struct SigilMcpServer<S: SensitivityScanner, A: AuditLogger> {
112 name: String,
113 version: String,
114 tools: HashMap<String, ToolEntry>,
115 scanner: Arc<S>,
116 audit: Arc<A>,
117 required_trust: TrustLevel,
119 keypair: Option<Arc<SigilKeypair>>,
122 did: String,
124}
125
126struct ToolEntry {
127 description: String,
128 schema: serde_json::Value,
129 handler: ToolHandler,
130 required_trust: Option<TrustLevel>,
132}
133
134impl<S: SensitivityScanner, A: AuditLogger> SigilMcpServer<S, A> {
135 pub fn new(name: &str, version: &str, scanner: Arc<S>, audit: Arc<A>) -> Self {
137 Self {
138 name: name.to_string(),
139 version: version.to_string(),
140 tools: HashMap::new(),
141 scanner,
142 audit,
143 required_trust: TrustLevel::Low,
144 keypair: None,
145 did: format!("did:sigil:{}", name.to_lowercase().replace(' ', "_")),
146 }
147 }
148
149 pub fn new_with_keypair(
151 name: &str,
152 version: &str,
153 scanner: Arc<S>,
154 audit: Arc<A>,
155 keypair: SigilKeypair,
156 did: &str,
157 ) -> Self {
158 Self {
159 name: name.to_string(),
160 version: version.to_string(),
161 tools: HashMap::new(),
162 scanner,
163 audit,
164 required_trust: TrustLevel::Low,
165 keypair: Some(Arc::new(keypair)),
166 did: did.to_string(),
167 }
168 }
169
170 pub fn verifying_key(&self) -> Option<String> {
172 self.keypair.as_ref().map(|kp| kp.verifying_key_base64())
173 }
174
175 pub fn set_required_trust(&mut self, level: TrustLevel) {
177 self.required_trust = level;
178 }
179
180 pub fn register_tool(&mut self, tool: ToolDef) {
182 self.tools.insert(
183 tool.name.clone(),
184 ToolEntry {
185 description: tool.description,
186 schema: tool.parameters_schema,
187 handler: tool.handler,
188 required_trust: None,
189 },
190 );
191 }
192
193 pub fn register_tool_with_trust(&mut self, tool: ToolDef, trust: TrustLevel) {
195 self.tools.insert(
196 tool.name.clone(),
197 ToolEntry {
198 description: tool.description,
199 schema: tool.parameters_schema,
200 handler: tool.handler,
201 required_trust: Some(trust),
202 },
203 );
204 }
205
206 pub async fn handle_request(
212 &self,
213 request: &str,
214 caller_trust: TrustLevel,
215 ) -> String {
216 let req: JsonRpcRequest = match serde_json::from_str(request) {
217 Ok(r) => r,
218 Err(e) => {
219 return serde_json::to_string(&JsonRpcResponse {
220 jsonrpc: "2.0".into(),
221 id: serde_json::Value::Null,
222 result: None,
223 error: Some(JsonRpcError {
224 code: -32700,
225 message: format!("Parse error: {e}"),
226 data: None,
227 }),
228 })
229 .unwrap_or_default();
230 }
231 };
232
233 let id = req.id.clone().unwrap_or(serde_json::Value::Null);
234
235 let response = match req.method.as_str() {
236 "initialize" => self.handle_initialize(&id),
237 "tools/list" => self.handle_tools_list(&id),
238 "tools/call" => self.handle_tools_call(&id, req.params, caller_trust).await,
239 _ => JsonRpcResponse {
240 jsonrpc: "2.0".into(),
241 id,
242 result: None,
243 error: Some(JsonRpcError {
244 code: -32601,
245 message: format!("Method not found: {}", req.method),
246 data: None,
247 }),
248 },
249 };
250
251 serde_json::to_string(&response).unwrap_or_default()
252 }
253
254 fn handle_initialize(&self, id: &serde_json::Value) -> JsonRpcResponse {
255 JsonRpcResponse {
256 jsonrpc: "2.0".into(),
257 id: id.clone(),
258 result: Some(serde_json::json!({
259 "protocolVersion": "2024-11-05",
260 "serverInfo": {
261 "name": self.name,
262 "version": self.version,
263 },
264 "capabilities": {
265 "tools": { "listChanged": false },
266 },
267 "sigil": {
268 "version": "0.1.0",
269 "requiredTrust": format!("{:?}", self.required_trust),
270 }
271 })),
272 error: None,
273 }
274 }
275
276 fn handle_tools_list(&self, id: &serde_json::Value) -> JsonRpcResponse {
277 let tools: Vec<serde_json::Value> = self
278 .tools
279 .iter()
280 .map(|(name, entry)| {
281 serde_json::json!({
282 "name": name,
283 "description": entry.description,
284 "inputSchema": entry.schema,
285 })
286 })
287 .collect();
288
289 JsonRpcResponse {
290 jsonrpc: "2.0".into(),
291 id: id.clone(),
292 result: Some(serde_json::json!({ "tools": tools })),
293 error: None,
294 }
295 }
296
297 async fn handle_tools_call(
298 &self,
299 id: &serde_json::Value,
300 params: serde_json::Value,
301 caller_trust: TrustLevel,
302 ) -> JsonRpcResponse {
303 let tool_name = params
304 .get("name")
305 .and_then(|v| v.as_str())
306 .unwrap_or("")
307 .to_string();
308
309 let arguments = params
310 .get("arguments")
311 .cloned()
312 .unwrap_or(serde_json::json!({}));
313
314 let inbound_sigil = params
316 .get("_sigil")
317 .and_then(|v| serde_json::from_value::<InboundSigil>(v.clone()).ok());
318
319 if let Some(ref sig) = inbound_sigil {
320 if let (Some(identity), Some(nonce)) = (&sig.identity, &sig.nonce) {
321 let _ = self.audit.log(
322 &AuditEvent::new(AuditEventType::McpToolGated).with_action(
323 format!("Inbound _sigil: identity={identity} nonce={nonce}"),
324 "low".into(),
325 true,
326 true,
327 ),
328 );
329 }
330 }
331
332 let entry = match self.tools.get(&tool_name) {
334 Some(e) => e,
335 None => {
336 return JsonRpcResponse {
337 jsonrpc: "2.0".into(),
338 id: id.clone(),
339 result: None,
340 error: Some(JsonRpcError {
341 code: -32602,
342 message: format!("Unknown tool: {tool_name}"),
343 data: None,
344 }),
345 };
346 }
347 };
348
349 let required = entry.required_trust.unwrap_or(self.required_trust);
351 if (caller_trust as u8) < (required as u8) {
352 let _ = self.audit.log(&AuditEvent::new(AuditEventType::PolicyViolation).with_action(
353 format!("Trust gate: {tool_name} requires {required:?}, caller has {caller_trust:?}"),
354 "high".into(),
355 false,
356 false,
357 ));
358 return JsonRpcResponse {
359 jsonrpc: "2.0".into(),
360 id: id.clone(),
361 result: None,
362 error: Some(JsonRpcError {
363 code: -32001,
364 message: format!(
365 "SIGIL trust gate: tool '{tool_name}' requires {required:?} trust"
366 ),
367 data: None,
368 }),
369 };
370 }
371
372 let args_str = serde_json::to_string(&arguments).unwrap_or_default();
374 let input_scan = self.scanner.scan(&args_str);
375 if input_scan.is_some() {
376 let _ = self.audit.log(&AuditEvent::new(AuditEventType::SigilInterception).with_action(
377 format!("Input scan: secrets detected in {tool_name} arguments"),
378 "high".into(),
379 true,
380 false,
381 ));
382 }
383
384 let result = (entry.handler)(arguments).await;
386
387 match result {
388 Ok(output) => {
389 let output_str = serde_json::to_string(&output).unwrap_or_default();
391 let output_scan = self.scanner.scan(&output_str);
392
393 let _ = self.audit.log(&AuditEvent::new(AuditEventType::McpToolGated).with_action(
394 format!(
395 "MCP tool {tool_name}: input_secrets={}, output_secrets={}",
396 input_scan.is_some(),
397 output_scan.is_some()
398 ),
399 "low".into(),
400 true,
401 true,
402 ));
403
404 let verdict = if output_scan.is_some() {
406 Verdict::Scanned
407 } else {
408 Verdict::Allowed
409 };
410 let reason = output_scan.clone().map(|cat| {
411 format!("Outbound sensitivity scan detected: {cat}")
412 });
413 let sigil_envelope = self.keypair.as_ref().and_then(|kp| {
414 SigilEnvelope::sign(&self.did, verdict, reason, kp).ok()
415 });
416
417 let mut result_obj = serde_json::json!({
418 "content": [{
419 "type": "text",
420 "text": output_str,
421 }],
422 "isError": false,
423 "sigil": {
424 "inputSecrets": input_scan.is_some(),
425 "outputSecrets": output_scan.is_some(),
426 }
427 });
428
429 if let Some(envelope) = sigil_envelope {
431 result_obj["_sigil"] = serde_json::to_value(&envelope).unwrap_or_default();
432 }
433
434 JsonRpcResponse {
435 jsonrpc: "2.0".into(),
436 id: id.clone(),
437 result: Some(result_obj),
438 error: None,
439 }
440 }
441 Err(e) => JsonRpcResponse {
442 jsonrpc: "2.0".into(),
443 id: id.clone(),
444 result: Some(serde_json::json!({
445 "content": [{
446 "type": "text",
447 "text": format!("Error: {e}"),
448 }],
449 "isError": true,
450 })),
451 error: None,
452 },
453 }
454 }
455}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 struct TestScanner;
463 impl SensitivityScanner for TestScanner {
464 fn scan(&self, text: &str) -> Option<String> {
465 if text.contains("sk-") {
466 Some("OpenAI Key".into())
467 } else {
468 None
469 }
470 }
471 }
472
473 struct TestAudit {
475 log_count: std::sync::atomic::AtomicU32,
476 }
477 impl TestAudit {
478 fn new() -> Self {
479 Self {
480 log_count: std::sync::atomic::AtomicU32::new(0),
481 }
482 }
483 fn count(&self) -> u32 {
484 self.log_count.load(std::sync::atomic::Ordering::SeqCst)
485 }
486 }
487 impl AuditLogger for TestAudit {
488 fn log(&self, _event: &AuditEvent) -> anyhow::Result<()> {
489 self.log_count
490 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
491 Ok(())
492 }
493 }
494
495 fn make_server() -> SigilMcpServer<TestScanner, TestAudit> {
496 let scanner = Arc::new(TestScanner);
497 let audit = Arc::new(TestAudit::new());
498 let mut server = SigilMcpServer::new("test-server", "0.1.0", scanner, audit);
499
500 server.register_tool(ToolDef {
501 name: "echo".into(),
502 description: "Echo input back".into(),
503 parameters_schema: serde_json::json!({"type": "object"}),
504 handler: Box::new(|args| {
505 Box::pin(async move { Ok(args) })
506 }),
507 });
508
509 server.register_tool_with_trust(
510 ToolDef {
511 name: "admin_reset".into(),
512 description: "Dangerous admin operation".into(),
513 parameters_schema: serde_json::json!({"type": "object"}),
514 handler: Box::new(|_| {
515 Box::pin(async move { Ok(serde_json::json!({"status": "reset"})) })
516 }),
517 },
518 TrustLevel::High,
519 );
520
521 server
522 }
523
524 #[tokio::test]
525 async fn initialize_returns_server_info() {
526 let server = make_server();
527 let req = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
528 let resp = server.handle_request(req, TrustLevel::Low).await;
529 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
530 assert_eq!(parsed["result"]["serverInfo"]["name"], "test-server");
531 assert!(parsed["result"]["sigil"].is_object());
532 }
533
534 #[tokio::test]
535 async fn tools_list_returns_registered_tools() {
536 let server = make_server();
537 let req = r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#;
538 let resp = server.handle_request(req, TrustLevel::Low).await;
539 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
540 let tools = parsed["result"]["tools"].as_array().unwrap();
541 assert_eq!(tools.len(), 2);
542 let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
543 assert!(names.contains(&"echo"));
544 assert!(names.contains(&"admin_reset"));
545 }
546
547 #[tokio::test]
548 async fn tools_call_echo_succeeds() {
549 let server = make_server();
550 let req = r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"message":"hello"}}}"#;
551 let resp = server.handle_request(req, TrustLevel::Low).await;
552 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
553 assert!(parsed["result"]["content"][0]["text"]
554 .as_str()
555 .unwrap()
556 .contains("hello"));
557 assert_eq!(parsed["result"]["isError"], false);
558 }
559
560 #[tokio::test]
561 async fn tools_call_unknown_tool_returns_error() {
562 let server = make_server();
563 let req = r#"{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"nonexistent","arguments":{}}}"#;
564 let resp = server.handle_request(req, TrustLevel::Low).await;
565 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
566 assert!(parsed["error"]["message"]
567 .as_str()
568 .unwrap()
569 .contains("Unknown tool"));
570 }
571
572 #[tokio::test]
573 async fn trust_gate_blocks_low_trust_from_high_trust_tool() {
574 let server = make_server();
575 let req = r#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"admin_reset","arguments":{}}}"#;
576 let resp = server.handle_request(req, TrustLevel::Low).await;
577 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
578 assert!(parsed["error"]["message"]
579 .as_str()
580 .unwrap()
581 .contains("trust gate"));
582 }
583
584 #[tokio::test]
585 async fn trust_gate_allows_high_trust_for_high_trust_tool() {
586 let server = make_server();
587 let req = r#"{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"admin_reset","arguments":{}}}"#;
588 let resp = server.handle_request(req, TrustLevel::High).await;
589 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
590 assert!(parsed["error"].is_null());
591 assert!(parsed["result"]["content"][0]["text"]
592 .as_str()
593 .unwrap()
594 .contains("reset"));
595 }
596
597 #[tokio::test]
598 async fn sigil_scan_detects_secrets_in_arguments() {
599 let server = make_server();
600 let req = r#"{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"echo","arguments":{"key":"sk-abc123def456"}}}"#;
601 let resp = server.handle_request(req, TrustLevel::Low).await;
602 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
603 assert_eq!(parsed["result"]["sigil"]["inputSecrets"], true);
605 assert!(server.audit.count() >= 2);
607 }
608
609 #[tokio::test]
610 async fn sigil_scan_no_secrets_in_clean_input() {
611 let server = make_server();
612 let req = r#"{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"echo","arguments":{"message":"safe text"}}}"#;
613 let resp = server.handle_request(req, TrustLevel::Low).await;
614 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
615 assert_eq!(parsed["result"]["sigil"]["inputSecrets"], false);
616 assert_eq!(parsed["result"]["sigil"]["outputSecrets"], false);
617 }
618
619 #[tokio::test]
620 async fn invalid_json_returns_parse_error() {
621 let server = make_server();
622 let resp = server.handle_request("not json", TrustLevel::Low).await;
623 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
624 assert_eq!(parsed["error"]["code"], -32700);
625 }
626
627 #[tokio::test]
628 async fn unknown_method_returns_method_not_found() {
629 let server = make_server();
630 let req = r#"{"jsonrpc":"2.0","id":10,"method":"resources/list","params":{}}"#;
631 let resp = server.handle_request(req, TrustLevel::Low).await;
632 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
633 assert_eq!(parsed["error"]["code"], -32601);
634 }
635
636 #[tokio::test]
637 async fn audit_logged_for_every_tool_call() {
638 let server = make_server();
639 let req = r#"{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"echo","arguments":{"msg":"hi"}}}"#;
640 let before = server.audit.count();
641 server.handle_request(req, TrustLevel::Low).await;
642 let after = server.audit.count();
643 assert!(after > before, "Audit log should record tool invocation");
644 }
645
646 #[tokio::test]
647 async fn signed_server_embeds_sigil_envelope_in_response() {
648 use crate::sigil_envelope::{SigilEnvelope, SigilKeypair};
649
650 let keypair = SigilKeypair::generate();
651 let verifying_key = keypair.verifying_key_base64();
652 let scanner = Arc::new(TestScanner);
653 let audit = Arc::new(TestAudit::new());
654
655 let mut server = SigilMcpServer::new_with_keypair(
656 "signed-server",
657 "0.1.0",
658 scanner,
659 audit,
660 keypair,
661 "did:sigil:signed_server",
662 );
663 server.register_tool(ToolDef {
664 name: "ping".into(),
665 description: "Returns pong".into(),
666 parameters_schema: serde_json::json!({"type": "object"}),
667 handler: Box::new(|_| {
668 Box::pin(async move { Ok(serde_json::json!({"pong": true})) })
669 }),
670 });
671
672 let req = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ping","arguments":{}}}"#;
673 let resp = server.handle_request(req, TrustLevel::Low).await;
674 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
675
676 let sigil = &parsed["result"]["_sigil"];
678 assert!(sigil.is_object(), "_sigil must be present in signed response");
679 assert_eq!(sigil["identity"], "did:sigil:signed_server");
680 assert_eq!(sigil["verdict"], "allowed");
681 assert!(sigil["signature"].is_string(), "Signature must be present");
682 assert!(sigil["nonce"].is_string(), "Nonce must be present");
683
684 let envelope: SigilEnvelope = serde_json::from_value(sigil.clone()).unwrap();
686 assert!(
687 envelope.verify(&verifying_key).unwrap(),
688 "Outbound _sigil signature must verify against server public key"
689 );
690 }
691
692 #[tokio::test]
693 async fn unsigned_server_works_without_sigil_envelope() {
694 let server = make_server();
696 let req = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{"x":1}}}"#;
697 let resp = server.handle_request(req, TrustLevel::Low).await;
698 let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
699 assert!(parsed["error"].is_null());
701 assert!(parsed["result"]["_sigil"].is_null());
702 }
703}