Entropic 2.11.1
Local-first agentic inference engine
Loading...
Searching...
No Matches
mcp_json_discovery.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 <array>
15#include <fstream>
16#include <functional>
17#include <sstream>
18#include <iomanip>
19
20static auto logger = entropic::log::get("mcp.discovery");
21
22namespace entropic {
23
30MCPJsonDiscovery::MCPJsonDiscovery(std::filesystem::path project_dir)
31 : project_dir_(std::move(project_dir)) {}
32
46std::vector<ExternalServerConfig> MCPJsonDiscovery::discover(
47 const std::set<std::string>& existing_names) const {
48
49 auto own_socket = compute_socket_path(project_dir_);
50 std::vector<ExternalServerConfig> result;
51 std::set<std::string> seen;
52
53 // Project-level first, then global fallback
54 auto project_mcp = project_dir_ / ".mcp.json";
55 auto home = std::filesystem::path(getenv("HOME") ? getenv("HOME") : "");
56 auto global_mcp = home / ".entropic" / ".mcp.json";
57
58 parse_mcp_json(project_mcp, own_socket, existing_names,
59 seen, result);
60 parse_mcp_json(global_mcp, own_socket, existing_names,
61 seen, result);
62
63 logger->info("Discovered {} external servers from .mcp.json",
64 result.size());
65 return result;
66}
67
78void MCPJsonDiscovery::parse_mcp_json(
79 const std::filesystem::path& path,
80 const std::filesystem::path& own_socket,
81 const std::set<std::string>& existing_names,
82 std::set<std::string>& seen,
83 std::vector<ExternalServerConfig>& out) const {
84
85 if (!std::filesystem::exists(path)) {
86 return;
87 }
88
89 nlohmann::json data;
90 try {
91 std::ifstream f(path);
92 data = nlohmann::json::parse(f);
93 } catch (const std::exception& e) {
94 logger->warn("Failed to read {}: {}", path.string(), e.what());
95 return;
96 }
97
98 auto servers_it = data.find("mcpServers");
99 if (servers_it == data.end() || !servers_it->is_object()) {
100 return;
101 }
102
103 for (auto& [name, cfg] : servers_it->items()) {
104 parse_server_entry(name, cfg, own_socket,
105 existing_names, seen, out);
106 }
107}
108
126void MCPJsonDiscovery::parse_server_entry(
127 const std::string& name,
128 const nlohmann::json& cfg,
129 const std::filesystem::path& own_socket,
130 const std::set<std::string>& existing_names,
131 std::set<std::string>& seen,
132 std::vector<ExternalServerConfig>& out) const {
133
134 // Skip if already discovered from a higher-priority file
135 if (seen.count(name) > 0) {
136 return;
137 }
138
139 // Self-detection: skip own socket
140 if (cfg.contains("path")) {
141 auto sock = std::filesystem::path(
142 cfg["path"].get<std::string>());
143 if (std::filesystem::weakly_canonical(sock) ==
144 std::filesystem::weakly_canonical(own_socket)) {
145 logger->debug("Skipping '{}' (matches own socket)", name);
146 seen.insert(name);
147 return;
148 }
149 }
150
151 // Shadow warning: skip if already registered
152 if (existing_names.count(name) > 0) {
153 logger->warn(".mcp.json entry '{}' shadowed by existing "
154 "config — skipping", name);
155 seen.insert(name);
156 return;
157 }
158
159 // Determine transport
160 ExternalServerConfig entry;
161 entry.name = name;
162 std::string type = cfg.value("type", "");
163
164 if (infer_transport(cfg, type, entry)) {
165 seen.insert(name);
166 out.push_back(std::move(entry));
167 logger->info("Discovered external server '{}' "
168 "(transport={})", name, entry.transport);
169 }
170}
171
184bool MCPJsonDiscovery::infer_transport(
185 const nlohmann::json& cfg,
186 const std::string& type,
187 ExternalServerConfig& entry) const {
188
189 bool has_url = cfg.contains("url");
190 bool has_command = cfg.contains("command");
191
192 // Explicit type overrides inference
193 if (type == "sse" || type == "http" ||
194 (type.empty() && has_url)) {
195 return parse_sse_entry(cfg, entry);
196 }
197 if (type == "stdio" || (type.empty() && has_command)) {
198 return parse_stdio_entry(cfg, entry);
199 }
200
201 logger->warn(".mcp.json entry '{}': cannot determine transport "
202 "(no url or command)", entry.name);
203 return false;
204}
205
215bool MCPJsonDiscovery::parse_sse_entry(
216 const nlohmann::json& cfg,
217 ExternalServerConfig& entry) const {
218
219 entry.url = cfg.value("url", "");
220 if (entry.url.empty()) {
221 logger->warn(".mcp.json entry '{}' missing 'url' — skipping",
222 entry.name);
223 return false;
224 }
225 entry.transport = "sse";
226 return true;
227}
228
243bool MCPJsonDiscovery::parse_stdio_entry(
244 const nlohmann::json& cfg,
245 ExternalServerConfig& entry) const {
246
247 entry.command = cfg.value("command", "");
248 if (entry.command.empty()) {
249 logger->warn(".mcp.json entry '{}' missing 'command' — "
250 "skipping", entry.name);
251 return false;
252 }
253 entry.transport = "stdio";
254
255 // Args
256 if (cfg.contains("args") && cfg["args"].is_array()) {
257 for (const auto& arg : cfg["args"]) {
258 entry.args.push_back(arg.get<std::string>());
259 }
260 }
261
262 // Env with blocklist enforcement
263 if (cfg.contains("env") && cfg["env"].is_object()) {
264 for (auto& [key, val] : cfg["env"].items()) {
265 if (is_blocked_env_var(key)) {
266 logger->warn(".mcp.json '{}': blocked env var '{}' "
267 "— skipping", entry.name, key);
268 continue;
269 }
270 entry.env[key] = val.get<std::string>();
271 }
272 }
273 return true;
274}
275
288std::filesystem::path compute_socket_path(
289 const std::filesystem::path& project_dir) {
290
291 auto resolved = std::filesystem::weakly_canonical(project_dir);
292 auto input = resolved.string();
293
294 // SHA-256 first 8 hex chars (simplified: use std::hash)
295 auto h = std::hash<std::string>{}(input);
296 std::ostringstream ss;
297 ss << std::hex << std::setfill('0') << std::setw(16) << h;
298 auto hash_str = ss.str().substr(0, 8);
299
300 auto home = std::filesystem::path(
301 getenv("HOME") ? getenv("HOME") : "/tmp");
302 return home / ".entropic" / "socks" / (hash_str + ".sock");
303}
304
312static bool has_blocked_prefix(const std::string& key) {
313 return (key.size() >= 9 && key.substr(0, 9) == "ENTROPIC_") ||
314 (key.size() >= 5 && key.substr(0, 5) == "DYLD_");
315}
316
332bool is_blocked_env_var(const std::string& key) {
333 static const std::array<std::string, 7> blocked = {{
334 "LD_PRELOAD", "LD_LIBRARY_PATH", "PATH",
335 "HOME", "SHELL", "DYLD_INSERT_LIBRARIES",
336 "DYLD_LIBRARY_PATH"
337 }};
338
339 bool exact_match = std::any_of(blocked.begin(), blocked.end(),
340 [&key](const std::string& b) { return key == b; });
341
342 return exact_match || has_blocked_prefix(key);
343}
344
345} // namespace entropic
std::vector< ExternalServerConfig > discover(const std::set< std::string > &existing_names) const
Discover servers from .mcp.json files.
MCPJsonDiscovery(std::filesystem::path project_dir)
Construct with project directory.
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
Discovers external MCP servers from .mcp.json files.
Activate model on GPU (WARM → ACTIVE).
static bool has_blocked_prefix(const std::string &key)
Check if key matches a blocked prefix pattern.
std::filesystem::path compute_socket_path(const std::filesystem::path &project_dir)
Compute project-unique Unix socket path for self-detection.
bool is_blocked_env_var(const std::string &key)
Environment variable blocklist for .mcp.json env field.