Skip to content

Commit 091da0d

Browse files
committed
rework of authentication with additional features and capabilities
1 parent 84a0e13 commit 091da0d

22 files changed

Lines changed: 1162 additions & 92 deletions

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ add_library(mcp_cpp STATIC
5151
src/mcp/Server.cpp
5252
src/mcp/auth/OAuthClient.cpp
5353
src/mcp/auth/OAuth2ClientCredentialsAuth.cpp
54+
src/mcp/auth/WwwAuthenticate.cpp
5455
src/mcp/auth/ServerAuth.cpp
5556
src/mcp/JsonRpcMessageRouter.cpp
5657
src/mcp/ContentLengthFramer.cpp

Dockerfile.demo

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ RUN bash -lc 'set -euo pipefail; \
4040
FROM build AS test
4141
ARG MCP_STDIOTRANSPORT_TIMEOUT_MS
4242
ENV MCP_STDIOTRANSPORT_TIMEOUT_MS=${MCP_STDIOTRANSPORT_TIMEOUT_MS}
43+
ENV MCP_IN_CONTAINER=1
4344
ENV GTEST_COLOR=yes
4445
ENV CTEST_OUTPUT_ON_FAILURE=1
4546
WORKDIR /src

docs/api/server.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ In this SDK the server and/or client use the experimental map for:
2727
- `capabilities.experimental.logLevel` (client → server) — see [Logging to client](#logging-to-client) for client-selected minimum log level.
2828
- `capabilities.experimental.resourceReadChunking` (server → client) — see [Resource read chunking (experimental)](#resource-read-chunking-experimental).
2929

30+
## Extensions capabilities (optional)
31+
32+
Clients may advertise optional extensions in the initialize request under `capabilities.extensions` (per SEP‑1724). The SDK now models these verbatim in `ClientCapabilities.extensions` so server code can detect extension support without schema loss. For MCP Apps (SEP‑1865), the extension identifier is `io.modelcontextprotocol/ui` (see `Extensions::uiExtensionId` in [include/mcp/Protocol.h](../../include/mcp/Protocol.h)).
33+
34+
Notes:
35+
36+
- Unknown extension objects are preserved as `JSONValue`s without interpretation.
37+
- Malformed shapes (non‑object `extensions`, wrong types inside entries) are ignored safely during parsing.
38+
3039
## Type aliases and handlers
3140
- using ToolResult = CallToolResult
3241
- using ResourceContent = ReadResourceResult
@@ -231,6 +240,29 @@ acceptor->Start().get();
231240
- std::vector<Tool> ListTools()
232241
- std::future<JSONValue> CallTool(const std::string& name, const JSONValue& arguments)
233242
243+
### Tools metadata (`_meta`)
244+
245+
- The `Tool` type supports an optional metadata field serialized as `_meta` in `tools/list` responses. This allows servers to attach arbitrary JSON metadata to tools without changing the core schema.
246+
- Example: Linking a tool to a UI resource (MCP Apps):
247+
248+
```json
249+
{
250+
"tools": [
251+
{
252+
"name": "get_weather",
253+
"description": "Get weather with interactive dashboard",
254+
"inputSchema": { "type": "object", "properties": { "location": {"type":"string"} } },
255+
"_meta": { "ui/resourceUri": "ui://weather-server/dashboard" }
256+
}
257+
]
258+
}
259+
```
260+
261+
Notes:
262+
263+
- `_meta` is omitted when not provided.
264+
- The value is a free-form JSON object (or any JSONValue preserved verbatim). Hosts that do not recognize `_meta` safely ignore it.
265+
234266
## Resources
235267
- void RegisterResource(const std::string& uri, ResourceHandler handler)
236268
- void UnregisterResource(const std::string& uri)

examples/logging_demo/main.cpp

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,25 +32,28 @@ int main() {
3232
auto client = factory.CreateClient(info);
3333
client->Connect(std::move(clientTrans)).get();
3434

35-
// Capture log notifications on client
35+
// Capture log notifications (notifications/message): level + data
3636
client->SetNotificationHandler(Methods::Log, [&](const std::string& method, const JSONValue& params){
3737
(void)method;
3838
if (std::holds_alternative<JSONValue::Object>(params.value)) {
3939
const auto& o = std::get<JSONValue::Object>(params.value);
4040
auto itLvl = o.find("level");
41-
auto itMsg = o.find("message");
42-
if (itLvl != o.end() && itMsg != o.end() &&
43-
std::holds_alternative<std::string>(itLvl->second->value) &&
44-
std::holds_alternative<std::string>(itMsg->second->value)) {
45-
std::cout << "log [" << std::get<std::string>(itLvl->second->value) << "]: "
46-
<< std::get<std::string>(itMsg->second->value) << "\n";
41+
auto itData = o.find("data");
42+
if (itLvl != o.end() && itData != o.end() &&
43+
std::holds_alternative<std::string>(itLvl->second->value)) {
44+
std::cout << "log [" << std::get<std::string>(itLvl->second->value) << "]: ";
45+
if (std::holds_alternative<std::string>(itData->second->value)) {
46+
std::cout << std::get<std::string>(itData->second->value);
47+
} else {
48+
std::cout << "(non-string payload)";
49+
}
50+
std::cout << "\n";
4751
}
4852
}
4953
});
5054

51-
// Initialize with client min log level WARN
52-
ClientCapabilities caps; caps.experimental["logLevel"] = JSONValue{std::string("WARN")};
53-
(void)client->Initialize(info, caps).get();
55+
// Initialize client
56+
ClientCapabilities caps; (void)client->Initialize(info, caps).get();
5457

5558
// INFO should be suppressed, ERROR delivered
5659
server.LogToClient("INFO", "info suppressed", std::nullopt);

include/mcp/HTTPTransport.hpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#include <future>
1313
#include <atomic>
1414
#include <functional>
15+
#include <optional>
16+
#include <vector>
1517

1618
#include "mcp/Transport.h"
1719
#include "mcp/auth/IAuth.hpp"
@@ -24,6 +26,17 @@ namespace mcp {
2426
//==========================================================================================================
2527
class HTTPTransport : public ITransport {
2628
public:
29+
//==========================================================================================================
30+
// HttpResponseInfo
31+
// Purpose: Exposes HTTP status and headers from the last completed HTTP request. Used for auth discovery
32+
// (e.g., parsing WWW-Authenticate on 401/403).
33+
//==========================================================================================================
34+
struct HttpResponseInfo {
35+
int status{0};
36+
std::vector<mcp::auth::HeaderKV> headers;
37+
std::string wwwAuthenticate; // convenience copy of WWW-Authenticate if present
38+
};
39+
2740
//==========================================================================================================
2841
// Options
2942
// Purpose: Configuration for HTTP/HTTPS endpoints and TLS verification.
@@ -115,6 +128,13 @@ class HTTPTransport : public ITransport {
115128
void SetAuth(mcp::auth::IAuth& auth);
116129
void SetAuth(std::shared_ptr<mcp::auth::IAuth> auth);
117130

131+
//==========================================================================================================
132+
// TryGetLastHttpResponse
133+
// Purpose: Copies the last observed HTTP response info (if any). Returns true on success.
134+
// Thread-safe; returns a snapshot and does not clear the stored value.
135+
//==========================================================================================================
136+
bool QueryLastHttpResponse(HttpResponseInfo& out) const;
137+
118138
private:
119139
class Impl;
120140
std::unique_ptr<Impl> pImpl;

include/mcp/Protocol.h

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ namespace mcp {
2020
//==========================================================================================================
2121
///////////////////////////////////////// Protocol constants ///////////////////////////////////////////
2222
// MCP Protocol version
23-
constexpr const char* PROTOCOL_VERSION = "2025-06-18";
23+
constexpr const char* PROTOCOL_VERSION = "2025-11-25";
24+
25+
// MCP Extensions identifiers (optional capability negotiation)
26+
namespace Extensions {
27+
// UI apps extension identifier (SEP-1865)
28+
constexpr const char* uiExtensionId = "io.modelcontextprotocol/ui";
29+
}
2430

2531
///////////////////////////////////////// Implementation ///////////////////////////////////////////
2632
// Implementation information
@@ -70,6 +76,8 @@ struct ServerCapabilities {
7076
struct ClientCapabilities {
7177
std::optional<SamplingCapability> sampling;
7278
std::unordered_map<std::string, JSONValue> experimental;
79+
// Optional negotiated extensions per SEP-1724 (e.g., io.modelcontextprotocol/ui)
80+
std::unordered_map<std::string, JSONValue> extensions;
7381
};
7482

7583
///////////////////////////////////////// Tools ///////////////////////////////////////////
@@ -78,10 +86,13 @@ struct Tool {
7886
std::string name;
7987
std::string description;
8088
JSONValue inputSchema; // JSON Schema for tool parameters
89+
std::optional<JSONValue> meta; // serialized as _meta in tools/list
8190

8291
Tool() = default;
83-
Tool(std::string name, std::string description, JSONValue inputSchema = JSONValue{})
84-
: name(std::move(name)), description(std::move(description)), inputSchema(std::move(inputSchema)) {}
92+
Tool(std::string name, std::string description, JSONValue inputSchema = JSONValue{},
93+
std::optional<JSONValue> metaValue = std::nullopt)
94+
: name(std::move(name)), description(std::move(description)),
95+
inputSchema(std::move(inputSchema)), meta(std::move(metaValue)) {}
8596
};
8697

8798
struct CallToolParams {
@@ -234,6 +245,7 @@ namespace Methods {
234245
constexpr const char* ReadResource = "resources/read";
235246
constexpr const char* Subscribe = "resources/subscribe";
236247
constexpr const char* Unsubscribe = "resources/unsubscribe";
248+
constexpr const char* SetLogLevel = "logging/setLevel";
237249
constexpr const char* ListResourceTemplates = "resources/templates/list";
238250
constexpr const char* ListPrompts = "prompts/list";
239251
constexpr const char* GetPrompt = "prompts/get";
@@ -245,7 +257,7 @@ namespace Methods {
245257
constexpr const char* Initialized = "notifications/initialized";
246258
constexpr const char* Progress = "notifications/progress";
247259
constexpr const char* Keepalive = "notifications/keepalive";
248-
constexpr const char* Log = "notifications/log";
260+
constexpr const char* Log = "notifications/message";
249261
constexpr const char* ResourceListChanged = "notifications/resources/list_changed";
250262
constexpr const char* ToolListChanged = "notifications/tools/list_changed";
251263
constexpr const char* PromptListChanged = "notifications/prompts/list_changed";
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//==========================================================================================================
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) 2025 Vinny Parla
4+
// File: WwwAuthenticate.hpp
5+
// Purpose: Parser for HTTP WWW-Authenticate Bearer challenges (RFC 6750/RFC 9728 parameters)
6+
//==========================================================================================================
7+
8+
#pragma once
9+
10+
#include <string>
11+
#include <unordered_map>
12+
13+
namespace mcp::auth {
14+
15+
//==========================================================================================================
16+
// WwwAuthChallenge
17+
// Purpose: Parsed representation of a single WWW-Authenticate challenge line.
18+
//==========================================================================================================
19+
struct WwwAuthChallenge {
20+
std::string scheme; // e.g., "Bearer" (canonicalized as lower-case)
21+
std::unordered_map<std::string, std::string> params; // key -> value (unquoted, unescaped)
22+
};
23+
24+
//==========================================================================================================
25+
// parseWwwAuthenticate
26+
// Purpose: Parse a single WWW-Authenticate header value. Supports Bearer scheme with comma-separated
27+
// key=value parameters (values may be quoted). Returns true on successful parse of Bearer scheme.
28+
// Notes:
29+
// - Returns false for unsupported schemes (e.g., Basic), or malformed inputs.
30+
// - Keys are case-sensitive as transmitted; scheme is returned lower-case.
31+
//==========================================================================================================
32+
bool parseWwwAuthenticate(const std::string& header, WwwAuthChallenge& out);
33+
34+
} // namespace mcp::auth

scripts/ci_all.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ echo "[ci_all] 1b/3 List discovered unit/integration tests"
3434
docker run --rm \
3535
-e GTEST_COLOR=yes \
3636
-e CTEST_OUTPUT_ON_FAILURE=1 \
37+
-e MCP_IN_CONTAINER=1 \
3738
--ipc=host \
3839
mcp-cpp-build \
3940
ctest --test-dir build -N -V
@@ -43,6 +44,7 @@ echo "[ci_all] 1c/3 Run unit/integration tests verbosely"
4344
docker run --rm \
4445
-e GTEST_COLOR=yes \
4546
-e CTEST_OUTPUT_ON_FAILURE=1 \
47+
-e MCP_IN_CONTAINER=1 \
4648
--ipc=host \
4749
mcp-cpp-build \
4850
ctest --test-dir build -VV --progress
@@ -64,3 +66,4 @@ chmod +x scripts/http_e2e.sh scripts/http_down.sh
6466
./scripts/http_down.sh || true
6567

6668
echo "[ci_all] SUCCESS: All tests passed"
69+

src/mcp/HTTPTransport.cpp

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ class HTTPTransport::Impl {
6969
std::shared_ptr<mcp::auth::IAuth> authStrong;
7070
mcp::auth::IAuth* authWeak{nullptr};
7171

72+
// Snapshot of the last HTTP response (status + headers) for auth discovery
73+
mutable std::mutex lastRespMutex;
74+
HTTPTransport::HttpResponseInfo lastResp;
75+
7276
explicit Impl(const HTTPTransport::Options& o) : opts(o) {
7377
// Random session id similar to InMemoryTransport
7478
std::random_device rd; std::mt19937 gen(rd()); std::uniform_int_distribution<> dis(1000, 9999);
@@ -131,6 +135,25 @@ class HTTPTransport::Impl {
131135
}
132136
}
133137

138+
void recordResponse(const http::response<http::string_body>& res) {
139+
HTTPTransport::HttpResponseInfo info;
140+
info.status = static_cast<int>(res.result_int());
141+
for (const auto& h : res.base()) {
142+
mcp::auth::HeaderKV kv;
143+
kv.name = std::string(h.name_string());
144+
kv.value = std::string(h.value());
145+
info.headers.push_back(std::move(kv));
146+
}
147+
auto const& wa = res[http::field::www_authenticate];
148+
if (!wa.empty()) {
149+
info.wwwAuthenticate = std::string(wa);
150+
}
151+
{
152+
std::lock_guard<std::mutex> lk(lastRespMutex);
153+
lastResp = std::move(info);
154+
}
155+
}
156+
134157
void setError(const std::string& msg) {
135158
if (errorHandler) {
136159
errorHandler(msg);
@@ -229,6 +252,7 @@ class HTTPTransport::Impl {
229252
if (errorHandler) {
230253
errorHandler(std::string("HTTP DEBUG: https status=") + std::to_string(res.result_int()) + std::string(" content-type=") + std::string(res[http::field::content_type]));
231254
}
255+
recordResponse(res);
232256
boost::system::error_code ec; stream.shutdown(ec);
233257
co_return res.body();
234258
} else {
@@ -277,6 +301,7 @@ class HTTPTransport::Impl {
277301
if (errorHandler) {
278302
errorHandler(std::string("HTTP DEBUG: http status=") + std::to_string(res.result_int()) + std::string(" content-type=") + std::string(res[http::field::content_type]));
279303
}
304+
recordResponse(res);
280305
boost::system::error_code ec; stream.socket().shutdown(tcp::socket::shutdown_both, ec);
281306
co_return res.body();
282307
}
@@ -606,6 +631,29 @@ void HTTPTransport::SetAuth(std::shared_ptr<mcp::auth::IAuth> auth) {
606631
}
607632
}
608633

634+
bool HTTPTransport::QueryLastHttpResponse(HttpResponseInfo& out) const {
635+
FUNC_SCOPE();
636+
// Rationale: return true as soon as ANY meaningful part of the last HTTP response snapshot
637+
// is available (OR semantics), rather than requiring ALL fields (AND semantics).
638+
// - Real responses may legitimately omit fields (e.g., 200 OK has no WWW-Authenticate).
639+
// - We record status/headers opportunistically; OR semantics avoids false negatives and remains
640+
// resilient if future changes only capture a subset (e.g., headers or a challenge line).
641+
{
642+
std::lock_guard<std::mutex> lk(pImpl->lastRespMutex);
643+
out = pImpl->lastResp;
644+
}
645+
if (out.status != 0) {
646+
return true;
647+
}
648+
if (!out.headers.empty()) {
649+
return true;
650+
}
651+
if (!out.wwwAuthenticate.empty()) {
652+
return true;
653+
}
654+
return false;
655+
}
656+
609657
//==========================================================================================================
610658
// HTTPTransportFactory::CreateTransport
611659
// Purpose: Parse semicolon-delimited key=value config into Options and create transport.

src/mcp/InMemoryTransport.cpp

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,20 @@ class InMemoryTransport::Impl {
8080
void processMessage(const std::string& message) {
8181
LOG_DEBUG("Processing in-memory message: {}", message);
8282

83-
// First, handle responses (have top-level result or error)
84-
if (message.find("\"result\"") != std::string::npos || message.find("\"error\"") != std::string::npos) {
83+
// Classify the message using the router to avoid false positives on nested keys/values.
84+
auto kind = router ? router->classify(message) : IJsonRpcMessageRouter::MessageKind::Unknown;
85+
86+
// Handle responses (top-level result or error)
87+
if (kind == IJsonRpcMessageRouter::MessageKind::Response) {
8588
JSONRPCResponse response;
8689
if (response.Deserialize(message)) {
8790
handleResponse(std::move(response));
8891
return;
8992
}
9093
}
91-
// Next, handle requests: must have a method and a TOP-LEVEL id
92-
if (message.find("\"method\"") != std::string::npos && router &&
93-
router->classify(message) == IJsonRpcMessageRouter::MessageKind::Request) {
94+
95+
// Handle requests (must have method and top-level id)
96+
if (kind == IJsonRpcMessageRouter::MessageKind::Request) {
9497
JSONRPCRequest request;
9598
if (request.Deserialize(message)) {
9699
if (requestHandler) {
@@ -119,7 +122,8 @@ class InMemoryTransport::Impl {
119122
}
120123
}
121124
}
122-
// Finally, treat as notification
125+
126+
// Finally, notification
123127
{
124128
JSONRPCNotification notification;
125129
if (notification.Deserialize(message)) {

0 commit comments

Comments
 (0)