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:
Bill
2026-02-11 05:40:32 +00:00
parent 9fcb5a0c8c
commit 06706aeacf
4 changed files with 532 additions and 0 deletions

View File

@@ -1370,4 +1370,25 @@ target_link_libraries(step245_test PRIVATE
tree_sitter_go
tree_sitter_org)
# Step 246: Standalone MCP server (no SDL/ImGui)
add_executable(whetstone_mcp src/mcp_main.cpp)
target_include_directories(whetstone_mcp PRIVATE src)
target_link_libraries(whetstone_mcp PRIVATE
nlohmann_json::nlohmann_json
unofficial::tree-sitter::tree-sitter
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go
tree_sitter_org)
add_executable(step246_test tests/step246_test.cpp)
target_include_directories(step246_test PRIVATE src)
target_link_libraries(step246_test PRIVATE
nlohmann_json::nlohmann_json
unofficial::tree-sitter::tree-sitter
tree_sitter_python tree_sitter_cpp tree_sitter_elisp
tree_sitter_javascript tree_sitter_typescript
tree_sitter_java tree_sitter_rust tree_sitter_go
tree_sitter_org)
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)

152
editor/src/mcp_main.cpp Normal file
View File

@@ -0,0 +1,152 @@
// Step 246: Standalone MCP server entry point.
//
// Wires HeadlessEditorState (Step 245) + MCPBridge (Step 212) into a
// standalone `whetstone_mcp` binary that any MCP client can launch
// over stdio. No SDL, no ImGui, no OpenGL.
//
// Usage: whetstone_mcp [--workspace <dir>] [--language <lang>] [--verbose]
#include "HeadlessEditorState.h"
#include "MCPBridge.h"
#include "ast/Serialization.h"
#include <csignal>
#include <cstdlib>
#include <iostream>
#include <string>
// -----------------------------------------------------------------------
// CLI helpers (same pattern as pipeline_main.cpp)
// -----------------------------------------------------------------------
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;
}
// -----------------------------------------------------------------------
// Signal handling (same pattern as orchestrator_main.cpp)
// -----------------------------------------------------------------------
static volatile sig_atomic_t g_running = 1;
static void signalHandler(int /*sig*/) {
g_running = 0;
exit(0);
}
// -----------------------------------------------------------------------
// Resource reader — routes whetstone:// URIs to headless state
// -----------------------------------------------------------------------
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") {
// Return available functions from primitives registry
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}};
}
// -----------------------------------------------------------------------
// main
// -----------------------------------------------------------------------
int main(int argc, char** argv) {
// Parse CLI args
std::string workspace = getArg(argc, argv, "--workspace");
std::string language = getArg(argc, argv, "--language");
bool verbose = hasFlag(argc, argv, "--verbose");
if (hasFlag(argc, argv, "--help") || hasFlag(argc, argv, "-h")) {
std::cerr << "Usage: whetstone_mcp [--workspace <dir>] "
"[--language <lang>] [--verbose]\n";
return 0;
}
// Signal handling
signal(SIGINT, signalHandler);
signal(SIGTERM, signalHandler);
// Create headless state
HeadlessEditorState state;
state.verbose = verbose;
if (!workspace.empty()) state.workspaceRoot = workspace;
if (!language.empty()) state.defaultLanguage = language;
// Create an empty default buffer so getAST etc. work immediately
state.openBuffer("(scratch)", "", state.defaultLanguage);
// Set default agent role to Refactor for full MCP access
state.setAgentRole("mcp-session", AgentRole::Refactor);
// Create bridge and wire handlers
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);
});
// Log startup info to stderr (stdout is the MCP transport)
std::cerr << "[whetstone-mcp] Starting MCP server v0.1.0\n";
if (!workspace.empty())
std::cerr << "[whetstone-mcp] Workspace: " << workspace << "\n";
std::cerr << "[whetstone-mcp] Language: " << state.defaultLanguage << "\n";
if (verbose)
std::cerr << "[whetstone-mcp] Verbose mode enabled\n";
std::cerr << "[whetstone-mcp] Listening on stdio...\n";
// Run the MCP stdio transport loop
bridge.runStdio();
return 0;
}

View 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;
}

View File

@@ -23,3 +23,25 @@ but with zero GUI dependencies (no ImGui, no SDL, no WebSocket).
- JSON roundtrip cloning (matching EditorUtils.h) instead of manual deep copy
- Separate HeadlessAgentState (no AgentMarketplace/WebSocket dependency chain)
- All files under 600-line architecture limit
### Step 246: mcp_main.cpp — Standalone MCP Server Entry Point
**Status:** PASS (12/12 tests)
Wires HeadlessEditorState + MCPBridge into a standalone `whetstone_mcp` binary
that any MCP client (Claude Code, Cursor, etc.) can launch over stdio.
**Files created:**
- `editor/src/mcp_main.cpp` — Standalone MCP server entry point with CLI arg
parsing (--workspace, --language, --verbose), signal handling, resource reader
lambda routing whetstone:// URIs, and MCPBridge stdio loop
- `editor/tests/step246_test.cpp` — 12 test cases: initialize, notifications,
tools/list (10+), resources/list (5), prompts/list (4), tools/call pipeline,
tools/call get_ast, resources/read ast, resources/read diagnostics, ping,
resources/read settings, CLI arg parsing
**Key design decisions:**
- No SDL, no ImGui, no OpenGL — headless only binary
- Fixed "mcp-session" session ID (MCP stdio is single-client)
- Default Refactor role for full MCP access
- Empty scratch buffer on startup so getAST works immediately
- All logging to stderr (stdout is the MCP transport)