Skip to content

Commit 0cf145c

Browse files
committed
Added new Acceptor design pattern and fixed static analyzer warnings
1 parent 3ad4d2e commit 0cf145c

16 files changed

Lines changed: 579 additions & 74 deletions

Dockerfile.demo

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ WORKDIR /src
2222
COPY . .
2323

2424
# Configure and build (Release + tests)
25+
# Show Ninja progress and limit parallelism to reduce resource contention inside Docker
26+
ENV NINJA_STATUS="[%f/%t %o/sec %es] "
27+
ENV CMAKE_BUILD_PARALLEL_LEVEL=6
2528
RUN cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DMCP_BUILD_TESTS=ON \
26-
&& cmake --build build -j
29+
&& cmake --build build -j -- -v
2730

2831
# 1b) Test stage: run unit tests inside the build environment
2932
FROM build AS test
@@ -32,7 +35,9 @@ ENV MCP_STDIOTRANSPORT_TIMEOUT_MS=${MCP_STDIOTRANSPORT_TIMEOUT_MS}
3235
ENV GTEST_COLOR=yes
3336
ENV CTEST_OUTPUT_ON_FAILURE=1
3437
WORKDIR /src
35-
RUN ctest --test-dir build -VV --progress
38+
# Set a per-test timeout to avoid indefinite hangs and show verbose progress
39+
ENV CTEST_PARALLEL_LEVEL=4
40+
RUN ctest --test-dir build -VV --progress --timeout 180
3641

3742
# 2) Demo runtime stage
3843
FROM ubuntu:24.04 AS demo

include/mcp/HTTPServer.hpp

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@
1111
#include <future>
1212
#include <functional>
1313
#include <memory>
14-
14+
#include "mcp/Transport.h"
1515
#include "mcp/JSONRPCTypes.h"
1616

