// 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 ] [--language ] [--verbose] #include "HeadlessEditorState.h" #include "MCPBridge.h" #include "MCPProjectConfig.h" #include "AgentPermissionPolicy.h" #include "ast/Serialization.h" #include #include #include #include // ----------------------------------------------------------------------- // 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; } static std::string runtimeVersion() { const char* envVersion = std::getenv("WHETSTONE_MCP_RUNTIME_VERSION"); if (envVersion != nullptr && *envVersion != '\0') return envVersion; return "0.8.4"; } // ----------------------------------------------------------------------- // 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 ] " "[--language ] [--verbose]\n"; return 0; } // Signal handling signal(SIGINT, signalHandler); signal(SIGTERM, signalHandler); // Create headless state HeadlessEditorState state; state.verbose = verbose; MCPProjectConfigLoadResult configLoad; if (!workspace.empty()) { configLoad = MCPProjectConfig::loadFromWorkspace(workspace); if (!configLoad.error.empty()) { std::cerr << "[whetstone-mcp] Config load warning: " << configLoad.error << "\n"; } } else { configLoad = MCPProjectConfig::discoverFromCwd(); if (!configLoad.error.empty()) { std::cerr << "[whetstone-mcp] Config discovery warning: " << configLoad.error << "\n"; } } if (configLoad.found && !configLoad.config.workspace.empty()) { state.workspaceRoot = configLoad.config.workspace; } else if (!workspace.empty()) { state.workspaceRoot = workspace; } else if (configLoad.found) { state.workspaceRoot = configLoad.sourceWorkspaceRoot; } if (!language.empty()) { state.defaultLanguage = language; } else if (configLoad.found && !configLoad.config.defaultLanguage.empty()) { state.defaultLanguage = configLoad.config.defaultLanguage; } // Create an empty default buffer so getAST etc. work immediately state.openBuffer("(scratch)", "", state.defaultLanguage); AgentRole role = AgentRole::Refactor; if (configLoad.found && !configLoad.config.agentRole.empty()) { role = AgentPermissionPolicy::roleFromString(configLoad.config.agentRole); } state.setAgentRole("mcp-session", role); // 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 v" << runtimeVersion() << "\n"; if (!state.workspaceRoot.empty()) std::cerr << "[whetstone-mcp] Workspace: " << state.workspaceRoot << "\n"; if (configLoad.found && !configLoad.config.mcpWorkspaceAlias.empty()) std::cerr << "[whetstone-mcp] Workspace alias: " << configLoad.config.mcpWorkspaceAlias << "\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; }