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
17 changes: 17 additions & 0 deletions include/mcp/client/mcp_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ struct McpClientConfig : public application::ApplicationBase::Config {
// Transport configuration
TransportType preferred_transport = TransportType::Stdio;
bool auto_negotiate_transport = true;
std::map<std::string, std::string> http_headers;

// Connection pool settings
size_t connection_pool_size = 10;
Expand Down Expand Up @@ -145,6 +146,7 @@ struct RequestContext {
RequestId id;
std::string method;
optional<Metadata> params;
std::map<std::string, std::string> http_headers;
std::chrono::steady_clock::time_point start_time;
std::promise<Response> promise;
size_t retry_count{0};
Expand Down Expand Up @@ -444,6 +446,10 @@ class McpClient : public application::ApplicationBase {
// Request methods with future-based async API
std::future<Response> sendRequest(const std::string& method,
const optional<Metadata>& params = nullopt);
std::future<Response> sendRequest(
const std::string& method,
const optional<Metadata>& params,
const std::map<std::string, std::string>& http_headers);

// Batch processing - sends multiple requests efficiently
std::vector<std::future<Response>> sendBatch(
Expand All @@ -463,8 +469,15 @@ class McpClient : public application::ApplicationBase {
// Tool operations
std::future<ListToolsResult> listTools(
const optional<Cursor>& cursor = nullopt);
std::future<ListToolsResult> listTools(
const optional<Cursor>& cursor,
const std::map<std::string, std::string>& http_headers);
std::future<CallToolResult> callTool(
const std::string& name, const optional<Metadata>& arguments = nullopt);
std::future<CallToolResult> callTool(
const std::string& name,
const optional<Metadata>& arguments,
const std::map<std::string, std::string>& http_headers);

// Prompt operations
std::future<ListPromptsResult> listPrompts(
Expand Down Expand Up @@ -544,6 +557,10 @@ class McpClient : public application::ApplicationBase {
RequestId generateRequestId();
std::shared_ptr<RequestContext> createRequestContext(
const std::string& method, const optional<Metadata>& params);
std::shared_ptr<RequestContext> createRequestContext(
const std::string& method,
const optional<Metadata>& params,
const std::map<std::string, std::string>& http_headers);
void sendRequestInternal(std::shared_ptr<RequestContext> context);
void handleTimeout(std::shared_ptr<RequestContext> context);
void retryRequest(std::shared_ptr<RequestContext> context);
Expand Down
29 changes: 29 additions & 0 deletions include/mcp/filter/http_codec_filter.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <functional>
#include <map>
#include <memory>

#include "mcp/buffer.h"
Expand All @@ -15,6 +16,20 @@
namespace mcp {
namespace filter {

/**
* Validate client-supplied HTTP header names and values before serialization.
* Generated headers are added separately; this only checks that passthrough
* headers cannot inject extra lines or terminate the request.
*/
bool isValidClientHeader(const std::string& name, const std::string& value);

/**
* Returns true for headers generated by the HTTP client codec itself.
* Client-supplied values for these names are ignored to keep framing
* deterministic and avoid conflicting Content-Length/Transfer-Encoding pairs.
*/
bool isGeneratedClientHeader(const std::string& name);

/**
* HttpCodecFilter - HTTP/1.1 codec supporting both client and server modes
*
Expand Down Expand Up @@ -175,6 +190,18 @@ class HttpCodecFilter : public network::Filter {
client_host_ = host;
}

/**
* Set extra client request headers for generated HTTP requests.
*/
void setClientHeaders(const std::map<std::string, std::string>& headers) {
client_headers_ = headers;
}

void setClientHeaderSource(
const std::shared_ptr<std::map<std::string, std::string>>& headers) {
client_header_source_ = headers;
}

/**
* Set the message endpoint for POST requests (client mode only)
* Called after receiving endpoint event from SSE stream
Expand Down Expand Up @@ -298,6 +325,8 @@ class HttpCodecFilter : public network::Filter {
bool is_server_;
std::string client_path_{"/rpc"}; // HTTP request path for client mode
std::string client_host_{"localhost"}; // HTTP Host header for client mode
std::map<std::string, std::string> client_headers_;
std::shared_ptr<std::map<std::string, std::string>> client_header_source_;
std::string message_endpoint_; // Endpoint for POST requests (from SSE
// endpoint event)
bool has_message_endpoint_{
Expand Down
33 changes: 20 additions & 13 deletions include/mcp/filter/http_sse_filter_chain_factory.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <functional>
#include <map>
#include <memory>
#include <string>

Expand Down Expand Up @@ -76,15 +77,19 @@ class HttpSseFilterChainFactory : public network::FilterChainFactory {
* callback URL advertised on GET /sse. Leave empty to
* derive the URL from the incoming Host header.
*/
HttpSseFilterChainFactory(event::Dispatcher& dispatcher,
McpProtocolCallbacks& message_callbacks,
bool is_server = true,
const std::string& http_path = "/rpc",
const std::string& http_host = "localhost",
bool use_sse = true,
const std::string& sse_path = "/sse",
const std::string& rpc_path = "/mcp",
const std::string& external_url = "");
HttpSseFilterChainFactory(
event::Dispatcher& dispatcher,
McpProtocolCallbacks& message_callbacks,
bool is_server = true,
const std::string& http_path = "/rpc",
const std::string& http_host = "localhost",
bool use_sse = true,
const std::string& sse_path = "/sse",
const std::string& rpc_path = "/mcp",
const std::string& external_url = "",
const std::map<std::string, std::string>& client_headers = {},
const std::shared_ptr<std::map<std::string, std::string>>&
client_header_source = nullptr);

// Destructor defined out-of-line so the unique_ptr<SseSessionRegistry>
// member can use the incomplete forward-declared type in this header.
Expand Down Expand Up @@ -184,9 +189,11 @@ class HttpSseFilterChainFactory : public network::FilterChainFactory {
bool is_server_;
std::string http_path_; // HTTP request path for client mode
std::string http_host_; // HTTP Host header for client mode
bool use_sse_; // True for SSE mode, false for Streamable HTTP
std::string sse_path_; // Server-side SSE endpoint path (e.g., "/sse")
std::string rpc_path_; // Server-side JSON-RPC endpoint path (e.g., "/mcp")
std::map<std::string, std::string> client_headers_;
std::shared_ptr<std::map<std::string, std::string>> client_header_source_;
bool use_sse_; // True for SSE mode, false for Streamable HTTP
std::string sse_path_; // Server-side SSE endpoint path (e.g., "/sse")
std::string rpc_path_; // Server-side JSON-RPC endpoint path (e.g., "/mcp")
std::string external_url_; // External URL for absolute SSE callback URLs
mutable bool enable_metrics_ = true; // Enable metrics by default

Expand Down Expand Up @@ -218,4 +225,4 @@ class HttpSseFilterChainFactory : public network::FilterChainFactory {
};

} // namespace filter
} // namespace mcp
} // namespace mcp
11 changes: 11 additions & 0 deletions include/mcp/mcp_connection_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#define MCP_MCP_CONNECTION_MANAGER_H

#include <functional>
#include <map>
#include <memory>

#include "mcp/core/result.h"
Expand Down Expand Up @@ -51,6 +52,8 @@ struct McpConnectionConfig {
std::string http_path{"/rpc"}; // Request path (e.g., /sse, /mcp)
std::string
http_host; // Host header value (auto-set from server_address if empty)
std::map<std::string, std::string> http_headers;
std::shared_ptr<std::map<std::string, std::string>> current_http_headers;
};

/**
Expand Down Expand Up @@ -161,6 +164,9 @@ class McpConnectionManager : public McpProtocolCallbacks,
* Send a request
*/
VoidResult sendRequest(const jsonrpc::Request& request);
VoidResult sendRequest(
const jsonrpc::Request& request,
const std::map<std::string, std::string>& http_headers);

/**
* Send a notification
Expand Down Expand Up @@ -232,6 +238,8 @@ class McpConnectionManager : public McpProtocolCallbacks,
void onError(const Error& error) override;
void onMessageEndpoint(const std::string& endpoint) override;
bool sendHttpPost(const std::string& json_body) override;
bool sendHttpPost(const std::string& json_body,
const std::map<std::string, std::string>& http_headers);

// ListenerCallbacks interface
void onAccept(network::ConnectionSocketPtr&& socket) override;
Expand All @@ -254,6 +262,9 @@ class McpConnectionManager : public McpProtocolCallbacks,

// Send JSON message
VoidResult sendJsonMessage(const json::JsonValue& message);
VoidResult sendJsonMessage(
const json::JsonValue& message,
const std::map<std::string, std::string>& http_headers);

event::Dispatcher& dispatcher_;
network::SocketInterface& socket_interface_;
Expand Down
50 changes: 41 additions & 9 deletions src/client/mcp_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,13 @@ InitializeResult McpClient::parseInitializeResponse(
// Send request with future-based async API
std::future<Response> McpClient::sendRequest(const std::string& method,
const optional<Metadata>& params) {
return sendRequest(method, params, {});
}

std::future<Response> McpClient::sendRequest(
const std::string& method,
const optional<Metadata>& params,
const std::map<std::string, std::string>& http_headers) {
// Check if circuit breaker allows request
if (!circuit_breaker_->allowRequest()) {
client_stats_.circuit_breaker_opens++;
Expand All @@ -666,6 +673,7 @@ std::future<Response> McpClient::sendRequest(const std::string& method,
// Create request context
auto context = std::make_shared<RequestContext>(id, method);
context->params = params;
context->http_headers = http_headers;
context->start_time = std::chrono::steady_clock::now();

// Track request
Expand Down Expand Up @@ -805,7 +813,8 @@ void McpClient::sendRequestInternal(std::shared_ptr<RequestContext> context) {
last_activity_time_ = std::chrono::steady_clock::now();

// Send through connection manager
auto send_result = connection_manager_->sendRequest(request);
auto send_result =
connection_manager_->sendRequest(request, context->http_headers);

GOPHER_LOG_DEBUG("sendRequest result: is_error={}",
is_error<std::nullptr_t>(send_result));
Expand Down Expand Up @@ -1023,6 +1032,10 @@ McpConnectionConfig McpClient::createConnectionConfig(TransportType transport) {
http_config.server_address = server_addr;
config.http_path = http_path;
config.http_host = server_addr;
config.http_headers = config_.http_headers;
config.current_http_headers =
std::make_shared<std::map<std::string, std::string>>(
config_.http_headers);

// Set SSL transport for HTTPS URLs
if (is_https) {
Expand Down Expand Up @@ -1074,6 +1087,10 @@ McpConnectionConfig McpClient::createConnectionConfig(TransportType transport) {
http_config.server_address = server_addr;
config.http_path = http_path;
config.http_host = server_addr;
config.http_headers = config_.http_headers;
config.current_http_headers =
std::make_shared<std::map<std::string, std::string>>(
config_.http_headers);

// Set SSL transport for HTTPS URLs
if (is_https) {
Expand Down Expand Up @@ -1354,6 +1371,12 @@ std::future<VoidResult> McpClient::unsubscribeResource(const std::string& uri) {
// List available tools
std::future<ListToolsResult> McpClient::listTools(
const optional<std::string>& cursor) {
return listTools(cursor, {});
}

std::future<ListToolsResult> McpClient::listTools(
const optional<std::string>& cursor,
const std::map<std::string, std::string>& http_headers) {
auto result_promise = std::make_shared<std::promise<ListToolsResult>>();

if (!main_dispatcher_) {
Expand All @@ -1380,10 +1403,11 @@ std::future<ListToolsResult> McpClient::listTools(
cursor.has_value() ? cursor.value() : "<none>");

// Step 1: Post to dispatcher to send the request (non-blocking)
main_dispatcher_->post([this, request_future_ptr, params_ptr]() {
*request_future_ptr =
sendRequest("tools/list", mcp::make_optional(*params_ptr));
});
main_dispatcher_->post(
[this, request_future_ptr, params_ptr, http_headers]() {
*request_future_ptr = sendRequest(
"tools/list", mcp::make_optional(*params_ptr), http_headers);
});

// Step 2: Use std::thread to wait for response on a worker thread (not
// dispatcher!)
Expand Down Expand Up @@ -1430,6 +1454,13 @@ std::future<ListToolsResult> McpClient::listTools(
// Call a tool
std::future<CallToolResult> McpClient::callTool(
const std::string& name, const optional<Metadata>& arguments) {
return callTool(name, arguments, {});
}

std::future<CallToolResult> McpClient::callTool(
const std::string& name,
const optional<Metadata>& arguments,
const std::map<std::string, std::string>& http_headers) {
auto result_promise = std::make_shared<std::promise<CallToolResult>>();

if (!main_dispatcher_) {
Expand Down Expand Up @@ -1464,10 +1495,11 @@ std::future<CallToolResult> McpClient::callTool(
: "<none>");

// Step 1: Post to dispatcher to send the request (non-blocking)
main_dispatcher_->post([this, request_future_ptr, params_ptr]() {
*request_future_ptr =
sendRequest("tools/call", mcp::make_optional(*params_ptr));
});
main_dispatcher_->post(
[this, request_future_ptr, params_ptr, http_headers]() {
*request_future_ptr = sendRequest(
"tools/call", mcp::make_optional(*params_ptr), http_headers);
});

// Step 2: Use std::thread to wait for response on a worker thread (not
// dispatcher!)
Expand Down
42 changes: 42 additions & 0 deletions src/filter/http_codec_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,42 @@
namespace mcp {
namespace filter {

bool isValidClientHeader(const std::string& name, const std::string& value) {
auto has_invalid_byte = [](const std::string& text) {
return text.find_first_of("\r\n\0", 0, 3) != std::string::npos;
};
return !name.empty() && !value.empty() && !has_invalid_byte(name) &&
!has_invalid_byte(value);
}

std::string toLowerHeaderName(std::string value) {
std::transform(
value.begin(), value.end(), value.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return value;
}

bool isGeneratedClientHeader(const std::string& name) {
const std::string lower = toLowerHeaderName(name);
return lower == "host" || lower == "content-length" ||
Comment thread
dIvYaNshhh marked this conversation as resolved.
lower == "transfer-encoding" || lower == "connection" ||
lower == "accept" || lower == "content-type" ||
lower == "user-agent" || lower == "cache-control";
}

namespace {
void appendClientHeaders(std::ostringstream& request,
const std::map<std::string, std::string>& headers) {
for (const auto& header : headers) {
if (!isValidClientHeader(header.first, header.second) ||
isGeneratedClientHeader(header.first)) {
continue;
}
request << header.first << ": " << header.second << "\r\n";
Comment thread
dIvYaNshhh marked this conversation as resolved.
}
}
} // namespace

// HttpFilterChainBridge implementation

HttpCodecFilter::HttpFilterChainBridge::HttpFilterChainBridge(
Expand Down Expand Up @@ -385,6 +421,9 @@ network::FilterStatus HttpCodecFilter::onWrite(Buffer& data, bool end_stream) {
request << "Cache-Control: no-cache\r\n";
request << "Connection: keep-alive\r\n";
request << "User-Agent: gopher-mcp/1.0\r\n";
appendClientHeaders(request, client_header_source_
? *client_header_source_
: client_headers_);
request << "\r\n";

sse_get_sent_ = true;
Expand Down Expand Up @@ -419,6 +458,9 @@ network::FilterStatus HttpCodecFilter::onWrite(Buffer& data, bool end_stream) {
request << "Accept: application/json, text/event-stream\r\n";
request << "Connection: keep-alive\r\n";
request << "User-Agent: gopher-mcp/1.0\r\n";
appendClientHeaders(request, client_header_source_
? *client_header_source_
: client_headers_);
request << "\r\n";
request << body_data;
}
Expand Down
Loading
Loading