Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,14 @@ jobs:
-DFASTMCPP_BUILD_TESTS=ON
-DFASTMCPP_BUILD_EXAMPLES=ON

- name: Build
- name: Build (Unix)
if: runner.os != 'Windows'
run: cmake --build build --config ${{ matrix.build_type }} --parallel

- name: Build (Windows)
if: runner.os == 'Windows'
# Single-threaded build on Windows to avoid compiler heap space issues with large test files
run: cmake --build build --config ${{ matrix.build_type }} --parallel 1

- name: Test
run: ctest --test-dir build -C ${{ matrix.build_type }} --output-on-failure
8 changes: 8 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ if(FASTMCPP_BUILD_TESTS)
target_link_libraries(fastmcpp_server_patterns PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_server_patterns COMMAND fastmcpp_server_patterns)

add_executable(fastmcpp_server_interactions tests/server/interactions.cpp)
target_link_libraries(fastmcpp_server_interactions PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_server_interactions COMMAND fastmcpp_server_interactions)

add_executable(fastmcpp_server_context_meta tests/server/context_meta.cpp)
target_link_libraries(fastmcpp_server_context_meta PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_server_context_meta COMMAND fastmcpp_server_context_meta)

add_executable(fastmcpp_client_transports tests/client/transports.cpp)
target_link_libraries(fastmcpp_client_transports PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_client_transports COMMAND fastmcpp_client_transports)
Expand Down
10 changes: 6 additions & 4 deletions include/fastmcpp/client/client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -594,10 +594,12 @@ class Client {
if (wrap_result && structured.contains("result")) {
result.data = coerce_to_schema(it->second["properties"]["result"], structured["result"]);
} else if (structured.contains("result")) {
result.data = coerce_to_schema(it->second.contains("properties") && it->second["properties"].contains("result")
? it->second["properties"]["result"]
: fastmcpp::Json::object(),
structured["result"]);
if (it != tool_output_schemas_.end() &&
it->second.contains("properties") && it->second["properties"].contains("result")) {
result.data = coerce_to_schema(it->second["properties"]["result"], structured["result"]);
} else {
result.data = structured["result"];
}
} else {
if (it != tool_output_schemas_.end()) {
result.data = coerce_to_schema(it->second, structured);
Expand Down
10 changes: 10 additions & 0 deletions include/fastmcpp/client/types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ struct CallToolResult {
std::optional<fastmcpp::Json> meta; ///< Request metadata
std::optional<fastmcpp::Json> data; ///< Parsed structured data (if available)
std::optional<fastmcpp::util::schema_type::SchemaValue> typedData; ///< Schema-mapped value

/// Helper to get text from the first TextContent block
std::string text() const {
for (const auto& block : content) {
if (auto* tc = std::get_if<TextContent>(&block)) {
return tc->text;
}
}
return "";
}
};

/// Helper to parse structured data into a concrete type using nlohmann::json conversion
Expand Down
13 changes: 13 additions & 0 deletions include/fastmcpp/server/context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ class Context {
/// Construct a Context with references to resource and prompt managers
Context(const resources::ResourceManager& rm,
const prompts::PromptManager& pm);
Context(const resources::ResourceManager& rm,
const prompts::PromptManager& pm,
std::optional<fastmcpp::Json> request_meta,
std::optional<std::string> request_id = std::nullopt,
std::optional<std::string> session_id = std::nullopt);

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

/// Request metadata accessors (may be unset before MCP session is ready)
const std::optional<fastmcpp::Json>& request_meta() const { return request_meta_; }
const std::optional<std::string>& request_id() const { return request_id_; }
const std::optional<std::string>& session_id() const { return session_id_; }

private:
const resources::ResourceManager* resource_mgr_;
const prompts::PromptManager* prompt_mgr_;
std::optional<fastmcpp::Json> request_meta_;
std::optional<std::string> request_id_;
std::optional<std::string> session_id_;
};

} // namespace fastmcpp::server
2 changes: 1 addition & 1 deletion include/fastmcpp/tools/manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class ToolManager {
return names;
}

const fastmcpp::Json& input_schema_for(const std::string& name) const {
fastmcpp::Json input_schema_for(const std::string& name) const {
return get(name).input_schema();
}

Expand Down
47 changes: 43 additions & 4 deletions include/fastmcpp/tools/tool.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once
#include <string>
#include <functional>
#include <vector>
#include "fastmcpp/types.hpp"

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

Tool() = default;
Tool(std::string name, fastmcpp::Json input_schema, fastmcpp::Json output_schema, Fn fn)
Tool(std::string name,
fastmcpp::Json input_schema,
fastmcpp::Json output_schema,
Fn fn,
std::vector<std::string> exclude_args = {})
: name_(std::move(name)), input_schema_(std::move(input_schema)),
output_schema_(std::move(output_schema)), fn_(std::move(fn)) {}
output_schema_(std::move(output_schema)), fn_(std::move(fn)),
exclude_args_(std::move(exclude_args)) {}

const std::string& name() const { return name_; }
const fastmcpp::Json& input_schema() const { return input_schema_; }
fastmcpp::Json input_schema() const {
if (exclude_args_.empty()) return input_schema_;
return prune_schema(input_schema_);
}
const fastmcpp::Json& output_schema() const { return output_schema_; }
fastmcpp::Json invoke(const fastmcpp::Json& input) const { return fn_(input); }

private:
fastmcpp::Json prune_schema(const fastmcpp::Json& schema) const {
// Work on a copy to avoid mutating shared $defs or properties
fastmcpp::Json pruned = schema;
if (!pruned.is_object()) return pruned;

// Remove excluded properties
if (pruned.contains("properties") && pruned["properties"].is_object()) {
for (const auto& key : exclude_args_) {
pruned["properties"].erase(key);
}
}

// Remove from required list if present
if (pruned.contains("required") && pruned["required"].is_array()) {
auto& req = pruned["required"];
fastmcpp::Json new_req = fastmcpp::Json::array();
for (const auto& item : req) {
if (!item.is_string()) continue;
std::string val = item.get<std::string>();
bool excluded = std::find(exclude_args_.begin(), exclude_args_.end(), val) != exclude_args_.end();
if (!excluded) {
new_req.push_back(val);
}
}
pruned["required"] = new_req;
}

return pruned;
}

std::string name_;
fastmcpp::Json input_schema_;
fastmcpp::Json output_schema_;
Fn fn_;
std::vector<std::string> exclude_args_;
};

} // namespace fastmcpp::tools

12 changes: 11 additions & 1 deletion src/server/context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,17 @@ namespace fastmcpp::server {

Context::Context(const resources::ResourceManager& rm,
const prompts::PromptManager& pm)
: resource_mgr_(&rm), prompt_mgr_(&pm)
: resource_mgr_(&rm), prompt_mgr_(&pm), request_meta_(std::nullopt), request_id_(std::nullopt), session_id_(std::nullopt)
{
}

Context::Context(const resources::ResourceManager& rm,
const prompts::PromptManager& pm,
std::optional<fastmcpp::Json> request_meta,
std::optional<std::string> request_id,
std::optional<std::string> session_id)
: resource_mgr_(&rm), prompt_mgr_(&pm), request_meta_(std::move(request_meta)),
request_id_(std::move(request_id)), session_id_(std::move(session_id))
{
}

Expand Down
54 changes: 38 additions & 16 deletions src/server/middleware.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,22 @@ void ToolInjectionMiddleware::add_prompt_tools(const prompts::PromptManager& pm)

Json prompt_list = Json::array();
for (const auto& [name, prompt] : prompts) {
prompt_list.push_back(Json{
Json prompt_obj = {
{"name", name},
{"template", prompt.template_string()}
});
{"description", prompt.template_string()},
{"arguments", Json::array()},
{"messages", Json::array({
Json{{"role", "user"},
{"content", Json::array({
Json{{"type", "text"}, {"text", prompt.template_string()}}
})}}
})}
};
prompt_list.push_back(prompt_obj);
}

return Json{
{"content", Json::array({
Json{{"type", "text"}, {"text", prompt_list.dump(2)}}
})}
{"prompts", prompt_list}
};
}
);
Expand Down Expand Up @@ -59,10 +65,18 @@ void ToolInjectionMiddleware::add_prompt_tools(const prompts::PromptManager& pm)
Context ctx(resources::ResourceManager(), pm);
std::string rendered = ctx.get_prompt(name, arguments);

