11#include <nlohmann/json.hpp>
29 std::unique_ptr<Transport> transport)
31 transport_(
std::
move(transport)) {}
46 if (!transport_->open()) {
47 logger->error(
"Transport open failed for '{}'", name_);
51 if (!send_initialize()) {
52 logger->error(
"MCP initialize failed for '{}'", name_);
58 logger->warn(
"tools/list failed for '{}' — "
59 "connected with 0 tools", name_);
62 logger->info(
"Connected to '{}': {} tools",
63 name_, cached_tool_names_.size());
74 std::lock_guard<std::mutex> lock(tools_mutex_);
75 cached_tools_json_ =
"[]";
76 cached_tool_names_.clear();
77 logger->info(
"Disconnected from '{}'", name_);
90 std::lock_guard<std::mutex> lock(tools_mutex_);
91 return cached_tools_json_;
112std::string ExternalMCPClient::empty_response_envelope(
113 const std::string& tool_name)
const {
114 if (transport_->is_interrupted()) {
115 return build_response(
116 "Error: tool '" + name_ +
"." + tool_name +
117 "' was not attempted — the transport is interrupted. "
118 "The interrupt is released at the start of the next run.",
121 return build_response(
122 "Error: tool '" + name_ +
"." + tool_name +
123 "' timed out or the transport failed.",
true);
141 const std::string& tool_name,
144 if (!transport_->is_connected()) {
152 return build_response(
153 "Error: server '" + name_ +
"' is disconnected. "
154 "Tool '" + name_ +
"." + tool_name +
"' unavailable.",
159 params[
"name"] = tool_name;
163 params[
"arguments"] = nlohmann::json::object();
167 auto request = build_request_id(
169 auto response = transport_->send_request(
199std::string ExternalMCPClient::response_problem(
200 const std::string& tool_name,
204 return empty_response_envelope(tool_name);
207 return mismatched_response_envelope(tool_name,
expected_id);
230std::string ExternalMCPClient::mismatched_response_envelope(
231 const std::string& tool_name,
int expected_id)
const {
232 logger->error(
"Server '{}' answered request {} with a response carrying "
233 "a different id; discarding it rather than returning "
234 "another request's result for tool '{}'",
236 return build_response(
237 "Error: tool '" + name_ +
"." + tool_name +
238 "' got a response that did not answer it (JSON-RPC id mismatch). "
239 "The transport had a stale reply queued from an abandoned request; "
240 "it has been discarded. Retry the call.",
true);
252 const std::set<std::string>&
a,
const std::set<std::string>&
b) {
253 std::vector<std::string> out;
254 std::set_difference(
a.begin(),
a.end(),
b.begin(),
b.end(),
255 std::back_inserter(out));
270std::pair<std::vector<std::string>, std::vector<std::string>>
272 auto snapshot = [
this] {
273 std::lock_guard<std::mutex> lock(tools_mutex_);
274 return std::set<std::string>(cached_tool_names_.begin(),
275 cached_tool_names_.end());
278 std::set<std::string>
old_names = snapshot();
280 std::set<std::string>
new_names = snapshot();
285 logger->info(
"Server '{}' tools refreshed: +{} -{}",
297 return transport_->is_connected();
310std::string ExternalMCPClient::build_request(
311 const std::string&
method,
312 const std::string&
params) {
328std::string ExternalMCPClient::build_request_id(
329 const std::string&
method,
330 const std::string&
params,
333 id = next_id_.fetch_add(1, std::memory_order_relaxed);
335 req[
"jsonrpc"] =
"2.0";
339 req[
"params"] = nlohmann::json::parse(
params);
341 req[
"params"] = nlohmann::json::object();
356bool ExternalMCPClient::response_matches(
365 if (
j.value(
"id", nlohmann::json()).is_string()) {
366 return j[
"id"].get<std::string>()
369 return j.contains(
"id") &&
j[
"id"].is_number_integer()
371 }
catch (
const nlohmann::json::exception&) {
383bool ExternalMCPClient::validate_init_response(
387 auto j = nlohmann::json::parse(
response);
388 if (
j.contains(
"error")) {
389 logger->error(
"Initialize error from '{}': {}",
390 name_,
j[
"error"].
dump());
408bool ExternalMCPClient::send_initialize() {
410 params[
"protocolVersion"] =
"2024-11-05";
411 params[
"capabilities"] = nlohmann::json::object();
412 params[
"clientInfo"][
"name"] =
"entropic";
413 params[
"clientInfo"][
"version"] =
"1.8.7";
416 auto request = build_request_id(
418 auto response = transport_->send_request(
424 return validate_init_response(
response);
440bool ExternalMCPClient::query_tools() {
443 auto response = transport_->send_request(
451 auto j = nlohmann::json::parse(
response);
452 auto tools =
j.at(
"result").at(
"tools");
455 std::vector<std::string>
names;
456 for (
auto& tool : tools) {
457 std::string
orig = tool[
"name"].get<std::string>();
458 tool[
"name"] = name_ +
"." +
orig;
462 std::lock_guard<std::mutex> lock(tools_mutex_);
463 cached_tools_json_ = tools.dump();
464 cached_tool_names_ = std::move(
names);
466 }
catch (
const nlohmann::json::exception&
e) {
467 logger->error(
"Failed to parse tools/list from '{}': {}",
487std::string ExternalMCPClient::extract_tool_result(
492 if (
j.contains(
"error")) {
493 return "Error: " +
j[
"error"][
"message"].get<std::string>();
495 const auto& result =
j.at(
"result");
496 for (
const auto&
item : result.
at(
"content")) {
497 if (
item.value(
"type",
"") ==
"text") {
498 text +=
item.at(
"text").get<std::string>();
501 if (result.value(
"isError",
false)) { text =
"Error: " + text; }
502 }
catch (
const nlohmann::json::exception&
e) {
503 text =
"Error parsing response: " + std::string(
e.what());
525std::string ExternalMCPClient::build_response(
533 resp[
"directives"] = nlohmann::json::array();
535 resp[
"is_error"] =
true;
547 if (
ms > 0) { init_timeout_ms_ =
ms; }
std::string list_tools() const
List tools as JSON array string (cached).
std::pair< std::vector< std::string >, std::vector< std::string > > refresh_tools()
Re-query tools/list and diff against cache.
void set_init_timeout_ms(uint32_t ms)
Bound for bringing the server up: initialize + tools/list (gh#175, v2.14.0).
bool is_connected() const
Check connection state.
bool connect()
Connect: open transport + MCP initialize + tools/list.
ExternalMCPClient(std::string name, std::unique_ptr< Transport > transport)
Construct with name and transport.
std::string execute(const std::string &tool_name, const std::string &args_json)
Execute a tool call via the external server.
void disconnect()
Disconnect: close transport.
Client for communicating with external MCP servers.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
@ optional
optional: every escape asks the host's path approver
@ request
GenerationParams::grammar (COMMON_GRAMMAR_TYPE_USER)
static std::vector< std::string > names_diff(const std::set< std::string > &a, const std::set< std::string > &b)
Names in a not in b (sorted set difference).