Entropic 2.3.8
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
40std::vector<ExternalServerConfig> MCPJsonDiscovery::discover(
41 const std::set<std::string>& existing_names) const {
42
43 auto own_socket = compute_socket_path(project_dir_);
44 std::vector<ExternalServerConfig> result;
45 std::set<std::string> seen;
46
47 // Project-level first, then global fallback
48 auto project_mcp = project_dir_ / ".mcp.json";
49 auto home = std::filesystem::path(getenv("HOME") ? getenv("HOME") : "");
50 auto global_mcp = home / ".entropic" / ".mcp.json";
51
52 parse_mcp_json(project_mcp, own_socket, existing_names,
53 seen, result);
54 parse_mcp_json(global_mcp, own_socket, existing_names,
55 seen, result);
56
57 logger->info("Discovered {} external servers from .mcp.json",
58 result.size());
59 return result;
60}
61
72void MCPJsonDiscovery::parse_mcp_json(
73 const std::filesystem::path& path,
74 const std::filesystem::path& own_socket,
75 const std::set<std::string>& existing_names,
76 std::set<std::string>& seen,
77 std::vector<ExternalServerConfig>& out) const {
78
79 if (!std::filesystem::exists(path)) {
80 return;
81 }
82
83 nlohmann::json data;
84 try {
85 std::ifstream f(path);
86 data = nlohmann::json::parse(f);
87 } catch (const std::exception& e) {
88 logger->warn("Failed to read {}: {}", path.string(), e.what());
89 return;
90 }
91
92 auto servers_it = data.find("mcpServers");
93 if (servers_it == data.end() || !servers_it->is_object()) {
94 return;
95 }
96
97 for (auto& [name, cfg] : servers_it->items()) {
98 parse_server_entry(name, cfg, own_socket,
99 existing_names, seen, out);
100 }
101}
102
114void MCPJsonDiscovery::parse_server_entry(
115 const std::string& name,
116 const nlohmann::json& cfg,
117 const std::filesystem::path& own_socket,
118 const std::set<std::string>& existing_names,
119 std::set<std::string>& seen,
120 std::vector<ExternalServerConfig>& out) const {
121
122 // Skip if already discovered from a higher-priority file
123 if (seen.count(name) > 0) {
124 return;
125 }
126
127 // Self-detection: skip own socket
128 if (cfg.contains("path")) {
129 auto sock = std::filesystem::path(
130 cfg["path"].get<std::string>());
131 if (std::filesystem::weakly_canonical(sock) ==
132 std::filesystem::weakly_canonical(own_socket)) {
133 logger->debug("Skipping '{}' (matches own socket)", name);
134 seen.insert(name);
135 return;
136 }
137 }
138
139 // Shadow warning: skip if already registered
140 if (existing_names.count(name) > 0) {
141 logger->warn(".mcp.json entry '{}' shadowed by existing "
142 "config — skipping", name);
143 seen.insert(name);
144 return;
145 }
146
147 // Determine transport
148 ExternalServerConfig entry;
149 entry.name = name;
150 std::string type = cfg.value("type", "");
151
152 if (infer_transport(cfg, type, entry)) {
153 seen.insert(name);
154 out.push_back(std::move(entry));
155 logger->info("Discovered external server '{}' "
156 "(transport={})", name, entry.transport);
157 }
158}
159
169bool MCPJsonDiscovery::infer_transport(
170 const nlohmann::json& cfg,
171 const std::string& type,
172 ExternalServerConfig& entry) const {
173
174 bool has_url = cfg.contains("url");
175 bool has_command = cfg.contains("command");
176
177 // Explicit type overrides inference
178 if (type == "sse" || type == "http" ||
179 (type.empty() && has_url)) {
180 return parse_sse_entry(cfg, entry);
181 }
182 if (type == "stdio" || (type.empty() && has_command)) {
183 return parse_stdio_entry(cfg, entry);
184 }
185
186 logger->warn(".mcp.json entry '{}': cannot determine transport "
187 "(no url or command)", entry.name);
188 return false;
189}
190
199bool MCPJsonDiscovery::parse_sse_entry(
200 const nlohmann::json& cfg,
201 ExternalServerConfig& entry) const {
202
203 entry.url = cfg.value("url", "");
204 if (entry.url.empty()) {
205 logger->warn(".mcp.json entry '{}' missing 'url' — skipping",
206 entry.name);
207 return false;
208 }
209 entry.transport = "sse";
210 return true;
211}
212
221bool MCPJsonDiscovery::parse_stdio_entry(
222 const nlohmann::json& cfg,
223 ExternalServerConfig& entry) const {
224
225 entry.command = cfg.value("command", "");
226 if (entry.command.empty()) {
227 logger->warn(".mcp.json entry '{}' missing 'command' — "
228 "skipping", entry.name);
229 return false;
230 }
231 entry.transport = "stdio";
232
233 // Args
234 if (cfg.contains("args") && cfg["args"].is_array()) {
235 for (const auto& arg : cfg["args"]) {
236 entry.args.push_back(arg.get<std::string>());
237 }
238 }
239
240 // Env with blocklist enforcement
241 if (cfg.contains("env") && cfg["env"].is_object()) {
242 for (auto& [key, val] : cfg["env"].items()) {
243 if (is_blocked_env_var(key)) {
244 logger->warn(".mcp.json '{}': blocked env var '{}' "
245 "— skipping", entry.name, key);
246 continue;
247 }
248 entry.env[key] = val.get<std::string>();
249 }
250 }
251 return true;
252}
253
261std::filesystem::path compute_socket_path(
262 const std::filesystem::path& project_dir) {
263
264 auto resolved = std::filesystem::weakly_canonical(project_dir);
265 auto input = resolved.string();
266
267 // SHA-256 first 8 hex chars (simplified: use std::hash)
268 auto h = std::hash<std::string>{}(input);
269 std::ostringstream ss;
270 ss << std::hex << std::setfill('0') << std::setw(16) << h;
271 auto hash_str = ss.str().substr(0, 8);
272
273 auto home = std::filesystem::path(
274 getenv("HOME") ? getenv("HOME") : "/tmp");
275 return home / ".entropic" / "socks" / (hash_str + ".sock");
276}
277
285static bool has_blocked_prefix(const std::string& key) {
286 return (key.size() >= 9 && key.substr(0, 9) == "ENTROPIC_") ||
287 (key.size() >= 5 && key.substr(0, 5) == "DYLD_");
288}
289
297bool is_blocked_env_var(const std::string& key) {
298 static const std::array<std::string, 7> blocked = {{
299 "LD_PRELOAD", "LD_LIBRARY_PATH", "PATH",
300 "HOME", "SHELL", "DYLD_INSERT_LIBRARIES",
301 "DYLD_LIBRARY_PATH"
302 }};
303
304 bool exact_match = std::any_of(blocked.begin(), blocked.end(),
305 [&key](const std::string& b) { return key == b; });
306
307 return exact_match || has_blocked_prefix(key);
308}
309
310} // 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.