Entropic 2.3.8
Local-first agentic inference engine
Loading...
Searching...
No Matches
server_manager.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
18
19#include <nlohmann/json.hpp>
20
21static auto logger = entropic::log::get("mcp.server_manager");
22
23namespace entropic {
24
33 const PermissionsConfig& permissions,
34 const std::filesystem::path& project_dir)
35 : permissions_(permissions.allow, permissions.deny),
36 project_dir_(project_dir) {}
37
47 const MCPConfig& mcp,
48 const std::vector<std::string>& tier_names,
49 const std::string& data_dir) {
50 if (mcp.enable_entropic) {
51 register_server(std::make_unique<EntropicServer>(
52 tier_names, data_dir));
53 }
54 if (mcp.enable_filesystem) {
55 register_server(std::make_unique<FilesystemServer>(
56 project_dir_, mcp.filesystem, data_dir));
57 }
58 if (mcp.enable_bash) {
59 register_server(std::make_unique<BashServer>(
60 project_dir_, data_dir));
61 }
62 if (mcp.enable_git) {
63 register_server(std::make_unique<GitServer>(
64 project_dir_, data_dir));
65 }
66 if (mcp.enable_diagnostics) {
67 register_server(std::make_unique<DiagnosticsServer>(
68 project_dir_, data_dir));
69 }
70 if (mcp.enable_web) {
71 register_server(std::make_unique<WebServer>(data_dir));
72 }
73}
74
82 std::unique_ptr<MCPServerBase> server) {
83 auto name = server->name();
84 if (servers_.count(name) > 0) {
85 logger->warn("Server '{}' already registered — replacing",
86 name);
87 }
88 servers_[name] = std::move(server);
89 logger->info("Registered server: {}", name);
90}
91
98 logger->info("Initializing {} in-process MCP servers",
99 servers_.size());
100 for (auto& [name, server] : servers_) {
101 logger->info("Server '{}' ready", name);
102 }
103
104 initialize_external_servers();
105}
106
113std::string ServerManager::list_tools() const {
114 auto all = nlohmann::json::array();
115
116 // In-process servers
117 for (const auto& [name, server] : servers_) {
118 auto tools_json = server->list_tools();
119 auto tools = nlohmann::json::parse(tools_json);
120 for (auto& tool : tools) {
121 std::string orig_name = tool["name"];
122 tool["name"] = name + "." + orig_name;
123 all.push_back(std::move(tool));
124 }
125 }
126
127 // External servers (tools already prefixed by ExternalMCPClient)
128 for (const auto& [name, client] : external_clients_) {
129 if (!client->is_connected()) {
130 continue;
131 }
132 auto tools_json = client->list_tools();
133 auto tools = nlohmann::json::parse(tools_json);
134 for (auto& tool : tools) {
135 all.push_back(std::move(tool));
136 }
137 }
138
139 logger->info("Tool list: {} tools from {} server(s) + {} external",
140 all.size(), servers_.size(),
141 external_clients_.size());
142 return all.dump();
143}
144
154 const std::string& tool_name,
155 const std::string& args_json) {
156
157 // Check permissions first
158 auto pattern = tool_name + ":" + args_to_pattern(args_json);
159 if (permissions_.is_denied(tool_name, pattern)) {
160 logger->warn("Permission denied: {}", tool_name);
161 nlohmann::json resp;
162 resp["result"] = "Error: Permission denied for " + tool_name;
163 resp["directives"] = nlohmann::json::array();
164 return resp.dump();
165 }
166
167 return route_tool_call(tool_name, args_json);
168}
169
177MCPServerBase* ServerManager::get_server(const std::string& name) const {
178 auto it = servers_.find(name);
179 return (it != servers_.end()) ? it->second.get() : nullptr;
180}
181
188std::vector<std::string> ServerManager::server_names() const {
189 std::vector<std::string> names;
190 for (const auto& [name, _] : servers_) {
191 names.push_back(name);
192 }
193 for (const auto& [name, _] : external_clients_) {
194 names.push_back(name);
195 }
196 return names;
197}
198
207 const std::string& tool_name) const {
208 auto prefix = extract_prefix(tool_name);
209 auto local_name = extract_local_name(tool_name);
210 auto it = servers_.find(prefix);
211 if (it != servers_.end()) {
212 auto* tool = it->second->registry().get_tool(local_name);
213 if (tool != nullptr) {
214 return tool->definition().input_schema;
215 }
216 }
217 return "";
218}
219
228std::string ServerManager::route_tool_call(
229 const std::string& tool_name,
230 const std::string& args_json) {
231
232 auto prefix = extract_prefix(tool_name);
233 auto local_name = extract_local_name(tool_name);
234
235 // Try in-process server first
236 auto it = servers_.find(prefix);
237 if (it != servers_.end()) {
238 return it->second->execute(local_name, args_json);
239 }
240
241 // Try external client
242 auto ext_it = external_clients_.find(prefix);
243 if (ext_it != external_clients_.end()) {
244 return route_external_call(
245 ext_it->second.get(), tool_name, local_name, args_json);
246 }
247
248 logger->warn("Unknown server: {}", prefix);
249 nlohmann::json resp;
250 resp["result"] = "Error: Unknown server '" + prefix + "'";
251 resp["directives"] = nlohmann::json::array();
252 return resp.dump();
253}
254
265std::string ServerManager::route_external_call(
266 ExternalMCPClient* client,
267 const std::string& tool_name,
268 const std::string& local_name,
269 const std::string& args_json) {
270
271 if (!client->is_connected()) {
272 auto prefix = extract_prefix(tool_name);
273 return disconnected_error(tool_name, prefix);
274 }
275 return client->execute(local_name, args_json);
276}
277
287 const std::string& tool_name,
288 const std::string& args_json) const {
289 auto pattern = tool_name + ":" + args_to_pattern(args_json);
290 return permissions_.is_allowed(tool_name, pattern);
291}
292
302 const std::string& tool_name,
303 const std::string& args_json) const {
304 auto prefix = extract_prefix(tool_name);
305 auto it = servers_.find(prefix);
306 if (it != servers_.end()) {
307 return it->second->get_permission_pattern(
308 tool_name, args_json);
309 }
310 return tool_name;
311}
312
321 const std::string& tool_name) const {
322 auto prefix = extract_prefix(tool_name);
323 auto it = servers_.find(prefix);
324 if (it != servers_.end()) {
325 auto local = extract_local_name(tool_name);
326 return it->second->skip_duplicate_check(local);
327 }
328 return false;
329}
330
339 const std::string& tool_name) const {
340 auto prefix = extract_prefix(tool_name);
341 auto it = servers_.find(prefix);
342 if (it != servers_.end()) {
343 auto local = extract_local_name(tool_name);
344 auto* tool = it->second->registry().get_tool(local);
345 if (tool != nullptr) {
346 return tool->required_access_level();
347 }
348 }
349 return MCPAccessLevel::WRITE; // Safe default
350}
351
360 const std::string& pattern, bool allow) {
361 permissions_.add_permission(pattern, allow);
362}
363
370 // Stop health monitor first
371 if (health_monitor_) {
372 health_monitor_->stop();
373 }
374
375 // Disconnect external clients
376 for (auto& [name, client] : external_clients_) {
377 client->disconnect();
378 }
379 external_clients_.clear();
380
381 // Destroy in-process servers
382 logger->info("Shutting down {} MCP servers", servers_.size());
383 servers_.clear();
384 server_info_.clear();
385}
386
394std::string ServerManager::extract_prefix(
395 const std::string& tool_name) {
396 auto dot = tool_name.find('.');
397 if (dot == std::string::npos) {
398 return tool_name;
399 }
400 return tool_name.substr(0, dot);
401}
402
410std::string ServerManager::extract_local_name(
411 const std::string& tool_name) {
412 auto dot = tool_name.find('.');
413 if (dot == std::string::npos) {
414 return tool_name;
415 }
416 return tool_name.substr(dot + 1);
417}
418
426std::string ServerManager::args_to_pattern(
427 const std::string& args_json) {
428 if (args_json.empty() || args_json == "{}") {
429 return "*";
430 }
431 try {
432 auto j = nlohmann::json::parse(args_json);
433 if (j.is_object() && !j.empty()) {
434 auto first = j.begin();
435 if (first->is_string()) {
436 return first->get<std::string>();
437 }
438 }
439 } catch (...) {
440 // Parse failure — treat as wildcard
441 }
442 return "*";
443}
444
445// ── v1.8.7: External server methods ─────────────────────
446
454 mcp_config_ = config;
455}
456
468 for (auto& [_, client] : external_clients_) {
469 if (client) { client->interrupt(); }
470 }
471}
472
478void ServerManager::initialize_external_servers() {
479 // Create .mcp.json discovery
480 mcp_json_discovery_ = std::make_unique<MCPJsonDiscovery>(
481 project_dir_);
482
483 // YAML config external_servers
484 for (const auto& [name, entry] : mcp_config_.external_servers) {
485 auto client = create_external_client(name, entry);
486 connect_and_register_external(name, std::move(client),
487 "config", entry.url,
488 entry.command);
489 }
490
491 // .mcp.json discovery
492 std::set<std::string> existing;
493 for (const auto& [name, _] : servers_) {
494 existing.insert(name);
495 }
496 for (const auto& [name, _] : external_clients_) {
497 existing.insert(name);
498 }
499
500 auto discovered = mcp_json_discovery_->discover(existing);
501 for (const auto& cfg : discovered) {
502 auto client = create_external_client(cfg);
503 connect_and_register_external(cfg.name, std::move(client),
504 "mcp_json", cfg.url,
505 cfg.command);
506 }
507
508 // Start health monitor
509 health_monitor_ = std::make_unique<HealthMonitor>(
510 ReconnectPolicy(mcp_config_.reconnect),
511 mcp_config_.health_check_interval_ms);
512
513 for (auto& [name, client] : external_clients_) {
514 health_monitor_->watch(name, client.get());
515 }
516 if (!external_clients_.empty()) {
517 health_monitor_->start();
518 }
519
520 logger->info("External MCP: {} servers connected",
521 external_clients_.size());
522}
523
534void ServerManager::connect_and_register_external(
535 const std::string& name,
536 std::unique_ptr<ExternalMCPClient> client,
537 const std::string& source,
538 const std::string& url,
539 const std::string& command) {
540
541 ServerInfo info;
542 info.name = name;
543 info.transport = url.empty() ? "stdio" : "sse";
544 info.url = url;
545 info.command = command;
546 info.source = source;
547 info.status = "disconnected";
548
549 bool ok = client->connect();
550 if (ok) {
551 info.status = "connected";
552 info.connected_at = std::chrono::system_clock::now();
553 } else {
554 info.status = "error";
555 logger->error("Failed to connect external server '{}'", name);
556 }
557
558 server_info_[name] = info;
559 external_clients_[name] = std::move(client);
560}
561
573std::unique_ptr<Transport> ServerManager::make_transport(
574 const ExternalServerConfig& spec) {
575 bool prefer_sse = (spec.transport == "sse")
576 || (!spec.url.empty() && spec.command.empty());
577 if (prefer_sse) {
578 return std::make_unique<SSETransport>(spec.url);
579 }
580 // gh#19 (v2.1.5): pass the registered server name as the display
581 // label so child stderr lines and lifecycle logs identify the
582 // server, not the resolved spawn command (which collides when
583 // multiple servers share an entrypoint like /usr/bin/env python).
584 return std::make_unique<StdioTransport>(
585 spec.name, spec.command, spec.args, spec.env,
586 /*default_timeout_ms=*/30000U);
587}
588
599 const ExternalServerConfig& spec) {
600
601 if (servers_.count(spec.name) > 0 ||
602 external_clients_.count(spec.name) > 0) {
603 logger->warn("Server '{}' already registered", spec.name);
604 return {};
605 }
606
607 auto client = std::make_unique<ExternalMCPClient>(
608 spec.name, make_transport(spec));
609
610 connect_and_register_external(spec.name, std::move(client),
611 "runtime", spec.url, spec.command);
612
613 auto& registered = external_clients_[spec.name];
614 if (health_monitor_) {
615 health_monitor_->watch(spec.name, registered.get());
616 }
617
618 // Parse tool names from cached list
619 std::vector<std::string> tool_names;
620 auto tools_json = registered->list_tools();
621 try {
622 auto tools = nlohmann::json::parse(tools_json);
623 for (const auto& t : tools) {
624 tool_names.push_back(t["name"].get<std::string>());
625 }
626 } catch (...) {}
627
628 return tool_names;
629}
630
641 const std::string& name,
642 const std::string& command,
643 const std::vector<std::string>& args,
644 const std::string& url) {
646 spec.name = name;
647 spec.command = command;
648 spec.args = args;
649 spec.url = url;
650 return connect_external_server(spec);
651}
652
660 const std::string& name) {
661
662 auto it = external_clients_.find(name);
663 if (it == external_clients_.end()) {
664 logger->warn("External server '{}' not found", name);
665 return;
666 }
667
668 if (health_monitor_) {
669 health_monitor_->unwatch(name);
670 }
671
672 it->second->disconnect();
673 external_clients_.erase(it);
674 server_info_.erase(name);
675
676 logger->info("External server '{}' disconnected", name);
677}
678
685std::map<std::string, ServerInfo>
687 auto result = server_info_;
688
689 // Add in-process servers
690 for (const auto& [name, _] : servers_) {
691 if (result.count(name) == 0) {
692 ServerInfo info;
693 info.name = name;
694 info.transport = "in_process";
695 info.status = "connected";
696 info.source = "builtin";
697 result[name] = info;
698 }
699 }
700 return result;
701}
702
709 if (health_monitor_) {
710 health_monitor_->process_events();
711 }
712}
713
728std::unique_ptr<ExternalMCPClient>
729ServerManager::create_external_client(
730 const std::string& name,
731 const ExternalServerEntry& entry) {
733 spec.name = name;
734 spec.command = entry.command;
735 spec.args = entry.args;
736 spec.env = std::map<std::string, std::string>(
737 entry.env.begin(), entry.env.end());
738 spec.url = entry.url;
739 spec.transport = entry.url.empty() ? "stdio" : "sse";
740 return std::make_unique<ExternalMCPClient>(
741 name, make_transport(spec));
742}
743
754std::unique_ptr<ExternalMCPClient>
755ServerManager::create_external_client(
756 const ExternalServerConfig& config) {
757 return std::make_unique<ExternalMCPClient>(
758 config.name, make_transport(config));
759}
760
769std::string ServerManager::disconnected_error(
770 const std::string& tool_name,
771 const std::string& server_name) {
772
773 nlohmann::json resp;
774 resp["result"] = "Server '" + server_name +
775 "' is disconnected. Tool '" + tool_name +
776 "' is unavailable. Use a different approach "
777 "or try again later.";
778 resp["directives"] = nlohmann::json::array();
779 resp["is_error"] = true;
780 return resp.dump();
781}
782
783} // namespace entropic
Bash MCP server — shell command execution.
Concrete base class for MCP servers (80% logic).
Definition server_base.h:66
bool is_denied(const std::string &tool_name, const std::string &pattern) const
Check if a tool call is explicitly denied.
void add_permission(const std::string &pattern, bool allow)
Add a permission pattern at runtime.
bool is_allowed(const std::string &tool_name, const std::string &pattern) const
Check if a tool call is explicitly allowed (skip prompting).
std::vector< std::string > connect_external_server(const ExternalServerConfig &spec)
Connect to an external MCP server at runtime (canonical spec-based API).
MCPAccessLevel get_required_access_level(const std::string &tool_name) const
Get the required access level for a tool.
bool is_explicitly_allowed(const std::string &tool_name, const std::string &args_json) const
Check if tool is explicitly allowed (skip prompting).
std::map< std::string, ServerInfo > list_server_info() const
Get snapshot of all servers with current status.
std::vector< std::string > server_names() const
List registered server names (in-process + external).
void process_health_events()
Process pending health events (call from engine loop).
MCPServerBase * get_server(const std::string &name) const
Get a registered in-process server by name.
std::string list_tools() const
List all tools from all connected servers.
ServerManager(const PermissionsConfig &permissions, const std::filesystem::path &project_dir)
Construct with permission config and project directory.
void register_server(std::unique_ptr< MCPServerBase > server)
Register a built-in server (in-process, ownership transferred).
void add_permission(const std::string &pattern, bool allow)
Add a runtime permission pattern.
bool skip_duplicate_check(const std::string &tool_name) const
Check if tool should skip duplicate detection.
void init_builtins(const MCPConfig &mcp_config, const std::vector< std::string > &tier_names, const std::string &data_dir)
Register built-in servers based on config flags.
std::string get_permission_pattern(const std::string &tool_name, const std::string &args_json) const
Generate permission pattern via server class delegation.
std::string get_tool_schema(const std::string &tool_name) const
Get the JSON Schema for a tool's input parameters.
void interrupt_external_tools()
Abort in-flight tool calls across every external MCP client.
std::string execute(const std::string &tool_name, const std::string &args_json)
Execute a tool call via the appropriate server.
void set_mcp_config(const MCPConfig &config)
Set MCP config for external server initialization.
void shutdown()
Shutdown all servers (in-process + external).
void disconnect_external_server(const std::string &name)
Disconnect and remove an external server.
void initialize()
Initialize all registered servers.
Diagnostics MCP server — LSP client for code diagnostics.
Entropic MCP server — engine-level tools including introspection.
Filesystem MCP server — read/write/edit/glob/grep/list_directory.
Git MCP server — version control operations.
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).
@ ok
Tool dispatched, returned non-empty content.
MCPAccessLevel
MCP tool access level for per-identity authorization.
Definition config.h:38
@ WRITE
Read + write operations (e.g., write_file, execute)
MCP server lifecycle management and tool routing.
Parsed server entry from .mcp.json.
std::map< std::string, std::string > env
Stdio env vars.
std::string command
Stdio command (empty for SSE)
std::string transport
"stdio" | "sse"
std::vector< std::string > args
Stdio command args.
std::string url
SSE URL (empty for stdio)
Configuration for a single external MCP server entry.
Definition config.h:444
std::string command
Stdio command (empty for SSE)
Definition config.h:445
std::vector< std::string > args
Stdio command arguments.
Definition config.h:446
std::string url
SSE endpoint URL (empty for stdio)
Definition config.h:448
std::unordered_map< std::string, std::string > env
Stdio environment variables.
Definition config.h:447
MCP server configuration.
Definition config.h:455
ReconnectConfig reconnect
Reconnection backoff policy.
Definition config.h:469
bool enable_entropic
Enable entropic internal server (handoff, delegate, pipeline)
Definition config.h:456
FilesystemConfig filesystem
Filesystem server config.
Definition config.h:462
bool enable_filesystem
Enable filesystem server.
Definition config.h:457
bool enable_git
Enable git server.
Definition config.h:459
bool enable_diagnostics
Enable diagnostics server.
Definition config.h:460
uint32_t health_check_interval_ms
Ping interval (0 = disabled)
Definition config.h:470
bool enable_bash
Enable bash server.
Definition config.h:458
bool enable_web
Enable web server.
Definition config.h:461
Tool permission configuration.
Definition config.h:400
Runtime state of a connected MCP server.
SSE transport for external MCP servers.
Stdio transport for external MCP servers.
Web MCP server — web_fetch + web_search.