Skip to main content

miku/
mcp.rs

1use anyhow::Result;
2use libsql::Connection;
3use serde::{Deserialize, Serialize};
4
5use crate::db;
6
7// ── JSON-RPC wire types ──────────────────────────────────────────────────────
8
9#[derive(Debug, Deserialize, Serialize)]
10pub struct JsonRpcRequest {
11    pub jsonrpc: String,
12    #[serde(default)]
13    pub id: serde_json::Value,
14    pub method: String,
15    pub params: Option<serde_json::Value>,
16}
17
18#[derive(Debug, Serialize)]
19pub struct JsonRpcResponse {
20    pub jsonrpc: String,
21    pub id: serde_json::Value,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub result: Option<serde_json::Value>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub error: Option<serde_json::Value>,
26}
27
28// ── Domain types (shared with db.rs callers) ────────────────────────────────
29
30#[derive(Debug, Deserialize, Serialize, Clone)]
31#[serde(rename_all = "camelCase")]
32pub struct EntityInput {
33    pub name: String,
34    pub entity_type: String,
35    pub observations: Vec<String>,
36}
37
38#[derive(Debug, Deserialize, Serialize, Clone)]
39#[serde(rename_all = "camelCase")]
40pub struct RelationInput {
41    pub from: String,
42    pub to: String,
43    pub relation_type: String,
44}
45
46#[derive(Debug, Deserialize, Clone)]
47#[serde(rename_all = "camelCase")]
48pub struct ObservationInput {
49    pub entity_name: String,
50    pub contents: Vec<String>,
51}
52
53#[derive(Debug, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct ObservationDeletion {
56    pub entity_name: String,
57    pub observations: Vec<String>,
58}
59
60#[derive(Debug, Serialize, Deserialize, Clone)]
61#[serde(rename_all = "camelCase")]
62pub struct EntityOutput {
63    pub name: String,
64    pub entity_type: String,
65    #[serde(skip_serializing_if = "Vec::is_empty", default)]
66    pub observations: Vec<String>,
67    pub truths: std::collections::BTreeMap<String, String>,
68    pub observation_count: usize,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub body: Option<String>,
71}
72
73#[derive(Debug, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct Graph {
76    pub entities: Vec<EntityOutput>,
77    pub relations: Vec<RelationInput>,
78}
79
80// ── MCP params structs ───────────────────────────────────────────────────────
81
82#[derive(Debug, Deserialize)]
83struct CreateEntitiesParams {
84    entities: Vec<EntityInput>,
85}
86
87#[derive(Debug, Deserialize)]
88struct CreateRelationsParams {
89    relations: Vec<RelationInput>,
90}
91
92#[derive(Debug, Deserialize)]
93struct AddObservationsParams {
94    observations: Vec<ObservationInput>,
95}
96
97#[derive(Debug, Deserialize)]
98#[serde(rename_all = "camelCase")]
99struct DeleteEntitiesParams {
100    entity_names: Vec<String>,
101}
102
103#[derive(Debug, Deserialize)]
104struct DeleteObservationsParams {
105    deletions: Vec<ObservationDeletion>,
106}
107
108#[derive(Debug, Deserialize)]
109struct DeleteRelationsParams {
110    relations: Vec<RelationInput>,
111}
112
113#[derive(Debug, Deserialize)]
114struct SearchNodesParams {
115    query: String,
116    limit: Option<usize>,
117}
118
119#[derive(Debug, Deserialize)]
120struct OpenNodesParams {
121    names: Vec<String>,
122}
123
124#[derive(Debug, Deserialize)]
125#[serde(rename_all = "camelCase")]
126struct AddTruthParams {
127    entity_name: String,
128    key: String,
129    value: String,
130}
131
132#[derive(Debug, Deserialize)]
133#[serde(rename_all = "camelCase")]
134struct DeleteTruthParams {
135    entity_name: String,
136    key: String,
137}
138
139// ── Server ───────────────────────────────────────────────────────────────────
140
141pub async fn run_server(conn: Connection) -> Result<()> {
142    use std::io::{self, BufRead};
143    let stdin = io::stdin();
144    let mut reader = stdin.lock();
145    let mut line = String::new();
146
147    while reader.read_line(&mut line)? > 0 {
148        let trimmed = line.trim();
149        if trimmed.is_empty() {
150            line.clear();
151            continue;
152        }
153
154        let req: JsonRpcRequest = match serde_json::from_str(trimmed) {
155            Ok(r) => r,
156            Err(_) => {
157                line.clear();
158                continue;
159            }
160        };
161
162        // JSON-RPC notifications have no id (or null id) — do not respond.
163        let is_notification = req.id.is_null();
164        let req_id = req.id.clone();
165
166        let response = match dispatch(&conn, req).await {
167            Ok(res) => res,
168            Err(e) => JsonRpcResponse {
169                jsonrpc: "2.0".to_string(),
170                id: req_id,
171                result: None,
172                error: Some(serde_json::json!({
173                    "code": -32603,
174                    "message": e.to_string()
175                })),
176            },
177        };
178
179        if !is_notification {
180            println!("{}", serde_json::to_string(&response)?);
181        }
182        line.clear();
183    }
184    Ok(())
185}
186
187async fn dispatch(conn: &Connection, req: JsonRpcRequest) -> Result<JsonRpcResponse> {
188    match req.method.as_str() {
189        "initialize" => Ok(handle_initialize(req.id)),
190        "notifications/initialized" => Ok(noop(req.id)),
191        "tools/list" => Ok(handle_tools_list(req.id)),
192        "tools/call" => handle_tools_call(conn, req.id, req.params.unwrap_or_default()).await,
193        _ => Ok(method_not_found(req.id, &req.method)),
194    }
195}
196
197// ── MCP handshake handlers ───────────────────────────────────────────────────
198
199fn handle_initialize(id: serde_json::Value) -> JsonRpcResponse {
200    JsonRpcResponse {
201        jsonrpc: "2.0".to_string(),
202        id,
203        result: Some(serde_json::json!({
204            "protocolVersion": "2024-11-05",
205            "capabilities": { "tools": {} },
206            "serverInfo": {
207                "name": "miku",
208                "version": env!("CARGO_PKG_VERSION")
209            }
210        })),
211        error: None,
212    }
213}
214
215fn handle_tools_list(id: serde_json::Value) -> JsonRpcResponse {
216    JsonRpcResponse {
217        jsonrpc: "2.0".to_string(),
218        id,
219        result: Some(serde_json::json!({
220            "tools": [
221                {
222                    "name": "create_entities",
223                    "description": "Create multiple new entities in the knowledge graph",
224                    "inputSchema": {
225                        "type": "object",
226                        "properties": {
227                            "entities": {
228                                "type": "array",
229                                "items": {
230                                    "type": "object",
231                                    "properties": {
232                                        "name": {"type": "string"},
233                                        "entityType": {"type": "string"},
234                                        "observations": {"type": "array", "items": {"type": "string"}}
235                                    },
236                                    "required": ["name", "entityType", "observations"]
237                                }
238                            }
239                        },
240                        "required": ["entities"]
241                    }
242                },
243                {
244                    "name": "create_relations",
245                    "description": "Create relations between entities in the knowledge graph",
246                    "inputSchema": {
247                        "type": "object",
248                        "properties": {
249                            "relations": {
250                                "type": "array",
251                                "items": {
252                                    "type": "object",
253                                    "properties": {
254                                        "from": {"type": "string"},
255                                        "to": {"type": "string"},
256                                        "relationType": {"type": "string"}
257                                    },
258                                    "required": ["from", "to", "relationType"]
259                                }
260                            }
261                        },
262                        "required": ["relations"]
263                    }
264                },
265                {
266                    "name": "add_observations",
267                    "description": "Add new observations to existing entities (subject to the observation limit cap, which defaults to 50)",
268                    "inputSchema": {
269                        "type": "object",
270                        "properties": {
271                            "observations": {
272                                "type": "array",
273                                "items": {
274                                    "type": "object",
275                                    "properties": {
276                                        "entityName": {"type": "string"},
277                                        "contents": {"type": "array", "items": {"type": "string"}}
278                                    },
279                                    "required": ["entityName", "contents"]
280                                }
281                            }
282                        },
283                        "required": ["observations"]
284                    }
285                },
286                {
287                    "name": "add_truth",
288                    "description": "Add or update a truth key-value pair for an entity",
289                    "inputSchema": {
290                        "type": "object",
291                        "properties": {
292                            "entityName": {"type": "string"},
293                            "key": {"type": "string"},
294                            "value": {"type": "string"}
295                        },
296                        "required": ["entityName", "key", "value"]
297                    }
298                },
299                {
300                    "name": "delete_truth",
301                    "description": "Delete a specific truth key by name from an entity",
302                    "inputSchema": {
303                        "type": "object",
304                        "properties": {
305                            "entityName": {"type": "string"},
306                            "key": {"type": "string"}
307                        },
308                        "required": ["entityName", "key"]
309                    }
310                },
311                {
312                    "name": "delete_entities",
313                    "description": "Delete entities and their associated relations",
314                    "inputSchema": {
315                        "type": "object",
316                        "properties": {
317                            "entityNames": {"type": "array", "items": {"type": "string"}}
318                        },
319                        "required": ["entityNames"]
320                    }
321                },
322                {
323                    "name": "delete_observations",
324                    "description": "Delete specific observations from entities",
325                    "inputSchema": {
326                        "type": "object",
327                        "properties": {
328                            "deletions": {
329                                "type": "array",
330                                "items": {
331                                    "type": "object",
332                                    "properties": {
333                                        "entityName": {"type": "string"},
334                                        "observations": {"type": "array", "items": {"type": "string"}}
335                                    },
336                                    "required": ["entityName", "observations"]
337                                }
338                            }
339                        },
340                        "required": ["deletions"]
341                    }
342                },
343                {
344                    "name": "delete_relations",
345                    "description": "Delete specific relations from the knowledge graph",
346                    "inputSchema": {
347                        "type": "object",
348                        "properties": {
349                            "relations": {
350                                "type": "array",
351                                "items": {
352                                    "type": "object",
353                                    "properties": {
354                                        "from": {"type": "string"},
355                                        "to": {"type": "string"},
356                                        "relationType": {"type": "string"}
357                                    },
358                                    "required": ["from", "to", "relationType"]
359                                }
360                            }
361                        },
362                        "required": ["relations"]
363                    }
364                },
365                {
366                    "name": "read_graph",
367                    "description": "Read the entire knowledge graph",
368                    "inputSchema": {"type": "object", "properties": {}}
369                },
370                {
371                    "name": "search_nodes",
372                    "description": "Search for nodes in the knowledge graph by name, type, or observation content",
373                    "inputSchema": {
374                        "type": "object",
375                        "properties": {
376                            "query": {"type": "string"},
377                            "limit": {
378                                "type": "integer",
379                                "minimum": 1,
380                                "description": "Maximum number of matched nodes to return"
381                            }
382                        },
383                        "required": ["query"]
384                    }
385                },
386                {
387                    "name": "open_nodes",
388                    "description": "Retrieve specific nodes and their relations by name",
389                    "inputSchema": {
390                        "type": "object",
391                        "properties": {
392                            "names": {"type": "array", "items": {"type": "string"}}
393                        },
394                        "required": ["names"]
395                    }
396                }
397            ]
398        })),
399        error: None,
400    }
401}
402
403// ── tools/call dispatcher ────────────────────────────────────────────────────
404
405pub async fn handle_tools_call(
406    conn: &Connection,
407    id: serde_json::Value,
408    params: serde_json::Value,
409) -> Result<JsonRpcResponse> {
410    let name = params["name"]
411        .as_str()
412        .ok_or_else(|| anyhow::anyhow!("tools/call missing 'name'"))?;
413    let args = params.get("arguments").cloned().unwrap_or_default();
414
415    let text = match name {
416        "create_entities" => {
417            let p: CreateEntitiesParams = serde_json::from_value(args)?;
418            let mut entities = p.entities;
419            let names: Vec<String> = entities
420                .iter()
421                .map(|e| crate::normalize::normalize_key(&e.name))
422                .collect();
423            for ent in &mut entities {
424                ent.name = crate::normalize::normalize_key(&ent.name);
425            }
426            db::mcp_create_entities(conn, entities).await?;
427            serde_json::to_string(&db::mcp_open_nodes(conn, names).await?)?
428        }
429        "create_relations" => {
430            let p: CreateRelationsParams = serde_json::from_value(args)?;
431            db::mcp_create_relations(conn, p.relations).await?;
432            "Relations created.".to_string()
433        }
434        "add_observations" => {
435            let mut p: AddObservationsParams = serde_json::from_value(args)?;
436            let mut obs_names: Vec<String> = Vec::new();
437            for obs in &mut p.observations {
438                let normalized = crate::normalize::normalize_key(&obs.entity_name);
439                obs.entity_name = normalized.clone();
440                obs_names.push(normalized);
441            }
442            let paths = crate::paths::MikuPaths::resolve();
443            let limit = std::env::var(crate::constant::ENV_OBSERVATION_LIMIT)
444                .ok()
445                .and_then(|v| v.parse::<usize>().ok())
446                .unwrap_or(paths.observation_limit.unwrap_or(50));
447            db::mcp_add_observations(conn, p.observations, limit).await?;
448            serde_json::to_string(&db::mcp_open_nodes(conn, obs_names).await?)?
449        }
450        "add_truth" => {
451            let p: AddTruthParams = serde_json::from_value(args)?;
452            let entity_name = crate::normalize::normalize_key(&p.entity_name);
453            db::truth_upsert(conn, &entity_name, &p.key, &p.value).await?;
454            serde_json::to_string(&db::mcp_open_nodes(conn, vec![entity_name]).await?)?
455        }
456        "delete_truth" => {
457            let p: DeleteTruthParams = serde_json::from_value(args)?;
458            let entity_name = crate::normalize::normalize_key(&p.entity_name);
459            db::truth_delete(conn, &entity_name, &p.key).await?;
460            serde_json::to_string(&db::mcp_open_nodes(conn, vec![entity_name]).await?)?
461        }
462        "delete_entities" => {
463            let p: DeleteEntitiesParams = serde_json::from_value(args)?;
464            let entity_names = p
465                .entity_names
466                .into_iter()
467                .map(|n| crate::normalize::normalize_key(&n))
468                .collect();
469            db::mcp_delete_entities(conn, entity_names).await?;
470            "Entities deleted.".to_string()
471        }
472        "delete_observations" => {
473            let p: DeleteObservationsParams = serde_json::from_value(args)?;
474            let mut deletions = p.deletions;
475            for del in &mut deletions {
476                del.entity_name = crate::normalize::normalize_key(&del.entity_name);
477            }
478            db::mcp_delete_observations(conn, deletions).await?;
479            "Observations deleted.".to_string()
480        }
481        "delete_relations" => {
482            let p: DeleteRelationsParams = serde_json::from_value(args)?;
483            let mut relations = p.relations;
484            for rel in &mut relations {
485                rel.from = crate::normalize::normalize_key(&rel.from);
486                rel.to = crate::normalize::normalize_key(&rel.to);
487            }
488            db::mcp_delete_relations(conn, relations).await?;
489            "Relations deleted.".to_string()
490        }
491        "read_graph" => {
492            let graph = db::mcp_read_graph(conn).await?;
493            serde_json::to_string(&graph)?
494        }
495        "search_nodes" => {
496            let p: SearchNodesParams = serde_json::from_value(args)?;
497            let graph = match p.limit {
498                Some(limit) => db::mcp_search_nodes_with_limit(conn, &p.query, limit).await?,
499                None => db::mcp_search_nodes(conn, &p.query).await?,
500            };
501            serde_json::to_string(&graph)?
502        }
503        "open_nodes" => {
504            let p: OpenNodesParams = serde_json::from_value(args)?;
505            let graph = db::mcp_open_nodes(conn, p.names).await?;
506            serde_json::to_string(&graph)?
507        }
508        unknown => {
509            return Ok(JsonRpcResponse {
510                jsonrpc: "2.0".to_string(),
511                id,
512                result: None,
513                error: Some(serde_json::json!({
514                    "code": -32601,
515                    "message": format!("Unknown tool: {}", unknown)
516                })),
517            });
518        }
519    };
520
521    Ok(JsonRpcResponse {
522        jsonrpc: "2.0".to_string(),
523        id,
524        result: Some(serde_json::json!({
525            "content": [{"type": "text", "text": text}],
526            "isError": false
527        })),
528        error: None,
529    })
530}
531
532// ── helpers ──────────────────────────────────────────────────────────────────
533
534fn noop(id: serde_json::Value) -> JsonRpcResponse {
535    JsonRpcResponse {
536        jsonrpc: "2.0".to_string(),
537        id,
538        result: Some(serde_json::Value::Null),
539        error: None,
540    }
541}
542
543fn method_not_found(id: serde_json::Value, method: &str) -> JsonRpcResponse {
544    JsonRpcResponse {
545        jsonrpc: "2.0".to_string(),
546        id,
547        result: None,
548        error: Some(serde_json::json!({
549            "code": -32601,
550            "message": format!("Method not found: {}", method)
551        })),
552    }
553}