Step 246: mcp_main.cpp — standalone MCP server entry point
Wires HeadlessEditorState + MCPBridge into a standalone whetstone_mcp binary that any MCP client (Claude Code, Cursor, etc.) can launch over stdio. No SDL, no ImGui, no OpenGL — headless only. 12/12 tests pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
337
editor/tests/step246_test.cpp
Normal file
337
editor/tests/step246_test.cpp
Normal file
@@ -0,0 +1,337 @@
|
||||
// Step 246 TDD Test: mcp_main.cpp — Standalone MCP Server Entry Point
|
||||
//
|
||||
// Tests the MCP server wiring end-to-end (in-process, not subprocess):
|
||||
// HeadlessEditorState + MCPBridge connected via the same lambdas as mcp_main.
|
||||
#include "HeadlessEditorState.h"
|
||||
#include "MCPBridge.h"
|
||||
#include "ast/Serialization.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
|
||||
static void expect(bool cond, const std::string& name,
|
||||
int& passed, int& failed) {
|
||||
if (cond) {
|
||||
std::cout << "Test " << (passed + failed + 1)
|
||||
<< " PASS: " << name << "\n";
|
||||
++passed;
|
||||
} else {
|
||||
std::cout << "Test " << (passed + failed + 1)
|
||||
<< " FAIL: " << name << "\n";
|
||||
++failed;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Resource reader (same as mcp_main.cpp)
|
||||
// -----------------------------------------------------------------------
|
||||
static json readResource(HeadlessEditorState& state,
|
||||
const std::string& uri) {
|
||||
if (uri == "whetstone://ast") {
|
||||
Module* ast = state.activeAST();
|
||||
if (ast) return toJson(ast);
|
||||
return {{"error", "No active AST"}};
|
||||
}
|
||||
if (uri == "whetstone://diagnostics") {
|
||||
return state.buildDiagnosticsJson();
|
||||
}
|
||||
if (uri == "whetstone://libraries") {
|
||||
state.library.primitives.setRoot(state.activeAST());
|
||||
state.library.primitives.setLanguage(state.defaultLanguage);
|
||||
auto funcs = state.library.primitives.getAvailableFunctions();
|
||||
json libs = json::array();
|
||||
for (const auto& p : funcs) {
|
||||
libs.push_back(json{{"name", p.name}, {"kind", p.kind},
|
||||
{"source", p.source}});
|
||||
}
|
||||
return libs;
|
||||
}
|
||||
if (uri == "whetstone://annotations") {
|
||||
Module* ast = state.activeAST();
|
||||
if (!ast) return json::array();
|
||||
json annotations = json::array();
|
||||
for (auto* child : ast->allChildren()) {
|
||||
if (isAnnotationNode(child)) {
|
||||
annotations.push_back(toJson(child));
|
||||
}
|
||||
}
|
||||
return annotations;
|
||||
}
|
||||
if (uri == "whetstone://settings") {
|
||||
return {
|
||||
{"workspace", state.workspaceRoot},
|
||||
{"language", state.defaultLanguage},
|
||||
{"verbose", state.verbose},
|
||||
{"bufferCount", (int)state.bufferStates.size()},
|
||||
{"mode", "headless-mcp"}
|
||||
};
|
||||
}
|
||||
return {{"error", "Unknown resource: " + uri}};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// CLI arg helpers (same signatures as mcp_main.cpp, for test #12)
|
||||
// -----------------------------------------------------------------------
|
||||
static std::string getArg(int argc, char** argv, const std::string& key) {
|
||||
for (int i = 1; i < argc - 1; ++i) {
|
||||
if (argv[i] == key) return argv[i + 1];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
static bool hasFlag(int argc, char** argv, const std::string& key) {
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (argv[i] == key) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
// --- Set up HeadlessEditorState + MCPBridge (same wiring as mcp_main) ---
|
||||
HeadlessEditorState state;
|
||||
state.workspaceRoot = "/tmp/mcp-test";
|
||||
state.defaultLanguage = "python";
|
||||
state.verbose = false;
|
||||
|
||||
// Open a buffer with Python source for AST tests
|
||||
std::string src = "def greet(name):\n return 'hello ' + name\n";
|
||||
state.openBuffer("test.py", src, "python");
|
||||
state.setAgentRole("mcp-session", AgentRole::Refactor);
|
||||
|
||||
MCPBridge bridge;
|
||||
bridge.setRequestHandler([&state](const json& request) -> json {
|
||||
return state.processAgentRequest(request, "mcp-session");
|
||||
});
|
||||
bridge.setResourceReader([&state](const std::string& uri) -> json {
|
||||
return readResource(state, uri);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 1: initialize request
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 1},
|
||||
{"method", "initialize"},
|
||||
{"params", {{"protocolVersion", "2024-11-05"},
|
||||
{"clientInfo", {{"name", "test"}}}}}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
json r = json::parse(resp);
|
||||
expect(r.contains("result") &&
|
||||
r["result"].contains("protocolVersion") &&
|
||||
r["result"].contains("serverInfo"),
|
||||
"initialize returns protocolVersion and serverInfo",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 2: notifications/initialized (no crash, empty response)
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"},
|
||||
{"method", "notifications/initialized"}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
expect(resp.empty(), "notifications/initialized returns empty",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 3: tools/list returns 10+ tools
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 3},
|
||||
{"method", "tools/list"}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
json r = json::parse(resp);
|
||||
int toolCount = 0;
|
||||
if (r.contains("result") && r["result"].contains("tools"))
|
||||
toolCount = (int)r["result"]["tools"].size();
|
||||
expect(toolCount >= 10,
|
||||
"tools/list returns 10+ tools (got " +
|
||||
std::to_string(toolCount) + ")",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 4: resources/list returns 5 resources
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 4},
|
||||
{"method", "resources/list"}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
json r = json::parse(resp);
|
||||
int resCount = 0;
|
||||
if (r.contains("result") && r["result"].contains("resources"))
|
||||
resCount = (int)r["result"]["resources"].size();
|
||||
expect(resCount == 5,
|
||||
"resources/list returns 5 resources (got " +
|
||||
std::to_string(resCount) + ")",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 5: prompts/list returns 4 prompts
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 5},
|
||||
{"method", "prompts/list"}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
json r = json::parse(resp);
|
||||
int promptCount = 0;
|
||||
if (r.contains("result") && r["result"].contains("prompts"))
|
||||
promptCount = (int)r["result"]["prompts"].size();
|
||||
expect(promptCount == 4,
|
||||
"prompts/list returns 4 prompts (got " +
|
||||
std::to_string(promptCount) + ")",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 6: tools/call whetstone_run_pipeline
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 6},
|
||||
{"method", "tools/call"},
|
||||
{"params", {{"name", "whetstone_run_pipeline"},
|
||||
{"arguments", {
|
||||
{"source", "def foo(): pass"},
|
||||
{"sourceLanguage", "python"},
|
||||
{"targetLanguage", "cpp"}
|
||||
}}}}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
json r = json::parse(resp);
|
||||
bool hasContent = r.contains("result") &&
|
||||
r["result"].contains("content") &&
|
||||
!r["result"]["content"].empty();
|
||||
std::string text = "";
|
||||
if (hasContent)
|
||||
text = r["result"]["content"][0].value("text", "");
|
||||
expect(hasContent && !text.empty() &&
|
||||
r["result"].value("isError", true) == false,
|
||||
"whetstone_run_pipeline generates code",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 7: tools/call whetstone_get_ast
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 7},
|
||||
{"method", "tools/call"},
|
||||
{"params", {{"name", "whetstone_get_ast"},
|
||||
{"arguments", json::object()}}}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
json r = json::parse(resp);
|
||||
bool hasContent = r.contains("result") &&
|
||||
r["result"].contains("content") &&
|
||||
!r["result"]["content"].empty();
|
||||
std::string text = "";
|
||||
if (hasContent)
|
||||
text = r["result"]["content"][0].value("text", "");
|
||||
expect(hasContent && !text.empty() &&
|
||||
text.find("ast") != std::string::npos,
|
||||
"whetstone_get_ast returns AST data",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 8: resources/read whetstone://ast
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 8},
|
||||
{"method", "resources/read"},
|
||||
{"params", {{"uri", "whetstone://ast"}}}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
json r = json::parse(resp);
|
||||
bool hasContents = r.contains("result") &&
|
||||
r["result"].contains("contents") &&
|
||||
!r["result"]["contents"].empty();
|
||||
std::string text = "";
|
||||
if (hasContents)
|
||||
text = r["result"]["contents"][0].value("text", "");
|
||||
expect(hasContents && !text.empty(),
|
||||
"resources/read whetstone://ast returns content",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 9: resources/read whetstone://diagnostics
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 9},
|
||||
{"method", "resources/read"},
|
||||
{"params", {{"uri", "whetstone://diagnostics"}}}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
json r = json::parse(resp);
|
||||
bool hasContents = r.contains("result") &&
|
||||
r["result"].contains("contents") &&
|
||||
!r["result"]["contents"].empty();
|
||||
expect(hasContents,
|
||||
"resources/read whetstone://diagnostics returns content",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 10: ping
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 10},
|
||||
{"method", "ping"}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
json r = json::parse(resp);
|
||||
expect(r.contains("result"),
|
||||
"ping returns result",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 11: resources/read whetstone://settings
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
json req = {{"jsonrpc", "2.0"}, {"id", 11},
|
||||
{"method", "resources/read"},
|
||||
{"params", {{"uri", "whetstone://settings"}}}};
|
||||
std::string resp = bridge.processMessage(req.dump());
|
||||
json r = json::parse(resp);
|
||||
bool hasContents = r.contains("result") &&
|
||||
r["result"].contains("contents") &&
|
||||
!r["result"]["contents"].empty();
|
||||
std::string text = "";
|
||||
if (hasContents)
|
||||
text = r["result"]["contents"][0].value("text", "");
|
||||
expect(hasContents && text.find("headless-mcp") != std::string::npos,
|
||||
"resources/read whetstone://settings has headless-mcp mode",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test 12: CLI arg parsing helpers
|
||||
// ---------------------------------------------------------------
|
||||
{
|
||||
const char* testArgs[] = {
|
||||
"whetstone_mcp",
|
||||
"--workspace", "/tmp/ws",
|
||||
"--language", "rust",
|
||||
"--verbose"
|
||||
};
|
||||
int testArgc = 6;
|
||||
char** testArgv = const_cast<char**>(testArgs);
|
||||
|
||||
std::string ws = getArg(testArgc, testArgv, "--workspace");
|
||||
std::string lang = getArg(testArgc, testArgv, "--language");
|
||||
bool verb = hasFlag(testArgc, testArgv, "--verbose");
|
||||
std::string missing = getArg(testArgc, testArgv, "--missing");
|
||||
|
||||
expect(ws == "/tmp/ws" && lang == "rust" && verb &&
|
||||
missing.empty(),
|
||||
"CLI arg parsing works correctly",
|
||||
passed, failed);
|
||||
}
|
||||
|
||||
std::cout << "\n=== Step 246 Results: " << passed << " passed, "
|
||||
<< failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
Reference in New Issue
Block a user