1use crate::db::AgentDB;
26use serde_json::{json, Value};
27use std::collections::HashMap;
28
29pub struct McpServer {
31 db: AgentDB,
32}
33
34impl McpServer {
35 pub fn new(db: AgentDB) -> Self {
37 Self { db }
38 }
39
40 pub fn handle_message(&self, input: &str) -> Option<String> {
45 let req: Value = match serde_json::from_str(input) {
46 Ok(v) => v,
47 Err(e) => {
48 return Some(
49 json!({
50 "jsonrpc": "2.0",
51 "id": null,
52 "error": { "code": -32700, "message": format!("Parse error: {e}") }
53 })
54 .to_string(),
55 );
56 }
57 };
58
59 let is_notification = !req.get("id").is_some_and(|v| !v.is_null());
60 let id = req.get("id").cloned().unwrap_or(Value::Null);
61 let method = req.get("method").and_then(|m| m.as_str()).unwrap_or("");
62 let params = req.get("params").cloned().unwrap_or(Value::Object(Default::default()));
63
64 if is_notification {
66 match method {
67 "initialized" | "notifications/cancelled" | "notifications/progress" => {}
68 _ => {}
69 }
70 return None;
71 }
72
73 let result = match method {
74 "initialize" => self.handle_initialize(¶ms),
75 "ping" => Ok(json!({})),
76 "tools/list" => self.handle_tools_list(),
77 "tools/call" => self.handle_tools_call(¶ms),
78 "resources/list" => self.handle_resources_list(),
79 "resources/read" => self.handle_resources_read(¶ms),
80 "prompts/list" => self.handle_prompts_list(),
81 "prompts/get" => self.handle_prompts_get(¶ms),
82 _ => Err((-32601, format!("Method not found: {method}"))),
83 };
84
85 Some(match result {
86 Ok(value) => json!({ "jsonrpc": "2.0", "id": id, "result": value }).to_string(),
87 Err((code, msg)) => {
88 json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": msg } })
89 .to_string()
90 }
91 })
92 }
93
94 fn handle_initialize(&self, _params: &Value) -> std::result::Result<Value, (i32, String)> {
95 Ok(json!({
96 "protocolVersion": "2024-11-05",
97 "capabilities": {
98 "tools": { "listChanged": false },
99 "resources": { "subscribe": false, "listChanged": false }
100 },
101 "serverInfo": {
102 "name": "agentdb",
103 "version": env!("CARGO_PKG_VERSION")
104 }
105 }))
106 }
107
108 fn handle_tools_list(&self) -> std::result::Result<Value, (i32, String)> {
109 Ok(json!({ "tools": self.tool_definitions() }))
110 }
111
112 fn handle_tools_call(&self, params: &Value) -> std::result::Result<Value, (i32, String)> {
113 let name = params
114 .get("name")
115 .and_then(|n| n.as_str())
116 .ok_or((-32602, "Missing 'name' parameter".to_string()))?;
117 let arguments = params
118 .get("arguments")
119 .cloned()
120 .unwrap_or(Value::Object(Default::default()));
121
122 let result = self.dispatch_tool(name, &arguments)?;
123
124 Ok(json!({
125 "content": [{
126 "type": "text",
127 "text": result.to_string()
128 }]
129 }))
130 }
131
132 fn handle_resources_list(&self) -> std::result::Result<Value, (i32, String)> {
133 Ok(json!({
134 "resources": [
135 {
136 "uri": "agentdb://stats",
137 "name": "Database Statistics",
138 "description": "Current AgentDB database statistics",
139 "mimeType": "application/json"
140 }
141 ]
142 }))
143 }
144
145 fn handle_resources_read(&self, params: &Value) -> std::result::Result<Value, (i32, String)> {
146 let uri = params
147 .get("uri")
148 .and_then(|u| u.as_str())
149 .ok_or((-32602, "Missing 'uri' parameter".to_string()))?;
150
151 match uri {
152 "agentdb://stats" => {
153 let stats = self
154 .db
155 .stats()
156 .map_err(|e| (-32000, format!("Stats error: {e}")))?;
157 Ok(json!({
158 "contents": [{
159 "uri": "agentdb://stats",
160 "mimeType": "application/json",
161 "text": serde_json::to_string(&stats).unwrap_or_default()
162 }]
163 }))
164 }
165 _ => Err((-32002, format!("Resource not found: {uri}"))),
166 }
167 }
168
169 fn handle_prompts_list(&self) -> std::result::Result<Value, (i32, String)> {
170 let templates = self
171 .db
172 .prompts()
173 .list_templates()
174 .map_err(|e| (-32000, e.to_string()))?;
175 let prompts: Vec<Value> = templates
176 .iter()
177 .map(|t| {
178 json!({
179 "name": t.name,
180 "description": format!("Prompt template '{}' v{}", t.name, t.version),
181 "arguments": [{
182 "name": "vars",
183 "description": "JSON object of template variables for {{placeholder}} substitution",
184 "required": false
185 }]
186 })
187 })
188 .collect();
189 let mut seen = std::collections::HashSet::new();
191 let unique: Vec<Value> = prompts
192 .into_iter()
193 .filter(|p| seen.insert(p["name"].as_str().unwrap_or("").to_string()))
194 .collect();
195 Ok(json!({ "prompts": unique }))
196 }
197
198 fn handle_prompts_get(&self, params: &Value) -> std::result::Result<Value, (i32, String)> {
199 let name = params
200 .get("name")
201 .and_then(|n| n.as_str())
202 .ok_or((-32602, "Missing 'name' parameter".to_string()))?;
203 let args = params.get("arguments").cloned().unwrap_or(Value::Object(Default::default()));
204 let vars: HashMap<String, String> = args
205 .get("vars")
206 .and_then(|v| serde_json::from_value(v.clone()).ok())
207 .unwrap_or_default();
208 let rendered = self
209 .db
210 .prompts()
211 .render(name, &vars)
212 .map_err(|e| (-32000, e.to_string()))?;
213 Ok(json!({
214 "description": format!("Rendered prompt template '{name}'"),
215 "messages": [{
216 "role": "user",
217 "content": { "type": "text", "text": rendered }
218 }]
219 }))
220 }
221
222 fn dispatch_tool(
223 &self,
224 name: &str,
225 args: &Value,
226 ) -> std::result::Result<Value, (i32, String)> {
227 let err = |e: crate::error::AgentDbError| (-32000, e.to_string());
228
229 match name {
230 "execute" => {
231 let sql = get_str(args, "sql")?;
232 let n = self.db.execute(sql).map_err(err)?;
233 Ok(json!({ "rows_affected": n }))
234 }
235 "query" => {
236 let sql = get_str(args, "sql")?;
237 let rows = self.db.query_json(sql).map_err(err)?;
238 Ok(Value::Array(rows))
239 }
240 "vector_upsert" => {
241 let collection = get_str(args, "collection")?;
242 let id = get_str(args, "id")?;
243 let vector: Vec<f32> = args
244 .get("vector")
245 .and_then(|v| serde_json::from_value(v.clone()).ok())
246 .ok_or((-32602, "Missing 'vector' array".to_string()))?;
247 let metadata = args.get("metadata").cloned();
248 let dim = vector.len();
249 let col = self.db.vectors().collection(collection, dim).map_err(err)?;
250 col.upsert(crate::vectors::VectorEntry {
251 id: id.to_string(),
252 vector,
253 metadata,
254 })
255 .map_err(err)?;
256 Ok(json!({ "ok": true }))
257 }
258 "vector_search" => {
259 let collection = get_str(args, "collection")?;
260 let query: Vec<f32> = args
261 .get("query")
262 .and_then(|v| serde_json::from_value(v.clone()).ok())
263 .ok_or((-32602, "Missing 'query' array".to_string()))?;
264 let top_k = args
265 .get("top_k")
266 .and_then(|v| v.as_u64())
267 .unwrap_or(10) as usize;
268 let filter = args.get("filter").cloned();
269 let dim = query.len();
270 let col = self.db.vectors().collection(collection, dim).map_err(err)?;
271 let results = col
272 .search(
273 &query,
274 crate::vectors::SearchOptions {
275 top_k,
276 metric: crate::vectors::DistanceMetric::Cosine,
277 filter,
278 },
279 )
280 .map_err(err)?;
281 let arr: Vec<Value> = results
282 .iter()
283 .map(|r| json!({"id": r.id, "score": r.score, "metadata": r.metadata}))
284 .collect();
285 Ok(Value::Array(arr))
286 }
287 "graph_add_node" => {
288 let id = get_str(args, "id")?;
289 let kind = get_str(args, "kind")?;
290 let data = args.get("data").cloned();
291 self.db.memory().add_node(id, kind, data).map_err(err)?;
292 Ok(json!({ "ok": true }))
293 }
294 "graph_add_edge" => {
295 let src = get_str(args, "src")?;
296 let dst = get_str(args, "dst")?;
297 let relation = get_str(args, "relation")?;
298 let weight = args.get("weight").and_then(|v| v.as_f64()).unwrap_or(1.0);
299 self.db
300 .memory()
301 .add_edge(src, dst, relation, weight)
302 .map_err(err)?;
303 Ok(json!({ "ok": true }))
304 }
305 "graph_neighbors" => {
306 let node_id = get_str(args, "node_id")?;
307 let max_depth = args.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(2) as usize;
308 let min_weight = args.get("min_weight").and_then(|v| v.as_f64()).unwrap_or(0.0);
309 let relation = args.get("relation").and_then(|v| v.as_str());
310 let results = self
311 .db
312 .memory()
313 .neighbors(
314 node_id,
315 crate::memory::TraversalOptions {
316 max_depth,
317 min_weight: Some(min_weight),
318 relation: relation.map(|s| s.to_string()),
319 },
320 )
321 .map_err(err)?;
322 let arr: Vec<Value> = results
323 .iter()
324 .map(|r| {
325 json!({"id": r.node.id, "kind": r.node.kind, "depth": r.depth, "weight": r.weight, "data": r.node.data})
326 })
327 .collect();
328 Ok(Value::Array(arr))
329 }
330 "tool_register" => {
331 let tool_name = get_str(args, "name")?;
332 let description = args.get("description").and_then(|v| v.as_str());
333 let schema = args.get("parameters_schema").cloned();
334 let version = args.get("version").and_then(|v| v.as_str());
335 let id = self
336 .db
337 .tools()
338 .register_tool(tool_name, description, schema, version)
339 .map_err(err)?;
340 Ok(json!({ "id": id }))
341 }
342 "tool_list" => {
343 let tools = self.db.tools().list_tools().map_err(err)?;
344 let arr: Vec<Value> = tools
345 .iter()
346 .map(|t| {
347 json!({
348 "id": t.id, "name": t.name,
349 "description": t.description,
350 "parameters_schema": t.parameters_schema,
351 "version": t.version
352 })
353 })
354 .collect();
355 Ok(Value::Array(arr))
356 }
357 "tool_log_call" => {
358 let tool_name = get_str(args, "tool_name")?;
359 let session_id = args.get("session_id").and_then(|v| v.as_str());
360 let arguments = args.get("arguments").cloned();
361 let result = args.get("result").cloned();
362 let error = args.get("error").and_then(|v| v.as_str());
363 let latency_ms = args.get("latency_ms").and_then(|v| v.as_i64()).unwrap_or(0);
364 let id = self
365 .db
366 .tools()
367 .log_tool_call(session_id, tool_name, arguments, result, error, Some(latency_ms))
368 .map_err(err)?;
369 Ok(json!({ "id": id }))
370 }
371 "audit_log" => {
372 let action = get_str(args, "action")?;
373 let table_name = get_str(args, "table_name")?;
374 let record_id = get_str(args, "record_id")?;
375 let actor = args.get("actor").and_then(|v| v.as_str());
376 let old_value = args.get("old_value").cloned();
377 let new_value = args.get("new_value").cloned();
378 let reason = args.get("reason").and_then(|v| v.as_str());
379 let id = self
380 .db
381 .audit()
382 .log(actor, action, table_name, record_id, old_value, new_value, reason)
383 .map_err(err)?;
384 Ok(json!({ "id": id }))
385 }
386 "audit_query_recent" => {
387 let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
388 let entries = self.db.audit().query_recent(Some(limit)).map_err(err)?;
389 let arr: Vec<Value> = entries
390 .iter()
391 .map(|e| {
392 json!({
393 "id": e.id, "timestamp": e.timestamp, "actor": e.actor,
394 "action": e.action, "table_name": e.table_name,
395 "record_id": e.record_id, "reason": e.reason
396 })
397 })
398 .collect();
399 Ok(Value::Array(arr))
400 }
401 "context_add" => {
402 let session_id = get_str(args, "session_id")?;
403 let source_type = get_str(args, "source_type")?;
404 let source_id = get_str(args, "source_id")?;
405 let content_preview = args.get("content_preview").and_then(|v| v.as_str());
406 let token_count = args
407 .get("token_count")
408 .and_then(|v| v.as_i64())
409 .ok_or((-32602, "Missing 'token_count'".to_string()))?;
410 let relevance_score = args
411 .get("relevance_score")
412 .and_then(|v| v.as_f64())
413 .unwrap_or(0.5);
414 let priority = args.get("priority").and_then(|v| v.as_i64()).unwrap_or(0);
415 let id = self
416 .db
417 .context()
418 .add_entry(
419 session_id,
420 source_type,
421 source_id,
422 content_preview,
423 token_count,
424 relevance_score,
425 priority,
426 )
427 .map_err(err)?;
428 Ok(json!({ "id": id }))
429 }
430 "context_build_window" => {
431 let session_id = get_str(args, "session_id")?;
432 let max_tokens = args
433 .get("max_tokens")
434 .and_then(|v| v.as_i64())
435 .ok_or((-32602, "Missing 'max_tokens'".to_string()))?;
436 let entries = self
437 .db
438 .context()
439 .build_window(session_id, max_tokens)
440 .map_err(err)?;
441 let arr: Vec<Value> = entries
442 .iter()
443 .map(|e| {
444 json!({
445 "id": e.id, "source_type": e.source_type,
446 "source_id": e.source_id, "content_preview": e.content_preview,
447 "token_count": e.token_count, "priority": e.priority
448 })
449 })
450 .collect();
451 Ok(Value::Array(arr))
452 }
453 "context_clear" => {
454 let session_id = get_str(args, "session_id")?;
455 self.db.context().clear_session(session_id).map_err(err)?;
456 Ok(json!({ "ok": true }))
457 }
458 "prompt_create" => {
459 let name = get_str(args, "name")?;
460 let template = get_str(args, "template")?;
461 let model_hint = args.get("model_hint").and_then(|v| v.as_str());
462 let max_tokens = args.get("max_tokens").and_then(|v| v.as_i64());
463 let metadata = args.get("metadata").cloned();
464 let id = self
465 .db
466 .prompts()
467 .create_template(name, template, model_hint, max_tokens, metadata)
468 .map_err(err)?;
469 Ok(json!({ "id": id }))
470 }
471 "prompt_render" => {
472 let name = get_str(args, "name")?;
473 let vars: HashMap<String, String> = args
474 .get("vars")
475 .and_then(|v| serde_json::from_value(v.clone()).ok())
476 .unwrap_or_default();
477 let rendered = self.db.prompts().render(name, &vars).map_err(err)?;
478 Ok(json!({ "text": rendered }))
479 }
480 "label_tag" => {
481 let table_name = get_str(args, "table_name")?;
482 let record_id = get_str(args, "record_id")?;
483 let label = get_str(args, "label")?;
484 let tagged_by = args.get("tagged_by").and_then(|v| v.as_str());
485 self.db
486 .labels()
487 .tag(table_name, record_id, label, tagged_by)
488 .map_err(err)?;
489 Ok(json!({ "ok": true }))
490 }
491 "label_untag" => {
492 let table_name = get_str(args, "table_name")?;
493 let record_id = get_str(args, "record_id")?;
494 let label = get_str(args, "label")?;
495 self.db
496 .labels()
497 .untag(table_name, record_id, label)
498 .map_err(err)?;
499 Ok(json!({ "ok": true }))
500 }
501 "label_get" => {
502 let table_name = get_str(args, "table_name")?;
503 let record_id = get_str(args, "record_id")?;
504 let labels = self
505 .db
506 .labels()
507 .get_labels(table_name, record_id)
508 .map_err(err)?;
509 let arr: Vec<Value> = labels
510 .iter()
511 .map(|l| {
512 json!({
513 "label": l.label, "tagged_by": l.tagged_by,
514 "tagged_at": l.tagged_at
515 })
516 })
517 .collect();
518 Ok(Value::Array(arr))
519 }
520 "label_has" => {
521 let table_name = get_str(args, "table_name")?;
522 let record_id = get_str(args, "record_id")?;
523 let label = get_str(args, "label")?;
524 let has = self
525 .db
526 .labels()
527 .has_label(table_name, record_id, label)
528 .map_err(err)?;
529 Ok(json!({ "has": has }))
530 }
531 "stats" => {
532 let stats = self.db.stats().map_err(err)?;
533 Ok(json!({
534 "collections": stats.collections,
535 "vectors": stats.vectors,
536 "nodes": stats.nodes,
537 "edges": stats.edges,
538 "conversations": stats.conversations,
539 "messages": stats.messages,
540 "workflows": stats.workflows,
541 "workflow_steps": stats.workflow_steps,
542 "traces": stats.traces,
543 "tools": stats.tools,
544 "tool_calls": stats.tool_calls,
545 "audit_entries": stats.audit_entries,
546 "prompt_templates": stats.prompt_templates
547 }))
548 }
549 _ => Err((-32601, format!("Unknown tool: {name}"))),
550 }
551 }
552
553 fn tool_definitions(&self) -> Value {
554 json!([
555 tool_def("execute", "Execute a raw SQL statement (DDL/DML)", json!({
556 "type": "object",
557 "properties": { "sql": { "type": "string", "description": "SQL statement" } },
558 "required": ["sql"]
559 })),
560 tool_def("query", "Execute a SELECT and return rows as JSON", json!({
561 "type": "object",
562 "properties": { "sql": { "type": "string", "description": "SELECT statement" } },
563 "required": ["sql"]
564 })),
565 tool_def("vector_upsert", "Insert or update a vector embedding", json!({
566 "type": "object",
567 "properties": {
568 "collection": { "type": "string" },
569 "id": { "type": "string" },
570 "vector": { "type": "array", "items": { "type": "number" } },
571 "metadata": { "type": "object" }
572 },
573 "required": ["collection", "id", "vector"]
574 })),
575 tool_def("vector_search", "Approximate nearest-neighbor search", json!({
576 "type": "object",
577 "properties": {
578 "collection": { "type": "string" },
579 "query": { "type": "array", "items": { "type": "number" } },
580 "top_k": { "type": "integer", "default": 10 },
581 "filter": { "type": "object" }
582 },
583 "required": ["collection", "query"]
584 })),
585 tool_def("graph_add_node", "Add or update a memory graph node", json!({
586 "type": "object",
587 "properties": {
588 "id": { "type": "string" },
589 "kind": { "type": "string" },
590 "data": { "type": "object" }
591 },
592 "required": ["id", "kind"]
593 })),
594 tool_def("graph_add_edge", "Add or update a directed graph edge", json!({
595 "type": "object",
596 "properties": {
597 "src": { "type": "string" },
598 "dst": { "type": "string" },
599 "relation": { "type": "string" },
600 "weight": { "type": "number", "default": 1.0 }
601 },
602 "required": ["src", "dst", "relation"]
603 })),
604 tool_def("graph_neighbors", "Traverse the memory graph from a node", json!({
605 "type": "object",
606 "properties": {
607 "node_id": { "type": "string" },
608 "max_depth": { "type": "integer", "default": 2 },
609 "min_weight": { "type": "number", "default": 0.0 },
610 "relation": { "type": "string" }
611 },
612 "required": ["node_id"]
613 })),
614 tool_def("tool_register", "Register or update a tool definition", json!({
615 "type": "object",
616 "properties": {
617 "name": { "type": "string" },
618 "description": { "type": "string" },
619 "parameters_schema": { "type": "object" },
620 "version": { "type": "string" }
621 },
622 "required": ["name"]
623 })),
624 tool_def("tool_list", "List all registered tools", json!({
625 "type": "object", "properties": {}
626 })),
627 tool_def("tool_log_call", "Log a tool invocation", json!({
628 "type": "object",
629 "properties": {
630 "tool_name": { "type": "string" },
631 "session_id": { "type": "string" },
632 "arguments": { "type": "object" },
633 "result": { "type": "object" },
634 "error": { "type": "string" },
635 "latency_ms": { "type": "integer" }
636 },
637 "required": ["tool_name"]
638 })),
639 tool_def("audit_log", "Append an entry to the audit log", json!({
640 "type": "object",
641 "properties": {
642 "action": { "type": "string" },
643 "table_name": { "type": "string" },
644 "record_id": { "type": "string" },
645 "actor": { "type": "string" },
646 "old_value": { "type": "object" },
647 "new_value": { "type": "object" },
648 "reason": { "type": "string" }
649 },
650 "required": ["action", "table_name", "record_id"]
651 })),
652 tool_def("audit_query_recent", "Query recent audit log entries", json!({
653 "type": "object",
654 "properties": { "limit": { "type": "integer", "default": 100 } }
655 })),
656 tool_def("context_add", "Add an entry to the context window", json!({
657 "type": "object",
658 "properties": {
659 "session_id": { "type": "string" },
660 "source_type": { "type": "string" },
661 "source_id": { "type": "string" },
662 "content_preview": { "type": "string" },
663 "token_count": { "type": "integer" },
664 "relevance_score": { "type": "number" },
665 "priority": { "type": "integer" }
666 },
667 "required": ["session_id", "source_type", "source_id", "token_count"]
668 })),
669 tool_def("context_build_window", "Build a token-budgeted context window", json!({
670 "type": "object",
671 "properties": {
672 "session_id": { "type": "string" },
673 "max_tokens": { "type": "integer" }
674 },
675 "required": ["session_id", "max_tokens"]
676 })),
677 tool_def("context_clear", "Clear all context entries for a session", json!({
678 "type": "object",
679 "properties": { "session_id": { "type": "string" } },
680 "required": ["session_id"]
681 })),
682 tool_def("prompt_create", "Create a new prompt template version", json!({
683 "type": "object",
684 "properties": {
685 "name": { "type": "string" },
686 "template": { "type": "string" },
687 "model_hint": { "type": "string" },
688 "max_tokens": { "type": "integer" },
689 "metadata": { "type": "object" }
690 },
691 "required": ["name", "template"]
692 })),
693 tool_def("prompt_render", "Render a prompt template with variables", json!({
694 "type": "object",
695 "properties": {
696 "name": { "type": "string" },
697 "vars": { "type": "object", "additionalProperties": { "type": "string" } }
698 },
699 "required": ["name"]
700 })),
701 tool_def("label_tag", "Tag a record with a classification label", json!({
702 "type": "object",
703 "properties": {
704 "table_name": { "type": "string" },
705 "record_id": { "type": "string" },
706 "label": { "type": "string" },
707 "tagged_by": { "type": "string" }
708 },
709 "required": ["table_name", "record_id", "label"]
710 })),
711 tool_def("label_untag", "Remove a label from a record", json!({
712 "type": "object",
713 "properties": {
714 "table_name": { "type": "string" },
715 "record_id": { "type": "string" },
716 "label": { "type": "string" }
717 },
718 "required": ["table_name", "record_id", "label"]
719 })),
720 tool_def("label_get", "Get all labels for a record", json!({
721 "type": "object",
722 "properties": {
723 "table_name": { "type": "string" },
724 "record_id": { "type": "string" }
725 },
726 "required": ["table_name", "record_id"]
727 })),
728 tool_def("label_has", "Check if a record has a specific label", json!({
729 "type": "object",
730 "properties": {
731 "table_name": { "type": "string" },
732 "record_id": { "type": "string" },
733 "label": { "type": "string" }
734 },
735 "required": ["table_name", "record_id", "label"]
736 })),
737 tool_def("stats", "Get database-wide statistics", json!({
738 "type": "object", "properties": {}
739 })),
740 ])
741 }
742}
743
744fn tool_def(name: &str, description: &str, input_schema: Value) -> Value {
745 json!({
746 "name": name,
747 "description": description,
748 "inputSchema": input_schema
749 })
750}
751
752fn get_str<'a>(args: &'a Value, key: &str) -> std::result::Result<&'a str, (i32, String)> {
753 args.get(key)
754 .and_then(|v| v.as_str())
755 .ok_or((-32602, format!("Missing required parameter: '{key}'")))
756}