windjammer-mcp 0.48.0

Model Context Protocol (MCP) server for Windjammer - AI-powered development
Documentation
//! MCP tools for Windjammer code understanding, generation, and refactoring

pub mod analyze_ssr_routing;
pub mod analyze_types;
pub mod catalog;
pub mod explain_error;
pub mod generate_code;
pub mod generate_component;
pub mod generate_game_entity;
pub mod get_definition;
pub mod get_language_info;
pub mod parse_code;
pub mod registry;
pub mod search_workspace;

pub mod refactor_extract_function;
pub mod refactor_inline_variable;
pub mod refactor_rename_symbol;

pub use registry::{error_response, text_response, ToolHandler};

use crate::error::{McpError, McpResult};
use crate::protocol::{Tool, ToolCallResult, ToolContent};
use catalog::{all_tool_specs, input_schema_for};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use windjammer_lsp::database::WindjammerDatabase;

pub struct ToolRegistry {
    db: Arc<Mutex<WindjammerDatabase>>,
    handlers: HashMap<String, ToolHandler>,
    tools: Vec<Tool>,
}

impl ToolRegistry {
    pub fn new(db: Arc<Mutex<WindjammerDatabase>>) -> Self {
        let mut registry = Self {
            db: db.clone(),
            handlers: HashMap::new(),
            tools: Vec::new(),
        };
        registry.register_all_tools();
        registry
    }

    fn register_all_tools(&mut self) {
        for spec in all_tool_specs() {
            let schema = input_schema_for(spec.name)
                .unwrap_or_else(|| serde_json::json!({"type": "object", "properties": {}}));
            let handler = self.handler_for(spec.name);
            self.tools.push(Tool {
                name: spec.name.to_string(),
                description: spec.description.to_string(),
                input_schema: schema,
            });
            self.handlers.insert(spec.name.to_string(), handler);
        }
    }

    fn handler_for(&self, name: &str) -> ToolHandler {
        let unknown_name = name.to_string();
        match name {
            "parse_code" => Box::new(move |d, args| Box::pin(parse_code::handle(d, args))),
            "analyze_types" => Box::new(move |d, args| Box::pin(analyze_types::handle(d, args))),
            "get_definition" => Box::new(move |d, args| Box::pin(get_definition::handle(d, args))),
            "generate_code" => Box::new(move |d, args| Box::pin(generate_code::handle(d, args))),
            "explain_error" => Box::new(move |d, args| Box::pin(explain_error::handle(d, args))),
            "search_workspace" => {
                Box::new(move |d, args| Box::pin(search_workspace::handle(d, args)))
            }
            "extract_function" => {
                Box::new(move |d, args| Box::pin(refactor_extract_function::handle(d, args)))
            }
            "inline_variable" => {
                Box::new(move |d, args| Box::pin(refactor_inline_variable::handle(d, args)))
            }
            "rename_symbol" => {
                Box::new(move |d, args| Box::pin(refactor_rename_symbol::handle(d, args)))
            }
            "generate_component" => Box::new(move |_d, args| {
                Box::pin(async move {
                    let result = generate_component::execute(args)
                        .map_err(|e: String| McpError::InternalError { message: e })?;
                    Ok(ToolCallResult {
                        content: result
                            .into_iter()
                            .map(|v| ToolContent::Text {
                                text: v["text"].as_str().unwrap_or("").to_string(),
                            })
                            .collect(),
                        is_error: false,
                    })
                })
            }),
            "generate_game_entity" => Box::new(move |_d, args| {
                Box::pin(async move {
                    let result = generate_game_entity::execute(args)
                        .map_err(|e: String| McpError::InternalError { message: e })?;
                    Ok(ToolCallResult {
                        content: result
                            .into_iter()
                            .map(|v| ToolContent::Text {
                                text: v["text"].as_str().unwrap_or("").to_string(),
                            })
                            .collect(),
                        is_error: false,
                    })
                })
            }),
            "analyze_ssr_routing" => Box::new(move |_d, args| {
                Box::pin(async move {
                    let result = analyze_ssr_routing::execute(args)
                        .map_err(|e: String| McpError::InternalError { message: e })?;
                    Ok(ToolCallResult {
                        content: result
                            .into_iter()
                            .map(|v| ToolContent::Text {
                                text: v["text"].as_str().unwrap_or("").to_string(),
                            })
                            .collect(),
                        is_error: false,
                    })
                })
            }),
            "get_language_info" => {
                Box::new(move |d, args| Box::pin(get_language_info::handle(d, args)))
            }
            _ => Box::new(move |_d, _args| {
                let tool_name = unknown_name.clone();
                Box::pin(async move { Err(McpError::ToolNotFound { tool_name }) })
            }),
        }
    }

    pub fn list_tools(&self) -> Vec<Tool> {
        self.tools.clone()
    }

    pub async fn call_tool(&self, name: &str, arguments: Value) -> McpResult<ToolCallResult> {
        let handler = self
            .handlers
            .get(name)
            .ok_or_else(|| McpError::ToolNotFound {
                tool_name: name.to_string(),
            })?;
        handler(self.db.clone(), arguments).await
    }
}

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

    #[tokio::test]
    async fn test_tool_registry_matches_catalog() {
        let db = Arc::new(Mutex::new(WindjammerDatabase::new()));
        let registry = ToolRegistry::new(db);
        assert_eq!(registry.list_tools().len(), all_tool_specs().len());
    }

    #[test]
    fn test_manifest_count_matches_catalog() {
        let manifest = catalog::manifest_json();
        assert_eq!(
            manifest["count"].as_u64(),
            Some(all_tool_specs().len() as u64)
        );
    }
}