Step 417: add MCP config and discovery utilities
This commit is contained in:
@@ -2632,4 +2632,13 @@ target_link_libraries(step416_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step417_test tests/step417_test.cpp)
|
||||
target_include_directories(step417_test PRIVATE src)
|
||||
target_link_libraries(step417_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)
|
||||
|
||||
# Step 12: Dear ImGui shell scaffolding created (main.cpp exists but not built due to dependencies)
|
||||
|
||||
246
editor/src/MCPServerConfig.h
Normal file
246
editor/src/MCPServerConfig.h
Normal file
@@ -0,0 +1,246 @@
|
||||
#pragma once
|
||||
|
||||
#include "MCPServer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class MCPServerConfig {
|
||||
public:
|
||||
struct Discovery {
|
||||
std::string binaryPath;
|
||||
std::string workspaceRoot;
|
||||
std::string primaryLanguage;
|
||||
int toolCount = 0;
|
||||
std::map<std::string, int> categoryCounts;
|
||||
};
|
||||
|
||||
static std::string resolveBinaryPath(
|
||||
const std::vector<std::string>& candidates = {},
|
||||
const std::string& pathEnv = "") {
|
||||
for (const auto& c : candidates) {
|
||||
if (isExecutable(c) || fileExists(c)) return std::filesystem::absolute(c).string();
|
||||
}
|
||||
|
||||
const std::vector<std::string> defaults = {
|
||||
"editor/build-native/whetstone_mcp",
|
||||
"editor/build-native/whetstone_mcp.exe",
|
||||
"build-native/whetstone_mcp",
|
||||
"build-native/whetstone_mcp.exe",
|
||||
"whetstone_mcp"
|
||||
};
|
||||
for (const auto& c : defaults) {
|
||||
if (isExecutable(c) || fileExists(c)) return std::filesystem::absolute(c).string();
|
||||
}
|
||||
|
||||
std::string pathValue = pathEnv.empty() ? getenvString("PATH") : pathEnv;
|
||||
auto pathEntries = splitPath(pathValue);
|
||||
for (const auto& entry : pathEntries) {
|
||||
std::filesystem::path full = std::filesystem::path(entry) / "whetstone_mcp";
|
||||
if (isExecutable(full.string()) || fileExists(full.string())) return full.string();
|
||||
std::filesystem::path fullExe = std::filesystem::path(entry) / "whetstone_mcp.exe";
|
||||
if (isExecutable(fullExe.string()) || fileExists(fullExe.string())) return fullExe.string();
|
||||
}
|
||||
return "whetstone_mcp";
|
||||
}
|
||||
|
||||
static std::string detectWorkspaceRoot(const std::string& startDir) {
|
||||
if (startDir.empty()) return "";
|
||||
std::filesystem::path cur = std::filesystem::absolute(startDir);
|
||||
while (true) {
|
||||
if (std::filesystem::exists(cur / ".git") ||
|
||||
std::filesystem::exists(cur / "CMakeLists.txt") ||
|
||||
std::filesystem::exists(cur / "progress.md")) {
|
||||
return cur.string();
|
||||
}
|
||||
if (cur == cur.root_path()) break;
|
||||
cur = cur.parent_path();
|
||||
}
|
||||
return std::filesystem::absolute(startDir).string();
|
||||
}
|
||||
|
||||
static std::string detectPrimaryLanguage(
|
||||
const std::string& workspaceRoot,
|
||||
size_t maxFilesToScan = 4000) {
|
||||
if (workspaceRoot.empty() || !std::filesystem::exists(workspaceRoot)) return "cpp";
|
||||
|
||||
std::unordered_map<std::string, int> extCount;
|
||||
size_t scanned = 0;
|
||||
for (const auto& e : std::filesystem::recursive_directory_iterator(workspaceRoot)) {
|
||||
if (scanned >= maxFilesToScan) break;
|
||||
if (!e.is_regular_file()) continue;
|
||||
auto ext = e.path().extension().string();
|
||||
if (ext.empty()) continue;
|
||||
extCount[toLower(ext)]++;
|
||||
scanned++;
|
||||
}
|
||||
|
||||
if (extCount.empty()) return "cpp";
|
||||
|
||||
const std::vector<std::pair<std::string, std::string>> mapping = {
|
||||
{".py", "python"}, {".cpp", "cpp"}, {".cc", "cpp"}, {".cxx", "cpp"},
|
||||
{".cs", "csharp"}, {".fs", "fsharp"}, {".vb", "vbnet"},
|
||||
{".sql", "postgresql"}, {".js", "javascript"}, {".ts", "typescript"},
|
||||
{".java", "java"}, {".rs", "rust"}, {".go", "go"}, {".c", "c"}
|
||||
};
|
||||
|
||||
std::string bestLang = "cpp";
|
||||
int bestCount = -1;
|
||||
for (const auto& [ext, lang] : mapping) {
|
||||
auto it = extCount.find(ext);
|
||||
if (it != extCount.end() && it->second > bestCount) {
|
||||
bestCount = it->second;
|
||||
bestLang = lang;
|
||||
}
|
||||
}
|
||||
return bestLang;
|
||||
}
|
||||
|
||||
static json buildMcpConfig(
|
||||
const std::string& binaryPath,
|
||||
const std::string& workspaceRoot,
|
||||
const std::string& language) {
|
||||
json args = json::array();
|
||||
if (!workspaceRoot.empty()) {
|
||||
args.push_back("--workspace");
|
||||
args.push_back(workspaceRoot);
|
||||
}
|
||||
if (!language.empty()) {
|
||||
args.push_back("--language");
|
||||
args.push_back(language);
|
||||
}
|
||||
|
||||
return {
|
||||
{"mcpServers", {
|
||||
{"whetstone", {
|
||||
{"command", binaryPath},
|
||||
{"args", args}
|
||||
}}
|
||||
}}
|
||||
};
|
||||
}
|
||||
|
||||
static bool writeMcpConfigFile(const std::string& outputPath, const json& config) {
|
||||
std::ofstream out(outputPath);
|
||||
if (!out.is_open()) return false;
|
||||
out << config.dump(2) << "\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
static json buildServerManifest(const MCPServer& server) {
|
||||
std::map<std::string, std::vector<json>> categories;
|
||||
for (const auto& t : server.getTools()) {
|
||||
std::string cat = categorizeTool(t.name);
|
||||
categories[cat].push_back({
|
||||
{"name", t.name},
|
||||
{"description", t.description}
|
||||
});
|
||||
}
|
||||
|
||||
json byCategory = json::object();
|
||||
for (const auto& [k, v] : categories) byCategory[k] = v;
|
||||
|
||||
return {
|
||||
{"toolCount", (int)server.getTools().size()},
|
||||
{"categories", byCategory}
|
||||
};
|
||||
}
|
||||
|
||||
static Discovery discover(const std::string& startDir) {
|
||||
Discovery d;
|
||||
d.binaryPath = resolveBinaryPath();
|
||||
d.workspaceRoot = detectWorkspaceRoot(startDir);
|
||||
d.primaryLanguage = detectPrimaryLanguage(d.workspaceRoot);
|
||||
|
||||
MCPServer server;
|
||||
d.toolCount = (int)server.getTools().size();
|
||||
auto manifest = buildServerManifest(server);
|
||||
if (manifest.contains("categories")) {
|
||||
for (auto it = manifest["categories"].begin(); it != manifest["categories"].end(); ++it) {
|
||||
d.categoryCounts[it.key()] = (int)it.value().size();
|
||||
}
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
static std::string categorizeTool(const std::string& toolName) {
|
||||
const std::string n = toLower(toolName);
|
||||
if (n.find("whetstone_get_ast") == 0 || n.find("batch_mutate") != std::string::npos ||
|
||||
n.find("call_hierarchy") != std::string::npos || n.find("_mutate") != std::string::npos) {
|
||||
return "AST";
|
||||
}
|
||||
if (n.find("diagnostic") != std::string::npos || n.find("quick_fix") != std::string::npos) {
|
||||
return "Diagnostics";
|
||||
}
|
||||
if (n.find("workflow") != std::string::npos || n.find("workitem") != std::string::npos ||
|
||||
n.find("review") != std::string::npos) {
|
||||
return "Workflow";
|
||||
}
|
||||
if (n.find("route") != std::string::npos || n.find("orchestrate") != std::string::npos) {
|
||||
return "Routing";
|
||||
}
|
||||
if (n.find("file_") != std::string::npos || n.find("save_") != std::string::npos) {
|
||||
return "FileOps";
|
||||
}
|
||||
if (n.find("workspace") != std::string::npos || n.find("open_file") != std::string::npos ||
|
||||
n.find("close_file") != std::string::npos || n.find("buffer") != std::string::npos ||
|
||||
n.find("project") != std::string::npos) {
|
||||
return "Project";
|
||||
}
|
||||
return "Misc";
|
||||
}
|
||||
|
||||
private:
|
||||
static std::string getenvString(const std::string& key) {
|
||||
const char* v = std::getenv(key.c_str());
|
||||
return v ? std::string(v) : "";
|
||||
}
|
||||
|
||||
static bool isExecutable(const std::string& path) {
|
||||
if (path.empty()) return false;
|
||||
std::error_code ec;
|
||||
auto p = std::filesystem::path(path);
|
||||
if (!std::filesystem::exists(p, ec) || std::filesystem::is_directory(p, ec)) return false;
|
||||
auto perms = std::filesystem::status(p, ec).permissions();
|
||||
if (ec) return false;
|
||||
using perms_t = std::filesystem::perms;
|
||||
return (perms & perms_t::owner_exec) != perms_t::none ||
|
||||
(perms & perms_t::group_exec) != perms_t::none ||
|
||||
(perms & perms_t::others_exec) != perms_t::none;
|
||||
}
|
||||
|
||||
static bool fileExists(const std::string& path) {
|
||||
if (path.empty()) return false;
|
||||
std::error_code ec;
|
||||
auto p = std::filesystem::path(path);
|
||||
return std::filesystem::exists(p, ec) && !std::filesystem::is_directory(p, ec);
|
||||
}
|
||||
|
||||
static std::vector<std::string> splitPath(const std::string& path) {
|
||||
std::vector<std::string> out;
|
||||
std::string cur;
|
||||
for (char c : path) {
|
||||
if (c == ':') {
|
||||
if (!cur.empty()) out.push_back(cur);
|
||||
cur.clear();
|
||||
} else {
|
||||
cur.push_back(c);
|
||||
}
|
||||
}
|
||||
if (!cur.empty()) out.push_back(cur);
|
||||
return out;
|
||||
}
|
||||
|
||||
static std::string toLower(const std::string& s) {
|
||||
std::string out = s;
|
||||
std::transform(out.begin(), out.end(), out.begin(),
|
||||
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
||||
return out;
|
||||
}
|
||||
};
|
||||
159
editor/tests/step417_test.cpp
Normal file
159
editor/tests/step417_test.cpp
Normal file
@@ -0,0 +1,159 @@
|
||||
// Step 417: MCP Server Configuration + Discovery Tests (12 tests)
|
||||
|
||||
#include "MCPServerConfig.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
static int passed = 0, failed = 0;
|
||||
#define TEST(name) { std::cout << " " << #name << "... "; }
|
||||
#define PASS() { std::cout << "PASS\n"; ++passed; }
|
||||
#define FAIL(msg) { std::cout << "FAIL: " << msg << "\n"; ++failed; }
|
||||
#define CHECK(cond, msg) if (!(cond)) { FAIL(msg); return; } else {}
|
||||
|
||||
static std::string makeTempDir(const std::string& name) {
|
||||
auto dir = std::filesystem::temp_directory_path() / ("whetstone_" + name);
|
||||
std::filesystem::create_directories(dir);
|
||||
return dir.string();
|
||||
}
|
||||
|
||||
void test_resolve_binary_path_from_candidate() {
|
||||
TEST(resolve_binary_path_from_candidate);
|
||||
std::string path = MCPServerConfig::resolveBinaryPath({"editor/build-native/whetstone_mcp"});
|
||||
CHECK(!path.empty(), "expected binary path from explicit candidate");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_resolve_binary_path_from_defaults() {
|
||||
TEST(resolve_binary_path_from_defaults);
|
||||
std::string path = MCPServerConfig::resolveBinaryPath();
|
||||
CHECK(!path.empty(), "expected binary path from default discovery");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_detect_workspace_root_from_repo() {
|
||||
TEST(detect_workspace_root_from_repo);
|
||||
std::string root = MCPServerConfig::detectWorkspaceRoot(".");
|
||||
CHECK(!root.empty(), "workspace root empty");
|
||||
CHECK(std::filesystem::exists(std::filesystem::path(root) / "progress.md"),
|
||||
"expected progress.md at workspace root");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_detect_primary_language_cpp_majority() {
|
||||
TEST(detect_primary_language_cpp_majority);
|
||||
auto dir = std::filesystem::path(makeTempDir("lang_cpp"));
|
||||
std::ofstream(dir / "a.cpp") << "int main() {}\n";
|
||||
std::ofstream(dir / "b.cpp") << "int add(int a,int b){return a+b;}\n";
|
||||
std::ofstream(dir / "c.py") << "print('x')\n";
|
||||
std::string lang = MCPServerConfig::detectPrimaryLanguage(dir.string());
|
||||
CHECK(lang == "cpp", "expected cpp primary language");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_detect_primary_language_python_majority() {
|
||||
TEST(detect_primary_language_python_majority);
|
||||
auto dir = std::filesystem::path(makeTempDir("lang_py"));
|
||||
std::ofstream(dir / "a.py") << "print('a')\n";
|
||||
std::ofstream(dir / "b.py") << "print('b')\n";
|
||||
std::ofstream(dir / "c.cpp") << "int main(){}\n";
|
||||
std::string lang = MCPServerConfig::detectPrimaryLanguage(dir.string());
|
||||
CHECK(lang == "python", "expected python primary language");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_build_mcp_config_structure() {
|
||||
TEST(build_mcp_config_structure);
|
||||
json cfg = MCPServerConfig::buildMcpConfig("/bin/whetstone_mcp", "/repo", "cpp");
|
||||
CHECK(cfg.contains("mcpServers"), "missing mcpServers");
|
||||
CHECK(cfg["mcpServers"].contains("whetstone"), "missing whetstone server key");
|
||||
CHECK(cfg["mcpServers"]["whetstone"]["command"] == "/bin/whetstone_mcp",
|
||||
"wrong command field");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_build_mcp_config_args_contains_workspace_language() {
|
||||
TEST(build_mcp_config_args_contains_workspace_language);
|
||||
json cfg = MCPServerConfig::buildMcpConfig("/bin/whetstone_mcp", "/repo", "python");
|
||||
auto args = cfg["mcpServers"]["whetstone"]["args"];
|
||||
std::string dumped = args.dump();
|
||||
CHECK(dumped.find("--workspace") != std::string::npos, "missing workspace arg");
|
||||
CHECK(dumped.find("--language") != std::string::npos, "missing language arg");
|
||||
CHECK(dumped.find("python") != std::string::npos, "missing language value");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_write_mcp_config_file() {
|
||||
TEST(write_mcp_config_file);
|
||||
auto dir = std::filesystem::path(makeTempDir("mcp_cfg"));
|
||||
auto path = (dir / ".mcp.json").string();
|
||||
json cfg = MCPServerConfig::buildMcpConfig("/bin/whetstone_mcp", "/repo", "cpp");
|
||||
CHECK(MCPServerConfig::writeMcpConfigFile(path, cfg), "write config failed");
|
||||
CHECK(std::filesystem::exists(path), "config file not created");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_manifest_tool_count_at_least_68() {
|
||||
TEST(manifest_tool_count_at_least_68);
|
||||
MCPServer server;
|
||||
json manifest = MCPServerConfig::buildServerManifest(server);
|
||||
CHECK(manifest["toolCount"].get<int>() >= 68, "expected at least 68 tools");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_manifest_required_categories_present() {
|
||||
TEST(manifest_required_categories_present);
|
||||
MCPServer server;
|
||||
json manifest = MCPServerConfig::buildServerManifest(server);
|
||||
auto cats = manifest["categories"];
|
||||
CHECK(cats.contains("AST"), "missing AST category");
|
||||
CHECK(cats.contains("Diagnostics"), "missing Diagnostics category");
|
||||
CHECK(cats.contains("Project"), "missing Project category");
|
||||
CHECK(cats.contains("FileOps"), "missing FileOps category");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_categorize_tool_names() {
|
||||
TEST(categorize_tool_names);
|
||||
CHECK(MCPServerConfig::categorizeTool("whetstone_get_ast") == "AST", "ast category");
|
||||
CHECK(MCPServerConfig::categorizeTool("whetstone_get_diagnostics") == "Diagnostics",
|
||||
"diagnostics category");
|
||||
CHECK(MCPServerConfig::categorizeTool("whetstone_workspace_list") == "Project",
|
||||
"project category");
|
||||
CHECK(MCPServerConfig::categorizeTool("whetstone_file_write") == "FileOps",
|
||||
"fileops category");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_discover_end_to_end() {
|
||||
TEST(discover_end_to_end);
|
||||
auto d = MCPServerConfig::discover(".");
|
||||
CHECK(!d.workspaceRoot.empty(), "workspace missing");
|
||||
CHECK(!d.binaryPath.empty(), "binary path missing");
|
||||
CHECK(d.toolCount >= 68, "expected at least 68 tools in discovery");
|
||||
CHECK(!d.primaryLanguage.empty(), "primary language missing");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 417: MCP Server Configuration + Discovery Tests\n";
|
||||
|
||||
test_resolve_binary_path_from_candidate(); // 1
|
||||
test_resolve_binary_path_from_defaults(); // 2
|
||||
test_detect_workspace_root_from_repo(); // 3
|
||||
test_detect_primary_language_cpp_majority(); // 4
|
||||
test_detect_primary_language_python_majority(); // 5
|
||||
test_build_mcp_config_structure(); // 6
|
||||
test_build_mcp_config_args_contains_workspace_language(); // 7
|
||||
test_write_mcp_config_file(); // 8
|
||||
test_manifest_tool_count_at_least_68(); // 9
|
||||
test_manifest_required_categories_present(); // 10
|
||||
test_categorize_tool_names(); // 11
|
||||
test_discover_end_to_end(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed)
|
||||
<< " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
51
progress.md
51
progress.md
@@ -4227,6 +4227,57 @@ annotation mapper, and cross-stack class-to-SQL flow checkpoints.
|
||||
- `editor/src/MCPServer.h` (`1679` > `600`)
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` (`2629` > `600`)
|
||||
|
||||
# Sprint 18 Progress — Claude Code Plugin + MCP Workflow Tools
|
||||
|
||||
## Phase 18a: Claude Code Integration
|
||||
|
||||
### Step 417: MCP Server Configuration + Discovery
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Added MCP server discovery/configuration utilities for plugin-style client setup.
|
||||
This provides a concrete path toward `.mcp.json` generation and manifest
|
||||
inspection without adding a custom plugin binary.
|
||||
|
||||
**Files created:**
|
||||
- `editor/src/MCPServerConfig.h` — MCP config/discovery support:
|
||||
- binary path resolution (`whetstone_mcp`) from explicit candidates, common
|
||||
build locations, and PATH fallback
|
||||
- workspace-root detection (walk-up for `.git`, `CMakeLists.txt`, `progress.md`)
|
||||
- primary-language detection from workspace extension frequency
|
||||
- `.mcp.json` config JSON construction and file writing
|
||||
- server manifest generation from `MCPServer::getTools()` with category grouping
|
||||
- unified discovery snapshot with binary path, workspace root, language,
|
||||
tool count, and per-category counts
|
||||
- `editor/tests/step417_test.cpp` — 12 tests covering:
|
||||
1. explicit candidate binary path resolution
|
||||
2. default binary path resolution
|
||||
3. workspace root detection
|
||||
4. primary-language detection (C++ majority)
|
||||
5. primary-language detection (Python majority)
|
||||
6. `.mcp.json` structure generation
|
||||
7. workspace/language arg generation
|
||||
8. `.mcp.json` write-out
|
||||
9. manifest tool-count threshold (68+)
|
||||
10. required category presence
|
||||
11. tool-name categorization mapping
|
||||
12. end-to-end discovery snapshot
|
||||
|
||||
**Files modified:**
|
||||
- `editor/CMakeLists.txt` — `step417_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `step417_test` — PASS (12/12) new step coverage
|
||||
- `step416_test` — PASS (8/8) regression coverage
|
||||
- `step415_test` — PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/MCPServerConfig.h` within header-size limit (`246` <= `600`)
|
||||
- `editor/tests/step417_test.cpp` within test-file size guidance (`159` lines)
|
||||
- Legacy oversized headers persist:
|
||||
- `editor/src/ast/Serialization.h` (`1427` > `600`)
|
||||
- `editor/src/MCPServer.h` (`1679` > `600`)
|
||||
- `editor/src/HeadlessAgentRPCHandler.h` (`2629` > `600`)
|
||||
|
||||
# Roadmap Planning — Sprints 12-25+
|
||||
|
||||
## Status: Planning Complete (Sprints 12-19 detailed, 20-25 in roadmap.md)
|
||||
|
||||
Reference in New Issue
Block a user