1pub mod watcher;
2
3use std::collections::{HashMap, HashSet};
4use std::path::PathBuf;
5use std::str::FromStr;
6use std::sync::Arc;
7
8use liwe::find::{DocumentFinder, FindOptions, FindOutput};
9use liwe::retrieve::{DocumentReader, RetrieveOptions, RetrieveOutput};
10use liwe::stats::{GraphStatistics, KeyStatistics};
11use liwe::fs::{new_for_path, new_from_hashmap};
12use liwe::graph::{Graph, GraphContext};
13use chrono::Local;
14use liwe::model::config::{ActionDefinition, CompletionOptions, Configuration, MarkdownOptions, NoteTemplate, DEFAULT_KEY_DATE_FORMAT};
15use liwe::model::node::{Node, NodeIter, NodePointer, Reference, ReferenceType};
16use liwe::model::tree::{Tree, TreeIter};
17use liwe::model::Key;
18use liwe::operations::{
19 delete as op_delete, extract as op_extract, inline as op_inline, rename as op_rename, Changes,
20 ExtractConfig, InlineConfig, OperationError,
21};
22use minijinja::{context, Environment};
23use rmcp::handler::server::router::prompt::PromptRouter;
24use rmcp::handler::server::router::tool::ToolRouter;
25use rmcp::handler::server::wrapper::Parameters;
26use rmcp::model::*;
27use rmcp::schemars::JsonSchema;
28use rmcp::service::RequestContext;
29use rmcp::{prompt, prompt_handler, prompt_router, tool, tool_router, RoleServer};
30use rmcp::{ErrorData as McpError, ServerHandler, tool_handler};
31use serde::{Deserialize, Serialize};
32use tokio::sync::Mutex;
33
34fn to_json_result<T: Serialize>(output: &T) -> Result<CallToolResult, McpError> {
35 let json =
36 serde_json::to_string(output).map_err(|e| McpError::internal_error(e.to_string(), None))?;
37 Ok(CallToolResult::success(vec![Content::text(json)]))
38}
39
40fn to_text_result(text: String) -> Result<CallToolResult, McpError> {
41 Ok(CallToolResult::success(vec![Content::text(text)]))
42}
43
44#[derive(Debug, Deserialize, JsonSchema)]
45pub struct FindParams {
46 #[schemars(description = "Fuzzy search query matching against document title and key")]
47 pub query: Option<String>,
48 #[schemars(description = "Only return root documents (no incoming block references)")]
49 pub roots: Option<bool>,
50 #[schemars(description = "Only return documents that reference this key")]
51 pub refs_to: Option<String>,
52 #[schemars(description = "Only return documents referenced by this key")]
53 pub refs_from: Option<String>,
54 #[schemars(description = "Maximum number of results to return")]
55 pub limit: Option<usize>,
56}
57
58impl From<FindParams> for FindOptions {
59 fn from(p: FindParams) -> Self {
60 FindOptions {
61 query: p.query,
62 roots: p.roots.unwrap_or(false),
63 refs_to: p.refs_to.map(|k| Key::name(&k)),
64 refs_from: p.refs_from.map(|k| Key::name(&k)),
65 limit: p.limit,
66 }
67 }
68}
69
70#[derive(Debug, Deserialize, JsonSchema)]
71pub struct RetrieveParams {
72 #[schemars(description = "Document keys to retrieve")]
73 pub keys: Vec<String>,
74 #[schemars(description = "Levels of block references to expand (0 = document only, 1 = include direct sub-documents). Default: 1")]
75 pub depth: Option<u8>,
76 #[schemars(description = "Levels of parent documents to include. Default: 1")]
77 pub context: Option<u8>,
78 #[schemars(description = "Include inline-linked documents. Default: false")]
79 pub links: Option<bool>,
80 #[schemars(description = "Include incoming inline references. Default: true")]
81 pub backlinks: Option<bool>,
82 #[schemars(description = "Document keys to exclude from results")]
83 pub exclude: Option<Vec<String>>,
84 #[schemars(description = "Return metadata only without document content. Default: false")]
85 pub no_content: Option<bool>,
86}
87
88impl From<RetrieveParams> for RetrieveOptions {
89 fn from(p: RetrieveParams) -> Self {
90 RetrieveOptions {
91 depth: p.depth.unwrap_or(1),
92 context: p.context.unwrap_or(1),
93 links: p.links.unwrap_or(false),
94 backlinks: p.backlinks.unwrap_or(true),
95 exclude: p
96 .exclude
97 .unwrap_or_default()
98 .into_iter()
99 .map(|k| Key::name(&k))
100 .collect::<HashSet<_>>(),
101 no_content: p.no_content.unwrap_or(false),
102 }
103 }
104}
105
106#[derive(Debug, Deserialize, JsonSchema)]
107pub struct TreeParams {
108 #[schemars(description = "Starting document keys. If empty, shows all root documents")]
109 pub keys: Option<Vec<String>>,
110 #[schemars(description = "Maximum traversal depth. Default: 4")]
111 pub depth: Option<u8>,
112}
113
114#[derive(Debug, Serialize)]
115struct TreeNode {
116 key: String,
117 title: String,
118 #[serde(skip_serializing_if = "Vec::is_empty")]
119 children: Vec<TreeNode>,
120}
121
122#[derive(Debug, Deserialize, JsonSchema)]
123pub struct StatsParams {
124 #[schemars(description = "Document key for per-document stats. Omit for aggregate graph statistics")]
125 pub key: Option<String>,
126}
127
128#[derive(Debug, Deserialize, JsonSchema)]
129pub struct SquashParams {
130 #[schemars(description = "Root document key to expand")]
131 pub key: String,
132 #[schemars(description = "Levels of references to expand. Default: 2")]
133 pub depth: Option<u8>,
134}
135
136#[derive(Debug, Deserialize, JsonSchema)]
137pub struct CreateParams {
138 #[schemars(description = "Document title")]
139 pub title: String,
140 #[schemars(description = "Markdown content body (without the title heading)")]
141 pub content: Option<String>,
142}
143
144#[derive(Debug, Deserialize, JsonSchema)]
145pub struct UpdateParams {
146 #[schemars(description = "Document key to update")]
147 pub key: String,
148 #[schemars(description = "New full markdown content")]
149 pub content: String,
150}
151
152#[derive(Debug, Deserialize, JsonSchema)]
153pub struct DeleteParams {
154 #[schemars(description = "Document key to delete")]
155 pub key: String,
156 #[schemars(description = "Preview changes without applying. Default: false")]
157 pub dry_run: Option<bool>,
158}
159
160#[derive(Debug, Deserialize, JsonSchema)]
161pub struct RenameParams {
162 #[schemars(description = "Current document key")]
163 pub old_key: String,
164 #[schemars(description = "New document key")]
165 pub new_key: String,
166 #[schemars(description = "Preview changes without applying. Default: false")]
167 pub dry_run: Option<bool>,
168}
169
170#[derive(Debug, Serialize)]
171struct ChangesOutput {
172 creates: Vec<ChangeEntry>,
173 updates: Vec<ChangeEntry>,
174 removes: Vec<String>,
175}
176
177#[derive(Debug, Serialize)]
178struct ChangeEntry {
179 key: String,
180 content: String,
181}
182
183impl From<&Changes> for ChangesOutput {
184 fn from(c: &Changes) -> Self {
185 ChangesOutput {
186 creates: c.creates.iter().map(|(k, v)| ChangeEntry { key: k.to_string(), content: v.clone() }).collect(),
187 updates: c.updates.iter().map(|(k, v)| ChangeEntry { key: k.to_string(), content: v.clone() }).collect(),
188 removes: c.removes.iter().map(|k| k.to_string()).collect(),
189 }
190 }
191}
192
193#[derive(Debug, Deserialize, JsonSchema)]
194pub struct ExtractParams {
195 #[schemars(description = "Source document key")]
196 pub key: String,
197 #[schemars(description = "Section title to extract (case-insensitive partial match)")]
198 pub section: Option<String>,
199 #[schemars(description = "Block number to extract (1-indexed, use list mode to discover)")]
200 pub block: Option<usize>,
201 #[schemars(description = "List all sections with block numbers instead of extracting. Default: false")]
202 pub list: Option<bool>,
203 #[schemars(description = "Preview changes without applying. Default: false")]
204 pub dry_run: Option<bool>,
205}
206
207#[derive(Debug, Deserialize, JsonSchema)]
208pub struct InlineParams {
209 #[schemars(description = "Document key containing the block reference")]
210 pub key: String,
211 #[schemars(description = "Reference key or title to inline (partial match)")]
212 pub reference: Option<String>,
213 #[schemars(description = "Block number to inline (1-indexed, use list mode to discover)")]
214 pub block: Option<usize>,
215 #[schemars(description = "List all block references instead of inlining. Default: false")]
216 pub list: Option<bool>,
217 #[schemars(description = "Inline as blockquote instead of section. Default: false")]
218 pub as_quote: Option<bool>,
219 #[schemars(description = "Keep the target document after inlining. Default: false")]
220 pub keep_target: Option<bool>,
221 #[schemars(description = "Preview changes without applying. Default: false")]
222 pub dry_run: Option<bool>,
223}
224
225#[derive(Debug, Serialize)]
226struct SectionEntry {
227 block_number: usize,
228 title: String,
229}
230
231#[derive(Debug, Serialize)]
232struct ReferenceEntry {
233 block_number: usize,
234 key: String,
235 title: String,
236}
237
238#[derive(Debug, Deserialize, JsonSchema)]
239pub struct AttachParams {
240 #[schemars(description = "Name of the configured attach action (e.g. 'today')")]
241 pub action: Option<String>,
242 #[schemars(description = "Document key to attach as a block reference in the target")]
243 pub key: Option<String>,
244 #[schemars(description = "Custom reference text (defaults to document title)")]
245 pub text: Option<String>,
246 #[schemars(description = "List available attach actions instead of executing. Default: false")]
247 pub list: Option<bool>,
248 #[schemars(description = "Preview changes without applying. Default: false")]
249 pub dry_run: Option<bool>,
250}
251
252#[derive(Debug, Serialize)]
253struct AttachActionEntry {
254 name: String,
255 title: String,
256 target_key: String,
257}
258
259#[derive(Debug, Serialize)]
260struct ConfigResource {
261 markdown: MarkdownOptions,
262 library: LibraryResourceView,
263 completion: CompletionOptions,
264 templates: HashMap<String, NoteTemplate>,
265 actions: Vec<ActionResourceView>,
266}
267
268#[derive(Debug, Serialize)]
269struct LibraryResourceView {
270 date_format: Option<String>,
271 default_template: Option<String>,
272 frontmatter_document_title: Option<String>,
273 locale: Option<String>,
274}
275
276#[derive(Debug, Serialize)]
277struct ActionResourceView {
278 name: String,
279 action_type: String,
280 title: String,
281 #[serde(skip_serializing_if = "Option::is_none")]
282 target_key: Option<String>,
283}
284
285impl ConfigResource {
286 fn from_config(config: &Configuration, server: &IweServer) -> Self {
287 let actions = config
288 .actions
289 .iter()
290 .map(|(name, action)| {
291 let (action_type, title, target_key) = match action {
292 ActionDefinition::Transform(a) => ("transform", a.title.clone(), None),
293 ActionDefinition::Attach(a) => (
294 "attach",
295 a.title.clone(),
296 Some(server.render_key_template(&a.key_template)),
297 ),
298 ActionDefinition::Sort(a) => ("sort", a.title.clone(), None),
299 ActionDefinition::Inline(a) => ("inline", a.title.clone(), None),
300 ActionDefinition::Extract(a) => ("extract", a.title.clone(), None),
301 ActionDefinition::ExtractAll(a) => ("extract_all", a.title.clone(), None),
302 ActionDefinition::Link(a) => ("link", a.title.clone(), None),
303 };
304 ActionResourceView {
305 name: name.clone(),
306 action_type: action_type.to_string(),
307 title,
308 target_key,
309 }
310 })
311 .collect();
312
313 Self {
314 markdown: config.markdown.clone(),
315 library: LibraryResourceView {
316 date_format: config.library.date_format.clone(),
317 default_template: config.library.default_template.clone(),
318 frontmatter_document_title: config.library.frontmatter_document_title.clone(),
319 locale: config.library.locale.clone(),
320 },
321 completion: config.completion.clone(),
322 templates: config.templates.clone(),
323 actions,
324 }
325 }
326}
327
328fn op_error_to_mcp(e: OperationError) -> McpError {
329 McpError::invalid_params(e.to_string(), None)
330}
331
332#[derive(Debug, Deserialize, JsonSchema)]
333pub struct ReviewPromptArgs {
334 #[schemars(description = "Document key to review")]
335 pub key: String,
336}
337
338#[derive(Debug, Deserialize, JsonSchema)]
339pub struct RefactorPromptArgs {
340 #[schemars(description = "Root document key to analyze for restructuring")]
341 pub key: String,
342}
343
344#[derive(Clone)]
345pub struct IweServer {
346 graph: Arc<Mutex<Graph>>,
347 base_path: Option<PathBuf>,
348 config: Configuration,
349 tool_router: ToolRouter<IweServer>,
350 prompt_router: PromptRouter<IweServer>,
351}
352
353#[tool_router]
354impl IweServer {
355 #[tool(description = "Search and discover documents in the knowledge graph by fuzzy query, structural filters, or reference relationships")]
356 async fn iwe_find(
357 &self,
358 Parameters(params): Parameters<FindParams>,
359 ) -> Result<CallToolResult, McpError> {
360 let graph = self.graph.lock().await;
361 let finder = DocumentFinder::new(&graph);
362 let output: FindOutput = finder.find(¶ms.into());
363 to_json_result(&output)
364 }
365
366 #[tool(description = "Retrieve documents from the knowledge graph with configurable depth expansion, parent context, backlinks, and linked documents")]
367 async fn iwe_retrieve(
368 &self,
369 Parameters(params): Parameters<RetrieveParams>,
370 ) -> Result<CallToolResult, McpError> {
371 let graph = self.graph.lock().await;
372 let reader = DocumentReader::new(&graph);
373 let keys: Vec<Key> = params.keys.iter().map(|k| Key::name(k)).collect();
374 let options: RetrieveOptions = params.into();
375 let output: RetrieveOutput = reader.retrieve_many(&keys, &options);
376 to_json_result(&output)
377 }
378
379 #[tool(description = "View the hierarchical tree structure of the knowledge graph showing how documents are connected via block references")]
380 async fn iwe_tree(
381 &self,
382 Parameters(params): Parameters<TreeParams>,
383 ) -> Result<CallToolResult, McpError> {
384 let graph = self.graph.lock().await;
385 let root_keys: Vec<Key> = if let Some(keys) = params.keys.filter(|k| !k.is_empty()) {
386 keys.iter().map(|k| Key::name(k)).collect()
387 } else {
388 let paths = graph.paths();
389 let mut keys: Vec<Key> = paths
390 .iter()
391 .filter(|n| n.ids().len() == 1)
392 .filter_map(|n| n.first_id())
393 .map(|id| (&*graph).node(id).node_key())
394 .collect();
395 keys.sort();
396 keys.dedup();
397 keys
398 };
399
400 let max_depth = params.depth.unwrap_or(4);
401 let mut trees: Vec<TreeNode> = Vec::new();
402 for root_key in &root_keys {
403 let mut visited: HashSet<Key> = HashSet::new();
404 if let Some(node) = build_tree_node(&graph, root_key, max_depth, &mut visited) {
405 trees.push(node);
406 }
407 }
408 to_json_result(&trees)
409 }
410
411 #[tool(description = "Get comprehensive statistics about the knowledge graph including document counts, reference patterns, broken links, and most connected documents")]
412 async fn iwe_stats(
413 &self,
414 Parameters(params): Parameters<StatsParams>,
415 ) -> Result<CallToolResult, McpError> {
416 let graph = self.graph.lock().await;
417 if let Some(key) = params.key {
418 let all_stats = KeyStatistics::from_graph(&graph);
419 let stat = all_stats
420 .into_iter()
421 .find(|s| s.key == key)
422 .ok_or_else(|| {
423 McpError::invalid_params(format!("Document '{}' not found", key), None)
424 })?;
425 to_json_result(&stat)
426 } else {
427 let stats = GraphStatistics::from_graph(&graph);
428 to_json_result(&stats)
429 }
430 }
431
432 #[tool(description = "Expand all block references into a single flat markdown document. Useful for export or generating a complete view of a document tree")]
433 async fn iwe_squash(
434 &self,
435 Parameters(params): Parameters<SquashParams>,
436 ) -> Result<CallToolResult, McpError> {
437 let graph = self.graph.lock().await;
438 let key = Key::name(¶ms.key);
439 let depth = params.depth.unwrap_or(2);
440
441 if (&*graph).get_node_id(&key).is_none() {
442 return Err(McpError::invalid_params(
443 format!("Document '{}' not found", params.key),
444 None,
445 ));
446 }
447
448 let squashed: Tree = (&*graph).squash(&key, depth);
449 let mut patch = Graph::new();
450 patch.build_key_from_iter(&key, TreeIter::new(&squashed));
451 let content = patch.export_key(&key).unwrap_or_default();
452 to_text_result(content)
453 }
454
455 #[tool(description = "Create a new document in the knowledge graph from a title and optional content")]
456 async fn iwe_create(
457 &self,
458 Parameters(params): Parameters<CreateParams>,
459 ) -> Result<CallToolResult, McpError> {
460 let slug = params
461 .title
462 .to_lowercase()
463 .chars()
464 .map(|c| if c.is_alphanumeric() { c } else { '-' })
465 .collect::<String>()
466 .split('-')
467 .filter(|s| !s.is_empty())
468 .collect::<Vec<_>>()
469 .join("-");
470
471 let content_body = params.content.unwrap_or_default();
472 let markdown = if content_body.is_empty() {
473 format!("# {}\n", params.title)
474 } else {
475 format!("# {}\n\n{}\n", params.title, content_body)
476 };
477
478 let key = Key::name(&slug);
479 let mut graph = self.graph.lock().await;
480
481 if (&*graph).get_node_id(&key).is_some() {
482 return Err(McpError::invalid_params(
483 format!("Document '{}' already exists", slug),
484 None,
485 ));
486 }
487
488 graph.insert_document(key.clone(), markdown.clone());
489 self.write_file(&key, &markdown);
490
491 #[derive(Serialize)]
492 struct CreateResult {
493 key: String,
494 }
495 to_json_result(&CreateResult {
496 key: slug,
497 })
498 }
499
500 #[tool(description = "Update the full markdown content of an existing document")]
501 async fn iwe_update(
502 &self,
503 Parameters(params): Parameters<UpdateParams>,
504 ) -> Result<CallToolResult, McpError> {
505 let key = Key::name(¶ms.key);
506 let mut graph = self.graph.lock().await;
507
508 if (&*graph).get_node_id(&key).is_none() {
509 return Err(McpError::invalid_params(
510 format!("Document '{}' not found", params.key),
511 None,
512 ));
513 }
514
515 let previous_title = (&*graph)
516 .get_key_title(&key)
517 .unwrap_or_else(|| params.key.clone());
518
519 graph.update_document(key.clone(), params.content.clone());
520 self.write_file(&key, ¶ms.content);
521
522 let new_title = (&*graph)
523 .get_key_title(&key)
524 .unwrap_or_else(|| params.key.clone());
525
526 #[derive(Serialize)]
527 struct UpdateResult {
528 key: String,
529 previous_title: String,
530 new_title: String,
531 }
532 to_json_result(&UpdateResult {
533 key: params.key,
534 previous_title,
535 new_title,
536 })
537 }
538
539 #[tool(description = "Delete a document from the knowledge graph. All block references and inline links to this document in other documents are cleaned up")]
540 async fn iwe_delete(
541 &self,
542 Parameters(params): Parameters<DeleteParams>,
543 ) -> Result<CallToolResult, McpError> {
544 let key = Key::name(¶ms.key);
545 let mut graph = self.graph.lock().await;
546 let changes = op_delete(&graph, &key).map_err(op_error_to_mcp)?;
547
548 if !params.dry_run.unwrap_or(false) {
549 Self::apply_changes(&mut graph, &changes);
550 self.write_changes(&changes);
551 }
552
553 to_json_result(&ChangesOutput::from(&changes))
554 }
555
556 #[tool(description = "Rename a document key. All block references and inline links across the entire graph are updated to point to the new key")]
557 async fn iwe_rename(
558 &self,
559 Parameters(params): Parameters<RenameParams>,
560 ) -> Result<CallToolResult, McpError> {
561 let old_key = Key::name(¶ms.old_key);
562 let new_key = Key::name(¶ms.new_key);
563 let mut graph = self.graph.lock().await;
564 let changes = op_rename(&graph, &old_key, &new_key).map_err(op_error_to_mcp)?;
565
566 if !params.dry_run.unwrap_or(false) {
567 Self::apply_changes(&mut graph, &changes);
568 self.write_changes(&changes);
569 }
570
571 to_json_result(&ChangesOutput::from(&changes))
572 }
573
574 #[tool(description = "Extract a section from a document into a new standalone document. The original section is replaced with a block reference. Use list mode to discover sections first")]
575 async fn iwe_extract(
576 &self,
577 Parameters(params): Parameters<ExtractParams>,
578 ) -> Result<CallToolResult, McpError> {
579 let source_key = Key::name(¶ms.key);
580 let mut graph = self.graph.lock().await;
581
582 if (&*graph).get_node_id(&source_key).is_none() {
583 return Err(McpError::invalid_params(
584 format!("Document '{}' not found", params.key),
585 None,
586 ));
587 }
588
589 let tree = (&*graph).collect(&source_key);
590 let sections = collect_sections(&tree);
591
592 if params.list.unwrap_or(false) {
593 return to_json_result(§ions);
594 }
595
596 let selected = if let Some(ref title) = params.section {
597 let matches: Vec<_> = sections
598 .iter()
599 .filter(|s| s.title.to_lowercase().contains(&title.to_lowercase()))
600 .collect();
601 if matches.is_empty() {
602 return Err(McpError::invalid_params(
603 format!("No section matches '{}'", title),
604 None,
605 ));
606 }
607 if matches.len() > 1 {
608 return Err(McpError::invalid_params(
609 format!(
610 "Multiple sections match '{}': {}",
611 title,
612 matches.iter().map(|s| s.title.as_str()).collect::<Vec<_>>().join(", ")
613 ),
614 None,
615 ));
616 }
617 matches[0].block_number
618 } else if let Some(block) = params.block {
619 if block == 0 || block > sections.len() {
620 return Err(McpError::invalid_params(
621 format!("Block number {} out of range (1-{})", block, sections.len()),
622 None,
623 ));
624 }
625 block
626 } else {
627 return Err(McpError::invalid_params(
628 "Must specify section, block, or list",
629 None,
630 ));
631 };
632
633 let section_id = tree
634 .children
635 .iter()
636 .flat_map(|c| collect_section_ids(c))
637 .nth(selected - 1)
638 .ok_or_else(|| McpError::invalid_params("Section not found", None))?;
639
640 let config = ExtractConfig::default();
641 let changes = op_extract(&graph, &source_key, section_id, &config).map_err(op_error_to_mcp)?;
642
643 if !params.dry_run.unwrap_or(false) {
644 Self::apply_changes(&mut graph, &changes);
645 self.write_changes(&changes);
646 }
647
648 to_json_result(&ChangesOutput::from(&changes))
649 }
650
651 #[tool(description = "Replace a block reference with the actual content of the referenced document. Use list mode to discover block references first")]
652 async fn iwe_inline(
653 &self,
654 Parameters(params): Parameters<InlineParams>,
655 ) -> Result<CallToolResult, McpError> {
656 let source_key = Key::name(¶ms.key);
657 let mut graph = self.graph.lock().await;
658
659 if (&*graph).get_node_id(&source_key).is_none() {
660 return Err(McpError::invalid_params(
661 format!("Document '{}' not found", params.key),
662 None,
663 ));
664 }
665
666 let tree = (&*graph).collect(&source_key);
667 let refs = collect_block_refs(&tree);
668
669 if params.list.unwrap_or(false) {
670 return to_json_result(&refs);
671 }
672
673 let selected = if let Some(ref reference) = params.reference {
674 let matches: Vec<_> = refs
675 .iter()
676 .filter(|r| {
677 r.title.to_lowercase().contains(&reference.to_lowercase())
678 || r.key.to_lowercase().contains(&reference.to_lowercase())
679 })
680 .collect();
681 if matches.is_empty() {
682 return Err(McpError::invalid_params(
683 format!("No reference matches '{}'", reference),
684 None,
685 ));
686 }
687 if matches.len() > 1 {
688 return Err(McpError::invalid_params(
689 format!(
690 "Multiple references match '{}': {}",
691 reference,
692 matches.iter().map(|r| r.key.as_str()).collect::<Vec<_>>().join(", ")
693 ),
694 None,
695 ));
696 }
697 matches[0].block_number
698 } else if let Some(block) = params.block {
699 if block == 0 || block > refs.len() {
700 return Err(McpError::invalid_params(
701 format!("Block number {} out of range (1-{})", block, refs.len()),
702 None,
703 ));
704 }
705 block
706 } else {
707 return Err(McpError::invalid_params(
708 "Must specify reference, block, or list",
709 None,
710 ));
711 };
712
713 let ref_id = collect_ref_ids(&tree)
714 .into_iter()
715 .nth(selected - 1)
716 .ok_or_else(|| McpError::invalid_params("Reference not found", None))?;
717
718 let inline_type = if params.as_quote.unwrap_or(false) {
719 liwe::model::config::InlineType::Quote
720 } else {
721 liwe::model::config::InlineType::Section
722 };
723
724 let config = InlineConfig {
725 inline_type,
726 keep_target: params.keep_target.unwrap_or(false),
727 };
728
729 let changes = op_inline(&graph, &source_key, ref_id, &config).map_err(op_error_to_mcp)?;
730
731 if !params.dry_run.unwrap_or(false) {
732 Self::apply_changes(&mut graph, &changes);
733 self.write_changes(&changes);
734 }
735
736 to_json_result(&ChangesOutput::from(&changes))
737 }
738
739 #[tool(description = "Normalize all document formatting across the knowledge graph. Re-parses and re-writes all documents to ensure consistent formatting")]
740 async fn iwe_normalize(&self) -> Result<CallToolResult, McpError> {
741 let mut graph = self.graph.lock().await;
742 let state = graph.export();
743 let original_count = state.len();
744
745 let mut changed = 0usize;
746 for (key_str, original_content) in &state {
747 let key = Key::name(key_str);
748 let new_content = graph.to_markdown(&key);
749 if new_content != *original_content {
750 graph.update_document(key.clone(), new_content.clone());
751 self.write_file(&key, &new_content);
752 changed += 1;
753 }
754 }
755
756 #[derive(Serialize)]
757 struct NormalizeResult {
758 total: usize,
759 normalized: usize,
760 }
761 to_json_result(&NormalizeResult {
762 total: original_count,
763 normalized: changed,
764 })
765 }
766
767 #[tool(description = "Attach a document as a block reference in a target document determined by a configured attach action. The target key is derived from the action's key_template (e.g. daily/{{today}}). Use list mode to discover available attach actions")]
768 async fn iwe_attach(
769 &self,
770 Parameters(params): Parameters<AttachParams>,
771 ) -> Result<CallToolResult, McpError> {
772 if params.list.unwrap_or(false) {
773 let entries: Vec<AttachActionEntry> = self
774 .config
775 .actions
776 .iter()
777 .filter_map(|(name, action)| {
778 if let ActionDefinition::Attach(attach) = action {
779 Some(AttachActionEntry {
780 name: name.clone(),
781 title: attach.title.clone(),
782 target_key: self.render_key_template(&attach.key_template),
783 })
784 } else {
785 None
786 }
787 })
788 .collect();
789 return to_json_result(&entries);
790 }
791
792 let action_name = params.action.as_deref().ok_or_else(|| {
793 McpError::invalid_params("'action' is required when not in list mode".to_string(), None)
794 })?;
795 let source_key_str = params.key.as_deref().ok_or_else(|| {
796 McpError::invalid_params("'key' is required when not in list mode".to_string(), None)
797 })?;
798
799 let attach = match self.config.actions.get(action_name) {
800 Some(ActionDefinition::Attach(a)) => a,
801 Some(_) => {
802 return Err(McpError::invalid_params(
803 format!("Action '{}' is not an attach action", action_name),
804 None,
805 ));
806 }
807 None => {
808 return Err(McpError::invalid_params(
809 format!("Action '{}' not found", action_name),
810 None,
811 ));
812 }
813 };
814
815 let target_key = Key::name(&self.render_key_template(&attach.key_template));
816 let source_key = Key::name(source_key_str);
817
818 let mut graph = self.graph.lock().await;
819
820 if (&*graph).get_node_id(&source_key).is_none() {
821 return Err(McpError::invalid_params(
822 format!("Document '{}' not found", source_key_str),
823 None,
824 ));
825 }
826
827 let reference_text = params.text.unwrap_or_else(|| {
828 (&*graph)
829 .get_key_title(&source_key)
830 .unwrap_or_else(|| source_key_str.to_string())
831 });
832
833 if (&*graph).get_node_id(&target_key).is_some() {
834 let tree = (&*graph).collect(&target_key);
835 if tree
836 .get_all_block_reference_keys()
837 .contains(&source_key)
838 {
839 return Err(McpError::invalid_params(
840 format!(
841 "Document '{}' is already attached in '{}'",
842 source_key_str, target_key
843 ),
844 None,
845 ));
846 }
847 }
848
849 let reference = Tree {
850 id: None,
851 node: Node::Reference(Reference {
852 key: source_key,
853 text: reference_text,
854 reference_type: ReferenceType::Regular,
855 }),
856 children: vec![],
857 };
858
859 let markdown_options = graph.markdown_options();
860 let changes = if (&*graph).get_node_id(&target_key).is_some() {
861 let tree = (&*graph).collect(&target_key);
862 let updated = tree.attach(reference);
863 Changes::new().update(
864 target_key.clone(),
865 updated
866 .iter()
867 .to_markdown(&target_key.parent(), &markdown_options),
868 )
869 } else {
870 let content = reference
871 .iter()
872 .to_markdown(&target_key.parent(), &markdown_options);
873 let document = self.render_document_template(
874 &attach.document_template,
875 &content,
876 );
877 Changes::new().create(target_key.clone(), document)
878 };
879
880 if !params.dry_run.unwrap_or(false) {
881 Self::apply_changes(&mut graph, &changes);
882 self.write_changes(&changes);
883 }
884
885 to_json_result(&ChangesOutput::from(&changes))
886 }
887}
888
889fn build_tree_node(
890 graph: &Graph,
891 key: &Key,
892 max_depth: u8,
893 visited: &mut HashSet<Key>,
894) -> Option<TreeNode> {
895 graph.get_node_id(key)?;
896
897 let title = graph.get_ref_text(key).unwrap_or_default();
898 let key_str = key.to_string();
899
900 if visited.contains(key) {
901 return Some(TreeNode {
902 key: key_str,
903 title,
904 children: vec![],
905 });
906 }
907 visited.insert(key.clone());
908
909 let children = if max_depth > 1 {
910 let ref_node_ids = graph.get_block_references_in(key);
911 let mut refs: Vec<Key> = ref_node_ids
912 .iter()
913 .filter_map(|id| graph.graph_node(*id).ref_key())
914 .collect();
915 refs.sort();
916 refs.into_iter()
917 .filter_map(|ref_key| build_tree_node(graph, &ref_key, max_depth - 1, visited))
918 .collect()
919 } else {
920 vec![]
921 };
922
923 Some(TreeNode {
924 key: key_str,
925 title,
926 children,
927 })
928}
929
930use liwe::model::tree::Tree as ModelTree;
931use liwe::model::NodeId;
932
933fn collect_sections(tree: &ModelTree) -> Vec<SectionEntry> {
934 let mut result = Vec::new();
935 collect_sections_rec(tree, &mut result);
936 result
937}
938
939fn collect_sections_rec(tree: &ModelTree, sections: &mut Vec<SectionEntry>) {
940 if let Node::Section(inlines) = &tree.node {
941 let title = inlines.iter().map(|i| i.plain_text()).collect::<String>();
942 sections.push(SectionEntry {
943 block_number: sections.len() + 1,
944 title,
945 });
946 }
947 for child in &tree.children {
948 collect_sections_rec(child, sections);
949 }
950}
951
952fn collect_section_ids(tree: &ModelTree) -> Vec<NodeId> {
953 let mut ids = Vec::new();
954 if tree.is_section() {
955 if let Some(id) = tree.id {
956 ids.push(id);
957 }
958 }
959 for child in &tree.children {
960 ids.extend(collect_section_ids(child));
961 }
962 ids
963}
964
965fn collect_block_refs(tree: &ModelTree) -> Vec<ReferenceEntry> {
966 let mut result = Vec::new();
967 collect_block_refs_rec(tree, &mut result);
968 result
969}
970
971fn collect_block_refs_rec(tree: &ModelTree, refs: &mut Vec<ReferenceEntry>) {
972 if let Node::Reference(reference) = &tree.node {
973 refs.push(ReferenceEntry {
974 block_number: refs.len() + 1,
975 key: reference.key.to_string(),
976 title: reference.text.clone(),
977 });
978 }
979 for child in &tree.children {
980 collect_block_refs_rec(child, refs);
981 }
982}
983
984fn collect_ref_ids(tree: &ModelTree) -> Vec<NodeId> {
985 let mut ids = Vec::new();
986 if let Node::Reference(_) = &tree.node {
987 if let Some(id) = tree.id {
988 ids.push(id);
989 }
990 }
991 for child in &tree.children {
992 ids.extend(collect_ref_ids(child));
993 }
994 ids
995}
996
997#[prompt_router]
998impl IweServer {
999 #[prompt(
1000 name = "explore",
1001 description = "Start exploring the knowledge graph. Provides an overview of size, structure, root entry points, broken links, and orphaned documents"
1002 )]
1003 async fn explore(&self) -> Result<GetPromptResult, McpError> {
1004 let graph = self.graph.lock().await;
1005 let stats = GraphStatistics::from_graph(&graph);
1006 let stats_json =
1007 serde_json::to_string_pretty(&stats).unwrap_or_else(|_| "{}".to_string());
1008
1009 let finder = DocumentFinder::new(&graph);
1010 let roots = finder.find(&FindOptions {
1011 roots: true,
1012 limit: Some(20),
1013 ..Default::default()
1014 });
1015 let roots_json =
1016 serde_json::to_string_pretty(&roots).unwrap_or_else(|_| "[]".to_string());
1017
1018 let messages = vec![PromptMessage::new_text(
1019 PromptMessageRole::User,
1020 format!(
1021 "Here is an overview of the IWE knowledge graph.\n\n## Statistics\n\n```json\n{}\n```\n\n## Root documents\n\n```json\n{}\n```\n\nExplore the graph using iwe_retrieve to read documents, iwe_find to search, and iwe_tree to navigate the structure.",
1022 stats_json, roots_json
1023 ),
1024 )];
1025
1026 Ok(GetPromptResult::new(messages)
1027 .with_description("Overview of the IWE knowledge graph"))
1028 }
1029
1030 #[prompt(
1031 name = "review",
1032 description = "Review a specific document within its graph context — its content, parents, children, and backlinks"
1033 )]
1034 async fn review(
1035 &self,
1036 Parameters(args): Parameters<ReviewPromptArgs>,
1037 ) -> Result<GetPromptResult, McpError> {
1038 let graph = self.graph.lock().await;
1039 let key = Key::name(&args.key);
1040 let reader = DocumentReader::new(&graph);
1041 let output = reader.retrieve(
1042 &key,
1043 &RetrieveOptions {
1044 depth: 2,
1045 context: 2,
1046 backlinks: true,
1047 ..Default::default()
1048 },
1049 );
1050 let json = serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string());
1051
1052 let messages = vec![PromptMessage::new_text(
1053 PromptMessageRole::User,
1054 format!(
1055 "Review this document and its context in the knowledge graph:\n\n```json\n{}\n```\n\nConsider: Is it well-placed in the graph? Are there missing links? Is the content clear and well-structured? What sections might be extracted into separate documents?",
1056 json
1057 ),
1058 )];
1059
1060 Ok(GetPromptResult::new(messages)
1061 .with_description(format!("Review of document '{}'", args.key)))
1062 }
1063
1064 #[prompt(
1065 name = "refactor",
1066 description = "Analyze a section of the knowledge graph and suggest restructuring using extract, inline, and rename operations"
1067 )]
1068 async fn refactor(
1069 &self,
1070 Parameters(args): Parameters<RefactorPromptArgs>,
1071 ) -> Result<GetPromptResult, McpError> {
1072 let graph = self.graph.lock().await;
1073 let key = Key::name(&args.key);
1074 let reader = DocumentReader::new(&graph);
1075 let output = reader.retrieve(
1076 &key,
1077 &RetrieveOptions {
1078 depth: 3,
1079 context: 1,
1080 backlinks: true,
1081 ..Default::default()
1082 },
1083 );
1084 let json = serde_json::to_string_pretty(&output).unwrap_or_else(|_| "{}".to_string());
1085
1086 let messages = vec![PromptMessage::new_text(
1087 PromptMessageRole::User,
1088 format!(
1089 "Analyze this document tree and suggest restructuring:\n\n```json\n{}\n```\n\nIdentify documents that are too large (should be extracted with iwe_extract), too small (should be inlined with iwe_inline), poorly named (should be renamed with iwe_rename), or missing connections. Propose a sequence of operations to improve the structure.",
1090 json
1091 ),
1092 )];
1093
1094 Ok(GetPromptResult::new(messages)
1095 .with_description(format!("Refactoring analysis for '{}'", args.key)))
1096 }
1097}
1098
1099#[tool_handler]
1100#[prompt_handler]
1101impl ServerHandler for IweServer {
1102 fn get_info(&self) -> ServerInfo {
1103 ServerInfo::new(
1104 ServerCapabilities::builder()
1105 .enable_tools()
1106 .enable_prompts()
1107 .enable_resources()
1108 .build(),
1109 )
1110 .with_server_info(Implementation::new("iwe", env!("CARGO_PKG_VERSION")))
1111 .with_instructions(
1112 "IWE knowledge graph server. Tools: iwe_find, iwe_retrieve, iwe_tree, iwe_stats, iwe_squash, iwe_create, iwe_update, iwe_delete, iwe_rename, iwe_extract, iwe_inline, iwe_normalize, iwe_attach. Prompts: explore, review, refactor. Resources: iwe://documents/{key}, iwe://tree, iwe://stats, iwe://config."
1113 .to_string(),
1114 )
1115 }
1116
1117 async fn list_resources(
1118 &self,
1119 _request: Option<PaginatedRequestParams>,
1120 _: RequestContext<RoleServer>,
1121 ) -> Result<ListResourcesResult, McpError> {
1122 let graph = self.graph.lock().await;
1123 let mut resources = vec![
1124 RawResource::new("iwe://tree", "tree")
1125 .with_description("Full document tree structure")
1126 .with_mime_type("application/json")
1127 .no_annotation(),
1128 RawResource::new("iwe://stats", "stats")
1129 .with_description("Aggregate graph statistics")
1130 .with_mime_type("application/json")
1131 .no_annotation(),
1132 RawResource::new("iwe://config", "config")
1133 .with_description("Project configuration: markdown options, templates, actions")
1134 .with_mime_type("application/json")
1135 .no_annotation(),
1136 ];
1137
1138 for key in graph.keys().iter().take(100) {
1139 let title = (&*graph)
1140 .get_key_title(key)
1141 .unwrap_or_else(|| key.to_string());
1142 resources.push(
1143 RawResource::new(format!("iwe://documents/{}", key), title)
1144 .with_mime_type("text/markdown")
1145 .no_annotation(),
1146 );
1147 }
1148
1149 Ok(ListResourcesResult {
1150 resources,
1151 next_cursor: None,
1152 meta: None,
1153 })
1154 }
1155
1156 async fn read_resource(
1157 &self,
1158 request: ReadResourceRequestParams,
1159 _: RequestContext<RoleServer>,
1160 ) -> Result<ReadResourceResult, McpError> {
1161 let uri = &request.uri;
1162 let graph = self.graph.lock().await;
1163
1164 if uri == "iwe://tree" {
1165 let paths = graph.paths();
1166 let mut root_keys: Vec<Key> = paths
1167 .iter()
1168 .filter(|n| n.ids().len() == 1)
1169 .filter_map(|n| n.first_id())
1170 .map(|id| (&*graph).node(id).node_key())
1171 .collect();
1172 root_keys.sort();
1173 root_keys.dedup();
1174
1175 let mut trees: Vec<TreeNode> = Vec::new();
1176 for root_key in &root_keys {
1177 let mut visited: HashSet<Key> = HashSet::new();
1178 if let Some(node) = build_tree_node(&graph, root_key, 4, &mut visited) {
1179 trees.push(node);
1180 }
1181 }
1182 let json = serde_json::to_string_pretty(&trees)
1183 .map_err(|e| McpError::internal_error(e.to_string(), None))?;
1184 return Ok(ReadResourceResult::new(vec![ResourceContents::text(
1185 json,
1186 uri.clone(),
1187 )]));
1188 }
1189
1190 if uri == "iwe://stats" {
1191 let stats = GraphStatistics::from_graph(&graph);
1192 let json = serde_json::to_string_pretty(&stats)
1193 .map_err(|e| McpError::internal_error(e.to_string(), None))?;
1194 return Ok(ReadResourceResult::new(vec![ResourceContents::text(
1195 json,
1196 uri.clone(),
1197 )]));
1198 }
1199
1200 if uri == "iwe://config" {
1201 let config_view = ConfigResource::from_config(&self.config, self);
1202 let json = serde_json::to_string_pretty(&config_view)
1203 .map_err(|e| McpError::internal_error(e.to_string(), None))?;
1204 return Ok(ReadResourceResult::new(vec![ResourceContents::text(
1205 json,
1206 uri.clone(),
1207 )]));
1208 }
1209
1210 if let Some(key_str) = uri.strip_prefix("iwe://documents/") {
1211 let key = Key::name(key_str);
1212 let content = graph
1213 .get_document(&key)
1214 .ok_or_else(|| {
1215 McpError::resource_not_found(
1216 format!("Document '{}' not found", key_str),
1217 None,
1218 )
1219 })?
1220 .to_string();
1221 return Ok(ReadResourceResult::new(vec![ResourceContents::text(
1222 content,
1223 uri.clone(),
1224 )]));
1225 }
1226
1227 Err(McpError::resource_not_found(
1228 format!("Unknown resource: {}", uri),
1229 None,
1230 ))
1231 }
1232
1233 async fn list_resource_templates(
1234 &self,
1235 _request: Option<PaginatedRequestParams>,
1236 _: RequestContext<RoleServer>,
1237 ) -> Result<ListResourceTemplatesResult, McpError> {
1238 Ok(ListResourceTemplatesResult {
1239 resource_templates: vec![
1240 RawResourceTemplate::new("iwe://documents/{key}", "document")
1241 .with_description("A document in the knowledge graph by key")
1242 .with_mime_type("text/markdown")
1243 .no_annotation(),
1244 ],
1245 next_cursor: None,
1246 meta: None,
1247 })
1248 }
1249}
1250
1251impl IweServer {
1252 pub fn new(base_path: &str, configuration: &Configuration) -> Self {
1253 let path = PathBuf::from_str(base_path).expect("valid path");
1254 let state = new_for_path(&path);
1255 let graph = Graph::from_state(
1256 state,
1257 false,
1258 configuration.markdown.clone().into(),
1259 configuration.library.frontmatter_document_title.clone(),
1260 );
1261 Self {
1262 graph: Arc::new(Mutex::new(graph)),
1263 base_path: Some(path),
1264 config: configuration.clone(),
1265 tool_router: Self::tool_router(),
1266 prompt_router: Self::prompt_router(),
1267 }
1268 }
1269
1270 pub fn from_documents(documents: Vec<(&str, &str)>) -> Self {
1271 Self::from_documents_with_config(documents, Configuration::default())
1272 }
1273
1274 pub fn from_documents_with_config(documents: Vec<(&str, &str)>, config: Configuration) -> Self {
1275 let state = new_from_hashmap(
1276 documents
1277 .into_iter()
1278 .map(|(k, v)| (k.to_string(), v.to_string()))
1279 .collect::<HashMap<String, String>>(),
1280 );
1281 let graph = Graph::from_state(state, true, MarkdownOptions::default(), None);
1282 Self {
1283 graph: Arc::new(Mutex::new(graph)),
1284 base_path: None,
1285 config,
1286 tool_router: Self::tool_router(),
1287 prompt_router: Self::prompt_router(),
1288 }
1289 }
1290
1291 fn apply_changes(graph: &mut Graph, changes: &Changes) {
1292 for key in &changes.removes {
1293 graph.remove_document(key.clone());
1294 }
1295 for (key, markdown) in &changes.creates {
1296 graph.insert_document(key.clone(), markdown.clone());
1297 }
1298 for (key, markdown) in &changes.updates {
1299 graph.update_document(key.clone(), markdown.clone());
1300 }
1301 }
1302
1303 fn write_file(&self, key: &Key, content: &str) {
1304 if let Some(base_path) = &self.base_path {
1305 let file_path = base_path.join(format!("{}.md", key));
1306 if let Some(parent) = file_path.parent() {
1307 std::fs::create_dir_all(parent).ok();
1308 }
1309 std::fs::write(&file_path, content).ok();
1310 }
1311 }
1312
1313 fn write_changes(&self, changes: &Changes) {
1314 if let Some(base_path) = &self.base_path {
1315 for key in &changes.removes {
1316 let file_path = base_path.join(format!("{}.md", key));
1317 if file_path.exists() {
1318 std::fs::remove_file(&file_path).ok();
1319 }
1320 }
1321 for (key, markdown) in &changes.creates {
1322 let file_path = base_path.join(format!("{}.md", key));
1323 if let Some(parent) = file_path.parent() {
1324 std::fs::create_dir_all(parent).ok();
1325 }
1326 std::fs::write(&file_path, markdown).ok();
1327 }
1328 for (key, markdown) in &changes.updates {
1329 let file_path = base_path.join(format!("{}.md", key));
1330 std::fs::write(&file_path, markdown).ok();
1331 }
1332 }
1333 }
1334
1335 pub fn start_watching(&self) {
1336 if let Some(base_path) = &self.base_path {
1337 watcher::start(self.graph.clone(), base_path.clone());
1338 }
1339 }
1340
1341 fn render_key_template(&self, template: &str) -> String {
1342 let now = Local::now();
1343 let date_format = self
1344 .config
1345 .library
1346 .date_format
1347 .as_deref()
1348 .unwrap_or(DEFAULT_KEY_DATE_FORMAT);
1349 let formatted = now.format(date_format).to_string();
1350 Environment::new()
1351 .template_from_str(template)
1352 .expect("valid key template")
1353 .render(context! {
1354 today => formatted,
1355 now => formatted,
1356 })
1357 .expect("key template to render")
1358 }
1359
1360 fn render_document_template(&self, template: &str, content: &str) -> String {
1361 let now = Local::now();
1362 let date_format = self
1363 .config
1364 .markdown
1365 .date_format
1366 .as_deref()
1367 .unwrap_or("%b %d, %Y");
1368 let formatted = now.format(date_format).to_string();
1369 Environment::new()
1370 .template_from_str(template)
1371 .expect("valid document template")
1372 .render(context! {
1373 today => formatted,
1374 now => formatted,
1375 content => content,
1376 })
1377 .expect("document template to render")
1378 }
1379}