Skip to content

Commit 75e10d0

Browse files
author
Nicola Spieser
committed
feat: integrate Auth & Metrics into Server, add 71 integration tests — bump to v0.29.0
- Auth: server.auth() accessor, automatic enforcement on HTTP POST/GET/DELETE - Metrics: server.metrics() accessor, auto per-method recording in dispatch, /metrics endpoint auto-registered on begin() - 71 new tests: auth lifecycle, metrics tracking, batch JSON-RPC, JSON-RPC edge cases, cross-module integration - Total: 1017 tests (946 → 1017)
1 parent 8c47881 commit 75e10d0

7 files changed

Lines changed: 698 additions & 18 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ test/native/test_robustness
2020
test/native/test_session
2121
test/native/test_content_transport
2222
test/native/test_advanced
23+
test/native/test_integration

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.29.0] - 2026-02-21
6+
7+
### Features
8+
- **Integrated Auth into Server**`server.auth()` accessor, automatic authentication enforcement on all HTTP endpoints (POST/GET/DELETE). Supports Bearer tokens, API keys via header/query, custom callbacks, and multi-key rotation.
9+
- **Integrated Metrics into Server**`server.metrics()` accessor, automatic per-method request counting and latency tracking in dispatch. Prometheus `/metrics` endpoint auto-registered on `begin()`. Error counting for unknown methods.
10+
11+
### Tests
12+
- Added **71 new integration tests** (`test_integration.cpp`):
13+
- Auth module: enable/disable, API key management, custom callbacks, re-enable after disable
14+
- Metrics module: initial state, request/error counting, dispatch-level recording, notification tracking
15+
- Batch JSON-RPC: single/multiple requests, all-notifications, mixed, empty array, metrics in batch
16+
- JSON-RPC edge cases: missing/wrong version, parse errors, string/numeric ID preservation, error messages for missing params
17+
- Cross-module integration: capabilities advertisement, tool/resource/prompt lifecycle, pagination, subscriptions, dynamic add/remove, rich tool handlers, exception handling, rate limit info in initialize, resource templates
18+
- **Total: 1017 tests** (946 → 1017)
19+
520
## [0.28.0] - 2026-02-21
621

722
### Tests

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
|---|:---:|:---:|:---:|
3434
| Runs on the MCU ||| ❌ CLI tool |
3535
| MCP spec compliant | ✅ 2025-03-26 | ❌ custom WS ||
36-
| Actually compiles |946 tests | ❌ self-described | N/A |
36+
| Actually compiles |1017 tests | ❌ self-described | N/A |
3737
| Streamable HTTP + SSE ||||
3838
| WebSocket transport ||||
3939
| Claude Desktop bridge ||||

src/mcpd.cpp

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ void Server::begin() {
182182
_httpServer->send(204);
183183
});
184184

185+
// Register Prometheus metrics endpoint
186+
_metrics.begin(*_httpServer);
187+
185188
_httpServer->begin();
186189

