Skip to main content

iwec/
lib.rs

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(&params.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(&params.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(&params.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, &params.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(&params.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(&params.old_key);
562        let new_key = Key::name(&params.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(&params.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(&sections);
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(&params.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}