Json messages = Json::array({
Json{{"role", "user"},
{"content", Json::array({
Json{{"type", "text"}, {"text", rendered}}
})}}
});

return Json{
{"content", Json::array({
Json{{"type", "text"}, {"text", rendered}}
})}
{"name", name},
{"description", nullptr},
{"arguments", Json::array()},
{"messages", messages}
};
}
);
Expand All @@ -79,22 +93,24 @@ void ToolInjectionMiddleware::add_resource_tools(const resources::ResourceManage
{"required", Json::array()}
},
[&rm](const Json& /*args*/) -> Json {
// Preserve full metadata in MCP-like structure
Context ctx(rm, prompts::PromptManager());
auto resources = ctx.list_resources();

Json resource_list = Json::array();
for (const auto& res : resources) {
resource_list.push_back(Json{
{"uri", res.id.value},
{"kind", resources::to_string(res.kind)},
{"name", res.id.value},
{"description", nullptr},
{"mimeType", res.metadata.value("mimeType", "text/plain")},
{"annotations", res.metadata.value("annotations", Json::object())},
{"metadata", res.metadata}
});
}

return Json{
{"content", Json::array({
Json{{"type", "text"}, {"text", resource_list.dump(2)}}
})}
{"resources", resource_list}
};
}
);
Expand All @@ -116,10 +132,16 @@ void ToolInjectionMiddleware::add_resource_tools(const resources::ResourceManage
Context ctx(rm, prompts::PromptManager());
std::string content = ctx.read_resource(uri);

Json contents = Json::array({
Json{
{"uri", uri},
{"mimeType", "text/plain"},
{"text", content}
}
});

return Json{
{"content", Json::array({
Json{{"type", "text"}, {"text", content}}
})}
{"contents", contents}
};
}
);
Expand Down
16 changes: 8 additions & 8 deletions tests/client/transports.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ void test_http_transport_basic() {
return Json{{"greeting", "Hello, " + in["name"].get<std::string>()}};
});

