11#include <nlohmann/json.hpp>
29 std::unique_ptr<Transport> transport)
30 : name_(std::move(name)),
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_;
108 const std::string& tool_name,
109 const std::string& args_json) {
111 if (!transport_->is_connected()) {
112 return build_response(
113 "Server '" + name_ +
"' is disconnected. "
114 "Tool '" + name_ +
"." + tool_name +
"' unavailable.",
118 nlohmann::json params;
119 params[
"name"] = tool_name;
121 params[
"arguments"] = nlohmann::json::parse(args_json);
123 params[
"arguments"] = nlohmann::json::object();
126 auto request = build_request(
"tools/call", params.dump());
127 auto response = transport_->send_request(
130 if (response.empty()) {
131 return build_response(
132 "Tool '" + name_ +
"." + tool_name +
133 "' timed out or transport error.",
true);
136 auto result_text = extract_tool_result(response);
137 return build_response(result_text);
149 const std::set<std::string>& a,
const std::set<std::string>& b) {
150 std::vector<std::string> out;
151 std::set_difference(a.begin(), a.end(), b.begin(), b.end(),
152 std::back_inserter(out));
167std::pair<std::vector<std::string>, std::vector<std::string>>
169 auto snapshot = [
this] {
170 std::lock_guard<std::mutex> lock(tools_mutex_);
171 return std::set<std::string>(cached_tool_names_.begin(),
172 cached_tool_names_.end());
175 std::set<std::string> old_names = snapshot();
177 std::set<std::string> new_names = snapshot();
179 auto added =
names_diff(new_names, old_names);
180 auto removed =
names_diff(old_names, new_names);
182 logger->info(
"Server '{}' tools refreshed: +{} -{}",
183 name_, added.size(), removed.size());
184 return {added, removed};
194 return transport_->is_connected();
207std::string ExternalMCPClient::build_request(
208 const std::string& method,
209 const std::string& params) {
212 req[
"jsonrpc"] =
"2.0";
213 req[
"id"] = next_id_++;
214 req[
"method"] = method;
216 req[
"params"] = nlohmann::json::parse(params);
218 req[
"params"] = nlohmann::json::object();
230bool ExternalMCPClient::validate_init_response(
231 const std::string& response) {
234 auto j = nlohmann::json::parse(response);
235 if (j.contains(
"error")) {
236 logger->error(
"Initialize error from '{}': {}",
237 name_, j[
"error"].dump());
254bool ExternalMCPClient::send_initialize() {
255 nlohmann::json params;
256 params[
"protocolVersion"] =
"2024-11-05";
257 params[
"capabilities"] = nlohmann::json::object();
258 params[
"clientInfo"][
"name"] =
"entropic";
259 params[
"clientInfo"][
"version"] =
"1.8.7";
261 auto request = build_request(
"initialize", params.dump());
262 auto response = transport_->send_request(
265 if (response.empty()) {
268 return validate_init_response(response);
283bool ExternalMCPClient::query_tools() {
284 auto request = build_request(
"tools/list");
285 auto response = transport_->send_request(
288 if (response.empty()) {
293 auto j = nlohmann::json::parse(response);
294 auto tools = j.at(
"result").at(
"tools");
297 std::vector<std::string> names;
298 for (
auto& tool : tools) {
299 std::string orig = tool[
"name"].get<std::string>();
300 tool[
"name"] = name_ +
"." + orig;
301 names.push_back(tool[
"name"].get<std::string>());
304 std::lock_guard<std::mutex> lock(tools_mutex_);
305 cached_tools_json_ = tools.dump();
306 cached_tool_names_ = std::move(names);
308 }
catch (
const nlohmann::json::exception& e) {
309 logger->error(
"Failed to parse tools/list from '{}': {}",
322std::string ExternalMCPClient::extract_tool_result(
323 const std::string& response_json) {
326 auto j = nlohmann::json::parse(response_json);
327 if (j.contains(
"error")) {
328 return "Error: " + j[
"error"][
"message"]
332 auto& content = j.at(
"result").at(
"content");
334 for (
const auto& item : content) {
335 if (item.value(
"type",
"") ==
"text") {
336 text += item.at(
"text").get<std::string>();
340 }
catch (
const nlohmann::json::exception& e) {
341 return "Error parsing response: " + std::string(e.what());
362std::string ExternalMCPClient::build_response(
363 const std::string& result_text,
367 resp[
"result"] = result_text;
370 resp[
"directives"] = nlohmann::json::array();
372 resp[
"is_error"] =
true;
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.
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).
@ 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).