Entropic 2.3.8
Local-first agentic inference engine
Loading...
Searching...
No Matches
filesystem.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
17
18#include <nlohmann/json.hpp>
19
20#include <filesystem>
21#include <fstream>
22#include <functional>
23#include <optional>
24#include <regex>
25#include <sstream>
26
27namespace fs = std::filesystem;
28using json = nlohmann::json;
29
30static auto logger = entropic::log::get("mcp.filesystem");
31
32namespace entropic {
33
34// ── FileAccessTracker ────────────────────────────────────
35
43void FileAccessTracker::record_read(const std::string& path,
44 size_t hash) {
45 reads_[path] = hash;
46 logger->info("Tracked read: {}", path);
47}
48
58 const std::string& path,
59 size_t current_hash) const {
60 auto it = reads_.find(path);
61 if (it == reads_.end()) {
62 return false;
63 }
64 return it->second == current_hash;
65}
66
74bool FileAccessTracker::was_read(const std::string& path) const {
75 return reads_.count(path) > 0;
76}
77
78// ── File-local helpers ───────────────────────────────────
79
80namespace {
81
87const std::vector<std::string> SKIP_DIRS = {
88 ".git", "node_modules", "__pycache__", ".venv"
89};
90
98bool should_skip_dir(const std::string& name) {
99 for (const auto& skip : SKIP_DIRS) {
100 if (name == skip) {
101 return true;
102 }
103 }
104 return false;
105}
106
115std::string read_file_contents(const fs::path& path) {
116 std::ifstream in(path, std::ios::binary);
117 if (!in.is_open()) {
118 throw std::runtime_error(
119 "Cannot open file: " + path.string());
120 }
121 std::ostringstream ss;
122 ss << in.rdbuf();
123 return ss.str();
124}
125
133void write_file_contents(const fs::path& path,
134 const std::string& content) {
135 fs::create_directories(path.parent_path());
136 std::ofstream out(path, std::ios::binary | std::ios::trunc);
137 if (!out.is_open()) {
138 throw std::runtime_error(
139 "Cannot write file: " + path.string());
140 }
141 out << content;
142}
143
151size_t hash_content(const std::string& s) {
152 return std::hash<std::string>{}(s);
153}
154
163std::string make_error(const std::string& code,
164 const std::string& message) {
165 json j;
166 j["error"] = code;
167 j["message"] = message;
168 return j.dump();
169}
170
179std::string build_read_result(const std::string& path,
180 const std::string& content) {
181 json result;
182 result["path"] = path;
183
184 std::istringstream stream(content);
185 std::string line;
186 json lines = json::object();
187 int num = 0;
188
189 while (std::getline(stream, line)) {
190 ++num;
191 lines[std::to_string(num)] = line;
192 }
193
194 result["total"] = num;
195 result["lines"] = std::move(lines);
196 return result.dump();
197}
198
216bool glob_match(const std::string& filename,
217 const std::string& pattern) {
218 std::string regex_str;
219 regex_str.reserve(pattern.size() * 2);
220
221 for (char ch : pattern) {
222 if (ch == '*') {
223 regex_str += ".*";
224 } else if (ch == '?') {
225 regex_str += '.';
226 } else if (ch == '.') {
227 regex_str += "\\.";
228 } else {
229 regex_str += ch;
230 }
231 }
232
233 try {
234 std::regex re(regex_str, std::regex::icase);
235 return std::regex_match(filename, re);
236 } catch (const std::regex_error& e) {
237 logger->warn(
238 "glob_match: malformed pattern '{}' → {} — treated as "
239 "non-match", pattern, e.what());
240 return false;
241 }
242}
243
272std::vector<std::string> split_brace_alternatives(
273 const std::string& body) {
274 std::vector<std::string> out;
275 std::string current;
276 for (char c : body) {
277 if (c == ',') {
278 out.push_back(std::move(current));
279 current.clear();
280 } else {
281 current += c;
282 }
283 }
284 out.push_back(std::move(current));
285 return out;
286}
287
293std::vector<std::string> multiply_alternatives(
294 const std::vector<std::string>& bases,
295 const std::vector<std::string>& alternatives) {
296 std::vector<std::string> next;
297 next.reserve(bases.size() * alternatives.size());
298 for (const auto& base : bases) {
299 for (const auto& alt : alternatives) {
300 next.push_back(base + alt);
301 }
302 }
303 return next;
304}
305
317std::vector<std::string> expand_braces(const std::string& pattern) {
318 std::vector<std::string> out{""};
319 size_t i = 0;
320 while (i < pattern.size()) {
321 char c = pattern[i];
322 auto close = (c == '{')
323 ? pattern.find('}', i + 1)
324 : std::string::npos;
325 bool is_group = (c == '{') && (close != std::string::npos);
326 if (!is_group) {
327 for (auto& s : out) { s += c; }
328 ++i;
329 continue;
330 }
331 auto body = pattern.substr(i + 1, close - i - 1);
332 out = multiply_alternatives(
333 out, split_brace_alternatives(body));
334 i = close + 1;
335 }
336 return out;
337}
338
347std::string check_read_before_write(
348 const FileAccessTracker& tracker,
349 const std::string& path) {
350 if (fs::exists(path) && !tracker.was_read(path)) {
351 logger->warn("Read-before-write violation: {}", path);
352 return make_error("read_before_write",
353 "File must be read before writing: " + path);
354 }
355 return "";
356}
357
371int count_occurrences(const std::string& content,
372 const std::string& needle) {
373 int count = 0;
374 size_t pos = 0;
375 while ((pos = content.find(needle, pos)) != std::string::npos) {
376 count++;
377 pos += needle.size();
378 }
379 return count;
380}
381
393std::optional<std::string>
394apply_str_replace(const std::string& content, const std::string& old_str, const std::string& new_str, bool replace_all, std::string& error_type) {
395
396 int occurrences = count_occurrences(content, old_str);
397 if (occurrences == 0) {
398 error_type = "not_found";
399 return std::nullopt;
400 }
401 if (!replace_all && occurrences > 1) {
402 error_type = "multiple_matches";
403 return std::nullopt;
404 }
405
406 std::string result = content;
407 auto pos = result.find(old_str);
408 while (pos != std::string::npos) {
409 result.replace(pos, old_str.size(), new_str);
410 if (!replace_all) { break; }
411 pos = result.find(old_str, pos + new_str.size());
412 }
413 return result;
414}
415
425std::string apply_insert(const std::string& content,
426 int line_num,
427 const std::string& new_str) {
428 // (#13/#15 v2.1.4: function body unchanged; doxygen-guard parser
429 // re-evaluates the position after surrounding helpers were added.)
430 std::istringstream stream(content);
431 std::ostringstream out;
432 std::string line;
433 int current = 0;
434
435 while (std::getline(stream, line)) {
436 ++current;
437 if (current == line_num) {
438 out << new_str << '\n';
439 }
440 out << line << '\n';
441 }
442
443 if (line_num > current) {
444 out << new_str << '\n';
445 }
446 return out.str();
447}
448
449
458bool any_glob_match(const std::string& filename,
459 const std::vector<std::string>& patterns) {
460 for (const auto& p : patterns) {
461 if (glob_match(filename, p)) { return true; }
462 }
463 return false;
464}
465
491enum class EntryAction {
492 kSkip,
493 kSkipPrune,
494 kTake
495};
496
509EntryAction classify_glob_entry(
510 const fs::directory_entry& entry,
511 const fs::path& root,
512 const std::vector<std::string>& patterns,
513 const IgnoreMatcher* ignore) {
514 bool is_dir = entry.is_directory();
515 EntryAction result = EntryAction::kSkip;
516 bool hardcoded_skip = is_dir
517 && should_skip_dir(entry.path().filename().string());
518 bool ignore_hit = false;
519 if (!hardcoded_skip && ignore != nullptr) {
520 auto rel = fs::relative(entry.path(), root)
521 .generic_string();
522 ignore_hit = !rel.empty()
523 && ignore->is_ignored(rel, is_dir);
524 }
525 if (hardcoded_skip || (ignore_hit && is_dir)) {
526 result = EntryAction::kSkipPrune;
527 } else if (ignore_hit) {
528 result = EntryAction::kSkip;
529 } else if (entry.is_regular_file()
530 && any_glob_match(
531 entry.path().filename().string(), patterns)) {
532 result = EntryAction::kTake;
533 }
534 return result;
535}
536
549std::vector<std::string> collect_glob_matches(
550 const fs::path& root,
551 const std::string& pattern,
552 int max_results,
553 const IgnoreMatcher* ignore = nullptr) {
554
555 auto patterns = expand_braces(pattern);
556 std::vector<std::string> matches;
557 auto it = fs::recursive_directory_iterator(
558 root, fs::directory_options::skip_permission_denied);
559
560 for (auto& entry : it) {
561 if (static_cast<int>(matches.size()) >= max_results) {
562 break;
563 }
564 auto action = classify_glob_entry(entry, root, patterns,
565 ignore);
566 if (action == EntryAction::kSkipPrune) {
567 it.disable_recursion_pending();
568 } else if (action == EntryAction::kTake) {
569 matches.push_back(entry.path().string());
570 }
571 }
572 return matches;
573}
574
584void grep_file(const fs::path& path,
585 const std::regex& re,
586 std::vector<json>& matches,
587 int limit) {
588 std::ifstream in(path);
589 if (!in.is_open()) {
590 return;
591 }
592
593 std::string line;
594 int line_num = 0;
595 while (std::getline(in, line)) {
596 ++line_num;
597 if (static_cast<int>(matches.size()) >= limit) {
598 return;
599 }
600 if (!std::regex_search(line, re)) {
601 continue;
602 }
603 json m;
604 m["path"] = path.string();
605 m["line"] = line_num;
606 m["content"] = line;
607 matches.push_back(std::move(m));
608 }
609}
610
618json entry_to_json(const fs::directory_entry& entry) {
619 json j;
620 j["name"] = entry.path().filename().string();
621
622 if (entry.is_directory()) {
623 j["type"] = "directory";
624 j["size"] = 0;
625 } else {
626 j["type"] = "file";
627 j["size"] = entry.is_regular_file()
628 ? static_cast<int64_t>(entry.file_size())
629 : 0;
630 }
631 return j;
632}
633
643std::vector<json> collect_entries(const fs::path& dir,
644 bool recursive,
645 int max_depth) {
646 std::vector<json> entries;
647
648 if (!recursive) {
649 for (auto& entry : fs::directory_iterator(dir)) {
650 entries.push_back(entry_to_json(entry));
651 }
652 return entries;
653 }
654
655 auto it = fs::recursive_directory_iterator(
656 dir, fs::directory_options::skip_permission_denied);
657 for (auto& entry : it) {
658 if (it.depth() > max_depth) {
659 it.disable_recursion_pending();
660 continue;
661 }
662 entries.push_back(entry_to_json(entry));
663 }
664 return entries;
665}
666
676std::string do_str_replace(const json& args,
677 const std::string& content,
678 std::string& out) {
679 auto old_str = args.at("old_string").get<std::string>();
680 auto new_str = args.at("new_string").get<std::string>();
681 bool replace_all = args.value("replace_all", false);
682
683 std::string error_type;
684 auto result = apply_str_replace(
685 content, old_str, new_str, replace_all, error_type);
686 if (!result.has_value()) {
687 auto msg = (error_type == "multiple_matches")
688 ? "old_string found multiple times — use replace_all"
689 : "old_string not found in file";
690 return make_error(error_type, msg);
691 }
692 out = result.value();
693 return "";
694}
695
705std::string do_insert(const json& args,
706 const std::string& content,
707 std::string& out) {
708 auto line_num = args.at("insert_line").get<int>();
709 auto new_str = args.at("new_string").get<std::string>();
710 out = apply_insert(content, line_num, new_str);
711 return "";
712}
713
723std::string apply_edit(const json& args,
724 const std::filesystem::path& resolved,
725 const std::string& path_str) {
726 auto content = read_file_contents(resolved);
727 std::string edited;
728 std::string err;
729
730 if (args.contains("old_string")) {
731 err = do_str_replace(args, content, edited);
732 } else if (args.contains("insert_line")) {
733 err = do_insert(args, content, edited);
734 } else {
735 return make_error("invalid_args",
736 "edit_file requires old_string or insert_line");
737 }
738
739 if (!err.empty()) {
740 return err;
741 }
742
743 write_file_contents(resolved, edited);
744 logger->info("Edited file: {}", path_str);
745
746 json j;
747 j["path"] = path_str;
748 j["message"] = "Edit applied successfully";
749 return j.dump();
750}
751
752} // anonymous namespace
753
754// ── ReadFileTool ─────────────────────────────────────────
755
761class ReadFileTool : public ToolBase {
762public:
771 const std::string& data_dir)
773 "read_file", "filesystem",
774 data_dir + "/tools")),
775 server_(server) {}
776
785 }
786
794 ServerResponse execute(const std::string& args_json) override;
795
803 std::string anchor_key(
804 const std::string& args_json) const override {
805 auto args = json::parse(args_json);
806 return "file:" + args.at("path").get<std::string>();
807 }
808
809private:
810 FilesystemServer& server_;
811};
812
830 const fs::path& resolved,
831 const std::string& path_str) {
832 std::string err;
833 if (!fs::exists(resolved)) {
834 err = make_error("not_found",
835 "File not found: " + path_str);
836 } else {
837 auto rel = fs::relative(resolved, server.root_dir())
838 .generic_string();
839 bool ignored = !rel.empty()
840 && server.ignore().is_ignored(rel, /*is_dir=*/false);
841 int size = static_cast<int>(fs::file_size(resolved));
842 int limit = server.max_read_bytes();
843 if (ignored) {
844 err = make_error("ignored",
845 "Path '" + rel + "' is excluded by .gitignore or "
846 ".explorerignore.");
847 } else if (limit > 0 && size > limit) {
848 err = make_error("size_exceeded",
849 "File " + path_str + " is " +
850 std::to_string(size) + " bytes (limit: " +
851 std::to_string(limit) + ")");
852 }
853 }
854 return err;
855}
856
866ServerResponse ReadFileTool::execute(const std::string& args_json) {
867 auto args = json::parse(args_json);
868 auto requested = args.at("path").get<std::string>();
869 auto resolved = server_.resolve_path(requested);
870 auto path_str = resolved.string();
871
872 auto err = check_read_gates(server_, resolved, path_str);
873 if (!err.empty()) { return {err, {}}; }
874
875 auto content = read_file_contents(resolved);
876 server_.tracker().record_read(path_str,
877 hash_content(content));
878 auto size = static_cast<int>(fs::file_size(resolved));
879 logger->info("Read file: {} ({} bytes)", path_str, size);
880 return {build_read_result(path_str, content), {}};
881}
882
883// ── WriteFileTool ────────────────────────────────────────
884
890class WriteFileTool : public ToolBase {
891public:
900 const std::string& data_dir)
902 "write_file", "filesystem",
903 data_dir + "/tools")),
904 server_(server) {}
905
913 ServerResponse execute(const std::string& args_json) override;
914
915private:
916 FilesystemServer& server_;
917};
918
927 const std::string& args_json) {
928
929 auto args = json::parse(args_json);
930 auto requested = args.at("path").get<std::string>();
931 auto content = args.at("content").get<std::string>();
932 auto resolved = server_.resolve_path(requested);
933 auto path_str = resolved.string();
934
935 auto violation = check_read_before_write(
936 server_.tracker(), path_str);
937 if (!violation.empty()) {
938 return {violation, {}};
939 }
940
941 write_file_contents(resolved, content);
942 logger->info("Wrote file: {} ({} bytes)",
943 path_str, content.size());
944
945 json result;
946 result["path"] = path_str;
947 result["bytes_written"] = content.size();
948 result["message"] = "File written successfully";
949 return {result.dump(), {}};
950}
951
952// ── EditFileTool ─────────────────────────────────────────
953
959class EditFileTool : public ToolBase {
960public:
969 const std::string& data_dir)
971 "edit_file", "filesystem",
972 data_dir + "/tools")),
973 server_(server) {}
974
982 ServerResponse execute(const std::string& args_json) override;
983
984private:
985 FilesystemServer& server_;
986};
987
995ServerResponse EditFileTool::execute(const std::string& args_json) {
996 auto args = json::parse(args_json);
997 auto requested = args.at("path").get<std::string>();
998 auto resolved = server_.resolve_path(requested);
999 auto path_str = resolved.string();
1000
1001 auto violation = check_read_before_write(
1002 server_.tracker(), path_str);
1003 if (!violation.empty()) {
1004 return {violation, {}};
1005 }
1006
1007 auto result = apply_edit(args, resolved, path_str);
1008 ServerResponse resp;
1009 resp.result = result;
1010 return resp;
1011}
1012
1013// ── GlobTool ─────────────────────────────────────────────
1014
1020class GlobTool : public ToolBase {
1021public:
1029 GlobTool(FilesystemServer& server, const std::string& data_dir)
1031 "glob", "filesystem",
1032 data_dir + "/tools")),
1033 server_(server) {}
1034
1042 return MCPAccessLevel::READ;
1043 }
1044
1052 ServerResponse execute(const std::string& args_json) override;
1053
1054private:
1055 FilesystemServer& server_;
1056};
1057
1069ServerResponse GlobTool::execute(const std::string& args_json) {
1070 auto args = json::parse(args_json);
1071 auto pattern = args.at("pattern").get<std::string>();
1072 constexpr int MAX_GLOB_RESULTS = 500;
1073
1074 // Issue #15 (v2.1.4): pass server's IgnoreMatcher so build/, vendor/,
1075 // doxygen/, and anything else listed in .gitignore + .explorerignore
1076 // is filtered out. Pre-2.1.4 only the hardcoded SKIP_DIRS were honored.
1077 // Issue #13 (v2.1.4): brace expansion handled inside.
1078 auto matches = collect_glob_matches(
1079 server_.root_dir(), pattern, MAX_GLOB_RESULTS,
1080 &server_.ignore());
1081
1082 logger->info("Glob '{}': {} matches (after ignore filtering)",
1083 pattern, matches.size());
1084 json result = matches;
1085 return {result.dump(), {}};
1086}
1087
1088// ── GrepTool ─────────────────────────────────────────────
1089
1095class GrepTool : public ToolBase {
1096public:
1103 GrepTool(FilesystemServer& server, const std::string& data_dir)
1105 "grep", "filesystem",
1106 /* tools dir: */ data_dir + "/tools")),
1107 server_(server) {}
1108
1116 return MCPAccessLevel::READ;
1117 }
1118
1126 ServerResponse execute(const std::string& args_json) override;
1127
1128private:
1129 FilesystemServer& server_;
1130};
1131
1142std::regex compile_grep_or_error(const std::string& pattern,
1143 std::string& err) {
1144 try {
1145 return std::regex(pattern);
1146 } catch (const std::regex_error& e) {
1147 err = make_error("invalid_regex", e.what());
1148 return std::regex("(?!)");
1149 }
1150}
1151
1172static std::vector<json> grep_search(
1173 const fs::path& root, const std::vector<std::string>& file_patterns,
1174 const std::regex& re, const IgnoreMatcher& ignore) {
1175 constexpr int MAX_GREP_RESULTS = 100;
1176 std::vector<json> matches;
1177 auto it = fs::recursive_directory_iterator(
1178 root, fs::directory_options::skip_permission_denied);
1179 for (auto& entry : it) {
1180 if (static_cast<int>(matches.size()) >= MAX_GREP_RESULTS) {
1181 break;
1182 }
1183 auto action = classify_glob_entry(entry, root, file_patterns,
1184 &ignore);
1185 if (action == EntryAction::kSkipPrune) {
1186 it.disable_recursion_pending();
1187 } else if (action == EntryAction::kTake) {
1188 grep_file(entry.path(), re, matches, MAX_GREP_RESULTS);
1189 }
1190 }
1191 return matches;
1192}
1193
1199ServerResponse GrepTool::execute(const std::string& args_json) {
1200 auto args = json::parse(args_json);
1201 auto pattern = args.at("pattern").get<std::string>();
1202 auto file_glob = args.value("glob", std::string("*"));
1203
1204 std::string err;
1205 auto re = compile_grep_or_error(pattern, err);
1206 if (!err.empty()) { return {err, {}}; }
1207
1208 auto file_patterns = expand_braces(file_glob);
1209 auto matches = grep_search(server_.root_dir(), file_patterns, re,
1210 server_.ignore());
1211
1212 logger->info("Grep '{}': {} matches (after ignore filtering)",
1213 pattern, matches.size());
1214 json result = matches;
1215 return {result.dump(), {}};
1216}
1217
1218// ── ListDirectoryTool ────────────────────────────────────
1219
1226public:
1235 const std::string& data_dir)
1237 "list_directory", "filesystem",
1238 data_dir + "/tools")),
1239 server_(server) {}
1240
1248 return MCPAccessLevel::READ;
1249 }
1250
1258 ServerResponse execute(const std::string& args_json) override;
1259
1260private:
1261 FilesystemServer& server_;
1262};
1263
1272 const std::string& args_json) {
1273
1274 auto args = json::parse(args_json);
1275 auto requested = args.at("path").get<std::string>();
1276 auto recursive = args.value("recursive", false);
1277 auto max_depth = args.value("max_depth", 3);
1278
1279 auto resolved = server_.resolve_path(requested);
1280 if (!fs::is_directory(resolved)) {
1281 return {make_error("not_directory",
1282 "Not a directory: " + resolved.string()), {}};
1283 }
1284
1285 auto entries = collect_entries(
1286 resolved, recursive, max_depth);
1287
1288 logger->info("Listed {}: {} entries",
1289 resolved.string(), entries.size());
1290 json result = entries;
1291 return {result.dump(), {}};
1292}
1293
1294// ── FilesystemServer ─────────────────────────────────────
1295
1305 int model_context_bytes) {
1306 if (config.max_read_bytes.has_value()) {
1307 return config.max_read_bytes.value();
1308 }
1309 if (model_context_bytes <= 0) {
1310 // Safe default: 32KB prevents a single file from blowing typical
1311 // context budgets (16-128K). Large files trigger size_exceeded and
1312 // the model must use offset/limit or docs.* tools instead.
1313 return 32 * 1024;
1314 }
1315 return static_cast<int>(
1316 model_context_bytes * config.max_read_context_pct);
1317}
1318
1333 const fs::path& root_dir,
1334 const FilesystemConfig& config,
1335 const std::string& data_dir,
1336 int model_context_bytes)
1337 : MCPServerBase("filesystem"),
1338 root_dir_(fs::weakly_canonical(root_dir)),
1339 config_(config),
1340 max_read_bytes_(compute_max_read_bytes(
1341 config, model_context_bytes)) {
1342
1343 create_fs_tools(data_dir);
1344 register_fs_tools();
1345
1346 // Issue #15 (v2.1.4): load .gitignore + .explorerignore so glob,
1347 // grep, and read_file can filter out build artifacts and vendor
1348 // blobs that pre-2.1.4 leaked into results.
1349 ignore_.load(root_dir_);
1350
1351 logger->info("FilesystemServer initialized: root={}, "
1352 "max_read_bytes={}, ignore_rules={}",
1353 root_dir_.string(),
1354 max_read_bytes_,
1355 ignore_.rule_count());
1356}
1357
1364void FilesystemServer::create_fs_tools(const std::string& data_dir) {
1365 read_file_ = std::make_unique<ReadFileTool>(*this, data_dir);
1366 write_file_ = std::make_unique<WriteFileTool>(*this, data_dir);
1367 edit_file_ = std::make_unique<EditFileTool>(*this, data_dir);
1368 glob_ = std::make_unique<GlobTool>(*this, data_dir);
1369 grep_ = std::make_unique<GrepTool>(*this, data_dir);
1370 list_dir_ = std::make_unique<ListDirectoryTool>(*this, data_dir);
1371}
1372
1378void FilesystemServer::register_fs_tools() {
1379 register_tool(read_file_.get());
1380 register_tool(write_file_.get());
1381 register_tool(edit_file_.get());
1382 register_tool(glob_.get());
1383 register_tool(grep_.get());
1384 register_tool(list_dir_.get());
1385}
1386
1393
1402 const std::string& tool_name) const {
1403 return tool_name == "read_file";
1404}
1405
1417bool FilesystemServer::set_working_dir(const std::string& path) {
1418 auto canonical = fs::weakly_canonical(path);
1419 if (!fs::is_directory(canonical)) {
1420 logger->error("set_working_dir: not a directory: {}",
1421 path);
1422 return false;
1423 }
1424 root_dir_ = canonical;
1425 // Issue #15 (v2.1.4): reload ignore rules for the new root.
1426 ignore_.load(root_dir_);
1427 logger->info("Working directory changed to: {} (ignore_rules={})",
1428 root_dir_.string(), ignore_.rule_count());
1429 return true;
1430}
1431
1438const fs::path& FilesystemServer::root_dir() const {
1439 return root_dir_;
1440}
1441
1449 return tracker_;
1450}
1451
1459 return ignore_;
1460}
1461
1469 return config_;
1470}
1471
1479 return max_read_bytes_;
1480}
1481
1499 const std::string& requested) const {
1500
1501 fs::path req_path(requested);
1502 fs::path resolved = req_path.is_absolute()
1503 ? fs::weakly_canonical(req_path)
1504 : fs::weakly_canonical(root_dir_ / req_path);
1505
1506 fs::path rel = resolved.lexically_relative(root_dir_);
1507 bool under_root = !rel.empty()
1508 && *rel.begin() != fs::path("..")
1509 && rel != fs::path("..");
1510
1511 if (!under_root && !config_.allow_outside_root) {
1512 logger->error("Path escape blocked: {} (root: {})",
1513 resolved.string(), root_dir_.string());
1514 throw std::runtime_error(
1515 "Path escapes project root: " + resolved.string());
1516 }
1517 return resolved;
1518}
1519
1520} // namespace entropic
Tool for in-place file editing (string replace or insert).
ServerResponse execute(const std::string &args_json) override
Edit a file via string replacement or line insertion.
EditFileTool(FilesystemServer &server, const std::string &data_dir)
Construct from server reference and data directory.
Tracks file read state for read-before-write enforcement.
Definition filesystem.h:30
bool was_read(const std::string &path) const
Check if a file was ever read.
void record_read(const std::string &path, size_t hash)
Record that a file was read.
bool was_read_unchanged(const std::string &path, size_t current_hash) const
Check if a file was read and content unchanged.
Filesystem MCP server with read-before-write enforcement.
Definition filesystem.h:74
int max_read_bytes() const
Get max read bytes (size gate).
const IgnoreMatcher & ignore() const
Get the ignore matcher (#15, v2.1.4).
bool skip_duplicate_check(const std::string &tool_name) const override
read_file must always execute (updates FileAccessTracker).
bool set_working_dir(const std::string &path) override
Set working directory (changes root_dir).
~FilesystemServer() override
Destructor (default, unique_ptr cleanup).
std::filesystem::path resolve_path(const std::string &requested) const
Resolve and validate a path against root.
const FilesystemConfig & config() const
Get the filesystem config.
FileAccessTracker & tracker()
Get the file access tracker.
FilesystemServer(const std::filesystem::path &root_dir, const FilesystemConfig &config, const std::string &data_dir, int model_context_bytes=0)
Construct with root directory, config, and data dir.
const std::filesystem::path & root_dir() const
Get the root directory.
Tool for recursive file pattern matching.
MCPAccessLevel required_access_level() const override
Read-only tool — requires READ access.
ServerResponse execute(const std::string &args_json) override
Find files matching a glob pattern.
GlobTool(FilesystemServer &server, const std::string &data_dir)
Construct with server reference and data directory.
Tool for regex content search across files.
ServerResponse execute(const std::string &args_json) override
Search files for regex pattern matches.
MCPAccessLevel required_access_level() const override
Read-only tool — requires READ access.
GrepTool(FilesystemServer &server, const std::string &data_dir)
Construct from data directory.
gitignore-style path matcher (#15, v2.1.4).
void load(const std::filesystem::path &root)
Load gitignore + explorerignore from a workspace root.
bool is_ignored(const std::string &rel_path, bool is_dir) const
Test whether a path is ignored.
std::size_t rule_count() const
Number of compiled rules (test surface).
Tool for listing directory contents.
MCPAccessLevel required_access_level() const override
Read-only tool — requires READ access.
ListDirectoryTool(FilesystemServer &server, const std::string &data_dir)
Construct from server reference and data directory.
ServerResponse execute(const std::string &args_json) override
List directory entries with optional recursion.
Concrete base class for MCP servers (80% logic).
Definition server_base.h:66
void register_tool(ToolBase *tool)
Register a tool with this server.
Tool for reading file contents with line numbering.
ReadFileTool(FilesystemServer &server, const std::string &data_dir)
Construct from server reference and data directory.
ServerResponse execute(const std::string &args_json) override
Read a file and return numbered lines as JSON.
MCPAccessLevel required_access_level() const override
Read-only tool — requires READ access.
std::string anchor_key(const std::string &args_json) const override
Anchor key for context deduplication.
Abstract base class for individual MCP tools.
Definition tool_base.h:45
Tool for writing file contents with read-before-write.
ServerResponse execute(const std::string &args_json) override
Write content to a file after read-before-write check.
WriteFileTool(FilesystemServer &server, const std::string &data_dir)
Construct from server reference and data directory.
Filesystem MCP server — read/write/edit/glob/grep/list_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
Activate model on GPU (WARM → ACTIVE).
ToolDefinition load_tool_definition(const std::string &tool_name, const std::string &server_prefix, const std::string &data_dir)
Load a tool definition from a JSON file.
Definition tool_base.cpp:81
static int compute_max_read_bytes(const FilesystemConfig &config, int model_context_bytes)
Compute max read bytes from config and model context.
std::regex compile_grep_or_error(const std::string &pattern, std::string &err)
Compile a regex or return a structured tool error.
MCPAccessLevel
MCP tool access level for per-identity authorization.
Definition config.h:38
@ READ
Read-only operations (e.g., read_file, list_directory)
static std::vector< json > grep_search(const fs::path &root, const std::vector< std::string > &file_patterns, const std::regex &re, const IgnoreMatcher &ignore)
Execute grep: brace-expand the file glob, compile the content regex (error-safe), iterate the tree ap...
std::string check_read_gates(FilesystemServer &server, const fs::path &resolved, const std::string &path_str)
Execute read_file: resolve, size-check, read, hash, track.
MCPServerBase concrete base class + ServerResponse.
Filesystem MCP server configuration.
Definition config.h:410
bool allow_outside_root
Allow file ops outside workspace root.
Definition config.h:414
std::optional< int > max_read_bytes
Max file read size (nullopt = derive from context)
Definition config.h:415
float max_read_context_pct
Max context % for single file read.
Definition config.h:416
Structured result from tool execution.
Definition server_base.h:33
std::string result
Human-readable result.
Definition server_base.h:34
Abstract base class for individual MCP tools.