Add step 621 runtime workspace switch MCP tool
This commit is contained in:
@@ -4468,4 +4468,13 @@ target_link_libraries(step620_test PRIVATE
|
||||
tree_sitter_javascript tree_sitter_typescript
|
||||
tree_sitter_java tree_sitter_rust tree_sitter_go)
|
||||
|
||||
add_executable(step621_test tests/step621_test.cpp)
|
||||
target_include_directories(step621_test PRIVATE src)
|
||||
target_link_libraries(step621_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)
|
||||
|
||||
@@ -80,6 +80,10 @@ struct AgentPermissionPolicy {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (method == "setWorkspaceContext") {
|
||||
return role == AgentRole::Refactor || role == AgentRole::Generator;
|
||||
}
|
||||
|
||||
// Mutation methods: Refactor and Generator only
|
||||
if (method == "generateCode" ||
|
||||
method == "applyMutation" ||
|
||||
|
||||
@@ -51,6 +51,30 @@
|
||||
});
|
||||
}
|
||||
|
||||
// --- setWorkspaceContext ---
|
||||
if (method == "setWorkspaceContext") {
|
||||
if (!AgentPermissionPolicy::canInvoke(role, method))
|
||||
return headlessRpcError(id, -32031, "Role not permitted");
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
: json::object();
|
||||
std::string workspace = params.value("workspace", "");
|
||||
std::string language = params.value("language", "");
|
||||
if (workspace.empty()) {
|
||||
return headlessRpcError(id, -32602, "Missing workspace");
|
||||
}
|
||||
|
||||
state.workspaceRoot = workspace;
|
||||
if (!language.empty()) state.defaultLanguage = language;
|
||||
state.project.scanWorkspace(workspace);
|
||||
const auto& idx = state.project.index;
|
||||
return headlessRpcResult(id, {
|
||||
{"workspace", state.workspaceRoot},
|
||||
{"language", state.defaultLanguage},
|
||||
{"fileCount", idx.fileCount()},
|
||||
{"dirCount", idx.dirCount()}
|
||||
});
|
||||
}
|
||||
|
||||
// --- getImportGraph ---
|
||||
if (method == "getImportGraph") {
|
||||
auto params = request.contains("params") ? request["params"]
|
||||
|
||||
@@ -38,6 +38,20 @@
|
||||
[this](const json& args) {
|
||||
return runQueueReady(args);
|
||||
};
|
||||
|
||||
tools_.push_back({"whetstone_set_workspace",
|
||||
"Switch active workspace and default language without restarting the MCP server.",
|
||||
{{"type", "object"}, {"properties", {
|
||||
{"workspace", {{"type", "string"},
|
||||
{"description", "Workspace root path."}}},
|
||||
{"language", {{"type", "string"},
|
||||
{"description", "Default language for new operations."}}}
|
||||
}}, {"required", json::array({"workspace", "language"})}}
|
||||
});
|
||||
toolHandlers_["whetstone_set_workspace"] =
|
||||
[this](const json& args) {
|
||||
return runSetWorkspace(args);
|
||||
};
|
||||
}
|
||||
|
||||
json runArchitectIntake(const json& args) {
|
||||
@@ -476,3 +490,63 @@
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
json runSetWorkspace(const json& args) {
|
||||
if (!args.contains("workspace")) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", "workspace_missing"}
|
||||
};
|
||||
}
|
||||
if (!args.contains("language")) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", "language_missing"}
|
||||
};
|
||||
}
|
||||
if (!args["workspace"].is_string()) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", "workspace_not_string"}
|
||||
};
|
||||
}
|
||||
if (!args["language"].is_string()) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", "language_not_string"}
|
||||
};
|
||||
}
|
||||
|
||||
const std::string workspace = args.value("workspace", "");
|
||||
const std::string language = args.value("language", "");
|
||||
if (workspace.empty()) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", "workspace_empty"}
|
||||
};
|
||||
}
|
||||
if (language.empty()) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", "language_empty"}
|
||||
};
|
||||
}
|
||||
|
||||
json result = callWhetstone("setWorkspaceContext", {
|
||||
{"workspace", workspace},
|
||||
{"language", language}
|
||||
});
|
||||
if (hasCallError(result)) {
|
||||
return {
|
||||
{"success", false},
|
||||
{"error", result["error"]}
|
||||
};
|
||||
}
|
||||
return {
|
||||
{"success", true},
|
||||
{"workspace", result.value("workspace", workspace)},
|
||||
{"language", result.value("language", language)},
|
||||
{"fileCount", result.value("fileCount", 0)},
|
||||
{"dirCount", result.value("dirCount", 0)}
|
||||
};
|
||||
}
|
||||
|
||||
229
editor/tests/step621_test.cpp
Normal file
229
editor/tests/step621_test.cpp
Normal file
@@ -0,0 +1,229 @@
|
||||
// Step 621: whetstone_set_workspace MCP tool (12 tests)
|
||||
|
||||
#include "HeadlessEditorState.h"
|
||||
#include "MCPServer.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 json callTool(MCPServer& mcp, const std::string& name, const json& args) {
|
||||
json req = {
|
||||
{"jsonrpc", "2.0"},
|
||||
{"id", 1},
|
||||
{"method", "tools/call"},
|
||||
{"params", {{"name", name}, {"arguments", args}}}
|
||||
};
|
||||
json resp = mcp.handleRequest(req);
|
||||
std::string text = resp["result"]["content"][0].value("text", "{}");
|
||||
return json::parse(text);
|
||||
}
|
||||
|
||||
static std::filesystem::path tempWorkspace() {
|
||||
std::filesystem::path root = std::filesystem::temp_directory_path() / "whetstone_step621";
|
||||
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_tool_registered_accessor() {
|
||||
TEST(tool_registered_accessor);
|
||||
MCPServer mcp;
|
||||
bool found = false;
|
||||
for (const auto& tool : mcp.getTools()) {
|
||||
if (tool.name == "whetstone_set_workspace") found = true;
|
||||
}
|
||||
CHECK(found, "tool should be registered");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_tool_schema_requires_workspace() {
|
||||
TEST(tool_schema_requires_workspace);
|
||||
MCPServer mcp;
|
||||
bool required = false;
|
||||
for (const auto& tool : mcp.getTools()) {
|
||||
if (tool.name != "whetstone_set_workspace") continue;
|
||||
for (const auto& req : tool.inputSchema["required"]) {
|
||||
if (req == "workspace") required = true;
|
||||
}
|
||||
}
|
||||
CHECK(required, "workspace should be required");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_tool_schema_requires_language() {
|
||||
TEST(tool_schema_requires_language);
|
||||
MCPServer mcp;
|
||||
bool required = false;
|
||||
for (const auto& tool : mcp.getTools()) {
|
||||
if (tool.name != "whetstone_set_workspace") continue;
|
||||
for (const auto& req : tool.inputSchema["required"]) {
|
||||
if (req == "language") required = true;
|
||||
}
|
||||
}
|
||||
CHECK(required, "language should be required");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_missing_workspace_errors() {
|
||||
TEST(missing_workspace_errors);
|
||||
MCPServer mcp;
|
||||
json out = callTool(mcp, "whetstone_set_workspace", {{"language", "cpp"}});
|
||||
CHECK(!out.value("success", true), "success should be false");
|
||||
CHECK(out.value("error", "") == "workspace_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_missing_language_errors() {
|
||||
TEST(missing_language_errors);
|
||||
MCPServer mcp;
|
||||
json out = callTool(mcp, "whetstone_set_workspace", {{"workspace", "/tmp/ws"}});
|
||||
CHECK(!out.value("success", true), "success should be false");
|
||||
CHECK(out.value("error", "") == "language_missing", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_workspace_must_be_string() {
|
||||
TEST(workspace_must_be_string);
|
||||
MCPServer mcp;
|
||||
json out = callTool(mcp, "whetstone_set_workspace",
|
||||
{{"workspace", 42}, {"language", "cpp"}});
|
||||
CHECK(!out.value("success", true), "success should be false");
|
||||
CHECK(out.value("error", "") == "workspace_not_string", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_language_must_be_string() {
|
||||
TEST(language_must_be_string);
|
||||
MCPServer mcp;
|
||||
json out = callTool(mcp, "whetstone_set_workspace",
|
||||
{{"workspace", "/tmp/ws"}, {"language", 42}});
|
||||
CHECK(!out.value("success", true), "success should be false");
|
||||
CHECK(out.value("error", "") == "language_not_string", "wrong error");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_handler_calls_set_workspace_context_method() {
|
||||
TEST(handler_calls_set_workspace_context_method);
|
||||
MCPServer mcp;
|
||||
std::string capturedMethod;
|
||||
json capturedParams;
|
||||
mcp.setRpcCallback([&](const json& request) {
|
||||
capturedMethod = request.value("method", "");
|
||||
capturedParams = request.value("params", json::object());
|
||||
return json{{"result", {{"workspace", "/tmp/a"}, {"language", "go"}, {"fileCount", 3}, {"dirCount", 1}}}};
|
||||
});
|
||||
|
||||
json out = callTool(mcp, "whetstone_set_workspace",
|
||||
{{"workspace", "/tmp/a"}, {"language", "go"}});
|
||||
CHECK(out.value("success", false), "success should be true");
|
||||
CHECK(capturedMethod == "setWorkspaceContext", "wrong rpc method");
|
||||
CHECK(capturedParams.value("workspace", "") == "/tmp/a", "workspace not forwarded");
|
||||
CHECK(capturedParams.value("language", "") == "go", "language not forwarded");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_handler_returns_rpc_values() {
|
||||
TEST(handler_returns_rpc_values);
|
||||
MCPServer mcp;
|
||||
mcp.setRpcCallback([&](const json&) {
|
||||
return json{{"result", {{"workspace", "/tmp/custom"}, {"language", "rust"},
|
||||
{"fileCount", 9}, {"dirCount", 4}}}};
|
||||
});
|
||||
json out = callTool(mcp, "whetstone_set_workspace",
|
||||
{{"workspace", "/tmp/a"}, {"language", "go"}});
|
||||
CHECK(out.value("workspace", "") == "/tmp/custom", "workspace mismatch");
|
||||
CHECK(out.value("language", "") == "rust", "language mismatch");
|
||||
CHECK(out.value("fileCount", 0) == 9, "fileCount mismatch");
|
||||
CHECK(out.value("dirCount", 0) == 4, "dirCount mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_handler_surfaces_rpc_error() {
|
||||
TEST(handler_surfaces_rpc_error);
|
||||
MCPServer mcp;
|
||||
mcp.setRpcCallback([&](const json&) {
|
||||
return json{{"error", {{"message", "bad workspace"}}}};
|
||||
});
|
||||
json out = callTool(mcp, "whetstone_set_workspace",
|
||||
{{"workspace", "/tmp/a"}, {"language", "go"}});
|
||||
CHECK(!out.value("success", true), "success should be false");
|
||||
CHECK(out.value("error", "") == "bad workspace", "error message mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_integration_switches_headless_workspace_and_language() {
|
||||
TEST(integration_switches_headless_workspace_and_language);
|
||||
auto root = tempWorkspace();
|
||||
writeFile(root / "main.py", "print('ok')\n");
|
||||
|
||||
HeadlessEditorState state;
|
||||
state.setAgentRole("mcp-session", AgentRole::Refactor);
|
||||
MCPServer mcp;
|
||||
mcp.setRpcCallback([&](const json& request) {
|
||||
return state.processAgentRequest(request, "mcp-session");
|
||||
});
|
||||
|
||||
json out = callTool(mcp, "whetstone_set_workspace",
|
||||
{{"workspace", root.string()}, {"language", "python"}});
|
||||
CHECK(out.value("success", false), "success should be true");
|
||||
CHECK(out.value("workspace", "") == root.string(), "workspace mismatch");
|
||||
CHECK(out.value("language", "") == "python", "language mismatch");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void test_integration_affects_workspace_list_tool() {
|
||||
TEST(integration_affects_workspace_list_tool);
|
||||
auto root = tempWorkspace();
|
||||
writeFile(root / "notes.txt", "hello\n");
|
||||
|
||||
HeadlessEditorState state;
|
||||
state.setAgentRole("mcp-session", AgentRole::Refactor);
|
||||
MCPServer mcp;
|
||||
mcp.setRpcCallback([&](const json& request) {
|
||||
return state.processAgentRequest(request, "mcp-session");
|
||||
});
|
||||
|
||||
(void)callTool(mcp, "whetstone_set_workspace",
|
||||
{{"workspace", root.string()}, {"language", "python"}});
|
||||
json listed = callTool(mcp, "whetstone_workspace_list", json::object());
|
||||
CHECK(listed.contains("files"), "workspace list should return files");
|
||||
bool foundNotes = false;
|
||||
for (const auto& item : listed["files"]) {
|
||||
if (item.value("path", "") == "notes.txt") foundNotes = true;
|
||||
}
|
||||
CHECK(foundNotes, "expected notes.txt in workspace list");
|
||||
PASS();
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Step 621: whetstone_set_workspace MCP tool\n";
|
||||
|
||||
test_tool_registered_accessor(); // 1
|
||||
test_tool_schema_requires_workspace(); // 2
|
||||
test_tool_schema_requires_language(); // 3
|
||||
test_missing_workspace_errors(); // 4
|
||||
test_missing_language_errors(); // 5
|
||||
test_workspace_must_be_string(); // 6
|
||||
test_language_must_be_string(); // 7
|
||||
test_handler_calls_set_workspace_context_method(); // 8
|
||||
test_handler_returns_rpc_values(); // 9
|
||||
test_handler_surfaces_rpc_error(); // 10
|
||||
test_integration_switches_headless_workspace_and_language();// 11
|
||||
test_integration_affects_workspace_list_tool(); // 12
|
||||
|
||||
std::cout << "\nResults: " << passed << "/" << (passed + failed) << " passed\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
36
progress.md
36
progress.md
@@ -12287,3 +12287,39 @@ explicit `--workspace` flag by finding the nearest `.whetstone.json` from CWD.
|
||||
- `editor/src/mcp_main.cpp` within main-size limit (`184` <= `1500`)
|
||||
- `editor/tests/step620_test.cpp` within test-file size guidance (`191` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
### Step 621: `whetstone_set_workspace` MCP tool
|
||||
**Status:** PASS (12/12 tests)
|
||||
|
||||
Adds runtime workspace/language switching for a live MCP daemon without
|
||||
restart, including a new headless RPC method to apply workspace context.
|
||||
|
||||
**Files modified:**
|
||||
- `editor/src/mcp/RegisterArchitectIntakeTools.h` - added:
|
||||
- `whetstone_set_workspace` tool registration, validation, and RPC bridge
|
||||
- forwarding to `setWorkspaceContext` with structured response mapping
|
||||
- `editor/src/headless_rpc/DispatchPart3.h` - added `setWorkspaceContext`:
|
||||
- applies workspace root + default language
|
||||
- re-indexes workspace and returns file/dir counts
|
||||
- `editor/src/AgentPermissionPolicy.h` - permission rule for `setWorkspaceContext`
|
||||
- `editor/CMakeLists.txt` - `step621_test` target
|
||||
|
||||
**Files added:**
|
||||
- `editor/tests/step621_test.cpp` - 12 tests covering:
|
||||
- tool registration/schema and input validation
|
||||
- RPC forwarding method/argument correctness
|
||||
- RPC success/error propagation
|
||||
- integration with HeadlessEditorState workspace switching + workspace listing
|
||||
|
||||
**Verification run:**
|
||||
- `cmake -S editor -B editor/build-native` - PASS
|
||||
- `cmake --build editor/build-native --target step621_test step620_test` - PASS
|
||||
- `./editor/build-native/step621_test` - PASS (12/12)
|
||||
- `./editor/build-native/step620_test` - PASS (12/12) regression coverage
|
||||
|
||||
**Architecture gate check:**
|
||||
- `editor/src/mcp/RegisterArchitectIntakeTools.h` within header-size limit (`552` <= `600`)
|
||||
- `editor/src/headless_rpc/DispatchPart3.h` within header-size limit (`494` <= `600`)
|
||||
- `editor/src/AgentPermissionPolicy.h` within header-size limit (`132` <= `600`)
|
||||
- `editor/tests/step621_test.cpp` within test-file size guidance (`229` lines)
|
||||
- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`
|
||||
|
||||
Reference in New Issue
Block a user