Skip to content
Merged
165 changes: 165 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,171 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

## [0.1.14] - 2026-07-22
Comment thread
dIvYaNshhh marked this conversation as resolved.

### Fixed

- Fix Streamable HTTP tool-call body parsing for pretty-printed JSON, newline-delimited JSON fallback, and malformed body error reporting.
- Propagate `Mcp-Session-Id` from Streamable HTTP requests as the transport session id.
- Bound reconnect readiness polling so it cannot consume the full request timeout.

## [0.1.13] - 2026-07-08

### Added

- Add MCP HTTP header passthrough support (#250).

### Fixed

- Fix SSL transport use-after-free in posted lambdas (#245).
- Null-guard OpenSSL 3.x error-string accessors to prevent crash (#246).
- Fix SSL transport data loss on full network BIO (#247).
- Route MCP responses to the originating connection (#248).
- Improve transport and MCP flow logging (#249).
- Skip TLS peer metadata when verification is disabled (#251).
- Avoid retaining streamed HTTP client bodies (#260).
- Guard oversized HTTP body callbacks (#255).
- Defer HTTP parser callback errors (#256).
- Post HTTP parser error callbacks (#252).
- Defer active connection destruction on close (#257).
- Bind llhttp symbols inside the shared library (#259).
- Honor preferred HTTP transport (#253).

## [0.1.12] - 2026-07-03

### Added

- Add MCP HTTP header passthrough support.
- Add MCP client and HTTP transport invoke logging controlled by `GOPHER_LOG_LEVEL`.

## [0.1.11] - 2026-07-02

### Changed

- Bind llhttp symbols inside the shared library.
- Defer active connection destruction on close.
- Post and defer HTTP parser error callbacks safely.
- Guard oversized HTTP body callbacks.
- Avoid retaining streamed HTTP client bodies.
- Skip TLS peer metadata when verification is disabled.

### Fixed

- Fix SSL transport data loss on full network BIO.
- Fix SSL transport use-after-free in posted lambdas.

## [0.1.10] - 2026-06-30

### Added

- Add MCP HTTP header passthrough support.
- Add MCP client and HTTP transport invoke logging controlled by `GOPHER_LOG_LEVEL`.

## [0.1.9] - 2026-06-30

### Added

- Add MCP client and HTTP transport invoke logging controlled by `GOPHER_LOG_LEVEL`.

### Changed

- Route MCP responses to the originating connection to fix concurrent request hangs.

### Fixed

- Fix SSL transport data loss on full network BIO.
- Fix SSL transport use-after-free in posted lambdas.

## [0.1.8] - 2026-06-24

### Added

- Add MCP client and HTTP transport invoke logging controlled by `GOPHER_LOG_LEVEL`.

### Changed

- Scope MCP flow logging to a dedicated `GOPHER_MCP_LOG_FLOW` switch.
- Demote filter registry initialization and registration logs to debug level.
- Surface SSL errors through warning logs.
- Null-guard OpenSSL 3.x error-string accessors.

### Fixed

- Fix SSL transport data loss on full network BIO.
- Fix SSL transport use-after-free in posted lambdas.

## [0.1.7] - 2026-06-22

### Changed

- Demote filter registry initialization and registration logs to debug level.
- Surface SSL errors through warning logs.
- Null-guard OpenSSL 3.x error-string accessors.

### Fixed

- Fix SSL transport use-after-free in posted lambdas.

## [0.1.6] - 2026-06-11

### Added

- Add request-scoped `_meta` storage to `SessionContext` for tool handlers.
- Add client-side notification handler registration to `McpClient`.
- Add server and client SSE state machines with integration coverage.
- Add runtime formatting support for dynamic format strings.

### Changed

- Populate `ReadResourceResult` contents in `McpClient::readResource`.
- Integrate SSE connection state machines into `HttpSseJsonRpcProtocolFilter`.
- Remove obsolete request-stream and SSE mode state from the HTTP/SSE filter.
- Improve MSVC build configuration.
- Update README architecture documentation.

### Fixed

- Fix `ConnectionPoolImpl` timeout crash from premature write events.

## [0.1.5] - 2026-04-21

### Added

- Add idle-read timeout handling to `ConnectionImpl` and `McpServer`.
- Add SSE server transport support with a per-factory session registry.
- Add configurable SSE/RPC paths and external URL parameters to the filter chain factory.
- Add `HttpAsyncClient` built on `HttpCodecFilter`.
- Add integration and lifecycle tests for HTTP, SSE, and connection cleanup behavior.

### Changed

- Match `/callback/{id}` requests under reverse-proxy path prefixes.
- Rename the default SSE path to `/sse`.
- Surface numeric `:status` pseudo-headers from the HTTP client codec.
- Disable body timeout for client-mode HTTP codec.
- Defer closed connection destruction through dispatcher cleanup.
- Drain active server connections during shutdown.

### Fixed

- Fix background-task timer lifetime in `McpServer`.
- Fix callback scheduling so deferred callbacks run after caller stack unwinds.

## [0.1.4] - 2026-04-08

### Added

- Add resource read handlers to example server registrations.
- Add `ResourceReadHandler` callback support to `ResourceManager`.
- Add tests for `resources/read` responses and resource manager handlers.

### Changed

- Run formatting on resource read implementation files.

### Fixed

- Fix `resources/read` responses to match the MCP schema.

## [0.1.1] - 2026-03-03

Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Please create a build directory and run cmake from there:
You may need to remove CMakeCache.txt and CMakeFiles/")
endif()

project(gopher-mcp VERSION 0.1.1 LANGUAGES C CXX)
project(gopher-mcp VERSION 0.1.14 LANGUAGES C CXX)
Comment thread
dIvYaNshhh marked this conversation as resolved.

# Set library version for shared libraries
set(GOPHER_MCP_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
Expand Down
2 changes: 2 additions & 0 deletions include/mcp/client/mcp_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,8 @@ class McpClient : public application::ApplicationBase {
void disconnect();
bool isConnected() const { return connected_; }
bool isConnectionOpen() const; // Check actual connection state
static std::chrono::milliseconds reconnectWaitBudgetForRequestTimeout(
std::chrono::milliseconds request_timeout);

// Shutdown the client (stops workers and event loop)
void shutdown() override;
Expand Down
3 changes: 2 additions & 1 deletion include/mcp/filter/http_routing_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class HttpRoutingFilter : public HttpCodecFilter::MessageCallbacks {

// State for POST requests that need body
bool pending_post_request_ = false;
bool suppress_current_request_ = false;
RequestContext pending_context_;
HandlerFunc pending_handler_;
std::string accumulated_body_;
Expand Down Expand Up @@ -164,4 +165,4 @@ class HttpRoutingFilterFactory {
} // namespace filter
} // namespace mcp

#endif // MCP_FILTER_HTTP_ROUTING_FILTER_H
#endif // MCP_FILTER_HTTP_ROUTING_FILTER_H
7 changes: 7 additions & 0 deletions include/mcp/filter/json_rpc_protocol_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,13 @@ class JsonRpcProtocolFilter : public network::Filter {
*/
bool parseMessage(const std::string& json_str);

/**
* Dispatch a parsed JSON-RPC message
* @param json_val Parsed JSON value
* @return True if message shape was valid
*/
bool dispatchMessage(const json::JsonValue& json_val);

/**
* Frame outgoing message with length prefix if needed
* @param data Buffer containing message to frame
Expand Down
27 changes: 20 additions & 7 deletions src/client/mcp_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,12 @@ bool McpClient::isConnectionOpen() const {
return connection_manager_->isConnected();
}

std::chrono::milliseconds McpClient::reconnectWaitBudgetForRequestTimeout(
std::chrono::milliseconds request_timeout) {
return std::min(std::max(request_timeout / 3, std::chrono::milliseconds(250)),
std::chrono::milliseconds(5000));
}

// Reconnect using stored URI
VoidResult McpClient::reconnect() {
if (current_uri_.empty()) {
Expand Down Expand Up @@ -744,10 +750,16 @@ void McpClient::sendRequestInternal(std::shared_ptr<RequestContext> context) {
"is_stale={}",
idle_seconds, kConnectionIdleTimeoutSec, is_stale);

// Check if connection is stale or not open - need to reconnect
// Maximum retries to wait for connection after reconnect (50 * 10ms = 500ms
// max)
static constexpr int kMaxReconnectRetries = 50;
// Check if connection is stale or not open - need to reconnect.
//
// Reconnect readiness is driven by dispatcher I/O and can take several
// seconds for remote HTTPS/SSE backends, but it must leave request-deadline
// headroom for the actual send and response.
static constexpr int kReconnectRetryDelayMs = 10;
const auto reconnect_wait_budget =
reconnectWaitBudgetForRequestTimeout(config_.request_timeout);
const auto max_reconnect_retries = static_cast<size_t>(std::max<int64_t>(
1, reconnect_wait_budget.count() / kReconnectRetryDelayMs));

// THREAD SAFETY: Use atomic connected_ flag instead of isConnectionOpen()
// isConnectionOpen() reads McpConnectionManager::active_connection_ without
Expand All @@ -756,7 +768,7 @@ void McpClient::sendRequestInternal(std::shared_ptr<RequestContext> context) {
if (is_stale || !connected_) {
// Track if this is a retry after reconnect
if (context->retry_count > 0 &&
context->retry_count <= kMaxReconnectRetries) {
context->retry_count <= max_reconnect_retries) {
// This is a retry - check if we're connected now
if (!connected_) {
// Still not connected, schedule another retry with timer delay
Expand All @@ -765,11 +777,12 @@ void McpClient::sendRequestInternal(std::shared_ptr<RequestContext> context) {
context->retry_count++;
context->retry_timer = main_dispatcher_->createTimer(
[this, context]() { sendRequestInternal(context); });
context->retry_timer->enableTimer(std::chrono::milliseconds(10));
context->retry_timer->enableTimer(
std::chrono::milliseconds(kReconnectRetryDelayMs));
return;
}
// Connected now, proceed with send below
} else if (context->retry_count > kMaxReconnectRetries) {
} else if (context->retry_count > max_reconnect_retries) {
// Too many retries
context->promise.set_value(Response::make_error(
context->id, Error(::mcp::jsonrpc::INTERNAL_ERROR,
Expand Down
32 changes: 30 additions & 2 deletions src/filter/http_routing_filter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,41 @@ void HttpRoutingFilter::onHeaders(
if (resp.status_code != 0) {
// Handler wants to handle this - send response immediately
// This is appropriate for endpoints that don't need the body
suppress_current_request_ = true;
sendResponse(resp);
return; // Don't forward to next layer
}
}

// No handler or handler returned 0 - pass through
// No registered handler matched, or the matched handler explicitly returned
// status 0. Give the default handler a chance to answer before forwarding to
// the protocol layer. Transport paths can still opt into pass-through by
// returning status 0 from the default handler.
{
RequestContext ctx;
ctx.method = method;
ctx.path = full_url;
ctx.headers = headers;
ctx.keep_alive = keep_alive;
Response resp = default_handler_(ctx);
if (resp.status_code != 0) {
suppress_current_request_ = true;
sendResponse(resp);
return; // Handled or rejected; do not forward to next layer.
}
}

// Default handler signalled pass-through - forward to next layer
if (next_callbacks_) {
next_callbacks_->onHeaders(headers, keep_alive);
}
}

void HttpRoutingFilter::onBody(const std::string& data, bool end_stream) {
if (suppress_current_request_) {
return;
}

// If we're accumulating body for a POST handler, buffer it
if (pending_post_request_) {
accumulated_body_ += data;
Expand All @@ -136,6 +159,11 @@ void HttpRoutingFilter::onBody(const std::string& data, bool end_stream) {
void HttpRoutingFilter::onMessageComplete() {
GOPHER_LOG_DEBUG("HttpRoutingFilter::onMessageComplete called");

if (suppress_current_request_) {
suppress_current_request_ = false;
return;
}

// If we have a pending POST request, now we have the complete body
if (pending_post_request_) {
pending_context_.body = accumulated_body_;
Expand Down Expand Up @@ -292,4 +320,4 @@ std::shared_ptr<HttpRoutingFilter> HttpRoutingFilterFactory::createWithHandlers(
}

} // namespace filter
} // namespace mcp
} // namespace mcp
Loading
Loading