180 lines
7.2 KiB
C++
180 lines
7.2 KiB
C++
// ============================================================================
|
|
// MCP Apps Extension - Capability Negotiation Tests
|
|
// PBI-083: MCP Apps Support - Phase 1
|
|
//
|
|
// Test IDs: mcp-apps-cap-01 through mcp-apps-cap-05
|
|
// ============================================================================
|
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <yams/mcp/mcp_server.h>
|
|
|
|
using namespace yams::mcp;
|
|
using json = nlohmann::json;
|
|
|
|
namespace {
|
|
|
|
class NullTransport : public yams::mcp::ITransport {
|
|
public:
|
|
void send(const json&) override {}
|
|
yams::mcp::MessageResult receive() override {
|
|
return yams::Error{yams::ErrorCode::NotImplemented, "Null transport"};
|
|
}
|
|
bool isConnected() const override { return false; }
|
|
void close() override {}
|
|
yams::mcp::TransportState getState() const override {
|
|
return yams::mcp::TransportState::Disconnected;
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("MCP Apps Capability - Extension Discovery",
|
|
"[mcp][apps][capability][mcp-apps-cap-01][phase1]") {
|
|
// Test implementation - verifying MCP Apps capability negotiation
|
|
|
|
auto transport = std::make_unique<NullTransport>();
|
|
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
|
|
|
|
json initRequest = {{"jsonrpc", "2.0"},
|
|
{"id", 1},
|
|
{"method", "initialize"},
|
|
{"params",
|
|
{{"protocolVersion", "2025-11-25"},
|
|
{"clientInfo", {{"name", "test"}, {"version", "1.0"}}},
|
|
{"capabilities",
|
|
{{"extensions",
|
|
{{"io.modelcontextprotocol/ui",
|
|
{{"mimeTypes", json::array({"text/html;profile=mcp-app"})}}}}}}}}}};
|
|
|
|
auto response = server->handleRequestPublic(initRequest);
|
|
REQUIRE(response.has_value());
|
|
|
|
json result = response.value();
|
|
REQUIRE(result.contains("result"));
|
|
REQUIRE(result["result"].contains("capabilities"));
|
|
REQUIRE(result["result"]["capabilities"].contains("extensions"));
|
|
REQUIRE(result["result"]["capabilities"]["extensions"].contains("io.modelcontextprotocol/ui"));
|
|
|
|
auto uiExt = result["result"]["capabilities"]["extensions"]["io.modelcontextprotocol/ui"];
|
|
REQUIRE(uiExt.contains("mimeTypes"));
|
|
CHECK(uiExt["mimeTypes"].is_array());
|
|
}
|
|
|
|
TEST_CASE("MCP Apps Capability - Extension Not Advertised When Disabled",
|
|
"[mcp][apps][capability][mcp-apps-cap-02][phase1]") {
|
|
auto transport = std::make_unique<NullTransport>();
|
|
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
|
|
|
|
json initRequest = {{"jsonrpc", "2.0"},
|
|
{"id", 1},
|
|
{"method", "initialize"},
|
|
{"params",
|
|
{{"protocolVersion", "2025-11-25"},
|
|
{"clientInfo", {{"name", "test"}, {"version", "1.0"}}},
|
|
{"capabilities", json::object()}}}};
|
|
|
|
auto response = server->handleRequestPublic(initRequest);
|
|
REQUIRE(response.has_value());
|
|
|
|
json result = response.value();
|
|
REQUIRE(result.contains("result"));
|
|
|
|
// Should NOT include UI extension
|
|
if (result["result"]["capabilities"].contains("extensions")) {
|
|
CHECK_FALSE(
|
|
result["result"]["capabilities"]["extensions"].contains("io.modelcontextprotocol/ui"));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("MCP Apps Capability - Mime Type Validation Supported",
|
|
"[mcp][apps][capability][mcp-apps-cap-03][phase1]") {
|
|
auto transport = std::make_unique<NullTransport>();
|
|
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
|
|
|
|
json initRequest = {{"jsonrpc", "2.0"},
|
|
{"id", 1},
|
|
{"method", "initialize"},
|
|
{"params",
|
|
{{"protocolVersion", "2025-11-25"},
|
|
{"clientInfo", {{"name", "test"}, {"version", "1.0"}}},
|
|
{"capabilities",
|
|
{{"extensions",
|
|
{{"io.modelcontextprotocol/ui",
|
|
{{"mimeTypes", json::array({"text/html;profile=mcp-app"})}}}}}}}}}};
|
|
|
|
auto response = server->handleRequestPublic(initRequest);
|
|
REQUIRE(response.has_value());
|
|
|
|
json result = response.value();
|
|
CHECK(result["result"]["protocolVersion"] == "2025-11-25");
|
|
|
|
// UI resources should be available after successful negotiation
|
|
// This will be tested more thoroughly in resource tests
|
|
}
|
|
|
|
TEST_CASE("MCP Apps Capability - Mime Type Validation Unsupported",
|
|
"[mcp][apps][capability][mcp-apps-cap-04][phase1]") {
|
|
auto transport = std::make_unique<NullTransport>();
|
|
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
|
|
|
|
json initRequest = {{"jsonrpc", "2.0"},
|
|
{"id", 1},
|
|
{"method", "initialize"},
|
|
{"params",
|
|
{{"protocolVersion", "2025-11-25"},
|
|
{"clientInfo", {{"name", "test"}, {"version", "1.0"}}},
|
|
{"capabilities",
|
|
{{"extensions",
|
|
{{"io.modelcontextprotocol/ui",
|
|
{{"mimeTypes", json::array({"application/vnd.custom"})}}}}}}}}}};
|
|
|
|
auto response = server->handleRequestPublic(initRequest);
|
|
REQUIRE(response.has_value());
|
|
|
|
json result = response.value();
|
|
|
|
// Should NOT include UI extension (unsupported mime type)
|
|
if (result["result"]["capabilities"].contains("extensions")) {
|
|
CHECK_FALSE(
|
|
result["result"]["capabilities"]["extensions"].contains("io.modelcontextprotocol/ui"));
|
|
}
|
|
|
|
// Server should not crash or return error
|
|
CHECK(result.contains("result"));
|
|
}
|
|
|
|
TEST_CASE("MCP Apps Capability - Graceful Fallback",
|
|
"[mcp][apps][capability][mcp-apps-cap-05][phase1]") {
|
|
auto transport = std::make_unique<NullTransport>();
|
|
auto server = std::make_shared<yams::mcp::MCPServer>(std::move(transport));
|
|
|
|
// Initialize without UI support
|
|
json initRequest = {{"jsonrpc", "2.0"},
|
|
{"id", 1},
|
|
{"method", "initialize"},
|
|
{"params",
|
|
{{"protocolVersion", "2025-11-25"},
|
|
{"clientInfo", {{"name", "test"}, {"version", "1.0"}}},
|
|
{"capabilities", json::object()}}}};
|
|
|
|
auto initResponse = server->handleRequestPublic(initRequest);
|
|
REQUIRE(initResponse.has_value());
|
|
|
|
// Call a tool that would have UI (if supported)
|
|
json toolRequest = {{"jsonrpc", "2.0"},
|
|
{"id", 2},
|
|
{"method", "tools/call"},
|
|
{"params", {{"name", "status"}, {"arguments", json::object()}}}};
|
|
|
|
auto toolResponse = server->handleRequestPublic(toolRequest);
|
|
REQUIRE(toolResponse.has_value());
|
|
|
|
json toolResult = toolResponse.value();
|
|
|
|
// Should return standard text response (no UI)
|
|
if (toolResult.contains("result")) {
|
|
// Result should not contain UI-specific fields
|
|
CHECK_FALSE(toolResult["result"].contains("_meta"));
|
|
}
|
|
}
|