Step 505: add MCP tool documentation resource and tests
This commit is contained in:
@@ -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)
|
||||
|
||||
192
editor/src/MCPToolDocumentation.h
Normal file
192
editor/src/MCPToolDocumentation.h
Normal file
@@ -0,0 +1,192 @@
|
||||
#pragma once
|
||||
|
||||
// Step 505: MCP Tool Documentation
|
||||
// Structured MCP tool docs with category index and exportable resource formats.
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<MCPToolErrorCode> 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<MCPToolDoc> tools;
|
||||
std::map<std::string, std::vector<std::string>> categories;
|
||||
std::vector<std::string> 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;
|
||||
}
|
||||
};
|
||||
167
editor/tests/step505_test.cpp
Normal file
167
editor/tests/step505_test.cpp
Normal file
@@ -0,0 +1,167 @@
|
||||
// Step 505: MCP Tool Documentation Tests (12 tests)
|
||||
|
||||
#include "MCPToolDocumentation.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
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<char>(in)), std::istreambuf_iterator<char>());
|
||||
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;
|
||||
}
|
||||
34
progress.md
34
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`
|
||||
|
||||
Reference in New Issue
Block a user