Skip to content
Merged
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
6 changes: 0 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,6 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
build_type: [Debug, Release]
include:
# ARM64 Linux builds
- os: ubuntu-24.04-arm
build_type: Debug
- os: ubuntu-24.04-arm
build_type: Release

runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} (${{ matrix.build_type }})
Expand Down
12 changes: 11 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.16)
project(fastmcpp VERSION 2.14.0 LANGUAGES CXX)
project(fastmcpp VERSION 2.14.1 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
Expand All @@ -18,6 +18,7 @@ add_library(fastmcpp_core
src/app.cpp
src/proxy.cpp
src/mcp/handler.cpp
src/mcp/tasks.cpp
src/resources/resource.cpp
src/resources/manager.cpp
src/resources/template.cpp
Expand All @@ -30,6 +31,7 @@ add_library(fastmcpp_core
src/server/context.cpp
src/server/middleware.cpp
src/server/security_middleware.cpp
src/server/sampling.cpp
src/server/http_server.cpp
src/server/stdio_server.cpp
src/server/sse_server.cpp
Expand Down Expand Up @@ -231,6 +233,10 @@ if(FASTMCPP_BUILD_TESTS)
target_link_libraries(fastmcpp_sse_mcp_format PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_sse_mcp_format COMMAND fastmcpp_sse_mcp_format)

add_executable(fastmcpp_sse_tasks_notifications tests/server/sse_tasks_notifications.cpp)
target_link_libraries(fastmcpp_sse_tasks_notifications PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_sse_tasks_notifications COMMAND fastmcpp_sse_tasks_notifications)

# Advanced test suites (Task 3.4)
add_executable(fastmcpp_tools_validation tests/tools/validation.cpp)
target_link_libraries(fastmcpp_tools_validation PRIVATE fastmcpp_core)
Expand Down Expand Up @@ -288,6 +294,10 @@ if(FASTMCPP_BUILD_TESTS)
target_link_libraries(fastmcpp_server_context_sampling PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_server_context_sampling COMMAND fastmcpp_server_context_sampling)

add_executable(fastmcpp_server_sampling_tools tests/server/test_sampling_tools.cpp)
target_link_libraries(fastmcpp_server_sampling_tools PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_server_sampling_tools COMMAND fastmcpp_server_sampling_tools)

add_executable(fastmcpp_server_elicitation_defaults tests/server/test_elicitation_defaults.cpp)
target_link_libraries(fastmcpp_server_elicitation_defaults PRIVATE fastmcpp_core)
add_test(NAME fastmcpp_server_elicitation_defaults COMMAND fastmcpp_server_elicitation_defaults)
Expand Down
15 changes: 15 additions & 0 deletions include/fastmcpp/app.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>

namespace fastmcpp
Expand All @@ -20,13 +21,17 @@ struct MountedApp
{
std::string prefix; // Prefix for tools/prompts (e.g., "weather")
class FastMCP* app; // Non-owning pointer to mounted app
std::optional<std::unordered_map<std::string, std::string>>
tool_names; // Optional tool name overrides
};

/// Proxy-mounted app with prefix (proxy mode)
struct ProxyMountedApp
{
std::string prefix; // Prefix for tools/prompts
std::unique_ptr<ProxyApp> proxy; // Owning pointer to proxy wrapper
std::optional<std::unordered_map<std::string, std::string>>
tool_names; // Optional tool name overrides
};

/// MCP Application - bundles server metadata with managers
Expand Down Expand Up @@ -125,8 +130,14 @@ class FastMCP
/// @param app The app to mount (must outlive this app in direct mode)
/// @param prefix Optional prefix (empty string = no prefix)
/// @param as_proxy If true, mount in proxy mode (uses MCP handler for communication)
/// @param tool_names Optional mapping of original tool names to custom names. Keys are the
/// original tool names from the mounted server (after any nested prefixing).
void mount(FastMCP& app, const std::string& prefix = "", bool as_proxy = false);

/// Mount another app with optional tool name overrides.
void mount(FastMCP& app, const std::string& prefix, bool as_proxy,
std::optional<std::unordered_map<std::string, std::string>> tool_names);

/// Get list of directly mounted apps
const std::vector<MountedApp>& mounted() const
{
Expand Down Expand Up @@ -173,6 +184,10 @@ class FastMCP
/// Get prompt messages by name (handles prefixed routing)
std::vector<prompts::PromptMessage> get_prompt(const std::string& name, const Json& args) const;

/// Get prompt result by name (handles prefixed routing)
/// Includes description and optional _meta parity with Python SDK (fastmcp 2.14.1+).
prompts::PromptResult get_prompt_result(const std::string& name, const Json& args) const;

private:
server::Server server_;
tools::ToolManager tools_;
Expand Down
11 changes: 8 additions & 3 deletions include/fastmcpp/mcp/handler.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class SseServerWrapper;
namespace fastmcpp::mcp
{

/// Session accessor callback type - retrieves ServerSession for a session_id
using SessionAccessor = std::function<std::shared_ptr<server::ServerSession>(const std::string&)>;

// Factory that produces a JSON-RPC handler compatible with ClaudeOptions::sdk_mcp_handlers.
// It supports a subset of MCP methods needed for in-process tools:
// - "initialize"
Expand Down Expand Up @@ -62,13 +65,15 @@ make_mcp_handler(const std::string& server_name, const std::string& version,
// Uses app's aggregated lists and routing for mounted sub-apps
std::function<fastmcpp::Json(const fastmcpp::Json&)> make_mcp_handler(const FastMCP& app);

// Overload: FastMCP handler with session access.
// Enables server-initiated features (e.g., task status push) keyed by params._meta.session_id.
std::function<fastmcpp::Json(const fastmcpp::Json&)>
make_mcp_handler(const FastMCP& app, SessionAccessor session_accessor);

// MCP handler from ProxyApp - supports proxying to backend server
// Uses app's aggregated lists (local + remote) and routing
std::function<fastmcpp::Json(const fastmcpp::Json&)> make_mcp_handler(const ProxyApp& app);

/// Session accessor callback type - retrieves ServerSession for a session_id
using SessionAccessor = std::function<std::shared_ptr<server::ServerSession>(const std::string&)>;

/// MCP handler with sampling support
/// The session_accessor callback is used to get ServerSession for sampling requests.
/// Session ID is extracted from params._meta.session_id (injected by SSE server).
Expand Down
26 changes: 26 additions & 0 deletions include/fastmcpp/mcp/tasks.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#pragma once

#include <string>

namespace fastmcpp::mcp::tasks
{

/// Report a status message for the currently executing background task (SEP-1686).
///
/// This sends best-effort `notifications/tasks/status` updates (via the transport/session)
/// when called from within a task execution context created by `mcp::make_mcp_handler(...)`.
///
/// No-op if called outside a background task context.
void report_status_message(const std::string& message);

namespace detail
{
using StatusMessageFn = void (*)(void* ctx, const std::string& task_id, const std::string& message);

// Internal: set/clear the task context for the current thread.
// Used by the MCP task execution runtime (TaskRegistry).
void set_current_task(void* ctx, StatusMessageFn fn, std::string task_id);
void clear_current_task();
} // namespace detail

} // namespace fastmcpp::mcp::tasks
10 changes: 10 additions & 0 deletions include/fastmcpp/prompts/prompt.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,21 @@ struct PromptMessage
std::string content; // Message content
};

/// Result of prompts/get (prompt rendering)
struct PromptResult
{
std::vector<PromptMessage> messages;
std::optional<std::string> description;
std::optional<fastmcpp::Json> meta; // Returned as _meta in MCP prompts/get
};

/// MCP Prompt definition
struct Prompt
{
std::string name;
std::optional<std::string> description;
std::optional<fastmcpp::Json>
meta; // Optional prompt metadata (returned as _meta in prompts/get)
std::vector<PromptArgument> arguments;
std::function<std::vector<PromptMessage>(const Json&)> generator; // Message generator
fastmcpp::TaskSupport task_support{fastmcpp::TaskSupport::Forbidden}; // SEP-1686 task mode
Expand Down
80 changes: 80 additions & 0 deletions include/fastmcpp/server/sampling.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#pragma once
#include "fastmcpp/server/session.hpp"
#include "fastmcpp/types.hpp"

#include <chrono>
#include <functional>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>

namespace fastmcpp::server::sampling
{

// ---------------------------------------------------------------------------
// SEP-1577 sampling-with-tools helpers (server-initiated sampling/createMessage)
// ---------------------------------------------------------------------------

struct Tool
{
std::string name;
std::optional<std::string> description;
fastmcpp::Json input_schema{fastmcpp::Json::object()};
std::function<fastmcpp::Json(const fastmcpp::Json&)> fn;
};

struct Message
{
std::string role; // "user" or "assistant"
fastmcpp::Json content; // MCP SamplingMessageContentBlock or list thereof
};

inline Message make_text_message(const std::string& role, const std::string& text)
{
return Message{role, fastmcpp::Json{{"type", "text"}, {"text", text}}};
}

struct Options
{
std::optional<std::string> system_prompt;
std::optional<float> temperature;
int max_tokens{512};
std::optional<fastmcpp::Json> model_preferences;
std::optional<std::vector<std::string>> stop_sequences;
std::optional<fastmcpp::Json> metadata;

std::optional<std::vector<Tool>> tools;
// Simplified tool choice: "auto", "required", or "none"
std::optional<std::string> tool_choice;

bool execute_tools{true};
bool mask_error_details{false};
int max_iterations{10};
std::chrono::milliseconds timeout{ServerSession::DEFAULT_TIMEOUT};
};

struct Step
{
fastmcpp::Json response; // CreateMessageResult(+WithTools) JSON
std::vector<Message> history;

bool is_tool_use() const;
std::optional<std::string> text() const;
std::vector<fastmcpp::Json> tool_calls() const;
};

struct Result
{
std::optional<std::string> text;
fastmcpp::Json response;
std::vector<Message> history;
};

Step sample_step(std::shared_ptr<ServerSession> session, const std::vector<Message>& messages,
const Options& options);

Result sample(std::shared_ptr<ServerSession> session, const std::vector<Message>& messages,
Options options);

} // namespace fastmcpp::server::sampling
23 changes: 22 additions & 1 deletion include/fastmcpp/server/session.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,17 @@ class ServerSession
capabilities_ = capabilities;

// Parse common capability flags
supports_sampling_ = false;
supports_sampling_tools_ = false;
supports_elicitation_ = false;
supports_roots_ = false;
if (capabilities.contains("sampling") && capabilities["sampling"].is_object())
{
supports_sampling_ = true;
const auto& sampling = capabilities["sampling"];
if (sampling.contains("tools") && sampling["tools"].is_object())
supports_sampling_tools_ = true;
}
if (capabilities.contains("elicitation") && capabilities["elicitation"].is_object())
supports_elicitation_ = true;
if (capabilities.contains("roots") && capabilities["roots"].is_object())
Expand All @@ -118,6 +127,13 @@ class ServerSession
return supports_sampling_;
}

/// Check if client supports sampling with tools (sampling.tools capability)
bool supports_sampling_tools() const
{
std::lock_guard lock(cap_mutex_);
return supports_sampling_tools_;
}

/// Check if client supports elicitation
bool supports_elicitation() const
{
Expand Down Expand Up @@ -262,10 +278,14 @@ class ServerSession
*
* @param method The JSON-RPC method name (e.g., "notifications/progress")
* @param params Notification parameters
* @param meta Optional top-level _meta for the notification
*/
void send_notification(const std::string& method, const Json& params = Json::object())
void send_notification(const std::string& method, const Json& params = Json::object(),
const std::optional<Json>& meta = std::nullopt)
{
Json notification = {{"jsonrpc", "2.0"}, {"method", method}, {"params", params}};
if (meta.has_value() && meta->is_object() && !meta->empty())
notification["_meta"] = *meta;

if (send_callback_)
send_callback_(notification);
Expand Down Expand Up @@ -331,6 +351,7 @@ class ServerSession
bool supports_sampling_{false};
bool supports_elicitation_{false};
bool supports_roots_{false};
bool supports_sampling_tools_{false};

// Pending requests
std::mutex pending_mutex_;
Expand Down
Loading
Loading