Skip to content

Commit 6a978b1

Browse files
committed
Add MCP parity features: request metadata, tool schema, middleware
Squashed commits from feature/mcp-parity-20251126: - Add optional request metadata to Context (request_id, session_id, meta) - Add tool schema exclude/prune support for parameter hiding - Refine middleware tool injection to return structured JSON - Add MCP-shaped middleware tests for prompt/resource injection - Fix test port conflicts for parallel execution Test coverage: 38/38 passing
1 parent 2125bf3 commit 6a978b1

14 files changed

Lines changed: 254 additions & 64 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,14 @@ jobs:
5555
-DFASTMCPP_BUILD_TESTS=ON
5656
-DFASTMCPP_BUILD_EXAMPLES=ON
5757
58-
- name: Build
58+
- name: Build (Unix)
59+
if: runner.os != 'Windows'
5960
run: cmake --build build --config ${{ matrix.build_type }} --parallel
6061

62+
- name: Build (Windows)
63+
if: runner.os == 'Windows'
64+
# Single-threaded build on Windows to avoid compiler heap space issues with large test files
65+
run: cmake --build build --config ${{ matrix.build_type }} --parallel 1
66+
6167
- name: Test
6268
run: ctest --test-dir build -C ${{ matrix.build_type }} --output-on-failure

CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,10 @@ if(FASTMCPP_BUILD_TESTS)
233233
target_link_libraries(fastmcpp_server_interactions PRIVATE fastmcpp_core)
234234
add_test(NAME fastmcpp_server_interactions COMMAND fastmcpp_server_interactions)
235235

236+
add_executable(fastmcpp_server_context_meta tests/server/context_meta.cpp)
237+
target_link_libraries(fastmcpp_server_context_meta PRIVATE fastmcpp_core)
238+
add_test(NAME fastmcpp_server_context_meta COMMAND fastmcpp_server_context_meta)
239+
236240
add_executable(fastmcpp_client_transports tests/client/transports.cpp)
237241
target_link_libraries(fastmcpp_client_transports PRIVATE fastmcpp_core)
238242
add_test(NAME fastmcpp_client_transports COMMAND fastmcpp_client_transports)

include/fastmcpp/client/types.hpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,16 @@ struct CallToolResult {
7070
std::optional<fastmcpp::Json> meta; ///< Request metadata
7171
std::optional<fastmcpp::Json> data; ///< Parsed structured data (if available)
7272
std::optional<fastmcpp::util::schema_type::SchemaValue> typedData; ///< Schema-mapped value
73+
74+
/// Helper to get text from the first TextContent block
75+
std::string text() const {
76+
for (const auto& block : content) {
77+
if (auto* tc = std::get_if<TextContent>(&block)) {
78+
return tc->text;
79+
}
80+
}
81+
return "";
82+
}
7383
};
7484

7585
/// Helper to parse structured data into a concrete type using nlohmann::json conversion

include/fastmcpp/server/context.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ class Context {
4848
/// Construct a Context with references to resource and prompt managers
4949
Context(const resources::ResourceManager& rm,
5050
const prompts::PromptManager& pm);
51+
Context(const resources::ResourceManager& rm,
52+
const prompts::PromptManager& pm,
53+
std::optional<fastmcpp::Json> request_meta,
54+
std::optional<std::string> request_id = std::nullopt,
55+
std::optional<std::string> session_id = std::nullopt);
5156

5257
/// List all available resources from the server
5358
/// @return Vector of Resource objects
@@ -71,9 +76,17 @@ class Context {
7176
/// @throws NotFoundError if resource doesn't exist
7277
std::string read_resource(const std::string& uri) const;
7378

79+
/// Request metadata accessors (may be unset before MCP session is ready)
80+
const std::optional<fastmcpp::Json>& request_meta() const { return request_meta_; }
81+
const std::optional<std::string>& request_id() const { return request_id_; }
82+
const std::optional<std::string>& session_id() const { return session_id_; }
83+
7484
private:
7585
const resources::ResourceManager* resource_mgr_;
7686
const prompts::PromptManager* prompt_mgr_;
87+
std::optional<fastmcpp::Json> request_meta_;
88+
std::optional<std::string> request_id_;
89+
std::optional<std::string> session_id_;
7790
};
7891

7992
} // namespace fastmcpp::server

