#include #include #include #include #include #include #include #include "fold/mcp/server_policy.hpp" #include "fold/mcp/tool_registry.hpp" namespace { template std::string invalidMessage(Function function) { try { function(); } catch (const std::invalid_argument& error) { return error.what(); } return {}; } struct TempTree { std::filesystem::path root = std::filesystem::temp_directory_path() / ("fold-mcp-policy-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); TempTree() { std::filesystem::create_directories(root); } ~TempTree() { std::filesystem::remove_all(root); } TempTree(const TempTree&) = delete; TempTree& operator=(const TempTree&) = delete; }; } // namespace TEST_CASE("MCP schema validation rejects unknown, mistyped, enum, and bounded values", "[mcp][schema]") { const nlohmann::json schema = { {"type", "object"}, {"additionalProperties", false}, {"properties", {{"mode", {{"type", "string"}, {"enum", {"source", "binary"}}}}, {"count", {{"type", "integer"}, {"minimum", 1}, {"maximum", 4}}}, {"paths", {{"type", "array"}, {"items", {{"type", "string"}}}, {"maxItems", 2}}}}}, {"required", {"mode"}}, }; REQUIRE_NOTHROW( fold::mcp::validateToolArguments(nlohmann::json{{"mode", "source"}, {"count", 4}}, schema)); REQUIRE(invalidMessage([&] { fold::mcp::validateToolArguments( nlohmann::json{{"mode", "source"}, {"unknown", true}}, schema); }) == "field `unknown`: unknown property"); REQUIRE(invalidMessage([&] { fold::mcp::validateToolArguments(nlohmann::json{{"mode", 1}}, schema); }) == "field `mode`: expected string"); REQUIRE(invalidMessage([&] { fold::mcp::validateToolArguments(nlohmann::json{{"mode", "hybrid"}}, schema); }) == "field `mode`: expected one of `source`, `binary`"); REQUIRE(invalidMessage([&] { fold::mcp::validateToolArguments(nlohmann::json{{"mode", "source"}, {"count", 5}}, schema); }) == "field `count`: expected a value <= 4"); REQUIRE(invalidMessage([&] { fold::mcp::validateToolArguments( nlohmann::json{{"mode", "source"}, {"paths", {"a", "b", "c"}}}, schema); }) == "field `paths`: expected at most 2 items"); } TEST_CASE("MCP filesystem policy canonicalizes allowed paths and rejects escapes", "[mcp][policy]") { TempTree allowed; TempTree outside; const auto source = allowed.root / "input.cpp"; std::ofstream(source) << "int main() { return 0; }\n"; const auto secret = outside.root / "secret.cpp"; std::ofstream(secret) << "secret\n"; const auto profile = allowed.root / "profile.json"; std::ofstream(profile) << "{}\n"; fold::mcp::ServerPolicy policy; policy.allowed_roots = {allowed.root}; const auto normalized = policy.normalizeArguments( nlohmann::json{{"compile_db", allowed.root.string()}, {"configuration_profile", profile.string()}, {"paths", nlohmann::json::array({source.string()})}}); REQUIRE(std::filesystem::path(normalized["compile_db"].get()).is_absolute()); REQUIRE(std::filesystem::path(normalized["paths"][0].get()) == std::filesystem::canonical(source)); REQUIRE(std::filesystem::path(normalized["configuration_profile"].get()) == std::filesystem::canonical(profile)); REQUIRE(invalidMessage([&] { (void)policy.normalizeArguments(nlohmann::json{{"paths", {secret.string()}}}); }) == "field `paths[0]`: path escapes configured MCP roots"); REQUIRE(invalidMessage([&] { (void)policy.normalizeArguments( nlohmann::json{{"binary", std::string("bad\0path", 8)}}); }) == "field `binary`: path contains NUL byte"); const auto database = allowed.root / "compile_commands.json"; std::ofstream(database) << nlohmann::json::array({{{"directory", outside.root.string()}, {"command", "clang++ -c secret.cpp"}, {"file", secret.string()}}}) .dump(); REQUIRE(invalidMessage([&] { (void)policy.normalizeArguments(nlohmann::json{{"compile_db", database.string()}}); }) == "field `compile_db.entries[0].directory`: path escapes configured MCP roots"); std::ofstream(database) << nlohmann::json::array( {{{"directory", allowed.root.string()}, {"arguments", {"clang++", "-Xclang", "-load", "plugin.so", "-c", source.string()}}, {"file", source.string()}}}) .dump(); REQUIRE(invalidMessage([&] { (void)policy.normalizeArguments(nlohmann::json{{"compile_db", database.string()}}); }) == "field `compile_db`: unsafe compiler argument `-load`"); std::error_code error; const auto link = allowed.root / "escape.cpp"; std::filesystem::create_symlink(secret, link, error); if (!error) { REQUIRE(invalidMessage([&] { (void)policy.normalizeArguments(nlohmann::json{{"paths", {link.string()}}}); }) == "field `paths[0]`: path escapes configured MCP roots"); } }