Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 "$<TARGET_FILE_DIR:fastmcpp_stdio_client>"
Expand Down
89 changes: 67 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,25 @@ int main() {
#include <fastmcpp/client/transports.hpp>

int main() {
auto transport = std::make_shared<fastmcpp::client::HttpTransport>(
"http://localhost:8080"
// Create client with HTTP transport
fastmcpp::client::Client client(
std::make_unique<fastmcpp::client::HttpTransport>("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;
}
```
Expand Down Expand Up @@ -207,31 +215,68 @@ int main() {
### Streamable HTTP client

```cpp
#include <fastmcpp/client/client.hpp>
#include <fastmcpp/client/transports.hpp>

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<fastmcpp::client::StreamableHttpTransport>(
"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 <fastmcpp/proxy.hpp>
#include <fastmcpp/server/sse_server.hpp>
#include <fastmcpp/util/schema_build.hpp>

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<int>() * 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:
Expand Down
57 changes: 57 additions & 0 deletions include/fastmcpp.hpp
Original file line number Diff line number Diff line change
@@ -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 <fastmcpp.hpp>
///
/// 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<fastmcpp::client::HttpTransport>("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"
50 changes: 50 additions & 0 deletions include/fastmcpp/proxy.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<client::HttpTransport>("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
4 changes: 3 additions & 1 deletion src/mcp/handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2583,7 +2583,9 @@ std::function<fastmcpp::Json(const fastmcpp::Json&)> 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}};
Expand Down
74 changes: 72 additions & 2 deletions src/proxy.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "fastmcpp/proxy.hpp"

#include "fastmcpp/client/transports.hpp"
#include "fastmcpp/exceptions.hpp"

#include <unordered_set>
Expand All @@ -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())}};
Expand Down Expand Up @@ -214,16 +215,25 @@ 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;
result.isError = false;

// 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<std::string>();
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&)
Expand Down Expand Up @@ -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<client::WebSocketTransport>(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<client::HttpTransport>(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<ITransport>, 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
Loading
Loading