Add step 619 per-project MCP config
This commit is contained in:
@@ -4450,4 +4450,13 @@ target_link_libraries(step618_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step619_test tests/step619_test.cpp)
|
||||
target_include_directories(step619_test PRIVATE src)
|
||||
target_link_libraries(step619_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)
|
||||
|
||||
102
editor/src/MCPProjectConfig.h
Normal file
102
editor/src/MCPProjectConfig.h
Normal file
@@ -0,0 +1,102 @@
|
||||
#pragma once
|
||||
// Step 619: Per-project MCP config (.whetstone.json)
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
struct MCPProjectConfigData {
|
||||
std::string workspace;
|
||||
std::string defaultLanguage;
|
||||
std::string agentRole;
|
||||
std::string mcpWorkspaceAlias;
|
||||
};
|
||||
|
||||
struct MCPProjectConfigLoadResult {
|
||||
bool found = false;
|
||||
MCPProjectConfigData config;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
class MCPProjectConfig {
|
||||
public:
|
||||
static bool parseText(const std::string& text,
|
||||
MCPProjectConfigData* out,
|
||||
std::string* error) {
|
||||
if (!out || !error) return false;
|
||||
error->clear();
|
||||
if (text.empty()) {
|
||||
*error = "config_empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
json parsed;
|
||||
try {
|
||||
parsed = json::parse(text);
|
||||
} catch (...) {
|
||||
*error = "config_json_parse_failed";
|
||||
return false;
|
||||
}
|
||||
return parseJson(parsed, out, error);
|
||||
}
|
||||
|
||||
static bool parseJson(const json& parsed,
|
||||
MCPProjectConfigData* out,
|
||||
std::string* error) {
|
||||
if (!out || !error) return false;
|
||||
error->clear();
|
||||
if (!parsed.is_object()) {
|
||||
*error = "config_not_object";
|
||||
return false;
|
||||
}
|
||||
|
||||
MCPProjectConfigData data;
|
||||
if (!readOptionalString(parsed, "workspace", &data.workspace, error)) return false;
|
||||
if (!readOptionalString(parsed, "defaultLanguage", &data.defaultLanguage, error)) return false;
|
||||
if (!readOptionalString(parsed, "agentRole", &data.agentRole, error)) return false;
|
||||
if (!readOptionalString(parsed, "mcpWorkspaceAlias", &data.mcpWorkspaceAlias, error)) return false;
|
||||
*out = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
static MCPProjectConfigLoadResult loadFromWorkspace(const std::string& workspaceRoot) {
|
||||
MCPProjectConfigLoadResult out;
|
||||
if (workspaceRoot.empty()) {
|
||||
out.error = "workspace_missing";
|
||||
return out;
|
||||
}
|
||||
|
||||
std::filesystem::path path = std::filesystem::path(workspaceRoot) / ".whetstone.json";
|
||||
if (!std::filesystem::exists(path)) return out;
|
||||
out.found = true;
|
||||
|
||||
std::ifstream in(path);
|
||||
if (!in.is_open()) {
|
||||
out.error = "config_open_failed";
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string text((std::istreambuf_iterator<char>(in)),
|
||||
std::istreambuf_iterator<char>());
|
||||
if (!parseText(text, &out.config, &out.error)) return out;
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
static bool readOptionalString(const json& parsed,
|
||||
const char* key,
|
||||
std::string* out,
|
||||
std::string* error) {
|
||||
if (!parsed.contains(key)) return true;
|
||||
if (!parsed[key].is_string()) {
|
||||
*error = std::string("config_field_invalid_type:") + key;
|
||||
return false;
|
||||
}
|
||||
*out = parsed[key].get<std::string>();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#include "HeadlessEditorState.h"
|
||||
#include "MCPBridge.h"
|
||||
#include "MCPProjectConfig.h"
|
||||
#include "AgentPermissionPolicy.h"
|
||||
#include "ast/Serialization.h"
|
||||
|
||||
#include <csignal>
|
||||
@@ -116,14 +118,34 @@ int main(int argc, char** argv) {
|
||||
// Create headless state
|
||||
HeadlessEditorState state;
|
||||
state.verbose = verbose;
|
||||
if (!workspace.empty()) state.workspaceRoot = workspace;
|
||||
if (!language.empty()) state.defaultLanguage = language;
|
||||
MCPProjectConfigLoadResult configLoad;
|
||||
if (!workspace.empty()) {
|
||||
configLoad = MCPProjectConfig::loadFromWorkspace(workspace);
|
||||
if (!configLoad.error.empty()) {
|
||||
std::cerr << "[whetstone-mcp] Config load warning: " << configLoad.error << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (configLoad.found && !configLoad.config.workspace.empty()) {
|
||||
state.workspaceRoot = configLoad.config.workspace;
|
||||
} else if (!workspace.empty()) {
|
||||
state.workspaceRoot = workspace;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// Set default agent role to Refactor for full MCP access
|
||||
state.setAgentRole("mcp-session", AgentRole::Refactor);
|
||||
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;
|
||||
@@ -138,8 +160,11 @@ int main(int argc, char** argv) {
|
||||
|
||||
// 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";
|
||||
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";
|
||||
|
||||
169
editor/tests/step619_test.cpp
Normal file
169
editor/tests/step619_test.cpp
Normal file
@@ -0,0 +1,169 @@
|
||||
// Step 619: .whetstone.json per-project config schema (12 tests)
|
||||
|
||||
#include "MCPProjectConfig.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
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::filesystem::path tempRoot() {
|
||||
std::filesystem::path root = std::filesystem::temp_directory_path() / "whetstone_step619";
|
||||
std::filesystem::remove_all(root);
|
||||
std::filesystem::create_directories(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
static void writeFile(const std::filesystem::path& path, const std::string& content) {
|
||||
std::ofstream out(path);
|
||||
out << content;
|
||||
}
|
||||
|
||||
void test_parse_valid_all_fields() {
|
||||
TEST(parse_valid_all_fields);
|
||||
MCPProjectConfigData config;
|
||||
std::string error;
|
||||
std::string text =
|
||||
R"({"workspace":"/tmp/ws","defaultLanguage":"rust","agentRole":"generator","mcpWorkspaceAlias":"alpha"})";
|
||||
CHECK(MCPProjectConfig::parseText(text, &config, &error), "parse should succeed");
|
||||
CHECK(config.workspace == "/tmp/ws", "workspace mismatch");
|
||||
CHECK(config.defaultLanguage == "rust", "language mismatch");
|
||||
CHECK(config.agentRole == "generator", "role mismatch");
|
||||
CHECK(config.mcpWorkspaceAlias == "alpha", "alias mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_valid_partial_fields() {
|
||||
TEST(parse_valid_partial_fields);
|
||||
MCPProjectConfigData config;
|
||||
std::string error;
|
||||
CHECK(MCPProjectConfig::parseText(R"({"defaultLanguage":"python"})", &config, &error),
|
||||
"parse should succeed");
|
||||
CHECK(config.defaultLanguage == "python", "language mismatch");
|
||||
CHECK(config.workspace.empty(), "workspace should be empty");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_empty_text_fails() {
|
||||
TEST(parse_empty_text_fails);
|
||||
MCPProjectConfigData config;
|
||||
std::string error;
|
||||
CHECK(!MCPProjectConfig::parseText("", &config, &error), "parse should fail");
|
||||
CHECK(error == "config_empty", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_invalid_json_fails() {
|
||||
TEST(parse_invalid_json_fails);
|
||||
MCPProjectConfigData config;
|
||||
std::string error;
|
||||
CHECK(!MCPProjectConfig::parseText("{bad", &config, &error), "parse should fail");
|
||||
CHECK(error == "config_json_parse_failed", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_non_object_fails() {
|
||||
TEST(parse_non_object_fails);
|
||||
MCPProjectConfigData config;
|
||||
std::string error;
|
||||
CHECK(!MCPProjectConfig::parseText(R"(["bad"])", &config, &error), "parse should fail");
|
||||
CHECK(error == "config_not_object", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_invalid_workspace_type_fails() {
|
||||
TEST(parse_invalid_workspace_type_fails);
|
||||
MCPProjectConfigData config;
|
||||
std::string error;
|
||||
CHECK(!MCPProjectConfig::parseText(R"({"workspace":42})", &config, &error), "parse should fail");
|
||||
CHECK(error == "config_field_invalid_type:workspace", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_invalid_default_language_type_fails() {
|
||||
TEST(parse_invalid_default_language_type_fails);
|
||||
MCPProjectConfigData config;
|
||||
std::string error;
|
||||
CHECK(!MCPProjectConfig::parseText(R"({"defaultLanguage":42})", &config, &error), "parse should fail");
|
||||
CHECK(error == "config_field_invalid_type:defaultLanguage", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_load_from_workspace_missing_workspace_fails() {
|
||||
TEST(load_from_workspace_missing_workspace_fails);
|
||||
auto result = MCPProjectConfig::loadFromWorkspace("");
|
||||
CHECK(!result.found, "found should be false");
|
||||
CHECK(result.error == "workspace_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_load_from_workspace_without_file_returns_not_found() {
|
||||
TEST(load_from_workspace_without_file_returns_not_found);
|
||||
std::filesystem::path root = tempRoot() / "ws-empty";
|
||||
std::filesystem::create_directories(root);
|
||||
auto result = MCPProjectConfig::loadFromWorkspace(root.string());
|
||||
CHECK(!result.found, "found should be false");
|
||||
CHECK(result.error.empty(), "error should be empty");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_load_from_workspace_reads_valid_config() {
|
||||
TEST(load_from_workspace_reads_valid_config);
|
||||
std::filesystem::path root = tempRoot() / "ws-valid";
|
||||
std::filesystem::create_directories(root);
|
||||
writeFile(root / ".whetstone.json",
|
||||
R"({"workspace":"/tmp/alt","defaultLanguage":"go","agentRole":"refactor","mcpWorkspaceAlias":"beta"})");
|
||||
auto result = MCPProjectConfig::loadFromWorkspace(root.string());
|
||||
CHECK(result.found, "found should be true");
|
||||
CHECK(result.error.empty(), "error should be empty");
|
||||
CHECK(result.config.defaultLanguage == "go", "language mismatch");
|
||||
CHECK(result.config.mcpWorkspaceAlias == "beta", "alias mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_load_from_workspace_reports_parse_error() {
|
||||
TEST(load_from_workspace_reports_parse_error);
|
||||
std::filesystem::path root = tempRoot() / "ws-bad";
|
||||
std::filesystem::create_directories(root);
|
||||
writeFile(root / ".whetstone.json", "{bad");
|
||||
auto result = MCPProjectConfig::loadFromWorkspace(root.string());
|
||||
CHECK(result.found, "found should be true when file exists");
|
||||
CHECK(result.error == "config_json_parse_failed", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_parse_includes_agent_role_and_alias_fields() {
|
||||
TEST(parse_includes_agent_role_and_alias_fields);
|
||||
MCPProjectConfigData config;
|
||||
std::string error;
|
||||
CHECK(MCPProjectConfig::parseText(R"({"agentRole":"linter","mcpWorkspaceAlias":"gamma"})", &config, &error),
|
||||
"parse should succeed");
|
||||
CHECK(config.agentRole == "linter", "agentRole mismatch");
|
||||
CHECK(config.mcpWorkspaceAlias == "gamma", "alias mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 619: .whetstone.json per-project config schema\n";
|
||||
|
||||
test_parse_valid_all_fields(); // 1
|
||||
test_parse_valid_partial_fields(); // 2
|
||||
test_parse_empty_text_fails(); // 3
|
||||
test_parse_invalid_json_fails(); // 4
|
||||
test_parse_non_object_fails(); // 5
|
||||
test_parse_invalid_workspace_type_fails(); // 6
|
||||
test_parse_invalid_default_language_type_fails(); // 7
|
||||
test_load_from_workspace_missing_workspace_fails(); // 8
|
||||
test_load_from_workspace_without_file_returns_not_found(); // 9
|
||||
test_load_from_workspace_reads_valid_config(); // 10
|
||||
test_load_from_workspace_reports_parse_error(); // 11
|
||||
test_parse_includes_agent_role_and_alias_fields(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
36
progress.md
36
progress.md
@@ -12216,3 +12216,39 @@ readiness tool.
|
||||
- `editor/src/Sprint36aIntegration.h` within header-size limit (`71` <= `600`)
|
||||
- `editor/tests/step618_test.cpp` within test-file size guidance (`121` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 619: `.whetstone.json` per-project config schema
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Introduces per-project MCP config parsing/loading for:
|
||||
`workspace`, `defaultLanguage`, `agentRole`, and `mcpWorkspaceAlias`.
|
||||
`whetstone_mcp` now reads `.whetstone.json` from the `--workspace` directory
|
||||
at startup when present.
|
||||
|
||||
**Files added:**
|
||||
- `editor/src/MCPProjectConfig.h` - project config schema and loader:
|
||||
- JSON text/object parsing with field type validation
|
||||
- workspace-root file loading from `.whetstone.json`
|
||||
- `editor/tests/step619_test.cpp` - 12 tests covering:
|
||||
- schema parse success/failure paths
|
||||
- invalid field type handling
|
||||
- workspace file discovery/load/error behavior
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/mcp_main.cpp` - startup config integration:
|
||||
- loads `.whetstone.json` for provided `--workspace`
|
||||
- applies configured workspace/language/agent role defaults
|
||||
- reports workspace alias in startup logs when provided
|
||||
- `editor/CMakeLists.txt` - `step619_test` target
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step619_test step618_test` - PASS
|
||||
- `./editor/build-native/step619_test` - PASS (12/12)
|
||||
- `./editor/build-native/step618_test` - PASS (8/8) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/MCPProjectConfig.h` within header-size limit (`102` <= `600`)
|
||||
- `editor/src/mcp_main.cpp` within main-size limit (`177` <= `1500`)
|
||||
- `editor/tests/step619_test.cpp` within test-file size guidance (`169` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user