lean-ctx-server 3.12.1

The LeanCTX MCP server and its tools on top of LeanCTX | OSS.
Documentation
// SPDX-License-Identifier: Apache-2.0

use rmcp::ErrorData;
use rmcp::model::Tool;
use serde_json::{Map, Value, json};

use crate::server::tool_trait::{McpTool, ToolContext, ToolOutput, get_str, get_str_array};
use crate::tools::ctx_doc::{Args, handle};
use lean_ctx::tool_defs::tool_def;

pub struct CtxDocTool;

impl McpTool for CtxDocTool {
    fn name(&self) -> &'static str {
        "ctx_doc"
    }

    fn tool_def(&self) -> Tool {
        tool_def(
            "ctx_doc",
            "Project documentation in the linked Hive vault: pages of sections, bound to code.\n\
             WORKFLOW: action=list or search first, then show slug (outline=true, section=<heading-slug>).\n\
             Reads are local (offline). action=write slug content='# Title\\n…' creates or revises a page;\n\
             action=edit slug find replace changes one unique stretch; action=note adds an addendum.\n\
             anchors=[file:…] binds a page to code; action=status lists stale pages, action=confirm re-pins after a check.\n\
             Writes are queued and sent in the background. Link pages as [text](/slug#heading).",
            json!({
                "type": "object",
                "properties": {
                    "action": { "type": "string", "description": "list|show|search|status|write|edit|note|confirm" },
                    "anchors": { "type": "array", "items": { "type": "string" }, "description": "write: code the page derives from (file:src/x.rs, dir:src/core, glob:src/**/*.rs, shape:docs); a change makes the page stale" },
                    "slug": { "type": "string", "description": "Page slug, e.g. guide/retry (list: a prefix)" },
                    "section": { "type": "string", "description": "show: one section by its heading slug" },
                    "outline": { "type": "boolean", "description": "show: headings only" },
                    "query": { "type": "string", "description": "search words" },
                    "content": { "type": "string", "description": "write: the whole page markdown; note: the note" },
                    "find": { "type": "string", "description": "edit: exact text that occurs once" },
                    "replace": { "type": "string", "description": "edit: its replacement" },
                    "type": { "type": "string", "description": "write (new page): reference|guide|design|…" },
                    "status": { "type": "string", "description": "write (new page): draft|canonical" }
                },
                "required": ["action"]
            }),
        )
    }

    fn handle(
        &self,
        args: &Map<String, Value>,
        ctx: &ToolContext,
    ) -> Result<ToolOutput, ErrorData> {
        let owned = |k: &str| get_str(args, k);
        let action = owned("action").unwrap_or_else(|| "list".to_owned());
        let (slug, section, query, content, find, replace, kind, status) = (
            owned("slug"),
            owned("section"),
            owned("query"),
            owned("content"),
            owned("find"),
            owned("replace"),
            owned("type"),
            owned("status"),
        );
        let anchors = get_str_array(args, "anchors").unwrap_or_default();
        let call = Args {
            anchors: &anchors,
            action: &action,
            slug: slug.as_deref(),
            section: section.as_deref(),
            query: query.as_deref(),
            content: content.as_deref(),
            find: find.as_deref(),
            replace: replace.as_deref(),
            kind: kind.as_deref(),
            status: status.as_deref(),
            outline: args
                .get("outline")
                .and_then(Value::as_bool)
                .unwrap_or(false),
        };
        Ok(ToolOutput::simple(handle(&ctx.project_root, &call)))
    }
}