Skip to content
Open
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: 4 additions & 1 deletion include/mcp/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,8 @@ struct Request {
// Generic result type for responses
// Note: ListResourcesResult and ListToolsResult are defined outside jsonrpc
// namespace but used here
// json::JsonValue added to support arbitrary nested JSON responses (e.g.,
// initialize)
using ResponseResult = variant<std::nullptr_t,
bool,
int,
Expand All @@ -510,7 +512,8 @@ using ResponseResult = variant<std::nullptr_t,
std::vector<Prompt>,
std::vector<Resource>,
ListResourcesResult,
ListToolsResult>;
ListToolsResult,
json::JsonValue>;

struct Response {
std::string jsonrpc = "2.0";
Expand Down
42 changes: 17 additions & 25 deletions src/config/file_config_source.cc
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,8 @@ class FileConfigSource : public ConfigSource {
int priority,
const Options& opts = Options{})
: name_(name), priority_(priority), options_(opts) {
GOPHER_LOG(Info, "FileConfigSource created: name={} priority={}",
name_, priority_);
GOPHER_LOG(Info, "FileConfigSource created: name={} priority={}", name_,
priority_);
}

std::string getName() const override { return name_; }
Expand All @@ -285,18 +285,15 @@ class FileConfigSource : public ConfigSource {
mcp::json::JsonValue loadConfiguration() override {
// Keep logs under config.file so tests that attach a sink to
// "config.file" see discovery start/end messages.
GOPHER_LOG(Info, "Starting configuration discovery for source: {}{}",
name_,
(options_.trace_id.empty()
? ""
: (" trace_id=" + options_.trace_id)));
GOPHER_LOG(
Info, "Starting configuration discovery for source: {}{}", name_,
(options_.trace_id.empty() ? "" : (" trace_id=" + options_.trace_id)));

// Determine the config file path using deterministic search order
std::string config_path = findConfigFile();

if (config_path.empty()) {
GOPHER_LOG(Warning, "No configuration file found for source: {}",
name_);
GOPHER_LOG(Warning, "No configuration file found for source: {}", name_);
return mcp::json::JsonValue::object();
}

Expand All @@ -321,12 +318,12 @@ class FileConfigSource : public ConfigSource {
base_config_path_ = config_path;

// Emit a brief summary and also dump top-level keys/types to aid debugging
GOPHER_LOG(
Info,
"Configuration discovery completed: files_parsed={} "
"includes_processed={} env_vars_expanded={} overlays_applied={}",
context.files_parsed_count, context.includes_processed_count,
context.env_vars_expanded_count, context.overlays_applied.size());
GOPHER_LOG(Info,
"Configuration discovery completed: files_parsed={} "
"includes_processed={} env_vars_expanded={} overlays_applied={}",
context.files_parsed_count, context.includes_processed_count,
context.env_vars_expanded_count,
context.overlays_applied.size());

if (config.isObject()) {
for (const auto& key : config.keys()) {
Expand Down Expand Up @@ -451,8 +448,7 @@ class FileConfigSource : public ConfigSource {
if (exists(path)) {
// Determine which source won
if (i == 0 && !explicit_config_path_.empty()) {
GOPHER_LOG(Info, "Configuration source won: CLI --config={}",
path);
GOPHER_LOG(Info, "Configuration source won: CLI --config={}", path);
} else if ((i == 0 || i == 1) && env_config) {
GOPHER_LOG(
Info,
Expand Down Expand Up @@ -483,8 +479,7 @@ class FileConfigSource : public ConfigSource {
}
size_t file_size = st.st_size;
if (file_size > options_.max_file_size) {
GOPHER_LOG(Error,
"File exceeds maximum size limit: {} size={} limit={}",
GOPHER_LOG(Error, "File exceeds maximum size limit: {} size={} limit={}",
filepath, file_size, options_.max_file_size);
throw std::runtime_error("File too large: " + filepath + " (" +
std::to_string(file_size) + " bytes)");
Expand All @@ -499,14 +494,12 @@ class FileConfigSource : public ConfigSource {
context.latest_mtime = file_mtime;
}

GOPHER_LOG(Debug,
"Loading configuration file: {} size={} last_modified={}",
GOPHER_LOG(Debug, "Loading configuration file: {} size={} last_modified={}",
filepath, file_size, last_modified);

std::ifstream file(filepath);
if (!file.is_open()) {
GOPHER_LOG(Error, "Failed to open configuration file: {}",
filepath);
GOPHER_LOG(Error, "Failed to open configuration file: {}", filepath);
throw std::runtime_error("Cannot open config file: " + filepath);
}

Expand Down Expand Up @@ -932,8 +925,7 @@ class FileConfigSource : public ConfigSource {
const mcp::json::JsonValue& base_config,
const path& overlay_dir,
ParseContext& context) {
GOPHER_LOG(Info, "Scanning config.d directory: {}",
overlay_dir.string());
GOPHER_LOG(Info, "Scanning config.d directory: {}", overlay_dir.string());

mcp::json::JsonValue result = base_config;
std::vector<path> overlay_files;
Expand Down
21 changes: 21 additions & 0 deletions src/filter/http_codec_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,15 @@ network::FilterStatus HttpCodecFilter::onWrite(Buffer& data, bool end_stream) {
std::string body_data(
static_cast<const char*>(data.linearize(body_length)), body_length);

// Check if data is already HTTP-formatted (from routing filter)
// If so, pass through without adding more HTTP framing
if (body_data.length() >= 5 && body_data.compare(0, 5, "HTTP/") == 0) {
GOPHER_LOG_DEBUG(
"HttpCodecFilter::onWrite - data already HTTP formatted, "
"passing through");
return network::FilterStatus::Continue;
}

// Clear the buffer to build formatted HTTP response
data.drain(body_length);

Expand Down Expand Up @@ -280,6 +289,12 @@ network::FilterStatus HttpCodecFilter::onWrite(Buffer& data, bool end_stream) {
response << "Cache-Control: no-cache\r\n";
response << "Connection: keep-alive\r\n";
response << "X-Accel-Buffering: no\r\n"; // Disable proxy buffering
// CORS headers for browser-based clients (e.g., MCP Inspector)
response << "Access-Control-Allow-Origin: *\r\n";
response << "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n";
response
<< "Access-Control-Allow-Headers: Content-Type, Authorization, "
"Accept, Mcp-Session-Id, Mcp-Protocol-Version\r\n";
response << "\r\n";
// SSE data is already formatted by SSE filter
response << body_data;
Expand All @@ -290,6 +305,12 @@ network::FilterStatus HttpCodecFilter::onWrite(Buffer& data, bool end_stream) {
GOPHER_LOG_TRACE("onWrite: Content-Length={} body_preview={}...",
body_length, body_data.substr(0, 50));
response << "Cache-Control: no-cache\r\n";
// CORS headers for browser-based clients (e.g., MCP Inspector)
response << "Access-Control-Allow-Origin: *\r\n";
response << "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n";
response
<< "Access-Control-Allow-Headers: Content-Type, Authorization, "
"Accept, Mcp-Session-Id, Mcp-Protocol-Version\r\n";
if (current_stream_) {
response << "Connection: "
<< (current_stream_->keep_alive ? "keep-alive" : "close")
Expand Down
61 changes: 60 additions & 1 deletion src/filter/http_sse_filter_chain_factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,27 @@ class HttpSseJsonRpcProtocolFilter

void onNotification(const jsonrpc::Notification& notification) override {
mcp_callbacks_.onNotification(notification);

// For HTTP transport, send HTTP 202 Accepted response
// JSON-RPC notifications don't have responses, but HTTP requires one
if (is_server_ && write_callbacks_) {
// Build minimal HTTP 202 response
std::string http_response =
"HTTP/1.1 202 Accepted\r\n"
"Content-Length: 0\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
"Access-Control-Allow-Headers: Content-Type, Authorization, Accept, "
"Mcp-Session-Id, Mcp-Protocol-Version\r\n"
"Connection: keep-alive\r\n"
"\r\n";

OwnedBuffer response_buffer;
response_buffer.add(http_response);
write_callbacks_->connection().write(response_buffer, false);
GOPHER_LOG_DEBUG(
"HttpSseJsonRpcProtocolFilter: Sent HTTP 202 for notification");
}
}

void onResponse(const jsonrpc::Response& response) override {
Expand Down Expand Up @@ -779,13 +800,36 @@ class HttpSseJsonRpcProtocolFilter
}

void setupRoutingHandlers() {
// Register CORS preflight handler for all paths
// Browser-based clients (like MCP Inspector) send OPTIONS before POST
auto corsHandler = [](const HttpRoutingFilter::RequestContext& req) {
HttpRoutingFilter::Response resp;
resp.status_code = 204; // No Content
resp.headers["Access-Control-Allow-Origin"] = "*";
resp.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
resp.headers["Access-Control-Allow-Headers"] =
"Content-Type, Authorization, Accept, Mcp-Session-Id, "
"Mcp-Protocol-Version";
resp.headers["Access-Control-Max-Age"] = "86400"; // Cache for 24 hours
resp.headers["Content-Length"] = "0";
return resp;
};

// Register OPTIONS for common MCP paths
routing_filter_->registerHandler("OPTIONS", "/mcp", corsHandler);
routing_filter_->registerHandler("OPTIONS", "/mcp/events", corsHandler);
routing_filter_->registerHandler("OPTIONS", "/rpc", corsHandler);
routing_filter_->registerHandler("OPTIONS", "/health", corsHandler);
routing_filter_->registerHandler("OPTIONS", "/info", corsHandler);

// Register health endpoint
routing_filter_->registerHandler(
"GET", "/health", [](const HttpRoutingFilter::RequestContext& req) {
HttpRoutingFilter::Response resp;
resp.status_code = 200;
resp.headers["content-type"] = "application/json";
resp.headers["cache-control"] = "no-cache";
resp.headers["Access-Control-Allow-Origin"] = "*";

resp.body = R"({"status":"healthy","timestamp":)" +
std::to_string(std::time(nullptr)) + "}";
Expand All @@ -803,6 +847,7 @@ class HttpSseJsonRpcProtocolFilter
HttpRoutingFilter::Response resp;
resp.status_code = 200;
resp.headers["content-type"] = "application/json";
resp.headers["Access-Control-Allow-Origin"] = "*";

resp.body = R"({
"server": "MCP Server",
Expand All @@ -820,9 +865,23 @@ class HttpSseJsonRpcProtocolFilter
return resp;
});

// Default handler passes through to MCP protocol handling
// Default handler - handle OPTIONS for CORS preflight on any path,
// pass through other methods to MCP protocol handling
routing_filter_->registerDefaultHandler(
[](const HttpRoutingFilter::RequestContext& req) {
// Handle OPTIONS for CORS preflight on any path
if (req.method == "OPTIONS") {
HttpRoutingFilter::Response resp;
resp.status_code = 204; // No Content
resp.headers["Access-Control-Allow-Origin"] = "*";
resp.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
resp.headers["Access-Control-Allow-Headers"] =
"Content-Type, Authorization, Accept, Mcp-Session-Id, "
"Mcp-Protocol-Version";
resp.headers["Access-Control-Max-Age"] = "86400";
resp.headers["Content-Length"] = "0";
return resp;
}
// Return status 0 to indicate pass-through for MCP endpoints
HttpRoutingFilter::Response resp;
resp.status_code = 0;
Expand Down
4 changes: 4 additions & 0 deletions src/json/json_serialization.cc
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ JsonValue serialize_ResponseResult(const jsonrpc::ResponseResult& result) {
[&json_result](const ListToolsResult& list_result) {
// ListToolsResult is a full result object with tools array
json_result = to_json(list_result);
},
[&json_result](const JsonValue& json_val) {
// Direct JsonValue passthrough for arbitrary nested JSON responses
json_result = json_val;
});

return json_result;
Expand Down
1 change: 1 addition & 0 deletions src/mcp_connection_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,7 @@ void McpConnectionManager::onNotification(
if (protocol_callbacks_) {
protocol_callbacks_->onNotification(notification);
}
// HTTP 202 response is sent by HttpSseJsonRpcProtocolFilter::onNotification
}

void McpConnectionManager::onResponse(const jsonrpc::Response& response) {
Expand Down
Loading