diff --git a/CMakeLists.txt b/CMakeLists.txt index f8d6683..b5455c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -474,6 +474,11 @@ if(FASTMCPP_BUILD_TESTS) target_link_libraries(fastmcpp_proxy_basic PRIVATE fastmcpp_core) add_test(NAME fastmcpp_proxy_basic COMMAND fastmcpp_proxy_basic) + # Main header compile test + add_executable(fastmcpp_main_header tests/main_header/compile_test.cpp) + target_link_libraries(fastmcpp_main_header PRIVATE fastmcpp_core) + add_test(NAME fastmcpp_main_header COMMAND fastmcpp_main_header) + set_tests_properties(fastmcpp_stdio_client PROPERTIES LABELS "conformance" WORKING_DIRECTORY "$" diff --git a/README.md b/README.md index 6c46f00..41e7a38 100644 --- a/README.md +++ b/README.md @@ -162,17 +162,25 @@ int main() { #include int main() { - auto transport = std::make_shared( - "http://localhost:8080" + // Create client with HTTP transport + fastmcpp::client::Client client( + std::make_unique("http://localhost:8080") ); - fastmcpp::client::Client client(transport); - auto response = client.call("tool/invoke", { - {"name", "calculator"}, - {"input", {{"operation", "add"}, {"a", 5}, {"b", 3}}} - }); + // Initialize MCP session + auto init = client.initialize(); + std::cout << "Connected to: " << init.serverInfo.name << std::endl; + + // List available tools + auto tools = client.list_tools(); + for (const auto& tool : tools) { + std::cout << "Tool: " << tool.name << std::endl; + } + + // Call a tool + auto result = client.call_tool("calculator", {{"a", 5}, {"b", 3}}); + std::cout << "Result: " << result.text() << std::endl; - std::cout << response.dump() << std::endl; return 0; } ``` @@ -207,31 +215,68 @@ int main() { ### Streamable HTTP client ```cpp +#include #include int main() { - fastmcpp::client::StreamableHttpTransport transport( - "http://localhost:8080", "/mcp" + // Create client with Streamable HTTP transport (MCP spec 2025-03-26) + fastmcpp::client::Client client( + std::make_unique( + "http://localhost:8080", "/mcp" + ) ); - // Send initialize request - auto init_response = transport.request("mcp", { - {"jsonrpc", "2.0"}, - {"id", 1}, - {"method", "initialize"}, - {"params", { - {"protocolVersion", "2024-11-05"}, - {"capabilities", {}}, - {"clientInfo", {{"name", "client"}, {"version", "1.0"}}} - }} + // Initialize MCP session (session ID managed automatically) + auto init = client.initialize(); + std::cout << "Server: " << init.serverInfo.name << std::endl; + + // Use the same clean API as other transports + auto tools = client.list_tools(); + auto result = client.call_tool("echo", {{"message", "Hello!"}}); + + return 0; +} +``` + +### Proxy server + +Create a proxy that forwards requests to a backend MCP server while allowing local overrides: + +```cpp +#include +#include +#include + +int main() { + using fastmcpp::util::schema_build::to_object_schema_from_simple; + + // Create a proxy to a remote backend + auto proxy = fastmcpp::create_proxy("http://backend:8080/mcp"); + + // Add local tools that extend or override remote capabilities + proxy.local_tools().register_tool({ + "double", + to_object_schema_from_simple({{"n", "number"}}), // input: {n: number} + {{"type", "number"}}, // output schema + [](const fastmcpp::Json& args) { return args["n"].get() * 2; } }); - // Session ID is automatically managed via Mcp-Session-Id header - std::cout << "Session: " << transport.session_id() << std::endl; + // Create MCP handler and serve via SSE + auto handler = fastmcpp::mcp::make_mcp_handler(proxy); + fastmcpp::server::SseServerWrapper server(handler, "127.0.0.1", 8080); + server.start(); + + // Server runs until stopped... return 0; } ``` +The `create_proxy()` factory function automatically detects the transport type from the URL: +- `http://` or `https://` URLs use HTTP transport +- `ws://` or `wss://` URLs use WebSocket transport + +Local tools, resources, and prompts take precedence over remote ones with the same name. + ## Examples See the `examples/` directory for complete programs, including: diff --git a/include/fastmcpp.hpp b/include/fastmcpp.hpp new file mode 100644 index 0000000..eafde11 --- /dev/null +++ b/include/fastmcpp.hpp @@ -0,0 +1,57 @@ +#pragma once + +/// @file fastmcpp.hpp +/// @brief Main header for fastmcpp - includes commonly used components +/// +/// This header provides convenient access to the most commonly used fastmcpp +/// components. For more specialized functionality, include the specific headers. +/// +/// Usage: +/// @code +/// #include +/// +/// int main() { +/// // Create a proxy to a remote server +/// auto proxy = fastmcpp::create_proxy("http://localhost:8080/mcp"); +/// +/// // Or use the client directly +/// fastmcpp::client::Client client( +/// std::make_unique("http://localhost:8080") +/// ); +/// +/// // Create an MCP handler +/// auto handler = fastmcpp::mcp::make_mcp_handler(proxy); +/// } +/// @endcode + +// Core types and exceptions +#include "fastmcpp/content.hpp" +#include "fastmcpp/exceptions.hpp" +#include "fastmcpp/settings.hpp" +#include "fastmcpp/types.hpp" + +// Client +#include "fastmcpp/client/client.hpp" +#include "fastmcpp/client/transports.hpp" +#include "fastmcpp/client/types.hpp" + +// Server +#include "fastmcpp/server/context.hpp" +#include "fastmcpp/server/server.hpp" + +// Tools, Resources, Prompts +#include "fastmcpp/prompts/manager.hpp" +#include "fastmcpp/prompts/prompt.hpp" +#include "fastmcpp/resources/manager.hpp" +#include "fastmcpp/resources/resource.hpp" +#include "fastmcpp/tools/manager.hpp" +#include "fastmcpp/tools/tool.hpp" + +// MCP handler +#include "fastmcpp/mcp/handler.hpp" + +// Proxy (create_proxy factory function) +#include "fastmcpp/proxy.hpp" + +// High-level app +#include "fastmcpp/app.hpp" diff --git a/include/fastmcpp/proxy.hpp b/include/fastmcpp/proxy.hpp index 9dc45fc..e66e01f 100644 --- a/include/fastmcpp/proxy.hpp +++ b/include/fastmcpp/proxy.hpp @@ -149,4 +149,54 @@ class ProxyApp static client::PromptInfo prompt_to_info(const prompts::Prompt& prompt); }; +// =============================================================================== +// Factory Functions +// =============================================================================== + +/// Create a proxy server for the given target. +/// +/// This is the recommended way to create a proxy server. For lower-level control, +/// use ProxyApp directly. +/// +/// The target can be: +/// - A client::Client instance +/// - A URL string (HTTP/SSE/WebSocket) +/// +/// Note: To proxy to another FastMCP server instance, use FastMCP::mount() instead. +/// For transports, create a Client first then pass it to create_proxy(). +/// +/// Session strategy: Always creates fresh sessions per request for safety. +/// This differs from Python's behavior which can reuse connected client sessions. +/// +/// Args: +/// target: The backend to proxy to (Client or URL) +/// name: Optional proxy server name (defaults to "proxy") +/// version: Optional version string (defaults to "1.0.0") +/// +/// Returns: +/// A ProxyApp that proxies to target +/// +/// Example: +/// ```cpp +/// // Create a proxy to a remote HTTP server +/// auto proxy = create_proxy("http://localhost:8080/mcp"); +/// +/// // Create a proxy from an existing client +/// client::Client client(std::make_unique("http://remote/mcp")); +/// auto proxy = create_proxy(std::move(client)); +/// ``` + +// Non-template overloads for common use cases (preferred for usability) + +/// Create proxy from URL string (lvalue or literal) +ProxyApp create_proxy(const std::string& url, std::string name = "proxy", + std::string version = "1.0.0"); + +/// Create proxy from string literal +ProxyApp create_proxy(const char* url, std::string name = "proxy", std::string version = "1.0.0"); + +/// Create proxy from existing Client (takes ownership) +ProxyApp create_proxy(client::Client&& client, std::string name = "proxy", + std::string version = "1.0.0"); + } // namespace fastmcpp diff --git a/src/mcp/handler.cpp b/src/mcp/handler.cpp index 0b7ec0d..f04d69f 100644 --- a/src/mcp/handler.cpp +++ b/src/mcp/handler.cpp @@ -2583,7 +2583,9 @@ std::function make_mcp_handler(const Prox std::string role_str = (msg.role == client::Role::Assistant) ? "assistant" : "user"; - messages_array.push_back({{"role", role_str}, {"content", content_array}}); + fastmcpp::Json content_val = + (content_array.size() == 1) ? content_array[0] : content_array; + messages_array.push_back({{"role", role_str}, {"content", content_val}}); } fastmcpp::Json response_result = {{"messages", messages_array}}; diff --git a/src/proxy.cpp b/src/proxy.cpp index 61817e1..147b6ec 100644 --- a/src/proxy.cpp +++ b/src/proxy.cpp @@ -1,5 +1,6 @@ #include "fastmcpp/proxy.hpp" +#include "fastmcpp/client/transports.hpp" #include "fastmcpp/exceptions.hpp" #include @@ -23,7 +24,7 @@ client::ToolInfo ProxyApp::tool_to_info(const tools::Tool& tool) info.name = tool.name(); info.description = tool.description(); info.inputSchema = tool.input_schema(); - if (!tool.output_schema().is_null()) + if (!tool.output_schema().is_null() && tool.output_schema().value("type", "") == "object") info.outputSchema = tool.output_schema(); if (tool.task_support() != TaskSupport::Forbidden) info.execution = fastmcpp::Json{{"taskSupport", to_string(tool.task_support())}}; @@ -214,6 +215,7 @@ client::CallToolResult ProxyApp::invoke_tool(const std::string& name, const Json try { auto result_json = local_tools_.invoke(name, args, enforce_timeout); + const auto& tool = local_tools_.get(name); // Convert to CallToolResult client::CallToolResult result; @@ -221,9 +223,17 @@ client::CallToolResult ProxyApp::invoke_tool(const std::string& name, const Json // Wrap result as text content client::TextContent text; - text.text = result_json.dump(); + // If result is already a string, use it directly; otherwise dump as JSON + if (result_json.is_string()) + text.text = result_json.get(); + else + text.text = result_json.dump(); result.content.push_back(text); + // If tool has output schema, set structuredContent + if (!tool.output_schema().is_null() && tool.output_schema().value("type", "") == "object") + result.structuredContent = result_json; + return result; } catch (const NotFoundError&) @@ -341,4 +351,64 @@ client::GetPromptResult ProxyApp::get_prompt(const std::string& name, const Json return client.get_prompt_mcp(name, args); } +// =============================================================================== +// Factory Functions Implementation +// =============================================================================== + +namespace +{ +// Helper to create client factory from URL +ProxyApp::ClientFactory make_url_factory(std::string url) +{ + return [url = std::move(url)]() -> client::Client + { + // Detect transport type from URL + if (url.find("ws://") == 0 || url.find("wss://") == 0) + { + return client::Client(std::make_unique(url)); + } + else if (url.find("http://") == 0 || url.find("https://") == 0) + { + // Default to HTTP transport for regular HTTP URLs + // For SSE, user should create HttpSseTransport explicitly + return client::Client(std::make_unique(url)); + } + else + { + throw std::invalid_argument("Unsupported URL scheme: " + url); + } + }; +} +} // anonymous namespace + +// Non-template overload for const std::string& (lvalue strings) +ProxyApp create_proxy(const std::string& url, std::string name, std::string version) +{ + return ProxyApp(make_url_factory(url), std::move(name), std::move(version)); +} + +// Non-template overload for const char* (string literals) +ProxyApp create_proxy(const char* url, std::string name, std::string version) +{ + return ProxyApp(make_url_factory(std::string(url)), std::move(name), std::move(version)); +} + +// Non-template overload for Client&& (takes ownership) +ProxyApp create_proxy(client::Client&& base_client, std::string name, std::string version) +{ + auto factory = [base_client = std::move(base_client)]() mutable -> client::Client + { + // Create fresh session from existing client configuration + return base_client.new_(); + }; + + return ProxyApp(std::move(factory), std::move(name), std::move(version)); +} + +// Note: To proxy to a unique_ptr, create a Client first: +// create_proxy(client::Client(std::move(transport))); +// +// Note: To proxy to another FastMCP server instance, use FastMCP::mount() instead. +// This avoids circular dependencies between FastMCP and ProxyApp + } // namespace fastmcpp diff --git a/tests/main_header/compile_test.cpp b/tests/main_header/compile_test.cpp new file mode 100644 index 0000000..fad9170 --- /dev/null +++ b/tests/main_header/compile_test.cpp @@ -0,0 +1,79 @@ +/// @file tests/main_header/compile_test.cpp +/// @brief Compile test for main fastmcpp.hpp header +/// +/// This test verifies that including just gives access to +/// all commonly used functionality including create_proxy(). + +#include "fastmcpp.hpp" + +#include +#include + +using namespace fastmcpp; + +int main() +{ + std::cout << "=== Main Header Compile Test ===" << std::endl; + + // Test 1: create_proxy is accessible + std::cout << "test_create_proxy_accessible..." << std::endl; + { + // Just verify it compiles - we can't connect to a real server + // The URL detection should work without network + auto proxy = create_proxy(std::string("http://localhost:9999/mcp")); + assert(proxy.name() == "proxy"); + assert(proxy.version() == "1.0.0"); + } + std::cout << " PASSED" << std::endl; + + // Test 2: Client types are accessible + std::cout << "test_client_types_accessible..." << std::endl; + { + // Verify client namespace is available + using Transport = client::ITransport; + using ClientType = client::Client; + (void)sizeof(Transport); + (void)sizeof(ClientType); + } + std::cout << " PASSED" << std::endl; + + // Test 3: Server types are accessible + std::cout << "test_server_types_accessible..." << std::endl; + { + auto srv = std::make_shared(); + assert(srv != nullptr); + } + std::cout << " PASSED" << std::endl; + + // Test 4: Tool/Resource/Prompt managers accessible + std::cout << "test_managers_accessible..." << std::endl; + { + tools::ToolManager tm; + resources::ResourceManager rm; + prompts::PromptManager pm; + (void)tm; + (void)rm; + (void)pm; + } + std::cout << " PASSED" << std::endl; + + // Test 5: MCP handler is accessible + std::cout << "test_mcp_handler_accessible..." << std::endl; + { + tools::ToolManager tm; + auto handler = mcp::make_mcp_handler("test", "1.0", tm); + assert(handler != nullptr); + } + std::cout << " PASSED" << std::endl; + + // Test 6: App class is accessible + std::cout << "test_app_accessible..." << std::endl; + { + using AppType = FastMCP; + (void)sizeof(AppType); + } + std::cout << " PASSED" << std::endl; + + std::cout << "\n=== All tests PASSED ===" << std::endl; + return 0; +} diff --git a/tests/proxy/basic.cpp b/tests/proxy/basic.cpp index 496efcc..8eaf3b9 100644 --- a/tests/proxy/basic.cpp +++ b/tests/proxy/basic.cpp @@ -344,6 +344,126 @@ void test_proxy_backend_unavailable() std::cout << " PASSED" << std::endl; } +// ========================================================================= +// create_proxy() factory function tests +// ========================================================================= + +void test_create_proxy_from_client() +{ + std::cout << "test_create_proxy_from_client..." << std::endl; + + // Create a client with mock transport + auto handler = create_backend_handler(); + client::Client base_client(std::make_unique(handler)); + + // Use create_proxy() factory function + auto proxy = create_proxy(std::move(base_client), "ClientProxy", "2.0.0"); + + assert(proxy.name() == "ClientProxy"); + assert(proxy.version() == "2.0.0"); + + // Should be able to list remote tools + auto tools = proxy.list_all_tools(); + assert(tools.size() == 2); // backend_add, backend_echo + + // Should be able to invoke remote tools + auto result = proxy.invoke_tool("backend_add", Json{{"a", 10}, {"b", 20}}); + assert(!result.isError); + + std::cout << " PASSED" << std::endl; +} + +void test_create_proxy_url_detection() +{ + std::cout << "test_create_proxy_url_detection..." << std::endl; + + // Test that create_proxy() correctly detects URL schemes + // Note: These will fail to connect but should parse correctly + + // HTTP URL - should create HttpTransport + try + { + auto proxy = create_proxy(std::string("http://localhost:9999/mcp")); + assert(proxy.name() == "proxy"); // default name + assert(proxy.version() == "1.0.0"); // default version + // Getting client will fail (no server), but proxy creation succeeded + std::cout << " HTTP URL: OK" << std::endl; + } + catch (const std::exception& e) + { + // Unexpected - URL parsing should work + std::cerr << " HTTP URL failed unexpectedly: " << e.what() << std::endl; + assert(false); + } + + // WebSocket URL - should create WebSocketTransport + try + { + auto proxy = create_proxy(std::string("ws://localhost:9999/mcp"), "WsProxy"); + assert(proxy.name() == "WsProxy"); + std::cout << " WS URL: OK" << std::endl; + } + catch (const std::exception& e) + { + std::cerr << " WS URL failed unexpectedly: " << e.what() << std::endl; + assert(false); + } + + // Invalid URL scheme - should throw + try + { + auto proxy = create_proxy(std::string("ftp://localhost/path")); + // Try to use it - this should fail + proxy.list_all_tools(); + assert(false); // Should have thrown + } + catch (const std::invalid_argument& e) + { + // Expected - unsupported scheme + std::cout << " Invalid scheme: correctly rejected" << std::endl; + } + catch (const std::exception&) + { + // Also acceptable - failed during use + std::cout << " Invalid scheme: rejected on use" << std::endl; + } + + std::cout << " PASSED" << std::endl; +} + +void test_create_proxy_with_local_tools() +{ + std::cout << "test_create_proxy_with_local_tools..." << std::endl; + + // Create proxy from client + auto handler = create_backend_handler(); + client::Client base_client(std::make_unique(handler)); + auto proxy = create_proxy(std::move(base_client)); + + // Add local tools + tools::Tool local_tool{"local_calc", + Json{{"type", "object"}, + {"properties", Json{{"n", Json{{"type", "number"}}}}}, + {"required", Json::array({"n"})}}, + Json{{"type", "number"}}, + [](const Json& args) { return args.at("n").get() * 100; }}; + proxy.local_tools().register_tool(local_tool); + + // Should see both local and remote tools + auto tools = proxy.list_all_tools(); + assert(tools.size() == 3); // local_calc + backend_add + backend_echo + + // Local tool should work + auto local_result = proxy.invoke_tool("local_calc", Json{{"n", 5}}); + assert(!local_result.isError); + + // Remote tool should work + auto remote_result = proxy.invoke_tool("backend_echo", Json{{"message", "test"}}); + assert(!remote_result.isError); + + std::cout << " PASSED" << std::endl; +} + int main() { std::cout << "=== ProxyApp Tests ===" << std::endl; @@ -358,6 +478,12 @@ int main() test_proxy_mcp_handler(); test_proxy_backend_unavailable(); + std::cout << "\n=== create_proxy() Factory Tests ===" << std::endl; + + test_create_proxy_from_client(); + test_create_proxy_url_detection(); + test_create_proxy_with_local_tools(); + std::cout << "\n=== All tests PASSED ===" << std::endl; return 0; }