include/fastmcpp/tools/manager.hpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class ToolManager {
2323
return names;
2424
}
2525

26-
const fastmcpp::Json& input_schema_for(const std::string& name) const {
26+
fastmcpp::Json input_schema_for(const std::string& name) const {
2727
return get(name).input_schema();
2828
}
2929

include/fastmcpp/tools/tool.hpp

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22
#include <string>
33
#include <functional>
4+
#include <vector>
45
#include "fastmcpp/types.hpp"
56

67
namespace fastmcpp::tools {
@@ -10,21 +11,59 @@ class Tool {
1011
using Fn = std::function<fastmcpp::Json(const fastmcpp::Json&)>;
1112

1213
Tool() = default;
13-
Tool(std::string name, fastmcpp::Json input_schema, fastmcpp::Json output_schema, Fn fn)
14+
Tool(std::string name,
15+
fastmcpp::Json input_schema,
16+
fastmcpp::Json output_schema,
17+
Fn fn,
18+
std::vector<std::string> exclude_args = {})
1419
: name_(std::move(name)), input_schema_(std::move(input_schema)),
15-
output_schema_(std::move(output_schema)), fn_(std::move(fn)) {}
20+
output_schema_(std::move(output_schema)), fn_(std::move(fn)),
21+
exclude_args_(std::move(exclude_args)) {}
1622

1723
const std::string& name() const { return name_; }
18-
const fastmcpp::Json& input_schema() const { return input_schema_; }
24+
fastmcpp::Json input_schema() const {
25+
if (exclude_args_.empty()) return input_schema_;
26+
return prune_schema(input_schema_);
27+
}
1928
const fastmcpp::Json& output_schema() const { return output_schema_; }
2029
fastmcpp::Json invoke(const fastmcpp::Json& input) const { return fn_(input); }
2130

2231
private:
32+
fastmcpp::Json prune_schema(const fastmcpp::Json& schema) const {
33+
// Work on a copy to avoid mutating shared $defs or properties
34+
fastmcpp::Json pruned = schema;
35+
if (!pruned.is_object()) return pruned;
36+
37+
// Remove excluded properties
38+
if (pruned.contains("properties") && pruned["properties"].is_object()) {
39+
for (const auto& key : exclude_args_) {
40+
pruned["properties"].erase(key);
41+
}
42+
}
43+
44+
// Remove from required list if present
45+
if (pruned.contains("required") && pruned["required"].is_array()) {
46+
auto& req = pruned["required"];
47+
fastmcpp::Json new_req = fastmcpp::Json::array();
48+
for (const auto& item : req) {
49+
if (!item.is_string()) continue;
50+
std::string val = item.get<std::string>();
51+
bool excluded = std::find(exclude_args_.begin(), exclude_args_.end(), val) != exclude_args_.end();
52+
if (!excluded) {
53+
new_req.push_back(val);
54+
}
55+
}
56+
pruned["required"] = new_req;
57+
}
58+
59+
return pruned;
60+
}
61+
2362
std::string name_;
2463
fastmcpp::Json input_schema_;
2564
fastmcpp::Json output_schema_;
2665
Fn fn_;
66+
std::vector<std::string> exclude_args_;
2767
};
2868

2969
} // namespace fastmcpp::tools
30-

src/server/context.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,17 @@ namespace fastmcpp::server {
88

99
Context::Context(const resources::ResourceManager& rm,
1010
const prompts::PromptManager& pm)
11-
: resource_mgr_(&rm), prompt_mgr_(&pm)
11+
: resource_mgr_(&rm), prompt_mgr_(&pm), request_meta_(std::nullopt), request_id_(std::nullopt), session_id_(std::nullopt)
12+
{
13+
}
14+
15+
Context::Context(const resources::ResourceManager& rm,
16+
const prompts::PromptManager& pm,
17+
std::optional<fastmcpp::Json> request_meta,
18+
std::optional<std::string> request_id,
19+
std::optional<std::string> session_id)
20+
: resource_mgr_(&rm), prompt_mgr_(&pm), request_meta_(std::move(request_meta)),
21+
request_id_(std::move(request_id)), session_id_(std::move(session_id))
1222
{
1323
}
1424

src/server/middleware.cpp

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,22 @@ void ToolInjectionMiddleware::add_prompt_tools(const prompts::PromptManager& pm)
2222

2323
Json prompt_list = Json::array();
2424
for (const auto& [name, prompt] : prompts) {
25-
prompt_list.push_back(Json{
25+
Json prompt_obj = {
2626
{"name", name},
27-
{"template", prompt.template_string()}
28-
});
27+
{"description", prompt.template_string()},
28+
{"arguments", Json::array()},
29+
{"messages", Json::array({
30+
Json{{"role", "user"},
31+
{"content", Json::array({
32+
Json{{"type", "text"}, {"text", prompt.template_string()}}
33+
})}}
34+
})}
35+
};
36+
prompt_list.push_back(prompt_obj);
2937
}
3038

3139
return Json{
32-
{"content", Json::array({
33-
Json{{"type", "text"}, {"text", prompt_list.dump(2)}}
34-
})}
40+
{"prompts", prompt_list}
3541
};
3642
}
3743
);
@@ -59,10 +65,18 @@ void ToolInjectionMiddleware::add_prompt_tools(const prompts::PromptManager& pm)
5965
Context ctx(resources::ResourceManager(), pm);
6066
std::string rendered = ctx.get_prompt(name, arguments);
6167

