11#include <nlohmann/json.hpp>
31 : project_dir_(std::move(project_dir)) {}
41 const std::set<std::string>& existing_names)
const {
44 std::vector<ExternalServerConfig> result;
45 std::set<std::string> seen;
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";
52 parse_mcp_json(project_mcp, own_socket, existing_names,
54 parse_mcp_json(global_mcp, own_socket, existing_names,
57 logger->info(
"Discovered {} external servers from .mcp.json",
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 {
79 if (!std::filesystem::exists(path)) {
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());
92 auto servers_it = data.find(
"mcpServers");
93 if (servers_it == data.end() || !servers_it->is_object()) {
97 for (
auto& [name, cfg] : servers_it->items()) {
98 parse_server_entry(name, cfg, own_socket,
99 existing_names, seen, out);
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 {
123 if (seen.count(name) > 0) {
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);
140 if (existing_names.count(name) > 0) {
141 logger->warn(
".mcp.json entry '{}' shadowed by existing "
142 "config — skipping", name);
148 ExternalServerConfig entry;
150 std::string type = cfg.value(
"type",
"");
152 if (infer_transport(cfg, type, entry)) {
154 out.push_back(std::move(entry));
155 logger->info(
"Discovered external server '{}' "
156 "(transport={})", name, entry.transport);
169bool MCPJsonDiscovery::infer_transport(
170 const nlohmann::json& cfg,
171 const std::string& type,
172 ExternalServerConfig& entry)
const {
174 bool has_url = cfg.contains(
"url");
175 bool has_command = cfg.contains(
"command");
178 if (type ==
"sse" || type ==
"http" ||
179 (type.empty() && has_url)) {
180 return parse_sse_entry(cfg, entry);
182 if (type ==
"stdio" || (type.empty() && has_command)) {
183 return parse_stdio_entry(cfg, entry);
186 logger->warn(
".mcp.json entry '{}': cannot determine transport "
187 "(no url or command)", entry.name);
199bool MCPJsonDiscovery::parse_sse_entry(
200 const nlohmann::json& cfg,
201 ExternalServerConfig& entry)
const {
203 entry.url = cfg.value(
"url",
"");
204 if (entry.url.empty()) {
205 logger->warn(
".mcp.json entry '{}' missing 'url' — skipping",
209 entry.transport =
"sse";
221bool MCPJsonDiscovery::parse_stdio_entry(
222 const nlohmann::json& cfg,
223 ExternalServerConfig& entry)
const {
225 entry.command = cfg.value(
"command",
"");
226 if (entry.command.empty()) {
227 logger->warn(
".mcp.json entry '{}' missing 'command' — "
228 "skipping", entry.name);
231 entry.transport =
"stdio";
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>());
241 if (cfg.contains(
"env") && cfg[
"env"].is_object()) {
242 for (
auto& [key, val] : cfg[
"env"].items()) {
244 logger->warn(
".mcp.json '{}': blocked env var '{}' "
245 "— skipping", entry.name, key);
248 entry.env[key] = val.get<std::string>();
262 const std::filesystem::path& project_dir) {
264 auto resolved = std::filesystem::weakly_canonical(project_dir);
265 auto input = resolved.string();
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);
273 auto home = std::filesystem::path(
274 getenv(
"HOME") ? getenv(
"HOME") :
"/tmp");
275 return home /
".entropic" /
"socks" / (hash_str +
".sock");
286 return (key.size() >= 9 && key.substr(0, 9) ==
"ENTROPIC_") ||
287 (key.size() >= 5 && key.substr(0, 5) ==
"DYLD_");
298 static const std::array<std::string, 7> blocked = {{
299 "LD_PRELOAD",
"LD_LIBRARY_PATH",
"PATH",
300 "HOME",
"SHELL",
"DYLD_INSERT_LIBRARIES",
304 bool exact_match = std::any_of(blocked.begin(), blocked.end(),
305 [&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.