11#include <nlohmann/json.hpp>
29 std::unique_ptr<Transport> transport)
30 : name_(std::move(name)),
31 transport_(std::move(transport)) {}
40 if (!transport_->open()) {
41 logger->error(
"Transport open failed for '{}'", name_);
45 if (!send_initialize()) {
46 logger->error(
"MCP initialize failed for '{}'", name_);
52 logger->warn(
"tools/list failed for '{}' — "
53 "connected with 0 tools", name_);
56 logger->info(
"Connected to '{}': {} tools",
57 name_, cached_tool_names_.size());
68 std::lock_guard<std::mutex> lock(tools_mutex_);
69 cached_tools_json_ =
"[]";
70 cached_tool_names_.clear();
71 logger->info(
"Disconnected from '{}'", name_);
81 std::lock_guard<std::mutex> lock(tools_mutex_);
82 return cached_tools_json_;
94 const std::string& tool_name,
95 const std::string& args_json) {
97 if (!transport_->is_connected()) {
98 return build_response(
99 "Server '" + name_ +
"' is disconnected. "
100 "Tool '" + name_ +
"." + tool_name +
"' unavailable.",
104 nlohmann::json params;
105 params[
"name"] = tool_name;
107 params[
"arguments"] = nlohmann::json::parse(args_json);
109 params[
"arguments"] = nlohmann::json::object();
112 auto request = build_request(
"tools/call", params.dump());
113 auto response = transport_->send_request(
114 request, DEFAULT_TIMEOUT_MS);
116 if (response.empty()) {
117 return build_response(
118 "Tool '" + name_ +
"." + tool_name +
119 "' timed out or transport error.",
true);
122 auto result_text = extract_tool_result(response);
123 return build_response(result_text);
135 const std::set<std::string>& a,
const std::set<std::string>& b) {
136 std::vector<std::string> out;
137 std::set_difference(a.begin(), a.end(), b.begin(), b.end(),
138 std::back_inserter(out));
148std::pair<std::vector<std::string>, std::vector<std::string>>
150 auto snapshot = [
this] {
151 std::lock_guard<std::mutex> lock(tools_mutex_);
152 return std::set<std::string>(cached_tool_names_.begin(),
153 cached_tool_names_.end());
156 std::set<std::string> old_names = snapshot();
158 std::set<std::string> new_names = snapshot();
160 auto added =
names_diff(new_names, old_names);
161 auto removed =
names_diff(old_names, new_names);
163 logger->info(
"Server '{}' tools refreshed: +{} -{}",
164 name_, added.size(), removed.size());
165 return {added, removed};
175 return transport_->is_connected();
186std::string ExternalMCPClient::build_request(
187 const std::string& method,
188 const std::string& params) {
191 req[
"jsonrpc"] =
"2.0";
192 req[
"id"] = next_id_++;
193 req[
"method"] = method;
195 req[
"params"] = nlohmann::json::parse(params);
197 req[
"params"] = nlohmann::json::object();
209bool ExternalMCPClient::validate_init_response(
210 const std::string& response) {
213 auto j = nlohmann::json::parse(response);
214 if (j.contains(
"error")) {
215 logger->error(
"Initialize error from '{}': {}",
216 name_, j[
"error"].dump());
231bool ExternalMCPClient::send_initialize() {
232 nlohmann::json params;
233 params[
"protocolVersion"] =
"2024-11-05";
234 params[
"capabilities"] = nlohmann::json::object();
235 params[
"clientInfo"][
"name"] =
"entropic";
236 params[
"clientInfo"][
"version"] =
"1.8.7";
238 auto request = build_request(
"initialize", params.dump());
239 auto response = transport_->send_request(
240 request, INIT_TIMEOUT_MS);
242 if (response.empty()) {
245 return validate_init_response(response);
254bool ExternalMCPClient::query_tools() {
255 auto request = build_request(
"tools/list");
256 auto response = transport_->send_request(
257 request, INIT_TIMEOUT_MS);
259 if (response.empty()) {
264 auto j = nlohmann::json::parse(response);
265 auto tools = j.at(
"result").at(
"tools");
268 std::vector<std::string> names;
269 for (
auto& tool : tools) {
270 std::string orig = tool[
"name"].get<std::string>();
271 tool[
"name"] = name_ +
"." + orig;
272 names.push_back(tool[
"name"].get<std::string>());
275 std::lock_guard<std::mutex> lock(tools_mutex_);
276 cached_tools_json_ = tools.dump();
277 cached_tool_names_ = std::move(names);
279 }
catch (
const nlohmann::json::exception& e) {
280 logger->error(
"Failed to parse tools/list from '{}': {}",
293std::string ExternalMCPClient::extract_tool_result(
294 const std::string& response_json) {
297 auto j = nlohmann::json::parse(response_json);
298 if (j.contains(
"error")) {
299 return "Error: " + j[
"error"][
"message"]
303 auto& content = j.at(
"result").at(
"content");
305 for (
const auto& item : content) {
306 if (item.value(
"type",
"") ==
"text") {
307 text += item.at(
"text").get<std::string>();
311 }
catch (
const nlohmann::json::exception& e) {
312 return "Error parsing response: " + std::string(e.what());
324std::string ExternalMCPClient::build_response(
325 const std::string& result_text,
329 resp[
"result"] = result_text;
332 resp[
"directives"] = nlohmann::json::array();
334 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).
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).