From 1e75f08b557c2becc780ae76a668af78d1f21fb9 Mon Sep 17 00:00:00 2001 From: Bill Date: Mon, 16 Feb 2026 22:46:22 -0700 Subject: [PATCH] Step 505: add MCP tool documentation resource and tests --- editor/CMakeLists.txt | 9 ++ editor/src/MCPToolDocumentation.h | 192 ++++++++++++++++++++++++++++++ editor/tests/step505_test.cpp | 167 ++++++++++++++++++++++++++ progress.md | 34 ++++++ 4 files changed, 402 insertions(+) create mode 100644 editor/src/MCPToolDocumentation.h create mode 100644 editor/tests/step505_test.cpp diff --git a/editor/CMakeLists.txt b/editor/CMakeLists.txt index 14026e5..ad6192d 100644 --- a/editor/CMakeLists.txt +++ b/editor/CMakeLists.txt @@ -3424,4 +3424,13 @@ target_link_libraries(step504_test PRIVATE tree_sitter_javascript tree_sitter_typescript tree_sitter_java tree_sitter_rust tree_sitter_go) +add_executable(step505_test tests/step505_test.cpp) +target_include_directories(step505_test PRIVATE src) +target_link_libraries(step505_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) diff --git a/editor/src/MCPToolDocumentation.h b/editor/src/MCPToolDocumentation.h new file mode 100644 index 0000000..edd33d9 --- /dev/null +++ b/editor/src/MCPToolDocumentation.h @@ -0,0 +1,192 @@ +#pragma once + +// Step 505: MCP Tool Documentation +// Structured MCP tool docs with category index and exportable resource formats. + +#include +#include +#include +#include +#include +#include + +struct MCPToolErrorCode { + std::string code; + std::string meaning; +}; + +struct MCPToolDoc { + std::string name; + std::string category; // AST, Diagnostics, Workflow, Routing, Security, etc. + std::string description; + std::string inputSchema; + std::string inputExample; + std::string outputSchema; + std::string outputFields; + std::vector errorCodes; + + bool isComplete() const { + return !name.empty() && !category.empty() && !description.empty() && + !inputSchema.empty() && !inputExample.empty() && + !outputSchema.empty() && !outputFields.empty() && + !errorCodes.empty(); + } +}; + +struct MCPDocumentationResource { + std::vector tools; + std::map> categories; + std::vector notes; +}; + +class MCPToolDocumentation { +public: + static MCPDocumentationResource buildDefault() { + MCPDocumentationResource out; + out.tools = { + {"whetstone_parse_ast", "AST", + "Parse source code into a normalized AST with semantic node identities.", + R"({"type":"object","properties":{"source":{"type":"string"},"language":{"type":"string"}},"required":["source","language"]})", + R"({"source":"fn add(a,b){return a+b;}","language":"javascript"})", + R"({"type":"object","properties":{"ast":{"type":"object"},"nodeCount":{"type":"integer"}}})", + "ast: parsed tree, nodeCount: number of nodes", + {{"-32602", "Invalid params"}, {"-32001", "Parse failure"}}}, + + {"whetstone_run_diagnostics", "Diagnostics", + "Run static diagnostics and return normalized findings with severity.", + R"({"type":"object","properties":{"source":{"type":"string"},"language":{"type":"string"}},"required":["source","language"]})", + R"({"source":"int x = gets(buf);","language":"c"})", + R"({"type":"object","properties":{"findings":{"type":"array"},"summary":{"type":"object"}}})", + "findings: diagnostic list, summary: by-severity counts", + {{"-32602", "Invalid params"}, {"-32003", "Diagnostics engine failure"}}}, + + {"whetstone_create_workflow", "Workflow", + "Create a workflow from skeleton annotations with dependency ordering.", + R"({"type":"object","properties":{"projectName":{"type":"string"}},"required":["projectName"]})", + R"({"projectName":"demo"})", + R"({"type":"object","properties":{"workflow":{"type":"object"},"itemCount":{"type":"integer"}}})", + "workflow: serialized workflow state, itemCount: total items", + {{"-32602", "Invalid params"}, {"-32000", "No active project"}}}, + + {"whetstone_route_work_items", "Routing", + "Route ready workflow items to deterministic/template/slm/llm/human workers.", + R"({"type":"object","properties":{"maxItems":{"type":"integer"}}})", + R"({"maxItems":25})", + R"({"type":"object","properties":{"routed":{"type":"array"},"counts":{"type":"object"}}})", + "routed: per-item routing decisions, counts: worker-type distribution", + {{"-32602", "Invalid params"}, {"-32010", "Routing state unavailable"}}}, + + {"whetstone_security_audit", "Security", + "Run security audit with CWE mappings and risk ratings.", + R"({"type":"object","properties":{"source":{"type":"string"},"language":{"type":"string"}},"required":["source","language"]})", + R"({"source":"char buf[8]; gets(buf);","language":"c"})", + R"({"type":"object","properties":{"findings":{"type":"array"},"overallRisk":{"type":"integer"}}})", + "findings: vulnerability details, overallRisk: max severity level", + {{"-32602", "Invalid params"}, {"-32020", "Security analysis failed"}}} + }; + + indexCategories(out); + out.notes.push_back("Default MCP tool documentation generated"); + out.notes.push_back("Includes AST, Diagnostics, Workflow, Routing, Security categories"); + return out; + } + + static bool validateCompleteness(const MCPDocumentationResource& docs, + std::string* error = nullptr) { + if (docs.tools.empty()) { + if (error) *error = "No tools documented"; + return false; + } + + for (const auto& t : docs.tools) { + if (!t.isComplete()) { + if (error) *error = "Incomplete tool documentation: " + t.name; + return false; + } + } + return true; + } + + static std::string toMarkdown(const MCPDocumentationResource& docs) { + std::ostringstream o; + o << "# MCP Tool Documentation\n\n"; + for (const auto& t : docs.tools) { + o << "## " << t.name << "\n"; + o << "- Category: " << t.category << "\n"; + o << "- Description: " << t.description << "\n"; + o << "- Input schema: `" << t.inputSchema << "`\n"; + o << "- Input example: `" << t.inputExample << "`\n"; + o << "- Output schema: `" << t.outputSchema << "`\n"; + o << "- Output fields: " << t.outputFields << "\n"; + o << "- Errors:\n"; + for (const auto& e : t.errorCodes) { + o << " - " << e.code << ": " << e.meaning << "\n"; + } + o << "\n"; + } + return o.str(); + } + + static std::string toJson(const MCPDocumentationResource& docs) { + std::ostringstream o; + o << "{\n \"tools\": [\n"; + for (size_t i = 0; i < docs.tools.size(); ++i) { + const auto& t = docs.tools[i]; + o << " {\n" + << " \"name\": \"" << escape(t.name) << "\",\n" + << " \"category\": \"" << escape(t.category) << "\",\n" + << " \"description\": \"" << escape(t.description) << "\"\n" + << " }"; + if (i + 1 < docs.tools.size()) o << ","; + o << "\n"; + } + o << " ]\n}\n"; + return o.str(); + } + + static bool writeResource(const MCPDocumentationResource& docs, + const std::string& directory, + std::string* markdownPath = nullptr, + std::string* jsonPath = nullptr) { + namespace fs = std::filesystem; + fs::path root = directory.empty() ? fs::path(".") : fs::path(directory); + std::error_code ec; + fs::create_directories(root, ec); + if (ec) return false; + + fs::path md = root / "mcp_tool_docs.md"; + fs::path js = root / "mcp_tool_docs.json"; + + { + std::ofstream f(md); + if (!f) return false; + f << toMarkdown(docs); + } + { + std::ofstream f(js); + if (!f) return false; + f << toJson(docs); + } + + if (markdownPath) *markdownPath = md.string(); + if (jsonPath) *jsonPath = js.string(); + return true; + } + +private: + static void indexCategories(MCPDocumentationResource& docs) { + docs.categories.clear(); + for (const auto& t : docs.tools) docs.categories[t.category].push_back(t.name); + } + + static std::string escape(const std::string& s) { + std::string out; + out.reserve(s.size()); + for (char c : s) { + if (c == '\"') out += "\\\""; + else if (c == '\\') out += "\\\\"; + else out.push_back(c); + } + return out; + } +}; diff --git a/editor/tests/step505_test.cpp b/editor/tests/step505_test.cpp new file mode 100644 index 0000000..b7b3d49 --- /dev/null +++ b/editor/tests/step505_test.cpp @@ -0,0 +1,167 @@ +// Step 505: MCP Tool Documentation Tests (12 tests) + +#include "MCPToolDocumentation.h" + +#include +#include +#include + +namespace fs = std::filesystem; + +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 MCPDocumentationResource buildDocs() { + return MCPToolDocumentation::buildDefault(); +} + +void test_default_docs_include_multiple_tools() { + TEST(default_docs_include_multiple_tools); + auto docs = buildDocs(); + CHECK(docs.tools.size() >= 5, "expected at least 5 documented tools"); + PASS(); +} + +void test_each_tool_has_required_documentation_sections() { + TEST(each_tool_has_required_documentation_sections); + auto docs = buildDocs(); + for (const auto& t : docs.tools) { + CHECK(t.isComplete(), "tool doc is incomplete"); + } + PASS(); +} + +void test_completeness_validator_passes_for_default_docs() { + TEST(completeness_validator_passes_for_default_docs); + auto docs = buildDocs(); + std::string err; + CHECK(MCPToolDocumentation::validateCompleteness(docs, &err), err.c_str()); + PASS(); +} + +void test_required_categories_are_present() { + TEST(required_categories_are_present); + auto docs = buildDocs(); + CHECK(docs.categories.count("AST") == 1, "missing AST category"); + CHECK(docs.categories.count("Diagnostics") == 1, "missing Diagnostics category"); + CHECK(docs.categories.count("Workflow") == 1, "missing Workflow category"); + CHECK(docs.categories.count("Routing") == 1, "missing Routing category"); + CHECK(docs.categories.count("Security") == 1, "missing Security category"); + PASS(); +} + +void test_error_codes_are_documented_for_all_tools() { + TEST(error_codes_are_documented_for_all_tools); + auto docs = buildDocs(); + for (const auto& t : docs.tools) { + CHECK(!t.errorCodes.empty(), "missing error codes"); + CHECK(!t.errorCodes[0].code.empty(), "error code missing code"); + CHECK(!t.errorCodes[0].meaning.empty(), "error code missing meaning"); + } + PASS(); +} + +void test_markdown_export_contains_tool_names_and_sections() { + TEST(markdown_export_contains_tool_names_and_sections); + auto docs = buildDocs(); + auto md = MCPToolDocumentation::toMarkdown(docs); + CHECK(md.find("# MCP Tool Documentation") != std::string::npos, "missing markdown header"); + CHECK(md.find("whetstone_parse_ast") != std::string::npos, "missing AST tool section"); + CHECK(md.find("Errors:") != std::string::npos, "missing error section"); + PASS(); +} + +void test_json_export_contains_tool_entries() { + TEST(json_export_contains_tool_entries); + auto docs = buildDocs(); + auto js = MCPToolDocumentation::toJson(docs); + CHECK(js.find("\"tools\"") != std::string::npos, "missing tools array"); + CHECK(js.find("\"name\": \"whetstone_security_audit\"") != std::string::npos, + "missing security tool entry"); + PASS(); +} + +void test_resource_write_materializes_markdown_and_json_files() { + TEST(resource_write_materializes_markdown_and_json_files); + auto docs = buildDocs(); + fs::path root = fs::temp_directory_path() / "whetstone_step505_docs"; + fs::remove_all(root); + + std::string mdPath, jsPath; + CHECK(MCPToolDocumentation::writeResource(docs, root.string(), &mdPath, &jsPath), + "resource write failed"); + CHECK(fs::exists(mdPath), "markdown resource file missing"); + CHECK(fs::exists(jsPath), "json resource file missing"); + + fs::remove_all(root); + PASS(); +} + +void test_written_markdown_resource_is_readable_by_client() { + TEST(written_markdown_resource_is_readable_by_client); + auto docs = buildDocs(); + fs::path root = fs::temp_directory_path() / "whetstone_step505_read"; + fs::remove_all(root); + + std::string mdPath; + CHECK(MCPToolDocumentation::writeResource(docs, root.string(), &mdPath, nullptr), + "resource write failed"); + std::ifstream in(mdPath); + CHECK(in.good(), "markdown resource not readable"); + std::string content((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + CHECK(content.find("whetstone_create_workflow") != std::string::npos, + "workflow tool missing from markdown resource"); + + fs::remove_all(root); + PASS(); +} + +void test_invalid_docs_are_rejected_by_completeness_validator() { + TEST(invalid_docs_are_rejected_by_completeness_validator); + MCPDocumentationResource docs; + docs.tools.push_back({"broken_tool", "", "", "", "", "", "", {}}); + std::string err; + CHECK(!MCPToolDocumentation::validateCompleteness(docs, &err), "invalid docs should fail"); + CHECK(!err.empty(), "expected error message for invalid docs"); + PASS(); +} + +void test_category_index_points_to_documented_tools() { + TEST(category_index_points_to_documented_tools); + auto docs = buildDocs(); + CHECK(!docs.categories["AST"].empty(), "AST category index empty"); + CHECK(!docs.categories["Workflow"].empty(), "Workflow category index empty"); + PASS(); +} + +void test_notes_include_generation_summary() { + TEST(notes_include_generation_summary); + auto docs = buildDocs(); + CHECK(!docs.notes.empty(), "missing documentation notes"); + CHECK(docs.notes[0].find("generated") != std::string::npos, "missing generation summary note"); + PASS(); +} + +int main() { + std::cout << "Step 505: MCP Tool Documentation Tests\n"; + + test_default_docs_include_multiple_tools(); // 1 + test_each_tool_has_required_documentation_sections(); // 2 + test_completeness_validator_passes_for_default_docs(); // 3 + test_required_categories_are_present(); // 4 + test_error_codes_are_documented_for_all_tools(); // 5 + test_markdown_export_contains_tool_names_and_sections(); // 6 + test_json_export_contains_tool_entries(); // 7 + test_resource_write_materializes_markdown_and_json_files(); // 8 + test_written_markdown_resource_is_readable_by_client(); // 9 + test_invalid_docs_are_rejected_by_completeness_validator(); // 10 + test_category_index_points_to_documented_tools(); // 11 + test_notes_include_generation_summary(); // 12 + + std::cout << "\nResults: " << passed << "/" << (passed + failed) + << " passed\n"; + return failed == 0 ? 0 : 1; +} diff --git a/progress.md b/progress.md index 9d8a815..1e3842f 100644 --- a/progress.md +++ b/progress.md @@ -7822,3 +7822,37 @@ representation generation, with sustained-run stability checks. - `editor/src/PerformanceOptimizationSuite.h` within header-size limit (`222` <= `600`) - `editor/tests/step504_test.cpp` within test-file size guidance (`136` lines) - Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md` + +### Step 505: MCP Tool Documentation +**Status:** PASS (12/12 tests) + +Implements structured MCP tool documentation generation with category indexing, +schema/example coverage, error-code metadata, and materialized markdown/json +resources for MCP client consumption. + +**Files added:** +- `editor/src/MCPToolDocumentation.h` - documentation resource builder: + - structured tool documentation model (description, input/output schema, examples, errors) + - category index generation (`AST`, `Diagnostics`, `Workflow`, `Routing`, `Security`) + - completeness validation checks + - markdown + JSON renderers + - resource materialization writer (`mcp_tool_docs.md`, `mcp_tool_docs.json`) +- `editor/tests/step505_test.cpp` - 12 tests covering: + - per-tool documentation completeness + - required category presence + - schema/error metadata coverage + - markdown/json export validity + - writable/readable documentation resource output + - invalid-doc rejection behavior +- `editor/CMakeLists.txt` - `step505_test` target + +**Verification run:** +- `cmake -S editor -B editor/build-native` - PASS +- `cmake --build editor/build-native --target step505_test step504_test` - PASS +- `./editor/build-native/step505_test` - PASS (12/12) +- `./editor/build-native/step504_test` - PASS (12/12) regression coverage + +**Architecture gate check:** +- `editor/src/MCPToolDocumentation.h` within header-size limit (`192` <= `600`) +- `editor/tests/step505_test.cpp` within test-file size guidance (`167` lines) +- Header-only architecture and naming conventions remain aligned with `ARCHITECTURE.md`