68+
Json messages = Json::array({
69+
Json{{"role", "user"},
70+
{"content", Json::array({
71+
Json{{"type", "text"}, {"text", rendered}}
72+
})}}
73+
});
74+
6275
return Json{
63-
{"content", Json::array({
64-
Json{{"type", "text"}, {"text", rendered}}
65-
})}
76+
{"name", name},
77+
{"description", nullptr},
78+
{"arguments", Json::array()},
79+
{"messages", messages}
6680
};
6781
}
6882
);
@@ -79,22 +93,24 @@ void ToolInjectionMiddleware::add_resource_tools(const resources::ResourceManage
7993
{"required", Json::array()}
8094
},
8195
[&rm](const Json& /*args*/) -> Json {
96+
// Preserve full metadata in MCP-like structure
8297
Context ctx(rm, prompts::PromptManager());
8398
auto resources = ctx.list_resources();
8499

85100
Json resource_list = Json::array();
86101
for (const auto& res : resources) {
87102
resource_list.push_back(Json{
88103
{"uri", res.id.value},
89-
{"kind", resources::to_string(res.kind)},
104+
{"name", res.id.value},
105+
{"description", nullptr},
106+
{"mimeType", res.metadata.value("mimeType", "text/plain")},
107+
{"annotations", res.metadata.value("annotations", Json::object())},
90108
{"metadata", res.metadata}
91109
});
92110
}
93111

94112
return Json{
95-
{"content", Json::array({
96-
Json{{"type", "text"}, {"text", resource_list.dump(2)}}
97-
})}
113+
{"resources", resource_list}
98114
};
99115
}
100116
);
@@ -116,10 +132,16 @@ void ToolInjectionMiddleware::add_resource_tools(const resources::ResourceManage
116132
Context ctx(rm, prompts::PromptManager());
117133
std::string content = ctx.read_resource(uri);
118134

135+
Json contents = Json::array({
136+
Json{
137+
{"uri", uri},
138+
{"mimeType", "text/plain"},
139+
{"text", content}
140+
}
141+
});
142+
119143
return Json{
120-
{"content", Json::array({
121-
Json{{"type", "text"}, {"text", content}}
122-
})}
144+
{"contents", contents}
123145
};
124146
}
125147
);

