Entropic 2.3.8
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
40 if (!transport_->open()) {
41 logger->error("Transport open failed for '{}'", name_);
42 return false;
43 }
44
45 if (!send_initialize()) {
46 logger->error("MCP initialize failed for '{}'", name_);
47 transport_->close();
48 return false;
49 }
50
51 if (!query_tools()) {
52 logger->warn("tools/list failed for '{}' — "
53 "connected with 0 tools", name_);
54 }
55
56 logger->info("Connected to '{}': {} tools",
57 name_, cached_tool_names_.size());
58 return true;
59}
60
67 transport_->close();
68 std::lock_guard<std::mutex> lock(tools_mutex_);
69 cached_tools_json_ = "[]";
70 cached_tool_names_.clear();
71 logger->info("Disconnected from '{}'", name_);
72}
73
80std::string ExternalMCPClient::list_tools() const {
81 std::lock_guard<std::mutex> lock(tools_mutex_);
82 return cached_tools_json_;
83}
84
94 const std::string& tool_name,
95 const std::string& args_json) {
96
97 if (!transport_->is_connected()) {
98 return build_response(
99 "Server '" + name_ + "' is disconnected. "
100 "Tool '" + name_ + "." + tool_name + "' unavailable.",
101 true);
102 }
103
104 nlohmann::json params;
105 params["name"] = tool_name;
106 try {
107 params["arguments"] = nlohmann::json::parse(args_json);
108 } catch (...) {
109 params["arguments"] = nlohmann::json::object();
110 }
111
112 auto request = build_request("tools/call", params.dump());
113 auto response = transport_->send_request(
114 request, DEFAULT_TIMEOUT_MS);
115
116 if (response.empty()) {
117 return build_response(
118 "Tool '" + name_ + "." + tool_name +
119 "' timed out or transport error.", true);
120 }
121
122 auto result_text = extract_tool_result(response);
123 return build_response(result_text);
124}
125
134static std::vector<std::string> names_diff(
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));
139 return out;
140}
141
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());
154 };
155
156 std::set<std::string> old_names = snapshot();
157 query_tools();
158 std::set<std::string> new_names = snapshot();
159
160 auto added = names_diff(new_names, old_names);
161 auto removed = names_diff(old_names, new_names);
162
163 logger->info("Server '{}' tools refreshed: +{} -{}",
164 name_, added.size(), removed.size());
165 return {added, removed};
166}
167
175 return transport_->is_connected();
176}
177
186std::string ExternalMCPClient::build_request(
187 const std::string& method,
188 const std::string& params) {
189
190 nlohmann::json req;
191 req["jsonrpc"] = "2.0";
192 req["id"] = next_id_++;
193 req["method"] = method;
194 try {
195 req["params"] = nlohmann::json::parse(params);
196 } catch (...) {
197 req["params"] = nlohmann::json::object();
198 }
199 return req.dump();
200}
201
209bool ExternalMCPClient::validate_init_response(
210 const std::string& response) {
211
212 try {
213 auto j = nlohmann::json::parse(response);
214 if (j.contains("error")) {
215 logger->error("Initialize error from '{}': {}",
216 name_, j["error"].dump());
217 return false;
218 }
219 return true;
220 } catch (...) {
221 return false;
222 }
223}
224
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";
237
238 auto request = build_request("initialize", params.dump());
239 auto response = transport_->send_request(
240 request, INIT_TIMEOUT_MS);
241
242 if (response.empty()) {
243 return false;
244 }
245 return validate_init_response(response);
246}
247
254bool ExternalMCPClient::query_tools() {
255 auto request = build_request("tools/list");
256 auto response = transport_->send_request(
257 request, INIT_TIMEOUT_MS);
258
259 if (response.empty()) {
260 return false;
261 }
262
263 try {
264 auto j = nlohmann::json::parse(response);
265 auto tools = j.at("result").at("tools");
266
267 // Prefix tool names with server name
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>());
273 }
274
275 std::lock_guard<std::mutex> lock(tools_mutex_);
276 cached_tools_json_ = tools.dump();
277 cached_tool_names_ = std::move(names);
278 return true;
279 } catch (const nlohmann::json::exception& e) {
280 logger->error("Failed to parse tools/list from '{}': {}",
281 name_, e.what());
282 return false;
283 }
284}
285
293std::string ExternalMCPClient::extract_tool_result(
294 const std::string& response_json) {
295
296 try {
297 auto j = nlohmann::json::parse(response_json);
298 if (j.contains("error")) {
299 return "Error: " + j["error"]["message"]
300 .get<std::string>();
301 }
302
303 auto& content = j.at("result").at("content");
304 std::string text;
305 for (const auto& item : content) {
306 if (item.value("type", "") == "text") {
307 text += item.at("text").get<std::string>();
308 }
309 }
310 return text;
311 } catch (const nlohmann::json::exception& e) {
312 return "Error parsing response: " + std::string(e.what());
313 }
314}
315
324std::string ExternalMCPClient::build_response(
325 const std::string& result_text,
326 bool is_error) {
327
328 nlohmann::json resp;
329 resp["result"] = result_text;
330 // SECURITY: External servers CANNOT inject directives.
331 // Directives array is always empty for external tool results.
332 resp["directives"] = nlohmann::json::array();
333 if (is_error) {
334 resp["is_error"] = true;
335 }
336 return resp.dump();
337}
338
339} // 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).
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).