#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 #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; } static bool hasCallError(const json& value) { return value.is_object() && value.contains("error"); } static void appendWarning(json& warnings, const std::string& message) { warnings.push_back(message); } static std::string toLowerCopy(std::string s) { std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return (char)std::tolower(c); }); return s; } static std::string languageForPath(const std::string& path) { auto dot = path.find_last_of('.'); if (dot == std::string::npos) return ""; std::string ext = toLowerCopy(path.substr(dot)); if (ext == ".cpp" || ext == ".cc" || ext == ".cxx" || ext == ".hpp" || ext == ".h" || ext == ".hh" || ext == ".hxx") return "cpp"; if (ext == ".py") return "python"; if (ext == ".js" || ext == ".mjs" || ext == ".cjs") return "javascript"; if (ext == ".ts" || ext == ".tsx") return "typescript"; if (ext == ".java") return "java"; if (ext == ".rs") return "rust"; if (ext == ".go") return "go"; return ""; } static bool isIgnoredOnboardPath(const std::string& path) { std::string lower = toLowerCopy(path); auto hasSegment = [&](const std::string& segment) { return lower.find(segment) == 0 || lower.find("/" + segment) != std::string::npos; }; return hasSegment(".git/") || hasSegment("build/") || hasSegment("build-native/") || hasSegment("node_modules/") || hasSegment(".whetstone/"); } static int filePriority(const std::string& path) { std::string lower = toLowerCopy(path); if (lower.find("main.") != std::string::npos || lower.find("app.") != std::string::npos || lower.find("index.") != std::string::npos || lower.find("lib.") != std::string::npos) return 0; if (lower.find("/src/") != std::string::npos) return 1; return 2; } static json selectOnboardingCandidates(const json& files, int maxFiles) { std::vector> ranked; std::map languageCounts; json candidates = json::array(); for (const auto& f : files) { if (!f.is_object()) continue; if (f.value("isDir", false)) continue; std::string path = f.value("path", ""); if (path.empty() || isIgnoredOnboardPath(path)) continue; std::string language = languageForPath(path); if (language.empty()) continue; languageCounts[language] += 1; ranked.push_back({filePriority(path), { {"path", path}, {"language", language} }}); } std::sort(ranked.begin(), ranked.end(), [](const auto& a, const auto& b) { if (a.first != b.first) return a.first < b.first; return a.second.value("path", "") < b.second.value("path", ""); }); for (const auto& item : ranked) { if ((int)candidates.size() >= maxFiles) break; candidates.push_back(item.second); } json langJson = json::object(); for (const auto& [lang, count] : languageCounts) langJson[lang] = count; return { {"candidates", candidates}, {"languageCounts", langJson} }; } json runWorkspaceOnboarding(const json& args) { json warnings = json::array(); int maxFiles = args.value("maxFiles", 8); if (maxFiles < 1) maxFiles = 1; if (maxFiles > 20) maxFiles = 20; json indexParams = json::object(); std::string root = args.value("root", ""); if (!root.empty()) indexParams["root"] = root; json indexRes = callWhetstone("indexWorkspace", indexParams); if (hasCallError(indexRes)) { return { {"success", false}, {"error", indexRes["error"]}, {"warnings", warnings} }; } json listRes = callWhetstone("workspaceList", json::object()); json files = listRes.value("files", json::array()); json selection = selectOnboardingCandidates(files, maxFiles); json candidates = selection["candidates"]; json processed = json::array(); int inferredCount = 0; int savedCount = 0; for (const auto& c : candidates) { std::string path = c.value("path", ""); std::string language = c.value("language", ""); json openRes = callWhetstone("openFile", { {"path", path}, {"language", language} }); if (hasCallError(openRes)) { appendWarning(warnings, "openFile failed for " + path); continue; } json inferRes = callWhetstone("inferAnnotations", json::object()); if (hasCallError(inferRes)) { appendWarning(warnings, "inferAnnotations failed for " + path); continue; } inferredCount += inferRes.value("count", 0); std::string openedPath = openRes.value("path", path); json saveRes = callWhetstone("saveAnnotatedAST", {{"path", openedPath}}); if (hasCallError(saveRes)) { appendWarning(warnings, "saveAnnotatedAST failed for " + openedPath); } else { savedCount += 1; } processed.push_back({ {"path", openedPath}, {"language", language}, {"suggestionCount", inferRes.value("count", 0)}, {"saved", !hasCallError(saveRes)} }); } std::string readme = "# Whetstone Workspace\n" "This directory stores onboarding metadata and sidecar annotation artifacts.\n"; json mkDirRes = callWhetstone("fileCreate", { {"path", ".whetstone/README.md"}, {"content", readme} }); if (hasCallError(mkDirRes)) appendWarning(warnings, "Unable to create .whetstone/README.md"); return { {"success", true}, {"workspace", indexRes.value("root", root)}, {"fileCount", indexRes.value("fileCount", 0)}, {"selectedFileCount", (int)candidates.size()}, {"processedFileCount", (int)processed.size()}, {"inferredAnnotationCount", inferredCount}, {"savedSidecarCount", savedCount}, {"languageCounts", selection["languageCounts"]}, {"processedFiles", processed}, {"workflowSuggestion", { {"nextTools", json::array({ "whetstone_create_skeleton", "whetstone_create_workflow", "whetstone_orchestrate_run_deterministic", "whetstone_get_blockers", "whetstone_get_review_queue" })}, {"note", "Onboarding complete. Use skeleton/workflow tools to start routed execution."} }}, {"warnings", warnings} }; } // --------------------------------------------------------------- // Step 208: Register AST query and mutation tools // --------------------------------------------------------------- #include "mcp/RegisterASTTools.h" #include "mcp/RegisterAnnotationTools.h" #include "mcp/RegisterResources.h" #include "mcp/RegisterPrompts.h" #include "mcp/RegisterFileTools.h" #include "mcp/RegisterDiagnosticTools.h" #include "mcp/RegisterBatchTools.h" #include "mcp/RegisterProjectTools.h" #include "mcp/RegisterSaveUndoTools.h" #include "mcp/RegisterSidecarTools.h" #include "mcp/RegisterSemanticAnnotationTools.h" #include "mcp/RegisterEnvironmentTools.h" #include "mcp/RegisterTrainingDataTools.h" #include "mcp/RegisterWorkflowTools.h" #include "mcp/RegisterWorkflowExecutionTools.h" #include "mcp/RegisterRoutingTools.h" #include "mcp/RegisterOrchestratorTools.h" #include "mcp/RegisterReviewTools.h" #include "mcp/RegisterOnboardingAndAllTools.h"