1717
namespace mcp {
1818

19-
class HTTPServer {
20-
public:
19+
// Note: HTTPServer implements the server-side acceptor role (ITransportAcceptor)
20+
class HTTPServer : public ITransportAcceptor {
21+
public:
2122
//==========================================================================================================
2223
// Options
2324
// Purpose: Configuration for bind address/port, JSON-RPC paths, and TLS files.
@@ -39,10 +40,6 @@ class HTTPServer {
3940
std::string keyFile; // PEM (required for https)
4041
};
4142

42-
using RequestHandler = std::function<std::unique_ptr<JSONRPCResponse>(const JSONRPCRequest&)>;
43-
using NotificationHandler = std::function<void(std::unique_ptr<JSONRPCNotification>)>;
44-
using ErrorHandler = std::function<void(const std::string&)>;
45-
4643
explicit HTTPServer(const Options& opts);
4744
~HTTPServer();
4845

@@ -51,39 +48,52 @@ class HTTPServer {
5148
// Returns:
5249
// Future that becomes ready once the I/O context is running.
5350
//==========================================================================================================
54-
std::future<void> Start();
51+
std::future<void> Start() override;
5552

5653
//==========================================================================================================
5754
// Stops the server: closes acceptor, stops I/O context, and joins background thread.
5855
// Returns:
5956
// Future that completes when shutdown has finished.
6057
//==========================================================================================================
61-
std::future<void> Stop();
58+
std::future<void> Stop() override;
6259

6360
//==========================================================================================================
6461
// Sets the request handler (JSON-RPC request -> response).
6562
// Args:
6663
// handler: Callback invoked per request; may return an error response.
6764
//==========================================================================================================
68-
void SetRequestHandler(RequestHandler handler);
65+
void SetRequestHandler(ITransport::RequestHandler handler) override;
6966

7067
//==========================================================================================================
7168
// Sets the notification handler (no response expected).
7269
// Args:
7370
// handler: Callback invoked per notification with ownership of the notification object.
7471
//==========================================================================================================
75-
void SetNotificationHandler(NotificationHandler handler);
72+
void SetNotificationHandler(ITransport::NotificationHandler handler) override;
7673

7774
//==========================================================================================================
7875
// Sets the error handler for transport/server errors.
7976
// Args:
8077
// handler: Callback invoked with error strings.
8178
//==========================================================================================================
82-
void SetErrorHandler(ErrorHandler handler);
79+
void SetErrorHandler(ITransport::ErrorHandler handler) override;
8380

8481
private:
8582
class Impl;
8683
std::unique_ptr<Impl> pImpl;
87-
};
84+
};
85+
86+
//==========================================================================================================
87+
// HTTPServerFactory
88+
// Purpose: Factory for creating HTTP/HTTPS server acceptors (ITransportAcceptor) from a configuration
89+
// string. The configuration format is uri format, intentionally simple and stable:
90+
// - "http://<address>:<port>" (e.g., http://127.0.0.1:0)
91+
// - "https://<address>:<port>?cert=<pem>&key=<pem>"
92+
// Unknown parameters are ignored. If scheme is omitted, defaults to http.
93+
//==========================================================================================================
94+
class HTTPServerFactory : public ITransportAcceptorFactory {
95+
public:
96+
std::unique_ptr<ITransportAcceptor> CreateTransportAcceptor(const std::string& config) override;
97+
};
8898

8999
} // namespace mcp

include/mcp/HTTPTransport.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ class HTTPTransport : public ITransport {
9898
std::unique_ptr<Impl> pImpl;
9999
};
100100

101+
//==========================================================================================================
102+
// HTTPTransportFactory
103+
// Purpose: Factory for creating HTTP/HTTPS transports.
104+
//==========================================================================================================
101105
class HTTPTransportFactory : public ITransportFactory {
102106
public:
103107
std::unique_ptr<ITransport> CreateTransport(const std::string& config) override;

include/mcp/InMemoryTransport.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ class InMemoryTransport : public ITransport {
7979
std::unique_ptr<Impl> pImpl;
8080
};
8181

82+
//==========================================================================================================
83+
// InMemoryTransportFactory
84+
// Purpose: Factory for creating in-memory transports.
85+
//==========================================================================================================
8286
class InMemoryTransportFactory : public ITransportFactory {
8387
public:
8488
std::unique_ptr<ITransport> CreateTransport(const std::string& config) override;

include/mcp/StdioTransport.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ class StdioTransport : public ITransport {
102102
std::unique_ptr<Impl> pImpl;
103103
};
104104

105+
//==========================================================================================================
106+
// StdioTransportFactory
107+
// Purpose: Factory for creating stdio transports.
108+
//==========================================================================================================
105109
class StdioTransportFactory : public ITransportFactory {
106110
public:
107111
std::unique_ptr<ITransport> CreateTransport(const std::string& config) override;

include/mcp/Transport.h

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ class ITransport {
124124
// Concrete transports are declared in their respective headers:
125125
// - mcp/StdioTransport.hpp
126126
// - mcp/InMemoryTransport.hpp
127+
// - mcp/HTTPTransport.hpp
128+
// - mcp/HTTPServer.hpp (implements ITransportAcceptor)
127129

128130
//==========================================================================================================
129131
// Transport factory interface
@@ -143,4 +145,79 @@ class ITransportFactory {
143145
virtual std::unique_ptr<ITransport> CreateTransport(const std::string& config) = 0;
144146
};
145147

148+
//==========================================================================================================
149+
// ITransportAcceptor
150+
// Purpose: Server-side acceptor interface which owns the listen lifecycle and dispatches incoming
151+
// JSON-RPC requests/notifications to registered handlers.
152+
// Notes:
153+
// - Keeps the multi-client accept/listen role separate from the single-session ITransport.
154+
// - Implementations should bind/listen in Start(), stop/teardown in Stop(), and invoke the
155+
// registered handlers upon incoming messages.
156+
// - This interface mirrors the handler signatures of ITransport for consistency.
157+
//==========================================================================================================
158+
class ITransportAcceptor {
159+
public:
160+
virtual ~ITransportAcceptor() = default;
161+
162+
//==========================================================================================================
163+
// Starts the acceptor (binds/listens/spawns accept loop as needed).
164+
// Args:
165+
// (none)
166+
// Returns:
167+
// Future that completes when the accept loop is running.
168+
//==========================================================================================================
169+
virtual std::future<void> Start() = 0;
170+
171+
//==========================================================================================================
172+
// Stops the acceptor and releases resources (closes listener and active sessions gracefully).
173+
// Args:
174+
// (none)
175+
// Returns:
176+
// Future that completes when the acceptor has stopped.
177+
//==========================================================================================================
178+
virtual std::future<void> Stop() = 0;
179+
180+
//==========================================================================================================
181+
// Registers the server-side JSON-RPC request handler. Invoked per request, must return a response.
182+
// Args:
183+
// handler: Callback taking const JSONRPCRequest& and returning unique_ptr<JSONRPCResponse>.
184+
// Returns:
185+
// (none)
186+
//==========================================================================================================
187+
virtual void SetRequestHandler(ITransport::RequestHandler handler) = 0;
188+
189+
//==========================================================================================================
190+
// Registers the JSON-RPC notification handler (no response expected).
191+
// Args:
192+
// handler: Callback invoked with ownership of the incoming notification object.
193+
// Returns:
194+
// (none)
195+
//==========================================================================================================
196+
virtual void SetNotificationHandler(ITransport::NotificationHandler handler) = 0;
197+
198+
//==========================================================================================================
199+
// Registers an error handler to receive acceptor/transport errors.
200+
// Args:
201+
// handler: Callback receiving error strings.
202+
virtual void SetErrorHandler(ITransport::ErrorHandler handler) = 0;
203+
};
204+
205+
//==========================================================================================================
206+
// Transport acceptor factory interface
207+
// Purpose: Factory for creating server-side acceptors from configuration strings.
208+
//==========================================================================================================
209+
class ITransportAcceptorFactory {
210+
public:
211+
virtual ~ITransportAcceptorFactory() = default;
212+
213+
//==========================================================================================================
214+
// Creates a server-side acceptor instance using the provided configuration.
215+
// Args:
216+
// config: Transport-specific configuration string (e.g., "http://127.0.0.1:9443").
217+
// Returns:
218+
// A unique_ptr to a newly created ITransportAcceptor.
219+
//==========================================================================================================
220+
virtual std::unique_ptr<ITransportAcceptor> CreateTransportAcceptor(const std::string& config) = 0;
221+
};
222+
146223
} // namespace mcp

src/mcp/HTTPServer.cpp

Lines changed: 113 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
#include <thread>
1010
#include <atomic>
1111
#include <sstream>
12+
#include <algorithm>
13+
#include <cctype>
1214

1315
#include <boost/asio.hpp>
1416
#include <boost/asio/ssl.hpp>
@@ -41,9 +43,9 @@ class HTTPServer::Impl {
4143
std::unique_ptr<ssl::context> sslCtx; // present when scheme==https
4244
std::thread ioThread;
4345

44-
HTTPServer::RequestHandler requestHandler;
45-
HTTPServer::NotificationHandler notificationHandler;
46-
HTTPServer::ErrorHandler errorHandler;
46+
ITransport::RequestHandler requestHandler;
47+
ITransport::NotificationHandler notificationHandler;
48+
ITransport::ErrorHandler errorHandler;
4749

4850
explicit Impl(const HTTPServer::Options& o) : opts(o) {
4951
if (opts.scheme == "https") {
@@ -206,6 +208,27 @@ class HTTPServer::Impl {
206208

207209
net::awaitable<void> acceptLoop() {
208210
try {
211+
// Validate port strictly: numeric and within [0, 65535]
212+
if (opts.port.empty()) {
213+
setError("HTTPServer invalid port: empty");
214+
co_return;
215+
}
216+
bool allDigits = std::all_of(opts.port.begin(), opts.port.end(), [](unsigned char ch){ return std::isdigit(ch) != 0; });
217+
if (!allDigits) {
218+
setError(std::string("HTTPServer invalid port (non-numeric): ") + opts.port);
219+
co_return;
220+
}
221+
unsigned long portNum = 0;
222+
try {
223+
portNum = std::stoul(opts.port);
224+
} catch (...) {
225+
setError(std::string("HTTPServer invalid port (parse failure): ") + opts.port);
226+
co_return;
227+
}
228+
if (portNum > 65535ul) {
229+
setError(std::string("HTTPServer invalid port (out of range): ") + opts.port);
230+
co_return;
231+
}
209232
tcp::resolver resolver(co_await net::this_coro::executor);
210233
auto r = resolver.resolve(opts.address, opts.port);
211234
tcp::endpoint ep = *r.begin();
@@ -284,14 +307,97 @@ std::future<void> HTTPServer::Stop() {
284307
return fut;
285308
}
286309

287-
void HTTPServer::SetRequestHandler(RequestHandler handler) {
310+
void HTTPServer::SetRequestHandler(ITransport::RequestHandler handler) {
288311
pImpl->requestHandler = std::move(handler);
289312
}
290-
void HTTPServer::SetNotificationHandler(NotificationHandler handler) {
313+
void HTTPServer::SetNotificationHandler(ITransport::NotificationHandler handler) {
291314
pImpl->notificationHandler = std::move(handler);
292315
}
293-
void HTTPServer::SetErrorHandler(ErrorHandler handler) {
294-
pImpl->errorHandler = std::move(handler);
316+
317+
void HTTPServer::SetErrorHandler(ITransport::ErrorHandler handler) {
318+
pImpl->errorHandler = std::move(handler);
319+
}
320+
321+
std::unique_ptr<ITransportAcceptor> HTTPServerFactory::CreateTransportAcceptor(const std::string& config) {
322+
HTTPServer::Options opts;
323+
// Factory default: http if scheme omitted
324+
opts.scheme = "http";
325+
326+
std::string cfg = config;
327+
// Trim leading/trailing spaces
328+
auto trim = [](std::string& s){
329+
auto notSpace = [](unsigned char c){ return !std::isspace(c); };
330+
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notSpace));
331+
s.erase(std::find_if(s.rbegin(), s.rend(), notSpace).base(), s.end());
332+
};
333+
trim(cfg);
334+
335+
// Detect scheme
336+
auto startsWith = [](const std::string& s, const char* pfx){ return s.rfind(pfx, 0) == 0; };
337+
if (startsWith(cfg, "http://")) {
338+
opts.scheme = "http";
339+
cfg = cfg.substr(7);
340+
} else if (startsWith(cfg, "https://")) {
341+
opts.scheme = "https";
342+
cfg = cfg.substr(8);
343+
}
344+
345+
// Split query params
346+
std::string hostPortPath = cfg;
347+
std::string query;
348+
auto qpos = cfg.find('?');
349+
if (qpos != std::string::npos) {
350+
hostPortPath = cfg.substr(0, qpos);
351+
query = cfg.substr(qpos + 1);
352+
}
353+
354+
// Strip path component if present
355+
std::string hostPort = hostPortPath;
356+
auto slash = hostPortPath.find('/');
357+
if (slash != std::string::npos) {
358+
hostPort = hostPortPath.substr(0, slash);
359+
}
360+
trim(hostPort);
361+
362+
// Parse host[:port] including IPv4/IPv6 in [addr]:port form
363+
if (!hostPort.empty()) {
364+
if (hostPort.front() == '[') {
365+
auto rb = hostPort.find(']');
366+
if (rb != std::string::npos) {
367+
opts.address = hostPort.substr(1, rb - 1);
368+
if (rb + 1 < hostPort.size() && hostPort[rb + 1] == ':') {
369+
opts.port = hostPort.substr(rb + 2);
370+
}
371+
}
372+
} else {
373+
auto colon = hostPort.rfind(':');
374+
if (colon != std::string::npos) {
375+
opts.address = hostPort.substr(0, colon);
376+
opts.port = hostPort.substr(colon + 1);
377+
} else {
378+
opts.address = hostPort;
379+
}
380+
}
381+
trim(opts.address);
382+
trim(opts.port);
383+
if (opts.port.empty()) opts.port = "9443"; // default
384+
}
385+
386+
// Parse query parameters: cert, key
387+
if (!query.empty()) {
388+
std::stringstream ss(query);
389+
std::string kv;
390+
while (std::getline(ss, kv, '&')) {
391+
auto eq = kv.find('=');
392+
std::string key = (eq == std::string::npos) ? kv : kv.substr(0, eq);
393+
std::string val = (eq == std::string::npos) ? std::string() : kv.substr(eq + 1);
394+
if (key == "cert") opts.certFile = val;
395+
else if (key == "key") opts.keyFile = val;
396+
}
397+
}
398+
399+
// Return server acceptor
400+
return std::unique_ptr<ITransportAcceptor>(new HTTPServer(opts));
295401
}
296402

297403
} // namespace mcp

0 commit comments

Comments
 (0)