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
44 changes: 40 additions & 4 deletions examples/mcp/mcp_example_server.cc
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ GetPromptResult getSamplePrompt(const std::string& name,
void setupServer(McpServer& server, bool verbose) {
g_logger->info("Configuring server resources and tools...");

// Register sample resources
// Register sample resources with read handlers that produce actual content
{
// Configuration resource
Resource config_resource;
Expand All @@ -533,7 +533,19 @@ void setupServer(McpServer& server, bool verbose) {
config_resource.mimeType =
mcp::make_optional(std::string("application/json"));

server.registerResource(config_resource);
server.registerResource(
config_resource,
[](const std::string& uri,
SessionContext& /*session*/) -> ReadResourceResult {
ReadResourceResult result;
TextResourceContents content;
content.uri = mcp::make_optional(uri);
content.mimeType =
mcp::make_optional(std::string("application/json"));
content.text = R"({"logLevel":"info","maxConnections":100})";
result.contents.push_back(content);
return result;
});

// Log resource
Resource log_resource;
Expand All @@ -543,7 +555,18 @@ void setupServer(McpServer& server, bool verbose) {
mcp::make_optional(std::string("Real-time server event log"));
log_resource.mimeType = mcp::make_optional(std::string("text/plain"));

server.registerResource(log_resource);
server.registerResource(
log_resource,
[](const std::string& uri,
SessionContext& /*session*/) -> ReadResourceResult {
ReadResourceResult result;
TextResourceContents content;
content.uri = mcp::make_optional(uri);
content.mimeType = mcp::make_optional(std::string("text/plain"));
content.text = "[2026-04-03T00:00:00Z] Server started\n";
result.contents.push_back(content);
return result;
});

// Metrics resource
Resource metrics_resource;
Expand All @@ -554,7 +577,20 @@ void setupServer(McpServer& server, bool verbose) {
metrics_resource.mimeType =
mcp::make_optional(std::string("application/json"));

server.registerResource(metrics_resource);
server.registerResource(
metrics_resource,
[](const std::string& uri,
SessionContext& /*session*/) -> ReadResourceResult {
ReadResourceResult result;
TextResourceContents content;
content.uri = mcp::make_optional(uri);
content.mimeType =
mcp::make_optional(std::string("application/json"));
content.text =
R"({"uptime_seconds":3600,"requests_total":1024,"errors_total":3})";
result.contents.push_back(content);
return result;
});

if (verbose) {
g_logger->debug("Registered 3 static resources");
Expand Down
15 changes: 13 additions & 2 deletions examples/mcp/mcp_https_example.cc
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,24 @@ class HttpsMcpServer {

private:
void registerHandlers() {
// Register example resource
// Register example resource with a read handler
Resource example_resource;
example_resource.uri = "file:///example.txt";
example_resource.name = "Example Resource";
example_resource.description = "An example resource over HTTPS";
example_resource.mimeType = "text/plain";
server_->registerResource(example_resource);
server_->registerResource(
example_resource,
[](const std::string& uri,
server::SessionContext& /*session*/) -> ReadResourceResult {
ReadResourceResult result;
TextResourceContents content;
content.uri = mcp::make_optional(uri);
content.mimeType = mcp::make_optional(std::string("text/plain"));
content.text = "Hello from the example HTTPS resource!";
result.contents.push_back(content);
return result;
});

// Register example tool
Tool example_tool;
Expand Down
57 changes: 40 additions & 17 deletions include/mcp/server/mcp_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,21 @@ class NotificationHandler {
*/
class ResourceManager {
public:
// Handler invoked on resources/read to produce the actual content.
// Receives the URI so a single handler can serve multiple resources.
using ResourceReadHandler = std::function<ReadResourceResult(
const std::string& uri, SessionContext& session)>;

ResourceManager(McpServerStats& stats) : stats_(stats) {}

// Register a resource
// Register a resource with a read handler that supplies content on read.
void registerResource(const Resource& resource, ResourceReadHandler handler) {
std::lock_guard<std::mutex> lock(mutex_);
resources_[resource.uri] = resource;
resource_handlers_[resource.uri] = handler;
}

// Register a resource without a read handler (metadata-only, e.g. for list).
void registerResource(const Resource& resource) {
std::lock_guard<std::mutex> lock(mutex_);
resources_[resource.uri] = resource;
Expand Down Expand Up @@ -315,24 +327,26 @@ class ResourceManager {
return result;
}

// Read resource content
ReadResourceResult readResource(const std::string& uri) {
// Read resource content by delegating to the registered handler.
// Returns an empty result when the URI is unknown, and throws if
// the resource was registered without a read handler.
ReadResourceResult readResource(const std::string& uri,
SessionContext& session) {
std::lock_guard<std::mutex> lock(mutex_);
ReadResourceResult result;

auto it = resources_.find(uri);
if (it != resources_.end()) {
// Create text content for the resource
TextResourceContents content;
content.uri = uri;
content.mimeType = it->second.mimeType;
content.text = "Resource content for: " +
uri; // Actual implementation would read real content

result.contents.push_back(content);
stats_.resources_served++;

auto res_it = resources_.find(uri);
if (res_it == resources_.end()) {
return ReadResourceResult{}; // unknown resource
}

auto handler_it = resource_handlers_.find(uri);
if (handler_it == resource_handlers_.end()) {
throw std::runtime_error("Resource registered without a read handler: " +
uri);
}

auto result = handler_it->second(uri, session);
stats_.resources_served++;
return result;
}

Expand Down Expand Up @@ -379,6 +393,7 @@ class ResourceManager {
private:
mutable std::mutex mutex_;
std::map<std::string, Resource> resources_;
std::map<std::string, ResourceReadHandler> resource_handlers_;
std::vector<ResourceTemplate> resource_templates_;
std::map<std::string, std::set<std::string>>
subscriptions_; // uri -> session_ids
Expand Down Expand Up @@ -694,7 +709,15 @@ class McpServer : public application::ApplicationBase,
std::function<void(const jsonrpc::Notification&, SessionContext&)>
handler);

// Resource management
// Resource management — register with a read handler for resources/read
void registerResource(
const Resource& resource,
std::function<ReadResourceResult(const std::string&, SessionContext&)>
handler) {
resource_manager_->registerResource(resource, handler);
}

// Register metadata only (appears in resources/list but has no read handler)
void registerResource(const Resource& resource) {
resource_manager_->registerResource(resource);
}
Expand Down
25 changes: 12 additions & 13 deletions src/server/mcp_server.cc
Original file line number Diff line number Diff line change
Expand Up @@ -926,19 +926,18 @@ jsonrpc::Response McpServer::handleReadResource(const jsonrpc::Request& request,

std::string uri = get<std::string>(uri_it->second);

// Read resource
auto result = resource_manager_->readResource(uri);

// Convert to response
// TODO: Serialize ReadResourceResult to ResponseResult
auto response_metadata =
make<Metadata>()
.add("uri", uri)
.add("contentCount", static_cast<int64_t>(result.contents.size()))
.build();

return jsonrpc::Response::success(request.id,
jsonrpc::ResponseResult(response_metadata));
// Delegate to the registered read handler via ResourceManager.
// The handler produces a ReadResourceResult with properly populated contents.
try {
auto result = resource_manager_->readResource(uri, session);
auto result_json = json::to_json(result);
return jsonrpc::Response::success(request.id,
jsonrpc::ResponseResult(result_json));
} catch (const std::exception& e) {
return jsonrpc::Response::make_error(
request.id, Error(jsonrpc::INVALID_PARAMS,
std::string("Resource read failed: ") + e.what()));
}
}

jsonrpc::Response McpServer::handleSubscribe(const jsonrpc::Request& request,
Expand Down
10 changes: 10 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ add_executable(test_template_serialization json/test_template_serialization.cc)
add_executable(test_short_json_api json/test_short_json_api.cc)
# Server tests
add_executable(test_mcp_server_responses server/test_mcp_server_responses.cc)
add_executable(test_resource_read server/test_resource_read.cc)
# CORS tests
add_executable(test_cors_headers filter/test_cors_headers.cc)
add_executable(test_options_notification filter/test_options_notification.cc)
Expand Down Expand Up @@ -245,6 +246,14 @@ target_link_libraries(test_mcp_server_responses
Threads::Threads
)

target_link_libraries(test_resource_read
gopher-mcp
gtest
gtest_main
Threads::Threads
fmt::fmt
)

target_link_libraries(test_cors_headers
gtest
gtest_main
Expand Down Expand Up @@ -1264,6 +1273,7 @@ add_test(NAME McpSerializationTest COMMAND test_mcp_serialization)
add_test(NAME McpSerializationExtensiveTest COMMAND test_mcp_serialization_extensive)
add_test(NAME TemplateSerializationTest COMMAND test_template_serialization)
add_test(NAME McpServerResponsesTest COMMAND test_mcp_server_responses)
add_test(NAME ResourceReadTest COMMAND test_resource_read)
add_test(NAME CorsHeadersTest COMMAND test_cors_headers)
add_test(NAME OptionsNotificationTest COMMAND test_options_notification)
add_test(NAME EventLoopTest COMMAND test_event_loop)
Expand Down
Loading
Loading