Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
22 changes: 12 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,26 @@ verbose:

# Format all source files (C++ and TypeScript)
format-cpp:
@echo "Formatting C++ files with clang-format..."
@if command -v clang-format >/dev/null 2>&1; then \
find . -path "./build*" -prune -o \( -name "*.h" -o -name "*.cpp" -o -name "*.cc" \) -print | xargs clang-format -i; \
@echo "Formatting C++ files with clang-format-14..."
@export PATH="$$HOME/bin:$$PATH"; \
if command -v clang-format-14 >/dev/null 2>&1; then \
find . -path "./build*" -prune -o \( -name "*.h" -o -name "*.cpp" -o -name "*.cc" \) -print | xargs clang-format-14 -i; \
echo "C++ formatting complete."; \
else \
echo "Warning: clang-format not found, skipping C++ formatting."; \
echo "Install clang-format to format C++ files: brew install clang-format (macOS) or apt-get install clang-format (Ubuntu)"; \
echo "Warning: clang-format-14 not found, skipping C++ formatting."; \
echo "Install clang-format-14: brew install llvm@14 && ln -sf /usr/local/opt/llvm@14/bin/clang-format ~/bin/clang-format-14"; \
fi

format:
@echo "Formatting all source files..."
@echo "Formatting C++ files with clang-format..."
@if command -v clang-format >/dev/null 2>&1; then \
find . -path "./build*" -prune -o \( -name "*.h" -o -name "*.cpp" -o -name "*.cc" \) -print | xargs clang-format -i; \
@echo "Formatting C++ files with clang-format-14..."
@export PATH="$$HOME/bin:$$PATH"; \
if command -v clang-format-14 >/dev/null 2>&1; then \
find . -path "./build*" -prune -o \( -name "*.h" -o -name "*.cpp" -o -name "*.cc" \) -print | xargs clang-format-14 -i; \
echo "C++ formatting complete."; \
else \
echo "Warning: clang-format not found, skipping C++ formatting."; \
echo "Install clang-format to format C++ files: brew install clang-format (macOS) or apt-get install clang-format (Ubuntu)"; \
echo "Warning: clang-format-14 not found, skipping C++ formatting."; \
echo "Install clang-format-14: brew install llvm@14 && ln -sf /usr/local/opt/llvm@14/bin/clang-format ~/bin/clang-format-14"; \
fi
@echo "Formatting TypeScript files with prettier..."
@if [ -d "sdk/typescript" ]; then \
Expand Down
10 changes: 9 additions & 1 deletion examples/mcp/mcp_example_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ struct ClientOptions {
std::string host = "localhost";
int port = 3000;
std::string transport = "http";
std::string url; // Full URL if provided (takes precedence)
bool demo = false;
bool metrics = false;
bool verbose = false;
Expand Down Expand Up @@ -162,6 +163,8 @@ void signal_handler(int signal) {
void printUsage(const char* program) {
std::cerr << "USAGE: " << program << " [options]\n\n";
std::cerr << "OPTIONS:\n";
std::cerr << " --url <url> Full server URL (e.g., "
"https://example.com/sse)\n";
std::cerr << " --host <hostname> Server hostname (default: localhost)\n";
std::cerr << " --port <port> Server port (default: 3000)\n";
std::cerr << " --transport <type> Transport type: http, stdio, websocket "
Expand All @@ -185,6 +188,8 @@ ClientOptions parseArguments(int argc, char* argv[]) {
if (arg == "--help" || arg == "-h") {
printUsage(argv[0]);
exit(0);
} else if (arg == "--url" && i + 1 < argc) {
options.url = argv[++i];
} else if (arg == "--host" && i + 1 < argc) {
options.host = argv[++i];
} else if (arg == "--port" && i + 1 < argc) {
Expand Down Expand Up @@ -871,7 +876,10 @@ int main(int argc, char* argv[]) {

// Build server URI based on transport type
std::string server_uri;
if (options.transport == "stdio") {
if (!options.url.empty()) {
// Use full URL if provided
server_uri = options.url;
} else if (options.transport == "stdio") {
server_uri = "stdio://";
} else if (options.transport == "websocket" || options.transport == "ws") {
std::ostringstream uri;
Expand Down
35 changes: 35 additions & 0 deletions include/mcp/core/compat.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,41 @@
#include <cstddef>
#include <type_traits>

// ============================================================================
// Windows/POSIX compatibility types
// Must be included at global scope BEFORE any namespace declarations
// ============================================================================
#ifdef _WIN32
// MinGW provides POSIX types in sys/types.h - include it at global scope
// to ensure types are defined globally, not inside a namespace
#include <sys/types.h>

// MSVC doesn't define these POSIX types - define them ourselves
#ifdef _MSC_VER
#ifndef _PID_T_DEFINED
#define _PID_T_DEFINED
typedef int pid_t;
#endif
#ifndef _MODE_T_DEFINED
#define _MODE_T_DEFINED
typedef unsigned short mode_t;
#endif
#ifndef _USECONDS_T_DEFINED
#define _USECONDS_T_DEFINED
typedef unsigned int useconds_t;
#endif
#ifndef _SSIZE_T_DEFINED
#define _SSIZE_T_DEFINED
#ifdef _WIN64
typedef long long ssize_t;
#else
typedef long ssize_t;
#endif
#endif
#endif // _MSC_VER
#endif // _WIN32
// ============================================================================

// Check C++ version and feature availability
// Can be overridden by CMake definition
#ifndef MCP_USE_STD_OPTIONAL_VARIANT
Expand Down
11 changes: 10 additions & 1 deletion include/mcp/event/libevent_dispatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ class LibeventDispatcher : public Dispatcher {
// Libevent timer implementation
class TimerImpl : public Timer {
public:
TimerImpl(LibeventDispatcher& dispatcher, TimerCb cb);
TimerImpl(LibeventDispatcher& dispatcher,
TimerCb cb,
std::shared_ptr<std::atomic<bool>> dispatcher_valid);
~TimerImpl() override;

void disableTimer() override;
Expand All @@ -144,6 +146,9 @@ class LibeventDispatcher : public Dispatcher {
TimerCb cb_;
libevent_event* event_;
bool enabled_;
// Shared flag to safely check if dispatcher is still valid without
// accessing the dispatcher reference (which may be dangling)
std::shared_ptr<std::atomic<bool>> dispatcher_valid_;
};

// Schedulable callback implementation
Expand Down Expand Up @@ -221,6 +226,10 @@ class LibeventDispatcher : public Dispatcher {
// Watchdog
std::unique_ptr<WatchdogRegistration> watchdog_registration_;

// Shared validity flag - allows timers to safely check if dispatcher is valid
// without accessing dispatcher members (which may be destroyed)
std::shared_ptr<std::atomic<bool>> dispatcher_valid_;

// Stats
DispatcherStats* stats_ = nullptr;

Expand Down
53 changes: 53 additions & 0 deletions include/mcp/filter/http_codec_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,51 @@ class HttpCodecFilter : public network::Filter {
return read_callbacks_;
}

/**
* Set client endpoint for HTTP requests (client mode only)
* @param path Request path (e.g., "/sse")
* @param host Host header value (e.g., "localhost:8080")
*/
void setClientEndpoint(const std::string& path, const std::string& host) {
client_path_ = path;
client_host_ = host;
}

/**
* Set the message endpoint for POST requests (client mode only)
* Called after receiving endpoint event from SSE stream
* @param endpoint The URL path for sending JSON-RPC messages
*/
void setMessageEndpoint(const std::string& endpoint) {
message_endpoint_ = endpoint;
has_message_endpoint_ = true;
}

/**
* Check if we have a message endpoint for POST requests
*/
bool hasMessageEndpoint() const { return has_message_endpoint_; }

/**
* Get the message endpoint
*/
const std::string& getMessageEndpoint() const { return message_endpoint_; }

/**
* Set whether to use GET for initial SSE connection (client mode only)
*/
void setUseSseGet(bool use_sse_get) { use_sse_get_ = use_sse_get; }

/**
* Check if initial SSE GET request has been sent
*/
bool hasSentSseGetRequest() const { return sse_get_sent_; }

/**
* Mark SSE GET request as sent
*/
void markSseGetSent() { sse_get_sent_ = true; }

private:
// Inner class implementing MessageEncoder
class MessageEncoderImpl : public MessageEncoder {
Expand Down Expand Up @@ -239,6 +284,14 @@ class HttpCodecFilter : public network::Filter {
MessageCallbacks* message_callbacks_;
event::Dispatcher& dispatcher_;
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::string message_endpoint_; // Endpoint for POST requests (from SSE
// endpoint event)
bool has_message_endpoint_{
false}; // Whether we have received the message endpoint
bool use_sse_get_{false}; // Whether to use GET for initial SSE connection
bool sse_get_sent_{false}; // Whether the initial SSE GET has been sent
network::ReadFilterCallbacks* read_callbacks_{nullptr};
network::WriteFilterCallbacks* write_callbacks_{nullptr};

Expand Down
18 changes: 16 additions & 2 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 <memory>
#include <string>

#include "mcp/event/event_loop.h"
#include "mcp/filter/http_codec_filter.h"
Expand Down Expand Up @@ -53,13 +54,23 @@ class HttpSseFilterChainFactory : public network::FilterChainFactory {
* @param dispatcher Event dispatcher for async operations
* @param message_callbacks MCP message callbacks for handling requests
* @param is_server True for server mode, false for client mode
* @param http_path HTTP request path for client mode (e.g., "/sse")
* @param http_host HTTP Host header value for client mode
* @param use_sse True for SSE mode (GET /sse first), false for Streamable
* HTTP (direct POST)
*/
HttpSseFilterChainFactory(event::Dispatcher& dispatcher,
McpProtocolCallbacks& message_callbacks,
bool is_server = true)
bool is_server = true,
const std::string& http_path = "/rpc",
const std::string& http_host = "localhost",
bool use_sse = true)
: dispatcher_(dispatcher),
message_callbacks_(message_callbacks),
is_server_(is_server) {}
is_server_(is_server),
http_path_(http_path),
http_host_(http_host),
use_sse_(use_sse) {}

/**
* Create filter chain for the connection
Expand Down Expand Up @@ -106,6 +117,9 @@ class HttpSseFilterChainFactory : public network::FilterChainFactory {
event::Dispatcher& dispatcher_;
McpProtocolCallbacks& message_callbacks_;
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
mutable bool enable_metrics_ = true; // Enable metrics by default

// Store filters for lifetime management
Expand Down
2 changes: 1 addition & 1 deletion include/mcp/filter/request_logger_filter.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class RequestLoggerFilter : public network::NetworkFilterBase,
Config config_;
JsonRpcProtocolFilter::MessageHandler* next_callbacks_{nullptr};
mutable std::mutex write_mutex_;
std::optional<std::ofstream> file_stream_;
mcp::optional<std::ofstream> file_stream_;
};

} // namespace filter
Expand Down
18 changes: 17 additions & 1 deletion include/mcp/json/json_serialization.h
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,23 @@ struct JsonSerializeTraits<Metadata> {
for (const auto& kv : metadata) {
match(
kv.second, [&](std::nullptr_t) { builder.addNull(kv.first); },
[&](const std::string& s) { builder.add(kv.first, s); },
[&](const std::string& s) {
// Check if string looks like JSON object or array
// This allows storing nested structures as JSON strings in Metadata
// which get serialized back to proper nested JSON
if (!s.empty() && ((s.front() == '{' && s.back() == '}') ||
(s.front() == '[' && s.back() == ']'))) {
try {
auto parsed = JsonValue::parse(s);
builder.add(kv.first, parsed);
} catch (...) {
// Not valid JSON, add as string
builder.add(kv.first, s);
}
} else {
builder.add(kv.first, s);
}
},
[&](int64_t i) { builder.add(kv.first, static_cast<int>(i)); },
[&](double d) { builder.add(kv.first, d); },
[&](bool b) { builder.add(kv.first, b); });
Expand Down
7 changes: 7 additions & 0 deletions include/mcp/logging/log_message.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
#include <map>
#include <string>
#include <thread>

// Platform-specific process ID header
#ifdef _WIN32
#include <process.h>
#define getpid _getpid
#else
#include <unistd.h>
#endif

#include "mcp/logging/log_level.h"

Expand Down
42 changes: 39 additions & 3 deletions include/mcp/mcp_connection_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ namespace mcp {
* MCP transport type
*/
enum class TransportType {
Stdio, // Standard I/O transport
HttpSse, // HTTP with Server-Sent Events
WebSocket // WebSocket transport (future)
Stdio, // Standard I/O transport
HttpSse, // HTTP with Server-Sent Events
StreamableHttp, // Streamable HTTP (simple POST request/response)
WebSocket // WebSocket transport (future)
};

/**
Expand All @@ -44,6 +45,11 @@ struct McpConnectionConfig {

// Protocol detection
bool use_protocol_detection{false}; // Enable automatic protocol detection

// HTTP endpoint configuration (for HTTP/SSE transport)
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)
};

/**
Expand Down Expand Up @@ -78,6 +84,24 @@ class McpProtocolCallbacks {
* Called on connection error
*/
virtual void onError(const Error& error) = 0;

/**
* Called when SSE endpoint is received (HTTP/SSE transport only)
* The endpoint is the URL to POST JSON-RPC messages to
*/
virtual void onMessageEndpoint(const std::string& endpoint) {
(void)endpoint; // Default implementation does nothing
}

/**
* Send a POST request to the message endpoint
* Used by HTTP/SSE transport to send messages on a separate connection
* Returns true if the POST was initiated successfully
*/
virtual bool sendHttpPost(const std::string& json_body) {
(void)json_body; // Default implementation does nothing
return false;
}
};

/**
Expand Down Expand Up @@ -142,6 +166,8 @@ class McpConnectionManager : public McpProtocolCallbacks,
void onResponse(const jsonrpc::Response& response) override;
void onConnectionEvent(network::ConnectionEvent event) override;
void onError(const Error& error) override;
void onMessageEndpoint(const std::string& endpoint) override;
bool sendHttpPost(const std::string& json_body) override;

// ListenerCallbacks interface
void onAccept(network::ConnectionSocketPtr&& socket) override;
Expand Down Expand Up @@ -183,6 +209,16 @@ class McpConnectionManager : public McpProtocolCallbacks,
// State
bool is_server_{false};
bool connected_{false};
bool processing_connected_event_{false}; // Guard against re-entrancy

// HTTP/SSE POST connection support
std::string
message_endpoint_; // URL for POST requests (from SSE endpoint event)
bool has_message_endpoint_{false};

// Active POST connection (for sending messages in HTTP/SSE mode)
std::unique_ptr<network::ClientConnection> post_connection_;
std::unique_ptr<network::ConnectionCallbacks> post_callbacks_;
};

/**
Expand Down
1 change: 1 addition & 0 deletions include/mcp/network/address.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#define _SOCKLEN_T_DEFINED
typedef int socklen_t;
#endif
// mode_t and other POSIX types are defined in mcp/core/compat.h
#else
#include <netinet/in.h>
#include <sys/socket.h>
Expand Down
Loading