tests/client/transports.cpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ void test_http_transport_basic() {
6060
return Json{{"greeting", "Hello, " + in["name"].get<std::string>()}};
6161
});
6262

63-
server::HttpServerWrapper http(srv, "127.0.0.1", 18100);
63+
server::HttpServerWrapper http(srv, "127.0.0.1", 18300);
6464
http.start();
6565
std::this_thread::sleep_for(std::chrono::milliseconds(100));
6666

67-
client::HttpTransport transport("127.0.0.1:18100");
67+
client::HttpTransport transport("127.0.0.1:18300");
6868
auto result = transport.request("greet", Json{{"name", "Alice"}});
6969
assert(result["greeting"] == "Hello, Alice");
7070

@@ -86,11 +86,11 @@ void test_http_transport_multiple_requests() {
8686
return Json{{"error", "unknown operation"}};
8787
});
8888

89-
server::HttpServerWrapper http(srv, "127.0.0.1", 18101);
89+
server::HttpServerWrapper http(srv, "127.0.0.1", 18301);
9090
http.start();
9191
std::this_thread::sleep_for(std::chrono::milliseconds(100));
9292

93-
client::HttpTransport transport("127.0.0.1:18101");
93+
client::HttpTransport transport("127.0.0.1:18301");
9494

9595
auto add_result = transport.request("calculate", Json{{"op", "add"}, {"a", 10}, {"b", 5}});
9696
assert(add_result["result"] == 15);
@@ -309,14 +309,14 @@ void test_multiple_http_clients() {
309309
auto srv = std::make_shared<server::Server>();
310310
srv->route("ping", [](const Json&){ return Json{{"pong", true}}; });
311311

312-
server::HttpServerWrapper http(srv, "127.0.0.1", 18103);
312+
server::HttpServerWrapper http(srv, "127.0.0.1", 18303);
313313
http.start();
314314
std::this_thread::sleep_for(std::chrono::milliseconds(100));
315315

316316
// Multiple clients
317-
client::HttpTransport client1("127.0.0.1:18103");
318-
client::HttpTransport client2("127.0.0.1:18103");
319-
client::HttpTransport client3("127.0.0.1:18103");
317+
client::HttpTransport client1("127.0.0.1:18303");
318+
client::HttpTransport client2("127.0.0.1:18303");
319+
client::HttpTransport client3("127.0.0.1:18303");
320320

321321
auto result1 = client1.request("ping", Json{});
322322
auto result2 = client2.request("ping", Json{});

tests/server/context_meta.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#include <cassert>
2+
#include "fastmcpp/resources/manager.hpp"
3+
#include "fastmcpp/prompts/manager.hpp"
4+
#include "fastmcpp/server/context.hpp"
5+
6+
using namespace fastmcpp;
7+
8+
int main() {
9+
resources::ResourceManager rm;
10+
prompts::PromptManager pm;
11+
pm.add("hello", prompts::Prompt{"Hello"});
12+
rm.register_resource(resources::Resource{Id{"file://test"}, resources::Kind::File, Json{}});
13+
14+
// Context without MCP session: request metadata unavailable
15+
server::Context ctx_no_session(rm, pm);
16+
assert(!ctx_no_session.request_id().has_value());
17+
assert(!ctx_no_session.session_id().has_value());
18+
assert(!ctx_no_session.request_meta().has_value());
19+
20+
// Context with MCP session metadata
21+
Json meta = Json{{"progressToken", "tok"}, {"client_id", "cid"}};
22+
server::Context ctx_with_session(rm, pm, meta, std::string{"req"}, std::string{"sess"});
23+
assert(ctx_with_session.request_id().value() == "req");
24+
assert(ctx_with_session.session_id().value() == "sess");
25+
assert(ctx_with_session.request_meta().has_value());
26+
assert(ctx_with_session.request_meta()->value("progressToken", "") == "tok");
27+
28+
return 0;
29+
}

0 commit comments

Comments
 (0)