server::HttpServerWrapper http(srv, "127.0.0.1", 18100);
server::HttpServerWrapper http(srv, "127.0.0.1", 18300);
http.start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));

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

Expand All @@ -86,11 +86,11 @@ void test_http_transport_multiple_requests() {
return Json{{"error", "unknown operation"}};
});

server::HttpServerWrapper http(srv, "127.0.0.1", 18101);
server::HttpServerWrapper http(srv, "127.0.0.1", 18301);
http.start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));

client::HttpTransport transport("127.0.0.1:18101");
client::HttpTransport transport("127.0.0.1:18301");

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

server::HttpServerWrapper http(srv, "127.0.0.1", 18103);
server::HttpServerWrapper http(srv, "127.0.0.1", 18303);
http.start();
std::this_thread::sleep_for(std::chrono::milliseconds(100));

// Multiple clients
client::HttpTransport client1("127.0.0.1:18103");
client::HttpTransport client2("127.0.0.1:18103");
client::HttpTransport client3("127.0.0.1:18103");
client::HttpTransport client1("127.0.0.1:18303");
client::HttpTransport client2("127.0.0.1:18303");
client::HttpTransport client3("127.0.0.1:18303");

auto result1 = client1.request("ping", Json{});
auto result2 = client2.request("ping", Json{});
Expand Down
29 changes: 29 additions & 0 deletions tests/server/context_meta.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#include <cassert>
#include "fastmcpp/resources/manager.hpp"
#include "fastmcpp/prompts/manager.hpp"
#include "fastmcpp/server/context.hpp"

using namespace fastmcpp;

int main() {
resources::ResourceManager rm;
prompts::PromptManager pm;
pm.add("hello", prompts::Prompt{"Hello"});
rm.register_resource(resources::Resource{Id{"file://test"}, resources::Kind::File, Json{}});

// Context without MCP session: request metadata unavailable
server::Context ctx_no_session(rm, pm);
assert(!ctx_no_session.request_id().has_value());
assert(!ctx_no_session.session_id().has_value());
assert(!ctx_no_session.request_meta().has_value());

// Context with MCP session metadata
Json meta = Json{{"progressToken", "tok"}, {"client_id", "cid"}};
server::Context ctx_with_session(rm, pm, meta, std::string{"req"}, std::string{"sess"});
assert(ctx_with_session.request_id().value() == "req");
assert(ctx_with_session.session_id().value() == "sess");
assert(ctx_with_session.request_meta().has_value());
assert(ctx_with_session.request_meta()->value("progressToken", "") == "tok");

return 0;
}
Loading
Loading