Entropic 2.11.1
Local-first agentic inference engine
Loading...
Searching...
No Matches
external_client.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
10
11#include <nlohmann/json.hpp>
12
13#include <algorithm>
14#include <set>
15
16static auto logger = entropic::log::get("mcp.external_client");
17
18namespace entropic {
19
28 std::string name,
29 std::unique_ptr<Transport> transport)
30 : name_(std::move(name)),
31 transport_(std::move(transport)) {}
32
46 if (!transport_->open()) {
47 logger->error("Transport open failed for '{}'", name_);
48 return false;
49 }
50
51 if (!send_initialize()) {
52 logger->error("MCP initialize failed for '{}'", name_);
53 transport_->close();
54 return false;
55 }
56
57 if (!query_tools()) {
58 logger->warn("tools/list failed for '{}' — "
59 "connected with 0 tools", name_);
60 }
61
62 logger->info("Connected to '{}': {} tools",
63 name_, cached_tool_names_.size());
64 return true;
65}
66
73 transport_->close();
74 std::lock_guard<std::mutex> lock(tools_mutex_);
75 cached_tools_json_ = "[]";
76 cached_tool_names_.clear();
77 logger->info("Disconnected from '{}'", name_);
78}
79
89std::string ExternalMCPClient::list_tools() const {
90 std::lock_guard<std::mutex> lock(tools_mutex_);
91 return cached_tools_json_;
92}
93
108 const std::string& tool_name,
109 const std::string& args_json) {
110
111 if (!transport_->is_connected()) {
112 return build_response(
113 "Server '" + name_ + "' is disconnected. "
114 "Tool '" + name_ + "." + tool_name + "' unavailable.",
115 true);
116 }
117
118 nlohmann::json params;
119 params["name"] = tool_name;
120 try {
121 params["arguments"] = nlohmann::json::parse(args_json);
122 } catch (...) {
123 params["arguments"] = nlohmann::json::object();
124 }
125
126 auto request = build_request("tools/call", params.dump());
127 auto response = transport_->send_request(
128 request, DEFAULT_TIMEOUT_MS);
129
130 if (response.empty()) {
131 return build_response(
132 "Tool '" + name_ + "." + tool_name +
133 "' timed out or transport error.", true);
134 }
135
136 auto result_text = extract_tool_result(response);
137 return build_response(result_text);
138}
139
148static std::vector<std::string> names_diff(
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));
153 return out;
154}
155
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());
173 };
174
175 std::set<std::string> old_names = snapshot();
176 query_tools();
177 std::set<std::string> new_names = snapshot();
178
179 auto added = names_diff(new_names, old_names);
180 auto removed = names_diff(old_names, new_names);
181
182 logger->info("Server '{}' tools refreshed: +{} -{}",
183 name_, added.size(), removed.size());
184 return {added, removed};
185}
186
194 return transport_->is_connected();
195}
196
207std::string ExternalMCPClient::build_request(
208 const std::string& method,
209 const std::string& params) {
210
211 nlohmann::json req;
212 req["jsonrpc"] = "2.0";
213 req["id"] = next_id_++;
214 req["method"] = method;
215 try {
216 req["params"] = nlohmann::json::parse(params);
217 } catch (...) {
218 req["params"] = nlohmann::json::object();
219 }
220 return req.dump();
221}
222
230bool ExternalMCPClient::validate_init_response(
231 const std::string& response) {
232
233 try {
234 auto j = nlohmann::json::parse(response);
235 if (j.contains("error")) {
236 logger->error("Initialize error from '{}': {}",
237 name_, j["error"].dump());
238 return false;
239 }
240 return true;
241 } catch (...) {
242 return false;
243 }
244}
245
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";
260
261 auto request = build_request("initialize", params.dump());
262 auto response = transport_->send_request(
263 request, INIT_TIMEOUT_MS);
264
265 if (response.empty()) {
266 return false;
267 }
268 return validate_init_response(response);
269}
270
283bool ExternalMCPClient::query_tools() {
284 auto request = build_request("tools/list");
285 auto response = transport_->send_request(
286 request, INIT_TIMEOUT_MS);
287
288 if (response.empty()) {
289 return false;
290 }
291
292 try {
293 auto j = nlohmann::json::parse(response);
294 auto tools = j.at("result").at("tools");
295
296 // Prefix tool names with server name
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>());
302 }
303
304 std::lock_guard<std::mutex> lock(tools_mutex_);
305 cached_tools_json_ = tools.dump();
306 cached_tool_names_ = std::move(names);
307 return true;
308 } catch (const nlohmann::json::exception& e) {
309 logger->error("Failed to parse tools/list from '{}': {}",
310 name_, e.what());
311 return false;
312 }
313}
314
322std::string ExternalMCPClient::extract_tool_result(
323 const std::string& response_json) {
324
325 try {
326 auto j = nlohmann::json::parse(response_json);
327 if (j.contains("error")) {
328 return "Error: " + j["error"]["message"]
329 .get<std::string>();
330 }
331
332 auto& content = j.at("result").at("content");
333 std::string text;
334 for (const auto& item : content) {
335 if (item.value("type", "") == "text") {
336 text += item.at("text").get<std::string>();
337 }
338 }
339 return text;
340 } catch (const nlohmann::json::exception& e) {
341 return "Error parsing response: " + std::string(e.what());
342 }
343}
344
362std::string ExternalMCPClient::build_response(
363 const std::string& result_text,
364 bool is_error) {
365
366 nlohmann::json resp;
367 resp["result"] = result_text;
368 // SECURITY: External servers CANNOT inject directives.
369 // Directives array is always empty for external tool results.
370 resp["directives"] = nlohmann::json::array();
371 if (is_error) {
372 resp["is_error"] = true;
373 }
374 return resp.dump();
375}
376
377} // namespace entropic
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.
Definition logging.cpp:211
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).