11#include <nlohmann/json.hpp>
31 : project_dir_(std::move(project_dir)) {}
47 const std::set<std::string>& existing_names)
const {
50 std::vector<ExternalServerConfig> result;
51 std::set<std::string> seen;
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";
58 parse_mcp_json(project_mcp, own_socket, existing_names,
60 parse_mcp_json(global_mcp, own_socket, existing_names,
63 logger->info(
"Discovered {} external servers from .mcp.json",
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 {
85 if (!std::filesystem::exists(path)) {
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());
98 auto servers_it = data.find(
"mcpServers");
99 if (servers_it == data.end() || !servers_it->is_object()) {
103 for (
auto& [name, cfg] : servers_it->items()) {
104 parse_server_entry(name, cfg, own_socket,
105 existing_names, seen, out);
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 {
135 if (seen.count(name) > 0) {
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);
152 if (existing_names.count(name) > 0) {
153 logger->warn(
".mcp.json entry '{}' shadowed by existing "
154 "config — skipping", name);
160 ExternalServerConfig entry;
162 std::string type = cfg.value(
"type",
"");
164 if (infer_transport(cfg, type, entry)) {
166 out.push_back(std::move(entry));
167 logger->info(
"Discovered external server '{}' "
168 "(transport={})", name, entry.transport);
184bool MCPJsonDiscovery::infer_transport(
185 const nlohmann::json& cfg,
186 const std::string& type,
187 ExternalServerConfig& entry)
const {
189 bool has_url = cfg.contains(
"url");
190 bool has_command = cfg.contains(
"command");
193 if (type ==
"sse" || type ==
"http" ||
194 (type.empty() && has_url)) {
195 return parse_sse_entry(cfg, entry);
197 if (type ==
"stdio" || (type.empty() && has_command)) {
198 return parse_stdio_entry(cfg, entry);
201 logger->warn(
".mcp.json entry '{}': cannot determine transport "
202 "(no url or command)", entry.name);
215bool MCPJsonDiscovery::parse_sse_entry(
216 const nlohmann::json& cfg,
217 ExternalServerConfig& entry)
const {
219 entry.url = cfg.value(
"url",
"");
220 if (entry.url.empty()) {
221 logger->warn(
".mcp.json entry '{}' missing 'url' — skipping",
225 entry.transport =
"sse";
243bool MCPJsonDiscovery::parse_stdio_entry(
244 const nlohmann::json& cfg,
245 ExternalServerConfig& entry)
const {
247 entry.command = cfg.value(
"command",
"");
248 if (entry.command.empty()) {
249 logger->warn(
".mcp.json entry '{}' missing 'command' — "
250 "skipping", entry.name);
253 entry.transport =
"stdio";
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>());
263 if (cfg.contains(
"env") && cfg[
"env"].is_object()) {
264 for (
auto& [key, val] : cfg[
"env"].items()) {
266 logger->warn(
".mcp.json '{}': blocked env var '{}' "
267 "— skipping", entry.name, key);
270 entry.env[key] = val.get<std::string>();
289 const std::filesystem::path& project_dir) {
291 auto resolved = std::filesystem::weakly_canonical(project_dir);
292 auto input = resolved.string();
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);
300 auto home = std::filesystem::path(
301 getenv(
"HOME") ? getenv(
"HOME") :
"/tmp");
302 return home /
".entropic" /
"socks" / (hash_str +
".sock");
313 return (key.size() >= 9 && key.substr(0, 9) ==
"ENTROPIC_") ||
314 (key.size() >= 5 && key.substr(0, 5) ==
"DYLD_");
333 static const std::array<std::string, 7> blocked = {{
334 "LD_PRELOAD",
"LD_LIBRARY_PATH",
"PATH",
335 "HOME",
"SHELL",
"DYLD_INSERT_LIBRARIES",
339 bool exact_match = std::any_of(blocked.begin(), blocked.end(),
340 [&key](
const std::string& b) { return key == b; });
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.
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.