Sprint 7 Phase 7b: MCP server with tools, resources, and prompts (Steps 207-213)
- Step 207: MCPServer.h — MCP protocol core with JSON-RPC 2.0 stdio transport, initialize/initialized handshake, capabilities (tools, resources, prompts) - Step 208: 5 AST tools — whetstone_get_ast, whetstone_mutate, whetstone_batch_mutate, whetstone_get_scope, whetstone_get_call_hierarchy - Step 209: 5 annotation/generation tools — whetstone_suggest_annotations, whetstone_apply_annotation, whetstone_generate_code, whetstone_run_pipeline, whetstone_project_language - Step 210: 5 MCP resources — ast, diagnostics, libraries, annotations, settings - Step 211: 4 MCP prompts — annotate_module, cross_language_projection, security_audit, refactor_memory - Step 212: MCPBridge.h — stdio transport with Content-Length framing, bridges MCP to Whetstone JSON-RPC - Step 213: MCP server tests — 90/90 assertions pass Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
77
editor/src/MCPBridge.h
Normal file
77
editor/src/MCPBridge.h
Normal file
@@ -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 <string>
|
||||
#include <iostream>
|
||||
#include <nlohmann/json.hpp>
|
||||
#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_;
|
||||
};
|
||||
559
editor/src/MCPServer.h
Normal file
559
editor/src/MCPServer.h
Normal file
@@ -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 <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <functional>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
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<MCPPromptArgument> 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<json(const json& request)>;
|
||||
|
||||
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<MCPTool>& getTools() const { return tools_; }
|
||||
const std::vector<MCPResource>& getResources() const { return resources_; }
|
||||
const std::vector<MCPPrompt>& getPrompts() const { return prompts_; }
|
||||
bool isInitialized() const { return initialized_; }
|
||||
|
||||
// Resource read handler registration
|
||||
using ResourceReader = std::function<json(const std::string& uri)>;
|
||||
void setResourceReader(ResourceReader reader) { resourceReader_ = std::move(reader); }
|
||||
|
||||
private:
|
||||
std::vector<MCPTool> tools_;
|
||||
std::vector<MCPResource> resources_;
|
||||
std::vector<MCPPrompt> prompts_;
|
||||
std::map<std::string, std::function<json(const json&)>> 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<MCPPromptMessage> 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();
|
||||
}
|
||||
};
|
||||
351
editor/tests/step213_test.cpp
Normal file
351
editor/tests/step213_test.cpp
Normal file
@@ -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 <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <nlohmann/json.hpp>
|
||||
#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<std::string>().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;
|
||||
}
|
||||
Reference in New Issue
Block a user