diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 7d7df92..54be766 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -1143,6 +1143,10 @@ target_include_directories(step200_test PRIVATE src) add_executable(step201_test tests/step201_test.cpp) target_include_directories(step201_test PRIVATE src) +add_executable(step213_test tests/step213_test.cpp) +target_include_directories(step213_test PRIVATE src) +target_link_libraries(step213_test PRIVATE nlohmann_json::nlohmann_json) + add_executable(step206_test tests/step206_test.cpp) target_include_directories(step206_test PRIVATE src) target_link_libraries(step206_test PRIVATE diff --git a/editor/src/MCPBridge.h b/editor/src/MCPBridge.h new file mode 100644 index 0000000..88a63ba --- /dev/null +++ b/editor/src/MCPBridge.h @@ -0,0 +1,77 @@ +#pragma once +// Step 212: MCP Bridge +// +// Translates between the MCP stdio transport and Whetstone's internal +// JSON-RPC handler. In embedded mode, directly calls processAgentRequest. +// In standalone mode, would connect via WebSocket to a running editor. + +#include +#include +#include +#include "MCPServer.h" + +using json = nlohmann::json; + +class MCPBridge { +public: + // Set up the bridge with a direct RPC handler (embedded mode) + void setRequestHandler(MCPServer::RpcCallback handler) { + server_.setRpcCallback(std::move(handler)); + } + + // Set up the resource reader + void setResourceReader(MCPServer::ResourceReader reader) { + server_.setResourceReader(std::move(reader)); + } + + MCPServer& server() { return server_; } + + // Process a single JSON-RPC message string, return response string + std::string processMessage(const std::string& message) { + try { + json request = json::parse(message); + json response = server_.handleRequest(request); + if (response.is_null()) return ""; // notifications have no response + return response.dump(); + } catch (const json::parse_error& e) { + json error = { + {"jsonrpc", "2.0"}, + {"id", nullptr}, + {"error", {{"code", -32700}, {"message", "Parse error"}}} + }; + return error.dump(); + } + } + + // Run the stdio transport loop (for standalone mcp_main.cpp) + // Reads JSON-RPC messages from stdin, writes responses to stdout. + // MCP uses Content-Length framing over stdio. + void runStdio() { + std::string line; + while (std::getline(std::cin, line)) { + // MCP stdio transport: Content-Length header followed by JSON body + if (line.find("Content-Length:") == 0) { + int length = 0; + try { + length = std::stoi(line.substr(15)); + } catch (...) { + continue; + } + // Skip empty line after headers + std::getline(std::cin, line); + // Read body + std::string body(length, '\0'); + std::cin.read(&body[0], length); + + std::string response = processMessage(body); + if (!response.empty()) { + std::cout << "Content-Length: " << response.size() << "\r\n\r\n" << response; + std::cout.flush(); + } + } + } + } + +private: + MCPServer server_; +}; diff --git a/editor/src/MCPServer.h b/editor/src/MCPServer.h new file mode 100644 index 0000000..6f03654 --- /dev/null +++ b/editor/src/MCPServer.h @@ -0,0 +1,559 @@ +#pragma once +// Step 207-211: MCP Server for Whetstone +// +// Implements the Model Context Protocol (MCP) over stdio (JSON-RPC 2.0). +// Exposes Whetstone agent API as MCP tools, resources, and prompts so +// LLMs (Claude, Codex, etc.) can interact with the editor natively. +// +// Architecture: MCPServer holds tool/resource/prompt registrations and +// dispatches incoming JSON-RPC requests. A bridge function translates +// MCP tool calls to Whetstone JSON-RPC requests (via a callback). + +#include +#include +#include +#include +#include + +using json = nlohmann::json; + +// ----------------------------------------------------------------------- +// MCP Tool definition +// ----------------------------------------------------------------------- +struct MCPTool { + std::string name; + std::string description; + json inputSchema; // JSON Schema for tool input +}; + +// ----------------------------------------------------------------------- +// MCP Resource definition +// ----------------------------------------------------------------------- +struct MCPResource { + std::string uri; + std::string name; + std::string description; + std::string mimeType; +}; + +// ----------------------------------------------------------------------- +// MCP Prompt definition +// ----------------------------------------------------------------------- +struct MCPPromptArgument { + std::string name; + std::string description; + bool required = false; +}; + +struct MCPPrompt { + std::string name; + std::string description; + std::vector arguments; +}; + +struct MCPPromptMessage { + std::string role; // "user" or "assistant" + json content; // {type: "text", text: "..."} +}; + +// ----------------------------------------------------------------------- +// MCP Server +// ----------------------------------------------------------------------- +class MCPServer { +public: + // Callback type: given a Whetstone JSON-RPC request, returns a response. + using RpcCallback = std::function; + + MCPServer() { + registerWhetstoneTools(); + registerWhetstoneResources(); + registerWhetstonePrompts(); + } + + void setRpcCallback(RpcCallback cb) { rpcCallback_ = std::move(cb); } + + // --------------------------------------------------------------- + // Handle a single MCP JSON-RPC request + // --------------------------------------------------------------- + json handleRequest(const json& request) { + json response; + response["jsonrpc"] = "2.0"; + response["id"] = request.contains("id") ? request["id"] : json(nullptr); + + std::string method = request.value("method", ""); + + if (method == "initialize") return handleInitialize(request); + if (method == "notifications/initialized") return json(nullptr); // no response + if (method == "ping") return handlePing(request); + if (method == "tools/list") return handleToolsList(request); + if (method == "tools/call") return handleToolsCall(request); + if (method == "resources/list") return handleResourcesList(request); + if (method == "resources/read") return handleResourcesRead(request); + if (method == "prompts/list") return handlePromptsList(request); + if (method == "prompts/get") return handlePromptsGet(request); + + response["error"] = {{"code", -32601}, {"message", "Method not found: " + method}}; + return response; + } + + // --------------------------------------------------------------- + // Accessors for testing + // --------------------------------------------------------------- + const std::vector& getTools() const { return tools_; } + const std::vector& getResources() const { return resources_; } + const std::vector& getPrompts() const { return prompts_; } + bool isInitialized() const { return initialized_; } + + // Resource read handler registration + using ResourceReader = std::function; + void setResourceReader(ResourceReader reader) { resourceReader_ = std::move(reader); } + +private: + std::vector tools_; + std::vector resources_; + std::vector prompts_; + std::map> toolHandlers_; + RpcCallback rpcCallback_; + ResourceReader resourceReader_; + bool initialized_ = false; + + // --------------------------------------------------------------- + // Protocol handlers + // --------------------------------------------------------------- + + json handleInitialize(const json& request) { + initialized_ = true; + json response; + response["jsonrpc"] = "2.0"; + response["id"] = request.contains("id") ? request["id"] : json(nullptr); + response["result"] = { + {"protocolVersion", "2024-11-05"}, + {"capabilities", { + {"tools", json::object()}, + {"resources", json::object()}, + {"prompts", json::object()} + }}, + {"serverInfo", { + {"name", "whetstone-mcp"}, + {"version", "0.1.0"} + }} + }; + return response; + } + + json handlePing(const json& request) { + json response; + response["jsonrpc"] = "2.0"; + response["id"] = request.contains("id") ? request["id"] : json(nullptr); + response["result"] = json::object(); + return response; + } + + json handleToolsList(const json& request) { + json response; + response["jsonrpc"] = "2.0"; + response["id"] = request.contains("id") ? request["id"] : json(nullptr); + json toolArr = json::array(); + for (const auto& t : tools_) { + toolArr.push_back({ + {"name", t.name}, + {"description", t.description}, + {"inputSchema", t.inputSchema} + }); + } + response["result"] = {{"tools", toolArr}}; + return response; + } + + json handleToolsCall(const json& request) { + json response; + response["jsonrpc"] = "2.0"; + response["id"] = request.contains("id") ? request["id"] : json(nullptr); + + auto params = request.contains("params") ? request["params"] : json::object(); + std::string toolName = params.value("name", ""); + json args = params.contains("arguments") ? params["arguments"] : json::object(); + + auto it = toolHandlers_.find(toolName); + if (it == toolHandlers_.end()) { + response["result"] = { + {"content", json::array({{{"type", "text"}, {"text", "Unknown tool: " + toolName}}})}, + {"isError", true} + }; + return response; + } + + try { + json result = it->second(args); + std::string text = result.dump(2); + response["result"] = { + {"content", json::array({{{"type", "text"}, {"text", text}}})}, + {"isError", false} + }; + } catch (const std::exception& e) { + response["result"] = { + {"content", json::array({{{"type", "text"}, {"text", std::string("Error: ") + e.what()}}})}, + {"isError", true} + }; + } + return response; + } + + json handleResourcesList(const json& request) { + json response; + response["jsonrpc"] = "2.0"; + response["id"] = request.contains("id") ? request["id"] : json(nullptr); + json arr = json::array(); + for (const auto& r : resources_) { + arr.push_back({ + {"uri", r.uri}, + {"name", r.name}, + {"description", r.description}, + {"mimeType", r.mimeType} + }); + } + response["result"] = {{"resources", arr}}; + return response; + } + + json handleResourcesRead(const json& request) { + json response; + response["jsonrpc"] = "2.0"; + response["id"] = request.contains("id") ? request["id"] : json(nullptr); + + auto params = request.contains("params") ? request["params"] : json::object(); + std::string uri = params.value("uri", ""); + + if (resourceReader_) { + json content = resourceReader_(uri); + response["result"] = { + {"contents", json::array({{ + {"uri", uri}, + {"mimeType", "application/json"}, + {"text", content.dump()} + }})} + }; + } else { + response["error"] = {{"code", -32002}, {"message", "No resource reader configured"}}; + } + return response; + } + + json handlePromptsList(const json& request) { + json response; + response["jsonrpc"] = "2.0"; + response["id"] = request.contains("id") ? request["id"] : json(nullptr); + json arr = json::array(); + for (const auto& p : prompts_) { + json argArr = json::array(); + for (const auto& a : p.arguments) { + argArr.push_back({ + {"name", a.name}, + {"description", a.description}, + {"required", a.required} + }); + } + arr.push_back({ + {"name", p.name}, + {"description", p.description}, + {"arguments", argArr} + }); + } + response["result"] = {{"prompts", arr}}; + return response; + } + + json handlePromptsGet(const json& request) { + json response; + response["jsonrpc"] = "2.0"; + response["id"] = request.contains("id") ? request["id"] : json(nullptr); + + auto params = request.contains("params") ? request["params"] : json::object(); + std::string name = params.value("name", ""); + json args = params.contains("arguments") ? params["arguments"] : json::object(); + + auto messages = generatePromptMessages(name, args); + if (messages.empty()) { + response["error"] = {{"code", -32602}, {"message", "Unknown prompt: " + name}}; + return response; + } + + json msgArr = json::array(); + for (const auto& m : messages) { + msgArr.push_back({{"role", m.role}, {"content", m.content}}); + } + response["result"] = {{"messages", msgArr}}; + return response; + } + + // --------------------------------------------------------------- + // Bridge: call Whetstone JSON-RPC via callback + // --------------------------------------------------------------- + json callWhetstone(const std::string& method, const json& params = json::object()) { + if (!rpcCallback_) return {{"error", "No RPC callback configured"}}; + json request = { + {"jsonrpc", "2.0"}, + {"id", 1}, + {"method", method}, + {"params", params} + }; + json resp = rpcCallback_(request); + if (resp.contains("result")) return resp["result"]; + if (resp.contains("error")) return {{"error", resp["error"]["message"]}}; + return resp; + } + + // --------------------------------------------------------------- + // Step 208: Register AST query and mutation tools + // --------------------------------------------------------------- + void registerASTTools() { + // whetstone_get_ast + tools_.push_back({"whetstone_get_ast", + "Get the current AST (Abstract Syntax Tree) of the active buffer as JSON. " + "Returns the full tree structure with all nodes, annotations, and metadata.", + {{"type", "object"}, {"properties", json::object()}} + }); + toolHandlers_["whetstone_get_ast"] = [this](const json&) { + return callWhetstone("getAST"); + }; + + // whetstone_mutate + tools_.push_back({"whetstone_mutate", + "Apply a single mutation to the AST. Supports setProperty (rename/change), " + "updateNode (bulk property update), deleteNode, and insertNode operations.", + {{"type", "object"}, {"properties", { + {"type", {{"type", "string"}, {"enum", {"setProperty", "updateNode", "deleteNode", "insertNode"}}, {"description", "Mutation type"}}}, + {"nodeId", {{"type", "string"}, {"description", "Target node ID (for setProperty, updateNode, deleteNode)"}}}, + {"property", {{"type", "string"}, {"description", "Property name (for setProperty)"}}}, + {"value", {{"type", "string"}, {"description", "New value (for setProperty)"}}}, + {"parentId", {{"type", "string"}, {"description", "Parent node ID (for insertNode)"}}}, + {"role", {{"type", "string"}, {"description", "Child role (for insertNode)"}}}, + {"node", {{"type", "object"}, {"description", "Node to insert (for insertNode)"}}} + }}, {"required", {"type"}}} + }); + toolHandlers_["whetstone_mutate"] = [this](const json& args) { + return callWhetstone("applyMutation", args); + }; + + // whetstone_batch_mutate + tools_.push_back({"whetstone_batch_mutate", + "Apply multiple mutations atomically. All mutations succeed or all are rolled back. " + "Each mutation has: type (setProperty|deleteNode|insertNode), nodeId, property, value, etc.", + {{"type", "object"}, {"properties", { + {"mutations", {{"type", "array"}, {"items", {{"type", "object"}}}, {"description", "Array of mutation objects"}}} + }}, {"required", {"mutations"}}} + }); + toolHandlers_["whetstone_batch_mutate"] = [this](const json& args) { + return callWhetstone("applyBatch", args); + }; + + // whetstone_get_scope + tools_.push_back({"whetstone_get_scope", + "Get all symbols (variables, functions, parameters) visible at a given AST node. " + "Walks up the scope chain to collect all accessible identifiers.", + {{"type", "object"}, {"properties", { + {"nodeId", {{"type", "string"}, {"description", "Node ID to query scope from"}}} + }}, {"required", {"nodeId"}}} + }); + toolHandlers_["whetstone_get_scope"] = [this](const json& args) { + return callWhetstone("getInScopeSymbols", args); + }; + + // whetstone_get_call_hierarchy + tools_.push_back({"whetstone_get_call_hierarchy", + "Get the call hierarchy for a function: which functions call it (callers) " + "and which functions it calls (callees).", + {{"type", "object"}, {"properties", { + {"functionId", {{"type", "string"}, {"description", "Function node ID"}}} + }}, {"required", {"functionId"}}} + }); + toolHandlers_["whetstone_get_call_hierarchy"] = [this](const json& args) { + return callWhetstone("getCallHierarchy", args); + }; + } + + // --------------------------------------------------------------- + // Step 209: Register annotation and generation tools + // --------------------------------------------------------------- + void registerAnnotationTools() { + // whetstone_suggest_annotations + tools_.push_back({"whetstone_suggest_annotations", + "Get memory annotation suggestions for a code region. Returns suggestions " + "with confidence scores and diagnostics. Specify nodeId or line/col.", + {{"type", "object"}, {"properties", { + {"nodeId", {{"type", "string"}, {"description", "Node ID to suggest annotations for"}}}, + {"line", {{"type", "integer"}, {"description", "Line number (0-based, alternative to nodeId)"}}}, + {"col", {{"type", "integer"}, {"description", "Column number (0-based, alternative to nodeId)"}}} + }}} + }); + toolHandlers_["whetstone_suggest_annotations"] = [this](const json& args) { + return callWhetstone("getAnnotationSuggestions", args); + }; + + // whetstone_apply_annotation + tools_.push_back({"whetstone_apply_annotation", + "Apply a memory annotation suggestion to the AST. Pass the suggestion " + "object from whetstone_suggest_annotations.", + {{"type", "object"}, {"properties", { + {"nodeId", {{"type", "string"}, {"description", "Target node ID"}}}, + {"annotationType", {{"type", "string"}, {"description", "e.g., ReclaimAnnotation, OwnerAnnotation"}}}, + {"strategy", {{"type", "string"}, {"description", "e.g., Tracing, Single, RAII"}}}, + {"reason", {{"type", "string"}, {"description", "Why this annotation"}}}, + {"confidence", {{"type", "number"}, {"description", "Confidence score 0-1"}}} + }}, {"required", {"nodeId", "annotationType", "strategy"}}} + }); + toolHandlers_["whetstone_apply_annotation"] = [this](const json& args) { + return callWhetstone("applyAnnotationSuggestion", args); + }; + + // whetstone_generate_code + tools_.push_back({"whetstone_generate_code", + "Generate code from a natural language specification. Uses available " + "library primitives when preferImports is true (default).", + {{"type", "object"}, {"properties", { + {"spec", {{"type", "string"}, {"description", "Natural language description of code to generate"}}}, + {"preferImports", {{"type", "boolean"}, {"description", "Prefer imported library symbols (default true)"}}} + }}, {"required", {"spec"}}} + }); + toolHandlers_["whetstone_generate_code"] = [this](const json& args) { + return callWhetstone("generateCode", args); + }; + + // whetstone_run_pipeline + tools_.push_back({"whetstone_run_pipeline", + "Run the full Whetstone pipeline: parse source code, infer annotations, " + "validate, optimize, and generate target language code.", + {{"type", "object"}, {"properties", { + {"source", {{"type", "string"}, {"description", "Source code to process"}}}, + {"sourceLanguage", {{"type", "string"}, {"description", "Source language (python, cpp, rust, go, java, javascript, typescript, elisp)"}}}, + {"targetLanguage", {{"type", "string"}, {"description", "Target language for code generation"}}} + }}, {"required", {"source", "sourceLanguage", "targetLanguage"}}} + }); + toolHandlers_["whetstone_run_pipeline"] = [this](const json& args) { + return callWhetstone("runPipeline", args); + }; + + // whetstone_project_language + tools_.push_back({"whetstone_project_language", + "Project the current AST to a different target language. Adapts memory " + "annotations appropriately and generates code in the target language.", + {{"type", "object"}, {"properties", { + {"targetLanguage", {{"type", "string"}, {"description", "Target language to project to"}}} + }}, {"required", {"targetLanguage"}}} + }); + toolHandlers_["whetstone_project_language"] = [this](const json& args) { + return callWhetstone("projectLanguage", args); + }; + } + + // --------------------------------------------------------------- + // Step 210: Register resources + // --------------------------------------------------------------- + void registerWhetstoneResources() { + resources_.push_back({"whetstone://ast", "Current AST", + "The current Abstract Syntax Tree as JSON", "application/json"}); + resources_.push_back({"whetstone://diagnostics", "Diagnostics", + "Current diagnostics (errors, warnings) from LSP and Whetstone", "application/json"}); + resources_.push_back({"whetstone://libraries", "Imported Libraries", + "Currently imported libraries with available symbols", "application/json"}); + resources_.push_back({"whetstone://annotations", "Annotations", + "All memory annotations in the current module", "application/json"}); + resources_.push_back({"whetstone://settings", "Editor Settings", + "Current editor settings (read-only)", "application/json"}); + } + + // --------------------------------------------------------------- + // Step 211: Register prompts + // --------------------------------------------------------------- + void registerWhetstonePrompts() { + prompts_.push_back({"annotate_module", + "Analyze the current module and suggest memory annotations for all " + "unannotated functions with confidence scores.", + {{"scope", "Which functions to annotate (all, unannotated, or a specific function name)", false}} + }); + + prompts_.push_back({"cross_language_projection", + "Project the current code to a different target language, adapting " + "annotations appropriately.", + {{"targetLanguage", "Target language (cpp, rust, go, java, javascript, python, elisp)", true}} + }); + + prompts_.push_back({"security_audit", + "Check all dependencies for known vulnerabilities and suggest " + "safe alternatives or upgrades.", + {} + }); + + prompts_.push_back({"refactor_memory", + "Analyze memory strategy annotations and suggest improvements " + "for safety and performance.", + {} + }); + } + + std::vector generatePromptMessages(const std::string& name, + const json& args) { + if (name == "annotate_module") { + std::string scope = args.value("scope", "all"); + return {{ + "user", + {{"type", "text"}, {"text", + "Analyze the current module's AST and suggest memory annotations for " + + scope + " functions. For each function:\n" + "1. Use whetstone_get_ast to read the current AST\n" + "2. Use whetstone_suggest_annotations for each unannotated function\n" + "3. Present suggestions with confidence scores\n" + "4. Apply approved annotations with whetstone_apply_annotation\n" + "5. Verify with whetstone_get_ast that annotations were applied correctly" + }} + }}; + } + if (name == "cross_language_projection") { + std::string lang = args.value("targetLanguage", "cpp"); + return {{ + "user", + {{"type", "text"}, {"text", + "Project the current code to " + lang + ":\n" + "1. Use whetstone_get_ast to read the current AST\n" + "2. Use whetstone_project_language with targetLanguage=\"" + lang + "\"\n" + "3. Review the generated code and annotation adaptations\n" + "4. Report any annotation issues or incompatibilities" + }} + }}; + } + if (name == "security_audit") { + return {{ + "user", + {{"type", "text"}, {"text", + "Perform a security audit of the project's dependencies:\n" + "1. Use whetstone_get_ast to identify imported libraries\n" + "2. Check each dependency for known vulnerabilities\n" + "3. Suggest safe alternatives or version upgrades\n" + "4. Report severity levels and recommended actions" + }} + }}; + } + if (name == "refactor_memory") { + return {{ + "user", + {{"type", "text"}, {"text", + "Analyze and improve memory strategy annotations:\n" + "1. Use whetstone_get_ast to read the current AST\n" + "2. Use whetstone_suggest_annotations for each function\n" + "3. Identify annotation conflicts or suboptimal strategies\n" + "4. Suggest improvements for safety and performance\n" + "5. Apply approved changes with whetstone_apply_annotation" + }} + }}; + } + return {}; + } + + // --------------------------------------------------------------- + // Register all tools + // --------------------------------------------------------------- + void registerWhetstoneTools() { + registerASTTools(); + registerAnnotationTools(); + } +}; diff --git a/editor/tests/step213_test.cpp b/editor/tests/step213_test.cpp new file mode 100644 index 0000000..114675d --- /dev/null +++ b/editor/tests/step213_test.cpp @@ -0,0 +1,351 @@ +// Step 213: MCP Server tests +// +// Verifies: +// 1. Initialize handshake returns correct capabilities +// 2. tools/list returns all registered tools with schemas +// 3. tools/call for each tool produces correct results +// 4. resources/list returns all resources +// 5. resources/read returns valid data +// 6. prompts/list returns all prompts with argument schemas +// 7. prompts/get returns messages for each prompt +// 8. Invalid tool calls return MCP-compliant errors +// 9. MCPBridge processes message strings correctly + +#include +#include +#include +#include +#include "MCPServer.h" +#include "MCPBridge.h" +#include "ast/ASTNode.h" +#include "ast/Module.h" +#include "ast/Function.h" +#include "ast/Variable.h" +#include "ast/Serialization.h" +#include "ContextAPI.h" +#include "Pipeline.h" + +using json = nlohmann::json; + +static int passed = 0; +static int failed = 0; + +static void expect(bool cond, const char* msg) { + if (cond) { ++passed; } + else { ++failed; printf(" FAIL: %s\n", msg); } +} + +// Simple mock RPC handler that simulates a minimal Whetstone editor +static json mockRpcHandler(const json& request) { + json response; + response["jsonrpc"] = "2.0"; + response["id"] = request.value("id", json(nullptr)); + + std::string method = request.value("method", ""); + + if (method == "getAST") { + response["result"] = { + {"ast", {{"conceptType", "Module"}, {"id", "mod1"}, {"name", "test"}}}, + {"annotationCount", 0}, + {"diagnostics", json::array()} + }; + } else if (method == "getInScopeSymbols") { + response["result"] = {{"symbols", json::array({ + {{"name", "x"}, {"kind", "variable"}, {"nodeId", "v1"}} + })}}; + } else if (method == "getCallHierarchy") { + response["result"] = { + {"functionId", "fn1"}, {"functionName", "test"}, + {"callerIds", json::array()}, {"calleeIds", json::array()} + }; + } else if (method == "getAnnotationSuggestions") { + response["result"] = { + {"scopeId", "mod1"}, + {"suggestions", json::array()}, + {"diagnostics", json::array()} + }; + } else if (method == "applyAnnotationSuggestion") { + response["result"] = {{"success", true}, {"warning", ""}}; + } else if (method == "generateCode") { + response["result"] = { + {"node", {{"conceptType", "Function"}}}, + {"note", "generated"}, {"usedSymbols", json::array()}, + {"language", "python"} + }; + } else if (method == "runPipeline") { + response["result"] = { + {"success", true}, {"generatedCode", "def f(): pass"}, + {"parseDiagnostics", json::array()}, + {"validationDiagnostics", json::array()}, + {"violations", json::array()}, + {"suggestions", json::array()}, + {"foldCount", 0}, {"dceCount", 0} + }; + } else if (method == "projectLanguage") { + response["result"] = { + {"ast", {{"conceptType", "Module"}}}, + {"generatedCode", "void f() {}"}, + {"sourceLanguage", "python"}, {"targetLanguage", "cpp"} + }; + } else if (method == "applyMutation") { + response["result"] = {{"success", true}, {"warning", ""}}; + } else if (method == "applyBatch") { + response["result"] = {{"success", true}, {"appliedCount", 2}}; + } else { + response["error"] = {{"code", -32601}, {"message", "Unknown method"}}; + } + return response; +} + +// --- Test 1: Initialize handshake --- +static void test_initialize() { + printf("Test 1: Initialize handshake...\n"); + MCPServer server; + server.setRpcCallback(mockRpcHandler); + + expect(!server.isInitialized(), "not initialized before handshake"); + + json req = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}, {"params", json::object()}}; + json resp = server.handleRequest(req); + + expect(server.isInitialized(), "initialized after handshake"); + expect(resp.contains("result"), "response has result"); + expect(resp["result"]["protocolVersion"] == "2024-11-05", "protocol version correct"); + expect(resp["result"]["serverInfo"]["name"] == "whetstone-mcp", "server name correct"); + expect(resp["result"]["capabilities"].contains("tools"), "has tools capability"); + expect(resp["result"]["capabilities"].contains("resources"), "has resources capability"); + expect(resp["result"]["capabilities"].contains("prompts"), "has prompts capability"); +} + +// --- Test 2: tools/list --- +static void test_toolsList() { + printf("Test 2: tools/list...\n"); + MCPServer server; + + json req = {{"jsonrpc", "2.0"}, {"id", 2}, {"method", "tools/list"}}; + json resp = server.handleRequest(req); + + expect(resp.contains("result"), "response has result"); + auto tools = resp["result"]["tools"]; + expect(tools.is_array(), "tools is array"); + expect(tools.size() >= 10, "at least 10 tools registered"); + + // Check specific tools exist + bool hasGetAST = false, hasMutate = false, hasBatch = false; + bool hasScope = false, hasSuggest = false, hasPipeline = false; + bool hasProject = false, hasGenerate = false, hasApplyAnno = false; + bool hasCallHierarchy = false; + for (const auto& t : tools) { + std::string name = t["name"]; + if (name == "whetstone_get_ast") hasGetAST = true; + if (name == "whetstone_mutate") hasMutate = true; + if (name == "whetstone_batch_mutate") hasBatch = true; + if (name == "whetstone_get_scope") hasScope = true; + if (name == "whetstone_suggest_annotations") hasSuggest = true; + if (name == "whetstone_run_pipeline") hasPipeline = true; + if (name == "whetstone_project_language") hasProject = true; + if (name == "whetstone_generate_code") hasGenerate = true; + if (name == "whetstone_apply_annotation") hasApplyAnno = true; + if (name == "whetstone_get_call_hierarchy") hasCallHierarchy = true; + + // Every tool must have description and inputSchema + expect(t.contains("description") && !t["description"].get().empty(), + ("tool " + name + " has description").c_str()); + expect(t.contains("inputSchema"), ("tool " + name + " has inputSchema").c_str()); + } + expect(hasGetAST, "whetstone_get_ast registered"); + expect(hasMutate, "whetstone_mutate registered"); + expect(hasBatch, "whetstone_batch_mutate registered"); + expect(hasScope, "whetstone_get_scope registered"); + expect(hasSuggest, "whetstone_suggest_annotations registered"); + expect(hasPipeline, "whetstone_run_pipeline registered"); + expect(hasProject, "whetstone_project_language registered"); + expect(hasGenerate, "whetstone_generate_code registered"); + expect(hasApplyAnno, "whetstone_apply_annotation registered"); + expect(hasCallHierarchy, "whetstone_get_call_hierarchy registered"); +} + +// --- Test 3: tools/call — various tools --- +static void test_toolsCall() { + printf("Test 3: tools/call...\n"); + MCPServer server; + server.setRpcCallback(mockRpcHandler); + + // Call whetstone_get_ast + json req = {{"jsonrpc", "2.0"}, {"id", 3}, {"method", "tools/call"}, + {"params", {{"name", "whetstone_get_ast"}, {"arguments", json::object()}}}}; + json resp = server.handleRequest(req); + expect(resp.contains("result"), "get_ast response has result"); + expect(resp["result"]["isError"] == false, "get_ast not error"); + expect(resp["result"]["content"].is_array(), "get_ast has content array"); + expect(resp["result"]["content"][0]["type"] == "text", "content is text type"); + + // Call whetstone_get_scope + req = {{"jsonrpc", "2.0"}, {"id", 4}, {"method", "tools/call"}, + {"params", {{"name", "whetstone_get_scope"}, {"arguments", {{"nodeId", "v1"}}}}}}; + resp = server.handleRequest(req); + expect(resp["result"]["isError"] == false, "get_scope not error"); + + // Call whetstone_run_pipeline + req = {{"jsonrpc", "2.0"}, {"id", 5}, {"method", "tools/call"}, + {"params", {{"name", "whetstone_run_pipeline"}, + {"arguments", {{"source", "def f(): pass"}, {"sourceLanguage", "python"}, {"targetLanguage", "python"}}}}}}; + resp = server.handleRequest(req); + expect(resp["result"]["isError"] == false, "run_pipeline not error"); + + // Call unknown tool + req = {{"jsonrpc", "2.0"}, {"id", 6}, {"method", "tools/call"}, + {"params", {{"name", "nonexistent_tool"}, {"arguments", json::object()}}}}; + resp = server.handleRequest(req); + expect(resp["result"]["isError"] == true, "unknown tool returns error"); +} + +// --- Test 4: resources/list --- +static void test_resourcesList() { + printf("Test 4: resources/list...\n"); + MCPServer server; + + json req = {{"jsonrpc", "2.0"}, {"id", 7}, {"method", "resources/list"}}; + json resp = server.handleRequest(req); + + expect(resp.contains("result"), "response has result"); + auto resources = resp["result"]["resources"]; + expect(resources.is_array(), "resources is array"); + expect(resources.size() >= 5, "at least 5 resources"); + + bool hasAST = false, hasDiags = false, hasLibs = false; + for (const auto& r : resources) { + if (r["uri"] == "whetstone://ast") hasAST = true; + if (r["uri"] == "whetstone://diagnostics") hasDiags = true; + if (r["uri"] == "whetstone://libraries") hasLibs = true; + expect(r.contains("name"), "resource has name"); + expect(r.contains("description"), "resource has description"); + } + expect(hasAST, "ast resource registered"); + expect(hasDiags, "diagnostics resource registered"); + expect(hasLibs, "libraries resource registered"); +} + +// --- Test 5: resources/read --- +static void test_resourcesRead() { + printf("Test 5: resources/read...\n"); + MCPServer server; + server.setResourceReader([](const std::string& uri) -> json { + if (uri == "whetstone://ast") return {{"conceptType", "Module"}, {"name", "test"}}; + if (uri == "whetstone://diagnostics") return json::array(); + return {{"error", "unknown resource"}}; + }); + + json req = {{"jsonrpc", "2.0"}, {"id", 8}, {"method", "resources/read"}, + {"params", {{"uri", "whetstone://ast"}}}}; + json resp = server.handleRequest(req); + expect(resp.contains("result"), "read response has result"); + expect(resp["result"]["contents"].is_array(), "contents is array"); + expect(resp["result"]["contents"][0]["uri"] == "whetstone://ast", "correct uri"); +} + +// --- Test 6: prompts/list --- +static void test_promptsList() { + printf("Test 6: prompts/list...\n"); + MCPServer server; + + json req = {{"jsonrpc", "2.0"}, {"id", 9}, {"method", "prompts/list"}}; + json resp = server.handleRequest(req); + + expect(resp.contains("result"), "response has result"); + auto prompts = resp["result"]["prompts"]; + expect(prompts.is_array(), "prompts is array"); + expect(prompts.size() >= 4, "at least 4 prompts"); + + bool hasAnnotate = false, hasCross = false, hasSecurity = false, hasRefactor = false; + for (const auto& p : prompts) { + if (p["name"] == "annotate_module") hasAnnotate = true; + if (p["name"] == "cross_language_projection") hasCross = true; + if (p["name"] == "security_audit") hasSecurity = true; + if (p["name"] == "refactor_memory") hasRefactor = true; + expect(p.contains("description"), "prompt has description"); + } + expect(hasAnnotate, "annotate_module prompt"); + expect(hasCross, "cross_language_projection prompt"); + expect(hasSecurity, "security_audit prompt"); + expect(hasRefactor, "refactor_memory prompt"); +} + +// --- Test 7: prompts/get --- +static void test_promptsGet() { + printf("Test 7: prompts/get...\n"); + MCPServer server; + + json req = {{"jsonrpc", "2.0"}, {"id", 10}, {"method", "prompts/get"}, + {"params", {{"name", "annotate_module"}, {"arguments", {{"scope", "all"}}}}}}; + json resp = server.handleRequest(req); + expect(resp.contains("result"), "get response has result"); + expect(resp["result"]["messages"].is_array(), "messages is array"); + expect(resp["result"]["messages"].size() >= 1, "at least 1 message"); + expect(resp["result"]["messages"][0]["role"] == "user", "first message from user"); + + // Unknown prompt + req = {{"jsonrpc", "2.0"}, {"id", 11}, {"method", "prompts/get"}, + {"params", {{"name", "nonexistent"}}}}; + resp = server.handleRequest(req); + expect(resp.contains("error"), "unknown prompt returns error"); +} + +// --- Test 8: MCPBridge string processing --- +static void test_bridge() { + printf("Test 8: MCPBridge...\n"); + MCPBridge bridge; + bridge.setRequestHandler(mockRpcHandler); + + // Valid message + json req = {{"jsonrpc", "2.0"}, {"id", 1}, {"method", "initialize"}}; + std::string resp = bridge.processMessage(req.dump()); + expect(!resp.empty(), "bridge returns response"); + json parsed = json::parse(resp); + expect(parsed.contains("result"), "bridge response has result"); + + // Invalid JSON + resp = bridge.processMessage("not json"); + parsed = json::parse(resp); + expect(parsed.contains("error"), "parse error for invalid JSON"); + expect(parsed["error"]["code"] == -32700, "correct parse error code"); +} + +// --- Test 9: Method not found --- +static void test_unknownMethod() { + printf("Test 9: unknown method...\n"); + MCPServer server; + + json req = {{"jsonrpc", "2.0"}, {"id", 12}, {"method", "nonexistent/method"}}; + json resp = server.handleRequest(req); + expect(resp.contains("error"), "unknown method returns error"); + expect(resp["error"]["code"] == -32601, "correct error code"); +} + +// --- Test 10: ping --- +static void test_ping() { + printf("Test 10: ping...\n"); + MCPServer server; + + json req = {{"jsonrpc", "2.0"}, {"id", 13}, {"method", "ping"}}; + json resp = server.handleRequest(req); + expect(resp.contains("result"), "ping returns result"); +} + +int main() { + printf("=== Step 213: MCP Server Tests ===\n\n"); + + test_initialize(); + test_toolsList(); + test_toolsCall(); + test_resourcesList(); + test_resourcesRead(); + test_promptsList(); + test_promptsGet(); + test_bridge(); + test_unknownMethod(); + test_ping(); + + printf("\n=== Results: %d passed, %d failed ===\n", passed, failed); + return failed ? 1 : 0; +}