187190
// mDNS advertisement
@@ -334,6 +337,12 @@ void Server::stop() {
334337
void Server::_handleMCPPost() {
335338
transport::setCORSHeaders(*_httpServer);
336339

340+
// Authentication check
341+
if (_auth.isEnabled() && !_auth.authenticate(*_httpServer)) {
342+
Auth::sendUnauthorized(*_httpServer);
343+
return;
344+
}
345+
337346
// Rate limit check
338347
if (_rateLimiter.isEnabled() && !_rateLimiter.tryAcquire()) {
339348
_httpServer->send(429, transport::CONTENT_TYPE_JSON,
@@ -377,6 +386,12 @@ void Server::_handleMCPPost() {
377386
void Server::_handleMCPGet() {
378387
transport::setCORSHeaders(*_httpServer);
379388

389+
// Authentication check
390+
if (_auth.isEnabled() && !_auth.authenticate(*_httpServer)) {
391+
Auth::sendUnauthorized(*_httpServer);
392+
return;
393+
}
394+
380395
// Check that the client wants SSE
381396
// Note: on ESP32 WebServer, we need to take over the client socket
382397
if (!_initialized || _sessionId.isEmpty()) {
@@ -408,6 +423,12 @@ void Server::_handleMCPGet() {
408423
void Server::_handleMCPDelete() {
409424
transport::setCORSHeaders(*_httpServer);
410425

426+
// Authentication check
427+
if (_auth.isEnabled() && !_auth.authenticate(*_httpServer)) {
428+
Auth::sendUnauthorized(*_httpServer);
429+
return;
430+
}
431+
411432
String clientSession = _httpServer->header(transport::HEADER_SESSION_ID);
412433
if (clientSession == _sessionId) {
413434
_initialized = false;
@@ -499,25 +520,34 @@ String Server::_dispatch(const char* method, JsonVariant params, JsonVariant id)
499520

500521
String m(method);
501522

502-
if (m == "initialize") return _handleInitialize(params, id);
503-
if (m == "ping") return _handlePing(id);
504-
if (m == "tools/list") return _handleToolsList(params, id);
505-
if (m == "tools/call") return _handleToolsCall(params, id);
506-
if (m == "resources/list") return _handleResourcesList(params, id);
507-
if (m == "resources/read") return _handleResourcesRead(params, id);
508-
if (m == "resources/templates/list") return _handleResourcesTemplatesList(params, id);
509-
if (m == "prompts/list") return _handlePromptsList(params, id);
510-
if (m == "prompts/get") return _handlePromptsGet(params, id);
511-
if (m == "logging/setLevel") return _handleLoggingSetLevel(params, id);
512-
if (m == "completion/complete") return _handleCompletionComplete(params, id);
513-
if (m == "resources/subscribe") return _handleResourcesSubscribe(params, id);
514-
if (m == "resources/unsubscribe") return _handleResourcesUnsubscribe(params, id);
515-
if (m == "roots/list") return _handleRootsList(params, id);
523+
// Record metrics for each dispatched method
524+
unsigned long _dispatchStart = millis();
525+
526+
auto _recordAndReturn = [&](const String& result) -> String {
527+
_metrics.recordRequest(m, millis() - _dispatchStart);
528+
return result;
529+
};
530+
531+
if (m == "initialize") return _recordAndReturn(_handleInitialize(params, id));
532+
if (m == "ping") return _recordAndReturn(_handlePing(id));
533+
if (m == "tools/list") return _recordAndReturn(_handleToolsList(params, id));
534+
if (m == "tools/call") return _recordAndReturn(_handleToolsCall(params, id));
535+
if (m == "resources/list") return _recordAndReturn(_handleResourcesList(params, id));
536+
if (m == "resources/read") return _recordAndReturn(_handleResourcesRead(params, id));
537+
if (m == "resources/templates/list") return _recordAndReturn(_handleResourcesTemplatesList(params, id));
538+
if (m == "prompts/list") return _recordAndReturn(_handlePromptsList(params, id));
539+
if (m == "prompts/get") return _recordAndReturn(_handlePromptsGet(params, id));
540+
if (m == "logging/setLevel") return _recordAndReturn(_handleLoggingSetLevel(params, id));
541+
if (m == "completion/complete") return _recordAndReturn(_handleCompletionComplete(params, id));
542+
if (m == "resources/subscribe") return _recordAndReturn(_handleResourcesSubscribe(params, id));
543+
if (m == "resources/unsubscribe") return _recordAndReturn(_handleResourcesUnsubscribe(params, id));
544+
if (m == "roots/list") return _recordAndReturn(_handleRootsList(params, id));
516545

517546
// notifications/initialized — no response needed
518-
if (m == "notifications/initialized") return "";
547+
if (m == "notifications/initialized") { _metrics.recordRequest(m, millis() - _dispatchStart); return ""; }
519548
// notifications/cancelled — cancel in-flight request
520549
if (m == "notifications/cancelled") {
550+
_metrics.recordRequest(m, millis() - _dispatchStart);
521551
if (!params.isNull()) {
522552
const char* rid = params["requestId"].as<const char*>();
523553
String reqId = rid ? rid : "";
@@ -529,6 +559,7 @@ String Server::_dispatch(const char* method, JsonVariant params, JsonVariant id)
529559
return "";
530560
}
531561

562+
_metrics.recordError();
532563
return _jsonRpcError(id, -32601, "Method not found");
533564
}
534565

src/mcpd.h

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@
3737
#include "MCPRateLimit.h"
3838
#include "MCPSession.h"
3939
#include "MCPHeap.h"
40+
#include "MCPAuth.h"
41+
#include "MCPMetrics.h"
4042

4143
#ifdef ESP32
4244
#include "MCPTransportBLE.h"
4345
#endif
4446

45-
#define MCPD_VERSION "0.28.0"
47+
#define MCPD_VERSION "0.29.0"
4648
#define MCPD_MCP_PROTOCOL_VERSION "2025-03-26"
4749

4850
namespace mcpd {
@@ -251,6 +253,16 @@ class Server {
251253
/** Access heap monitor for memory diagnostics */
252254
HeapMonitor& heap() { return _heapMonitor; }
253255

256+
// ── Authentication ─────────────────────────────────────────────────
257+
258+
/** Access the authentication module */
259+
Auth& auth() { return _auth; }
260+
261+
// ── Metrics ────────────────────────────────────────────────────────
262+
263+
/** Access the Prometheus metrics module */
264+
Metrics& metrics() { return _metrics; }
265+
254266
// ── Lifecycle Hooks ────────────────────────────────────────────────
255267

256268
using LifecycleCallback = std::function<void()>;
@@ -349,6 +361,8 @@ class Server {
349361
RateLimiter _rateLimiter;
350362
SessionManager _sessionManager;
351363
HeapMonitor _heapMonitor;
364+
Auth _auth;
365+
Metrics _metrics;
352366

353367
// Lifecycle callbacks
354368
InitCallback _onInitializeCb;

test/native/Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ CXXFLAGS = -std=c++17 -Wall -Wextra -Wno-unused-parameter -DMCPD_TEST -pthread
33
INCLUDES = -I../../src -I../mock_includes -I..
44

55
# Targets
6-
TESTS = test_jsonrpc test_tools test_mcp_http test_infrastructure test_modules test_auth_platform test_robustness test_session test_content_transport test_advanced
6+
TESTS = test_jsonrpc test_tools test_mcp_http test_infrastructure test_modules test_auth_platform test_robustness test_session test_content_transport test_advanced test_integration
77

88
.PHONY: all clean test
99

@@ -39,6 +39,9 @@ test_content_transport: ../test_content_transport.cpp ../arduino_mock.h ../test_
3939
test_advanced: ../test_advanced.cpp ../arduino_mock.h ../test_framework.h ../../src/mcpd.h ../../src/mcpd.cpp
4040
$(CXX) $(CXXFLAGS) $(INCLUDES) -o $@ ../test_advanced.cpp
4141

42+
test_integration: ../test_integration.cpp ../arduino_mock.h ../test_framework.h ../../src/mcpd.h ../../src/mcpd.cpp ../../src/MCPAuth.h ../../src/MCPMetrics.h
43+
$(CXX) $(CXXFLAGS) $(INCLUDES) -o $@ ../test_integration.cpp
44+
4245
test: $(TESTS)
4346
@echo ""
4447
@echo "══════════════════════════════════════════"
@@ -55,6 +58,7 @@ test: $(TESTS)
5558
@./test_session
5659
@./test_content_transport
5760
@./test_advanced
61+
@./test_integration
5862
@echo "All test suites completed."
5963

6064
clean:

0 commit comments

Comments
 (0)