trusty-analyzer-mcp 0.1.0

MCP server for trusty-analyzer: exposes complexity_hotspots, find_smells, analyze_quality tools
Documentation
//! trusty-analyzer-mcp — MCP tool definitions for the analysis daemon.
//!
//! Why: parity with `trusty-search-mcp`. Exposes analysis tools backed by the
//! analyzer's HTTP daemon. The JSON-RPC stdio loop and HTTP/SSE transports
//! land in a follow-up; for now this crate publishes the tool *schemas* so
//! both the daemon and external clients (Claude Code, MCP debuggers) can
//! reference a single authoritative list.
//!
//! What: a `tool_definitions()` function returning the static tool catalogue
//! (`name`, `description`, `input_schema`, upstream HTTP route). Three
//! analysis tools are exposed today: `cluster_concepts`, `ner_extract`,
//! `ingest_scip`. Plus the existing complexity / smells / quality tools the
//! analyzer already serves (added so the catalogue is complete).
//!
//! Test: see `#[cfg(test)]` — round-trips the catalogue through `serde_json`
//! and asserts the expected tool names are present.

use serde::{Deserialize, Serialize};

/// A single MCP tool definition. Mirrors the shape used by `trusty-search-mcp`
/// so future transport code can ship the catalogue verbatim.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
    /// Tool name as exposed to the MCP client.
    pub name: &'static str,
    /// Short user-facing description (≤200 chars).
    pub description: &'static str,
    /// JSON-schema describing the tool's input payload.
    pub input_schema: serde_json::Value,
    /// HTTP route this tool proxies to on the analyzer daemon.
    pub http_route: &'static str,
    /// HTTP method (`"GET"` or `"POST"`).
    pub http_method: &'static str,
}

/// Static catalogue of MCP tools served by the analyzer daemon.
///
/// Why: callers (transport code, tests, doc generators) all need the same
/// list. Centralising it here prevents drift between the daemon's actual
/// routes and the catalogue advertised to MCP clients.
/// What: returns a `Vec<ToolDefinition>` covering the three new analysis
/// tools plus the existing complexity / smells / quality / facts routes.
/// Test: `tool_definitions_includes_new_analysis_tools`.
pub fn tool_definitions() -> Vec<ToolDefinition> {
    vec![
        ToolDefinition {
            name: "cluster_concepts",
            description:
                "Cluster doc-comment themes from a set of source contents. Returns ConceptCluster entities labelled by nearest vocab word.",
            input_schema: serde_json::json!({
                "type": "object",
                "properties": {
                    "contents": {
                        "type": "array",
                        "items": { "type": "string" },
                        "description": "Raw chunk source contents."
                    },
                    "file": {
                        "type": "string",
                        "description": "Anchor filename for emitted entity ids."
                    }
                },
                "required": ["contents"]
            }),
            http_route: "/analyze/concept-cluster",
            http_method: "POST",
        },
        ToolDefinition {
            name: "ner_extract",
            description:
                "Extract NaturalLanguagePhrase entities from doc-comment text via the optional ONNX NER model. Returns an empty list when the model is not installed.",
            input_schema: serde_json::json!({
                "type": "object",
                "properties": {
                    "text": { "type": "string", "description": "Source text or pre-extracted doc text." },
                    "file": { "type": "string" },
                    "extract_doc_comments_first": {
                        "type": "boolean",
                        "description": "When true, pull /// and //! lines from `text` before running NER."
                    }
                },
                "required": ["text"]
            }),
            http_route: "/analyze/ner",
            http_method: "POST",
        },
        ToolDefinition {
            name: "ingest_scip",
            description:
                "Ingest SCIP-derived entity references and edges. Returns the materialised RawEntity list and edge tuples.",
            input_schema: serde_json::json!({
                "type": "object",
                "properties": {
                    "refs": { "type": "array", "description": "ScipEntityRef objects." },
                    "edges": { "type": "array", "description": "ScipEdge objects." }
                }
            }),
            http_route: "/analyze/scip-ingest",
            http_method: "POST",
        },
        ToolDefinition {
            name: "complexity_hotspots",
            description: "Return the top-N chunks by cyclomatic complexity for an index.",
            input_schema: serde_json::json!({
                "type": "object",
                "properties": {
                    "index_id": { "type": "string" },
                    "top_n": { "type": "integer", "default": 20 }
                },
                "required": ["index_id"]
            }),
            http_route: "/analyze/{index_id}/complexity_hotspots",
            http_method: "GET",
        },
        ToolDefinition {
            name: "find_smells",
            description: "List chunks with one or more code-smell findings.",
            input_schema: serde_json::json!({
                "type": "object",
                "properties": { "index_id": { "type": "string" } },
                "required": ["index_id"]
            }),
            http_route: "/analyze/{index_id}/smells",
            http_method: "GET",
        },
        ToolDefinition {
            name: "analyze_quality",
            description: "Aggregate quality stats for an index (avg cyclomatic, %A grade, smell count).",
            input_schema: serde_json::json!({
                "type": "object",
                "properties": { "index_id": { "type": "string" } },
                "required": ["index_id"]
            }),
            http_route: "/analyze/{index_id}/quality",
            http_method: "GET",
        },
    ]
}

/// Marker for downstream callers that the MCP transport layer is reserved but
/// not yet wired up. Replace with `McpServer` + transports when implementing.
pub fn placeholder() -> &'static str {
    "trusty-analyzer-mcp: transport layer pending; tool definitions available via tool_definitions()"
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn tool_definitions_includes_new_analysis_tools() {
        let defs = tool_definitions();
        let names: Vec<&str> = defs.iter().map(|d| d.name).collect();
        assert!(names.contains(&"cluster_concepts"));
        assert!(names.contains(&"ner_extract"));
        assert!(names.contains(&"ingest_scip"));
    }

    #[test]
    fn tool_definitions_serialise() {
        let defs = tool_definitions();
        let json = serde_json::to_string(&defs).expect("serialise");
        assert!(json.contains("cluster_concepts"));
    }

    #[test]
    fn tool_definitions_have_non_empty_descriptions() {
        for def in tool_definitions() {
            assert!(
                !def.description.is_empty(),
                "{} missing description",
                def.name
            );
            assert!(
                !def.http_route.is_empty(),
                "{} missing http_route",
                def.name
            );
        }
    }
}