archtracker-mcp 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/mcp/index.ts
4
4
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { z as z2 } from "zod";
6
+ import { z as z3 } from "zod";
7
7
 
8
8
  // src/analyzer/analyze.ts
9
9
  import { resolve as resolve3 } from "path";
@@ -1087,6 +1087,13 @@ var java = {
1087
1087
  if (projectFiles.has(full)) return full;
1088
1088
  }
1089
1089
  }
1090
+ for (let i = 1; i < segments.length; i++) {
1091
+ const filePath = segments.slice(i).join("/") + ".java";
1092
+ for (const srcRoot of ["", "src/main/java/", "src/", "app/src/main/java/"]) {
1093
+ const full = join3(rootDir, srcRoot, filePath);
1094
+ if (projectFiles.has(full)) return full;
1095
+ }
1096
+ }
1090
1097
  return null;
1091
1098
  },
1092
1099
  defaultExclude: ["build", "target", "\\.gradle", "\\.idea"]
@@ -1141,12 +1148,18 @@ var php = {
1141
1148
  importPatterns: [
1142
1149
  // require/include/require_once/include_once 'path'
1143
1150
  { regex: /\b(?:require|include)(?:_once)?\s+['"]([^'"]+)['"]/gm },
1151
+ // require_once __DIR__ . '/path' (common PHP pattern)
1152
+ { regex: /\b(?:require|include)(?:_once)?\s+__DIR__\s*\.\s*['"]([^'"]+)['"]/gm },
1144
1153
  // Bug #9 fix: use Namespace\Class — skip `function` and `const` keywords
1145
1154
  { regex: /^use\s+(?:function\s+|const\s+)?([\w\\]+)/gm }
1146
1155
  ],
1147
1156
  resolveImport(importPath, sourceFile, rootDir, projectFiles) {
1148
- if (importPath.includes("/") || importPath.endsWith(".php")) {
1149
- const withExt = importPath.endsWith(".php") ? importPath : importPath + ".php";
1157
+ let normalizedPath = importPath;
1158
+ if (normalizedPath.startsWith("/")) {
1159
+ normalizedPath = normalizedPath.slice(1);
1160
+ }
1161
+ if (normalizedPath.includes("/") || normalizedPath.endsWith(".php")) {
1162
+ const withExt = normalizedPath.endsWith(".php") ? normalizedPath : normalizedPath + ".php";
1150
1163
  const fromSource = resolve2(dirname(sourceFile), withExt);
1151
1164
  if (projectFiles.has(fromSource)) return fromSource;
1152
1165
  const fromRoot2 = join3(rootDir, withExt);
@@ -1162,15 +1175,46 @@ var php = {
1162
1175
  },
1163
1176
  defaultExclude: ["vendor"]
1164
1177
  };
1178
+ var SWIFT_SKIP_FILES = /* @__PURE__ */ new Set(["Package", "main", "AppDelegate", "SceneDelegate"]);
1165
1179
  var swift = {
1166
1180
  id: "swift",
1167
1181
  extensions: [".swift"],
1168
1182
  commentStyle: "c-style",
1169
- importPatterns: [
1170
- // Bug #10 fix: import ModuleName and @testable import ModuleName
1171
- { regex: /^(?:@testable\s+)?import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+|typealias\s+)?(\w+)/gm }
1172
- ],
1173
- resolveImport(importPath, sourceFile, rootDir, projectFiles) {
1183
+ importPatterns: [],
1184
+ // handled by extractImports
1185
+ extractImports(content, filePath, _rootDir, projectFiles) {
1186
+ const imports = [];
1187
+ const moduleRegex = /^(?:@testable\s+)?import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+|typealias\s+)?(\w+)/gm;
1188
+ let match;
1189
+ while ((match = moduleRegex.exec(content)) !== null) {
1190
+ imports.push(match[1]);
1191
+ }
1192
+ const typeMap = /* @__PURE__ */ new Map();
1193
+ for (const f of projectFiles) {
1194
+ if (f === filePath || !f.endsWith(".swift")) continue;
1195
+ const basename = f.split("/").pop().replace(/\.swift$/, "");
1196
+ if (!basename || SWIFT_SKIP_FILES.has(basename)) continue;
1197
+ typeMap.set(basename, f);
1198
+ }
1199
+ if (typeMap.size > 0) {
1200
+ const escaped = [...typeMap.keys()].map(
1201
+ (n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
1202
+ );
1203
+ const combined = new RegExp(`\\b(${escaped.join("|")})\\b`, "g");
1204
+ const matched = /* @__PURE__ */ new Set();
1205
+ while ((match = combined.exec(content)) !== null) {
1206
+ const typeName = match[1];
1207
+ const targetPath = typeMap.get(typeName);
1208
+ if (targetPath && !matched.has(targetPath)) {
1209
+ matched.add(targetPath);
1210
+ imports.push(targetPath);
1211
+ }
1212
+ }
1213
+ }
1214
+ return imports;
1215
+ },
1216
+ resolveImport(importPath, _sourceFile, rootDir, projectFiles) {
1217
+ if (projectFiles.has(importPath)) return importPath;
1174
1218
  const spmDir = join3(rootDir, "Sources", importPath);
1175
1219
  for (const f of projectFiles) {
1176
1220
  if (f.startsWith(spmDir + "/") && f.endsWith(".swift")) return f;
@@ -1195,7 +1239,8 @@ var kotlin = {
1195
1239
  if (cleanPath.endsWith(".")) {
1196
1240
  cleanPath = cleanPath.slice(0, -1);
1197
1241
  }
1198
- const filePath = cleanPath.replace(/\./g, "/");
1242
+ const segments = cleanPath.split(".");
1243
+ const filePath = segments.join("/");
1199
1244
  for (const ext of [".kt", ".kts"]) {
1200
1245
  for (const srcRoot of [
1201
1246
  "",
@@ -1209,6 +1254,22 @@ var kotlin = {
1209
1254
  if (projectFiles.has(full)) return full;
1210
1255
  }
1211
1256
  }
1257
+ for (let i = 1; i < segments.length; i++) {
1258
+ const suffixPath = segments.slice(i).join("/");
1259
+ for (const ext of [".kt", ".kts"]) {
1260
+ for (const srcRoot of [
1261
+ "",
1262
+ "src/main/kotlin/",
1263
+ "src/main/java/",
1264
+ "src/",
1265
+ "app/src/main/kotlin/",
1266
+ "app/src/main/java/"
1267
+ ]) {
1268
+ const full = join3(rootDir, srcRoot, suffixPath + ext);
1269
+ if (projectFiles.has(full)) return full;
1270
+ }
1271
+ }
1272
+ }
1212
1273
  return null;
1213
1274
  },
1214
1275
  defaultExclude: ["build", "\\.gradle", "\\.idea"]
@@ -1294,8 +1355,10 @@ var dart = {
1294
1355
  const prefix = `package:${ownPackage}/`;
1295
1356
  if (!importPath.startsWith(prefix)) return null;
1296
1357
  const relPath = importPath.slice(prefix.length);
1297
- const full = join3(rootDir, "lib", relPath);
1298
- if (projectFiles.has(full)) return full;
1358
+ const libPath = join3(rootDir, "lib", relPath);
1359
+ if (projectFiles.has(libPath)) return libPath;
1360
+ const rootPath = join3(rootDir, relPath);
1361
+ if (projectFiles.has(rootPath)) return rootPath;
1299
1362
  return null;
1300
1363
  }
1301
1364
  const resolved = resolve2(dirname(sourceFile), importPath);
@@ -1358,6 +1421,15 @@ var scala = {
1358
1421
  }
1359
1422
  }
1360
1423
  }
1424
+ for (let i = 1; i < segments.length; i++) {
1425
+ const suffixPath = segments.slice(i).join("/");
1426
+ for (const ext of [".scala", ".sc"]) {
1427
+ for (const srcRoot of ["", "src/main/scala/", "src/", "app/"]) {
1428
+ const full = join3(rootDir, srcRoot, suffixPath + ext);
1429
+ if (projectFiles.has(full)) return full;
1430
+ }
1431
+ }
1432
+ }
1361
1433
  return null;
1362
1434
  },
1363
1435
  defaultExclude: ["target", "\\.bsp", "\\.metals", "\\.bloop"]
@@ -1539,6 +1611,11 @@ var en = {
1539
1611
  "analyze.snapshotSaved": "\nSnapshot saved alongside analysis.",
1540
1612
  // CI
1541
1613
  "ci.generated": "GitHub Actions workflow generated: {path}",
1614
+ // Layers
1615
+ "layers.alreadyExists": "layers.json already exists. Edit it manually to modify.",
1616
+ "layers.created": "Created .archtracker/layers.json \u2014 edit it to configure your layers.",
1617
+ "layers.notFound": "No .archtracker/layers.json found. Run 'archtracker layers init' to create one.",
1618
+ "layers.header": "Configured layers ({count}):",
1542
1619
  // Web viewer
1543
1620
  "web.starting": "Starting architecture viewer...",
1544
1621
  "web.listening": "Architecture graph available at: http://localhost:{port}",
@@ -1627,6 +1704,11 @@ var ja = {
1627
1704
  "analyze.snapshotSaved": "\n\u5206\u6790\u3068\u540C\u6642\u306B\u30B9\u30CA\u30C3\u30D7\u30B7\u30E7\u30C3\u30C8\u3092\u4FDD\u5B58\u3057\u307E\u3057\u305F\u3002",
1628
1705
  // CI
1629
1706
  "ci.generated": "GitHub Actions \u30EF\u30FC\u30AF\u30D5\u30ED\u30FC\u3092\u751F\u6210\u3057\u307E\u3057\u305F: {path}",
1707
+ // Layers
1708
+ "layers.alreadyExists": "layers.json \u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059\u3002\u76F4\u63A5\u7DE8\u96C6\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1709
+ "layers.created": ".archtracker/layers.json \u3092\u4F5C\u6210\u3057\u307E\u3057\u305F\u3002\u30EC\u30A4\u30E4\u30FC\u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1710
+ "layers.notFound": ".archtracker/layers.json \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002'archtracker layers init' \u3067\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
1711
+ "layers.header": "\u8A2D\u5B9A\u6E08\u307F\u30EC\u30A4\u30E4\u30FC ({count}\u4EF6):",
1630
1712
  // Web viewer
1631
1713
  "web.starting": "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\u30D3\u30E5\u30FC\u30A2\u30FC\u3092\u8D77\u52D5\u4E2D...",
1632
1714
  "web.listening": "\u30A2\u30FC\u30AD\u30C6\u30AF\u30C1\u30E3\u30B0\u30E9\u30D5: http://localhost:{port}",
@@ -1761,13 +1843,396 @@ function formatAnalysisReport(graph, options = {}) {
1761
1843
  return lines.join("\n");
1762
1844
  }
1763
1845
 
1846
+ // src/analyzer/multi-layer.ts
1847
+ import { resolve as resolve4, join as join4 } from "path";
1848
+ import { readFileSync as readFileSync2 } from "fs";
1849
+ var LAYER_COLORS = [
1850
+ "#58a6ff",
1851
+ "#3fb950",
1852
+ "#d2a8ff",
1853
+ "#f0883e",
1854
+ "#79c0ff",
1855
+ "#56d4dd",
1856
+ "#db61a2",
1857
+ "#f778ba",
1858
+ "#ffa657",
1859
+ "#7ee787"
1860
+ ];
1861
+ async function analyzeMultiLayer(projectRoot, layerDefs) {
1862
+ const layers = {};
1863
+ const layerMetadata = [];
1864
+ for (let idx = 0; idx < layerDefs.length; idx++) {
1865
+ const def = layerDefs[idx];
1866
+ const targetDir = resolve4(projectRoot, def.targetDir);
1867
+ const graph = await analyzeProject(targetDir, {
1868
+ exclude: def.exclude,
1869
+ language: def.language
1870
+ });
1871
+ const language = def.language ?? await detectLanguage(targetDir) ?? "javascript";
1872
+ layers[def.name] = graph;
1873
+ layerMetadata.push({
1874
+ name: def.name,
1875
+ originalRootDir: graph.rootDir,
1876
+ language,
1877
+ color: def.color ?? LAYER_COLORS[idx % LAYER_COLORS.length],
1878
+ description: def.description,
1879
+ fileCount: graph.totalFiles,
1880
+ edgeCount: graph.totalEdges
1881
+ });
1882
+ }
1883
+ const merged = mergeLayerGraphs(projectRoot, layers);
1884
+ return { layers, layerMetadata, merged };
1885
+ }
1886
+ function detectCrossLayerConnections(layers, layerDefs) {
1887
+ const MIN_NAME_LENGTH = 6;
1888
+ const MIN_SCORE_THRESHOLD = 10;
1889
+ const layerIdentifiers = /* @__PURE__ */ new Map();
1890
+ for (const [layerName, graph] of Object.entries(layers)) {
1891
+ const identifiers = /* @__PURE__ */ new Map();
1892
+ for (const filePath of Object.keys(graph.files)) {
1893
+ const basename = filePath.split("/").pop();
1894
+ const nameNoExt = basename.replace(/\.[^.]+$/, "");
1895
+ if (nameNoExt.length < MIN_NAME_LENGTH || GENERIC_BASENAMES.has(nameNoExt.toLowerCase())) continue;
1896
+ identifiers.set(nameNoExt, filePath);
1897
+ }
1898
+ layerIdentifiers.set(layerName, identifiers);
1899
+ }
1900
+ const nameLayerCount = /* @__PURE__ */ new Map();
1901
+ for (const [, ids] of layerIdentifiers) {
1902
+ for (const name of ids.keys()) {
1903
+ nameLayerCount.set(name, (nameLayerCount.get(name) ?? 0) + 1);
1904
+ }
1905
+ }
1906
+ const pairBest = /* @__PURE__ */ new Map();
1907
+ function tryAdd(pairKey, conn, score) {
1908
+ if (score < MIN_SCORE_THRESHOLD) return;
1909
+ const existing = pairBest.get(pairKey);
1910
+ if (!existing || score > existing.score) {
1911
+ pairBest.set(pairKey, { conn, score });
1912
+ }
1913
+ }
1914
+ function isSelfDefined(content, name) {
1915
+ const defPatterns = [
1916
+ new RegExp(`\\b(?:class|struct|enum|interface|protocol|type|object)\\s+${escapeRegex(name)}\\b`),
1917
+ new RegExp(`\\b(?:def|func|fun|fn)\\s+${escapeRegex(name)}\\b`),
1918
+ new RegExp(`\\b${escapeRegex(name)}\\s*=\\s*(?:class|struct|type|interface)\\b`)
1919
+ ];
1920
+ return defPatterns.some((re) => re.test(content));
1921
+ }
1922
+ function isLocalImportOnly(content, name) {
1923
+ const regex = new RegExp(`\\b${escapeRegex(name)}\\b`, "g");
1924
+ const lines = content.split("\n");
1925
+ let crossLayerRef = false;
1926
+ for (const line of lines) {
1927
+ if (!regex.test(line)) continue;
1928
+ regex.lastIndex = 0;
1929
+ const isLocalImport = /^\s*(?:from\s+[.'"]|import\s+[.'"]|require\s*\(\s*['"][.\/]|#include\s*")/.test(line);
1930
+ if (!isLocalImport) {
1931
+ crossLayerRef = true;
1932
+ break;
1933
+ }
1934
+ }
1935
+ return !crossLayerRef;
1936
+ }
1937
+ for (const [sourceLayer, graph] of Object.entries(layers)) {
1938
+ const ownNames = layerIdentifiers.get(sourceLayer) ?? /* @__PURE__ */ new Map();
1939
+ for (const filePath of Object.keys(graph.files)) {
1940
+ const absPath = join4(graph.rootDir, filePath);
1941
+ let content;
1942
+ try {
1943
+ content = readFileSync2(absPath, "utf-8");
1944
+ } catch {
1945
+ continue;
1946
+ }
1947
+ for (const [targetLayer, targetIds] of layerIdentifiers) {
1948
+ if (targetLayer === sourceLayer) continue;
1949
+ for (const [targetName, targetFile] of targetIds) {
1950
+ if (ownNames.has(targetName)) continue;
1951
+ if ((nameLayerCount.get(targetName) ?? 0) > 1) continue;
1952
+ if (!content.includes(targetName)) continue;
1953
+ const regex = new RegExp(`\\b${escapeRegex(targetName)}\\b`);
1954
+ if (!regex.test(content)) continue;
1955
+ if (isSelfDefined(content, targetName)) continue;
1956
+ if (isLocalImportOnly(content, targetName)) continue;
1957
+ const pairKey = `${sourceLayer}\u2192${targetLayer}`;
1958
+ const isPascalCase = /^[A-Z][a-z]/.test(targetName);
1959
+ const baseScore = targetName.length + (isPascalCase ? 5 : 0);
1960
+ tryAdd(pairKey, {
1961
+ fromLayer: sourceLayer,
1962
+ fromFile: filePath,
1963
+ toLayer: targetLayer,
1964
+ toFile: targetFile,
1965
+ type: "auto",
1966
+ label: targetName
1967
+ }, baseScore);
1968
+ }
1969
+ }
1970
+ for (const def of layerDefs) {
1971
+ if (def.name === sourceLayer) continue;
1972
+ const pairKey = `${sourceLayer}\u2192${def.name}`;
1973
+ const layerName = def.name;
1974
+ const suffixes = ["Client", "Service", "API", "Handler", "Provider", "Manager", "Gateway", "Proxy", "Adapter", "Connector"];
1975
+ const typedRe = new RegExp(`\\b${escapeRegex(layerName)}(?:${suffixes.join("|")})\\b`);
1976
+ if (typedRe.test(content)) {
1977
+ const targetGraph = layers[def.name];
1978
+ if (!targetGraph) continue;
1979
+ const entryFile = findEntryPoint(targetGraph);
1980
+ if (entryFile) {
1981
+ tryAdd(pairKey, {
1982
+ fromLayer: sourceLayer,
1983
+ fromFile: filePath,
1984
+ toLayer: def.name,
1985
+ toFile: entryFile,
1986
+ type: "auto",
1987
+ label: `${layerName}*`
1988
+ }, 25);
1989
+ }
1990
+ }
1991
+ }
1992
+ for (const def of layerDefs) {
1993
+ if (def.name === sourceLayer) continue;
1994
+ const pairKey = `${sourceLayer}\u2192${def.name}`;
1995
+ const dirName = def.targetDir.split("/").pop();
1996
+ const isShortName = dirName.length <= 4;
1997
+ const patterns = [];
1998
+ if (!isShortName) {
1999
+ patterns.push({ re: new RegExp(`(?:from|require|import)\\s+['"].*\\b${escapeRegex(dirName)}\\b`, "i"), score: 15 });
2000
+ patterns.push({ re: new RegExp(`['"\`/]${escapeRegex(dirName)}/[\\w]`, "i"), score: 12 });
2001
+ } else {
2002
+ patterns.push({ re: new RegExp(`(?:from|require|import)\\s+['"].*/${escapeRegex(dirName)}/`, "i"), score: 13 });
2003
+ patterns.push({ re: new RegExp(`['"\`]\\s*(?:https?://[^'"]*)?/${escapeRegex(dirName)}/[\\w]`, "i"), score: 11 });
2004
+ }
2005
+ for (const { re, score } of patterns) {
2006
+ if (re.test(content)) {
2007
+ const targetGraph = layers[def.name];
2008
+ if (!targetGraph) continue;
2009
+ const entryFile = findEntryPoint(targetGraph);
2010
+ if (entryFile) {
2011
+ tryAdd(pairKey, {
2012
+ fromLayer: sourceLayer,
2013
+ fromFile: filePath,
2014
+ toLayer: def.name,
2015
+ toFile: entryFile,
2016
+ type: "auto",
2017
+ label: `\u2192 ${def.name}`
2018
+ }, score);
2019
+ }
2020
+ break;
2021
+ }
2022
+ }
2023
+ }
2024
+ }
2025
+ }
2026
+ return [...pairBest.values()].map((v) => v.conn);
2027
+ }
2028
+ function escapeRegex(s) {
2029
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2030
+ }
2031
+ function findEntryPoint(graph) {
2032
+ const files = Object.values(graph.files);
2033
+ if (files.length === 0) return null;
2034
+ const sorted = files.sort((a, b) => b.dependents.length - a.dependents.length);
2035
+ if (sorted[0].dependents.length > 0) return sorted[0].path;
2036
+ const entryNames = ["main", "index", "app", "server", "lib", "mod"];
2037
+ for (const name of entryNames) {
2038
+ const entry = files.find((f) => {
2039
+ const basename = f.path.split("/").pop().replace(/\.[^.]+$/, "").toLowerCase();
2040
+ return basename === name;
2041
+ });
2042
+ if (entry) return entry.path;
2043
+ }
2044
+ return files[0].path;
2045
+ }
2046
+ var GENERIC_BASENAMES = /* @__PURE__ */ new Set([
2047
+ // Build / project structure
2048
+ "index",
2049
+ "main",
2050
+ "app",
2051
+ "config",
2052
+ "setup",
2053
+ "init",
2054
+ "mod",
2055
+ "package",
2056
+ "build",
2057
+ "makefile",
2058
+ "dockerfile",
2059
+ "rakefile",
2060
+ "gemfile",
2061
+ "podfile",
2062
+ // Common modules
2063
+ "utils",
2064
+ "helpers",
2065
+ "types",
2066
+ "models",
2067
+ "views",
2068
+ "controllers",
2069
+ "services",
2070
+ "lib",
2071
+ "src",
2072
+ "test",
2073
+ "spec",
2074
+ "tests",
2075
+ "bench",
2076
+ "example",
2077
+ "examples",
2078
+ // Infrastructure / patterns
2079
+ "server",
2080
+ "client",
2081
+ "routes",
2082
+ "middleware",
2083
+ "database",
2084
+ "engine",
2085
+ "error",
2086
+ "errors",
2087
+ "logger",
2088
+ "logging",
2089
+ "constants",
2090
+ "common",
2091
+ "base",
2092
+ "core",
2093
+ "data",
2094
+ "manager",
2095
+ "handler",
2096
+ "factory",
2097
+ "context",
2098
+ "state",
2099
+ "store",
2100
+ "cache",
2101
+ "queue",
2102
+ "task",
2103
+ "worker",
2104
+ "adapter",
2105
+ "bridge",
2106
+ // UI / presentation
2107
+ "event",
2108
+ "events",
2109
+ "model",
2110
+ "view",
2111
+ "home",
2112
+ "user",
2113
+ "page",
2114
+ "layout",
2115
+ "router",
2116
+ "provider",
2117
+ "component",
2118
+ "widget",
2119
+ "screen",
2120
+ "template",
2121
+ "header",
2122
+ "footer",
2123
+ "sidebar",
2124
+ "navbar",
2125
+ "dialog",
2126
+ "modal",
2127
+ "panel",
2128
+ // Data / IO
2129
+ "reader",
2130
+ "writer",
2131
+ "parser",
2132
+ "formatter",
2133
+ "serializer",
2134
+ "converter",
2135
+ "loader",
2136
+ "exporter",
2137
+ "importer",
2138
+ "transformer",
2139
+ "mapper",
2140
+ "reducer",
2141
+ "filter",
2142
+ "sorter",
2143
+ "validator",
2144
+ "checker",
2145
+ "scanner",
2146
+ "analyzer",
2147
+ // Auth / Security (generic enough to exist in many layers)
2148
+ "login",
2149
+ "register",
2150
+ "verify",
2151
+ "token",
2152
+ "session",
2153
+ "credential",
2154
+ "password",
2155
+ "permission",
2156
+ "profile",
2157
+ "account",
2158
+ "settings",
2159
+ // Network / API
2160
+ "request",
2161
+ "response",
2162
+ "endpoint",
2163
+ "controller",
2164
+ "service",
2165
+ "gateway",
2166
+ "proxy",
2167
+ "connector",
2168
+ "socket",
2169
+ "channel",
2170
+ "stream",
2171
+ "pipeline",
2172
+ // Storage / DB
2173
+ "schema",
2174
+ "migration",
2175
+ "seed",
2176
+ "fixture",
2177
+ "record",
2178
+ "entity",
2179
+ "repository",
2180
+ "storage",
2181
+ "driver",
2182
+ "connection",
2183
+ "pool",
2184
+ // Testing
2185
+ "mock",
2186
+ "stub",
2187
+ "fake",
2188
+ "helper",
2189
+ "fixture",
2190
+ "factory"
2191
+ ]);
2192
+ function mergeLayerGraphs(projectRoot, layers) {
2193
+ const mergedFiles = {};
2194
+ const mergedEdges = [];
2195
+ const mergedCircular = [];
2196
+ for (const [layerName, graph] of Object.entries(layers)) {
2197
+ for (const [origPath, node] of Object.entries(graph.files)) {
2198
+ const prefixedPath = `${layerName}/${origPath}`;
2199
+ mergedFiles[prefixedPath] = {
2200
+ path: prefixedPath,
2201
+ exists: node.exists,
2202
+ dependencies: node.dependencies.map((d) => `${layerName}/${d}`),
2203
+ dependents: node.dependents.map((d) => `${layerName}/${d}`)
2204
+ };
2205
+ }
2206
+ for (const edge of graph.edges) {
2207
+ mergedEdges.push({
2208
+ source: `${layerName}/${edge.source}`,
2209
+ target: `${layerName}/${edge.target}`,
2210
+ type: edge.type
2211
+ });
2212
+ }
2213
+ for (const circ of graph.circularDependencies) {
2214
+ mergedCircular.push({
2215
+ cycle: circ.cycle.map((f) => `${layerName}/${f}`)
2216
+ });
2217
+ }
2218
+ }
2219
+ return {
2220
+ rootDir: resolve4(projectRoot),
2221
+ files: mergedFiles,
2222
+ edges: mergedEdges,
2223
+ circularDependencies: mergedCircular,
2224
+ totalFiles: Object.keys(mergedFiles).length,
2225
+ totalEdges: mergedEdges.length
2226
+ };
2227
+ }
2228
+
1764
2229
  // src/storage/snapshot.ts
1765
2230
  import { mkdir, writeFile, readFile as readFile2, access } from "fs/promises";
1766
- import { join as join4 } from "path";
2231
+ import { join as join5 } from "path";
1767
2232
  import { z } from "zod";
1768
2233
 
1769
2234
  // src/types/schema.ts
1770
- var SCHEMA_VERSION = "1.0";
2235
+ var SCHEMA_VERSION = "1.1";
1771
2236
 
1772
2237
  // src/storage/snapshot.ts
1773
2238
  var ARCHTRACKER_DIR = ".archtracker";
@@ -1791,26 +2256,27 @@ var DependencyGraphSchema = z.object({
1791
2256
  totalEdges: z.number()
1792
2257
  });
1793
2258
  var SnapshotSchema = z.object({
1794
- version: z.literal(SCHEMA_VERSION),
2259
+ version: z.enum([SCHEMA_VERSION, "1.0"]),
1795
2260
  timestamp: z.string(),
1796
2261
  rootDir: z.string(),
1797
2262
  graph: DependencyGraphSchema
1798
2263
  });
1799
- async function saveSnapshot(projectRoot, graph) {
1800
- const dirPath = join4(projectRoot, ARCHTRACKER_DIR);
1801
- const filePath = join4(dirPath, SNAPSHOT_FILE);
2264
+ async function saveSnapshot(projectRoot, graph, multiLayer) {
2265
+ const dirPath = join5(projectRoot, ARCHTRACKER_DIR);
2266
+ const filePath = join5(dirPath, SNAPSHOT_FILE);
1802
2267
  const snapshot = {
1803
2268
  version: SCHEMA_VERSION,
1804
2269
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1805
2270
  rootDir: graph.rootDir,
1806
- graph
2271
+ graph,
2272
+ ...multiLayer ? { multiLayer } : {}
1807
2273
  };
1808
2274
  await mkdir(dirPath, { recursive: true });
1809
2275
  await writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
1810
2276
  return snapshot;
1811
2277
  }
1812
2278
  async function loadSnapshot(projectRoot) {
1813
- const filePath = join4(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
2279
+ const filePath = join5(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
1814
2280
  let raw;
1815
2281
  try {
1816
2282
  raw = await readFile2(filePath, "utf-8");
@@ -1947,11 +2413,76 @@ function arraysEqual(a, b) {
1947
2413
  return true;
1948
2414
  }
1949
2415
 
2416
+ // src/storage/layers.ts
2417
+ import { readFile as readFile3, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
2418
+ import { join as join6 } from "path";
2419
+ import { z as z2 } from "zod";
2420
+ var ARCHTRACKER_DIR2 = ".archtracker";
2421
+ var LAYERS_FILE = "layers.json";
2422
+ var LayerDefinitionSchema = z2.object({
2423
+ name: z2.string().min(1).regex(
2424
+ /^[a-zA-Z0-9_-]+$/,
2425
+ "Layer name must be alphanumeric (hyphens/underscores allowed)"
2426
+ ),
2427
+ targetDir: z2.string().min(1),
2428
+ language: z2.enum(LANGUAGE_IDS).optional(),
2429
+ exclude: z2.array(z2.string()).optional(),
2430
+ color: z2.string().optional(),
2431
+ description: z2.string().optional()
2432
+ });
2433
+ var CrossLayerConnectionSchema = z2.object({
2434
+ fromLayer: z2.string(),
2435
+ fromFile: z2.string(),
2436
+ toLayer: z2.string(),
2437
+ toFile: z2.string(),
2438
+ type: z2.enum(["api-call", "event", "data-flow", "manual"]),
2439
+ label: z2.string().optional()
2440
+ });
2441
+ var LayerConfigSchema = z2.object({
2442
+ version: z2.literal("1.0"),
2443
+ layers: z2.array(LayerDefinitionSchema).min(1).refine(
2444
+ (layers) => {
2445
+ const names = layers.map((l) => l.name);
2446
+ return new Set(names).size === names.length;
2447
+ },
2448
+ { message: "Layer names must be unique" }
2449
+ ),
2450
+ connections: z2.array(CrossLayerConnectionSchema).optional()
2451
+ });
2452
+ async function loadLayerConfig(projectRoot) {
2453
+ const filePath = join6(projectRoot, ARCHTRACKER_DIR2, LAYERS_FILE);
2454
+ let raw;
2455
+ try {
2456
+ raw = await readFile3(filePath, "utf-8");
2457
+ } catch (error) {
2458
+ if (isNodeError2(error) && error.code === "ENOENT") {
2459
+ return null;
2460
+ }
2461
+ throw new Error(`Failed to read ${filePath}`);
2462
+ }
2463
+ let parsed;
2464
+ try {
2465
+ parsed = JSON.parse(raw);
2466
+ } catch {
2467
+ throw new Error(`Invalid JSON in ${filePath}`);
2468
+ }
2469
+ const result = LayerConfigSchema.safeParse(parsed);
2470
+ if (!result.success) {
2471
+ const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
2472
+ throw new Error(`layers.json validation failed:
2473
+ ${issues}`);
2474
+ }
2475
+ return result.data;
2476
+ }
2477
+ function isNodeError2(error) {
2478
+ return error instanceof Error && "code" in error;
2479
+ }
2480
+
1950
2481
  // src/utils/path-guard.ts
1951
- import { resolve as resolve4 } from "path";
2482
+ import { resolve as resolve5 } from "path";
1952
2483
  function validatePath(inputPath, boundary) {
1953
- const resolved = resolve4(inputPath);
1954
- const root = boundary ? resolve4(boundary) : process.cwd();
2484
+ const resolved = resolve5(inputPath);
2485
+ const root = boundary ? resolve5(boundary) : process.cwd();
1955
2486
  if (!resolved.startsWith(root)) {
1956
2487
  throw new PathTraversalError(
1957
2488
  t("pathGuard.traversal", { input: inputPath, resolved, boundary: root })
@@ -1967,14 +2498,14 @@ var PathTraversalError = class extends Error {
1967
2498
  };
1968
2499
 
1969
2500
  // src/utils/version.ts
1970
- import { readFileSync as readFileSync2 } from "fs";
1971
- import { join as join5, dirname as dirname2 } from "path";
2501
+ import { readFileSync as readFileSync3 } from "fs";
2502
+ import { join as join7, dirname as dirname2 } from "path";
1972
2503
  import { fileURLToPath } from "url";
1973
2504
  function loadVersion() {
1974
2505
  let dir = dirname2(fileURLToPath(import.meta.url));
1975
2506
  for (let i = 0; i < 5; i++) {
1976
2507
  try {
1977
- const pkg = JSON.parse(readFileSync2(join5(dir, "package.json"), "utf-8"));
2508
+ const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf-8"));
1978
2509
  return pkg.version;
1979
2510
  } catch {
1980
2511
  dir = dirname2(dir);
@@ -1989,34 +2520,76 @@ var server = new McpServer({
1989
2520
  name: "archtracker",
1990
2521
  version: VERSION
1991
2522
  });
1992
- var languageEnum = z2.enum(LANGUAGE_IDS);
2523
+ var languageEnum = z3.enum(LANGUAGE_IDS);
1993
2524
  var LANG_DISPLAY = {
1994
2525
  javascript: "JS/TS",
1995
2526
  "c-cpp": "C/C++",
1996
2527
  "c-sharp": "C#"
1997
2528
  };
1998
2529
  var languageList = LANGUAGE_IDS.map((id) => LANG_DISPLAY[id] ?? id.charAt(0).toUpperCase() + id.slice(1)).join(", ");
2530
+ async function resolveGraphForMcp(opts) {
2531
+ if (opts.targetDir === "src") {
2532
+ const layerConfig = await loadLayerConfig(opts.projectRoot);
2533
+ if (layerConfig) {
2534
+ const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers);
2535
+ const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
2536
+ const manualConnections = layerConfig.connections ?? [];
2537
+ const manualKeys = new Set(manualConnections.map(
2538
+ (c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
2539
+ ));
2540
+ const allConnections = [
2541
+ ...manualConnections,
2542
+ ...autoConnections.filter(
2543
+ (c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
2544
+ )
2545
+ ];
2546
+ return { graph: multi.merged, multiLayer: multi, layerMetadata: multi.layerMetadata, crossEdges: allConnections };
2547
+ }
2548
+ }
2549
+ const graph = await analyzeProject(opts.targetDir, {
2550
+ exclude: opts.exclude,
2551
+ language: opts.language
2552
+ });
2553
+ return { graph };
2554
+ }
2555
+ function formatLayerSummary(metadata) {
2556
+ return metadata.map(
2557
+ (m) => ` [${m.name}] ${m.fileCount} files, ${m.edgeCount} edges (${m.language})`
2558
+ ).join("\n");
2559
+ }
1999
2560
  server.tool(
2000
2561
  "generate_map",
2001
- `Analyze dependency graph of a directory and return file import/export structure as JSON. Supports ${languageList}.`,
2562
+ `Analyze dependency graph and return raw JSON structure for programmatic use. For human-readable reports, use analyze_existing_architecture instead. Auto-detects multi-layer projects when .archtracker/layers.json exists. Supports ${languageList}.`,
2002
2563
  {
2003
- targetDir: z2.string().default("src").describe("Target directory path (default: src)"),
2004
- exclude: z2.array(z2.string()).optional().describe("Array of regex patterns to exclude (e.g. ['test', 'mock'])"),
2005
- maxDepth: z2.number().int().min(0).optional().describe("Max analysis depth (0 = unlimited)"),
2564
+ targetDir: z3.string().default("src").describe("Target directory path (default: src). When layers.json exists and this is 'src', multi-layer analysis is used automatically."),
2565
+ projectRoot: z3.string().default(".").describe("Project root (where .archtracker/ is located)"),
2566
+ exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude (e.g. ['test', 'mock'])"),
2567
+ maxDepth: z3.number().int().min(0).optional().describe("Max analysis depth (0 = unlimited)"),
2006
2568
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2007
2569
  },
2008
- async ({ targetDir, exclude, maxDepth, language }) => {
2570
+ async ({ targetDir, projectRoot, exclude, maxDepth, language }) => {
2009
2571
  try {
2010
2572
  validatePath(targetDir);
2011
- const graph = await analyzeProject(targetDir, { exclude, maxDepth, language });
2573
+ validatePath(projectRoot);
2574
+ const { graph, layerMetadata, crossEdges } = await resolveGraphForMcp({
2575
+ targetDir,
2576
+ projectRoot,
2577
+ exclude,
2578
+ language
2579
+ });
2012
2580
  const summary = [
2013
2581
  t("mcp.analyzeComplete", { files: graph.totalFiles, edges: graph.totalEdges }),
2014
- graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone")
2582
+ graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone"),
2583
+ ...layerMetadata ? ["\nLayers:\n" + formatLayerSummary(layerMetadata)] : [],
2584
+ ...crossEdges?.length ? [`
2585
+ Cross-layer connections: ${crossEdges.length}`] : []
2015
2586
  ].join("\n");
2587
+ const result = { ...graph };
2588
+ if (crossEdges?.length) result.crossLayerConnections = crossEdges;
2016
2589
  return {
2017
2590
  content: [
2018
2591
  { type: "text", text: summary },
2019
- { type: "text", text: JSON.stringify(graph, null, 2) }
2592
+ { type: "text", text: JSON.stringify(result, null, 2) }
2020
2593
  ]
2021
2594
  };
2022
2595
  } catch (error) {
@@ -2028,24 +2601,40 @@ server.tool(
2028
2601
  "analyze_existing_architecture",
2029
2602
  `Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${LANGUAGE_IDS.length} languages.`,
2030
2603
  {
2031
- targetDir: z2.string().default("src").describe("Target directory path (default: src)"),
2032
- exclude: z2.array(z2.string()).optional().describe("Array of regex patterns to exclude"),
2033
- topN: z2.number().int().min(1).max(50).optional().describe("Number of top items to show in each section (default: 10)"),
2034
- saveSnapshot: z2.boolean().optional().describe("Also save a snapshot after analysis (default: false)"),
2035
- projectRoot: z2.string().default(".").describe("Project root (needed only when saveSnapshot is true)"),
2604
+ targetDir: z3.string().default("src").describe("Target directory path (default: src)"),
2605
+ exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude"),
2606
+ topN: z3.number().int().min(1).max(50).optional().describe("Number of top items to show in each section (default: 10)"),
2607
+ saveSnapshot: z3.boolean().optional().describe("Also save a snapshot after analysis (default: false)"),
2608
+ projectRoot: z3.string().default(".").describe("Project root (needed only when saveSnapshot is true)"),
2036
2609
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2037
2610
  },
2038
2611
  async ({ targetDir, exclude, topN, saveSnapshot: doSave, projectRoot, language }) => {
2039
2612
  try {
2040
2613
  validatePath(targetDir);
2041
- const graph = await analyzeProject(targetDir, { exclude, language });
2614
+ const { graph, multiLayer, layerMetadata, crossEdges } = await resolveGraphForMcp({
2615
+ targetDir,
2616
+ projectRoot,
2617
+ exclude,
2618
+ language
2619
+ });
2042
2620
  const report = formatAnalysisReport(graph, { topN: topN ?? 10 });
2043
2621
  const content = [
2044
2622
  { type: "text", text: report }
2045
2623
  ];
2624
+ if (layerMetadata) {
2625
+ content.push({ type: "text", text: "\nLayers:\n" + formatLayerSummary(layerMetadata) });
2626
+ }
2627
+ if (crossEdges?.length) {
2628
+ const crossSummary = crossEdges.map(
2629
+ (c) => ` ${c.fromLayer}/${c.fromFile} \u2192 ${c.toLayer}/${c.toFile} [${c.type}] ${c.label ?? ""}`
2630
+ ).join("\n");
2631
+ content.push({ type: "text", text: `
2632
+ Cross-layer connections (${crossEdges.length}):
2633
+ ${crossSummary}` });
2634
+ }
2046
2635
  if (doSave) {
2047
2636
  validatePath(projectRoot);
2048
- await saveSnapshot(projectRoot, graph);
2637
+ await saveSnapshot(projectRoot, graph, multiLayer);
2049
2638
  content.push({ type: "text", text: t("analyze.snapshotSaved") });
2050
2639
  }
2051
2640
  return { content };
@@ -2058,22 +2647,27 @@ server.tool(
2058
2647
  "save_architecture_snapshot",
2059
2648
  "Save the current dependency graph as a snapshot to .archtracker/snapshot.json",
2060
2649
  {
2061
- targetDir: z2.string().default("src").describe("Target directory path"),
2062
- projectRoot: z2.string().default(".").describe("Project root (where .archtracker is placed)"),
2650
+ targetDir: z3.string().default("src").describe("Target directory path"),
2651
+ projectRoot: z3.string().default(".").describe("Project root (where .archtracker is placed)"),
2063
2652
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2064
2653
  },
2065
2654
  async ({ targetDir, projectRoot, language }) => {
2066
2655
  try {
2067
2656
  validatePath(targetDir);
2068
2657
  validatePath(projectRoot);
2069
- const graph = await analyzeProject(targetDir, { language });
2070
- const snapshot = await saveSnapshot(projectRoot, graph);
2658
+ const { graph, multiLayer, layerMetadata } = await resolveGraphForMcp({
2659
+ targetDir,
2660
+ projectRoot,
2661
+ language
2662
+ });
2663
+ const snapshot = await saveSnapshot(projectRoot, graph, multiLayer);
2071
2664
  const keyComponents = Object.values(graph.files).sort((a, b) => b.dependents.length - a.dependents.length).slice(0, 5).map((f) => ` ${t("cli.dependedBy", { path: f.path, count: f.dependents.length })}`);
2072
2665
  const report = [
2073
2666
  t("mcp.snapshotSaved"),
2074
2667
  t("cli.timestamp", { ts: snapshot.timestamp }),
2075
2668
  t("cli.fileCount", { count: graph.totalFiles }),
2076
2669
  t("cli.edgeCount", { count: graph.totalEdges }),
2670
+ ...layerMetadata ? ["", "Layers:", formatLayerSummary(layerMetadata)] : [],
2077
2671
  "",
2078
2672
  t("cli.keyComponents"),
2079
2673
  ...keyComponents
@@ -2088,8 +2682,8 @@ server.tool(
2088
2682
  "check_architecture_diff",
2089
2683
  "Compare saved snapshot with current code dependencies and warn about files that may need updates",
2090
2684
  {
2091
- targetDir: z2.string().default("src").describe("Target directory path"),
2092
- projectRoot: z2.string().default(".").describe("Project root (where .archtracker is placed)"),
2685
+ targetDir: z3.string().default("src").describe("Target directory path"),
2686
+ projectRoot: z3.string().default(".").describe("Project root (where .archtracker is placed)"),
2093
2687
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2094
2688
  },
2095
2689
  async ({ targetDir, projectRoot, language }) => {
@@ -2098,8 +2692,12 @@ server.tool(
2098
2692
  validatePath(projectRoot);
2099
2693
  const existingSnapshot = await loadSnapshot(projectRoot);
2100
2694
  if (!existingSnapshot) {
2101
- const graph = await analyzeProject(targetDir, { language });
2102
- await saveSnapshot(projectRoot, graph);
2695
+ const { graph, multiLayer } = await resolveGraphForMcp({
2696
+ targetDir,
2697
+ projectRoot,
2698
+ language
2699
+ });
2700
+ await saveSnapshot(projectRoot, graph, multiLayer);
2103
2701
  return {
2104
2702
  content: [
2105
2703
  {
@@ -2113,7 +2711,11 @@ server.tool(
2113
2711
  ]
2114
2712
  };
2115
2713
  }
2116
- const currentGraph = await analyzeProject(targetDir, { language });
2714
+ const { graph: currentGraph } = await resolveGraphForMcp({
2715
+ targetDir,
2716
+ projectRoot,
2717
+ language
2718
+ });
2117
2719
  const diff = computeDiff(existingSnapshot.graph, currentGraph);
2118
2720
  const report = formatDiffReport(diff);
2119
2721
  return { content: [{ type: "text", text: report }] };
@@ -2126,16 +2728,20 @@ server.tool(
2126
2728
  "get_current_context",
2127
2729
  "Get current valid file paths and architecture summary for AI session initialization",
2128
2730
  {
2129
- targetDir: z2.string().default("src").describe("Target directory path"),
2130
- projectRoot: z2.string().default(".").describe("Project root"),
2731
+ targetDir: z3.string().default("src").describe("Target directory path"),
2732
+ projectRoot: z3.string().default(".").describe("Project root"),
2131
2733
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2132
2734
  },
2133
2735
  async ({ targetDir, projectRoot, language }) => {
2134
2736
  try {
2135
2737
  let snapshot = await loadSnapshot(projectRoot);
2136
2738
  if (!snapshot) {
2137
- const graph2 = await analyzeProject(targetDir, { language });
2138
- snapshot = await saveSnapshot(projectRoot, graph2);
2739
+ const { graph: graph2, multiLayer } = await resolveGraphForMcp({
2740
+ targetDir,
2741
+ projectRoot,
2742
+ language
2743
+ });
2744
+ snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
2139
2745
  }
2140
2746
  const graph = snapshot.graph;
2141
2747
  const keyComponents = Object.values(graph.files).filter((f) => f.dependents.length > 0 || f.dependencies.length > 0).sort((a, b) => b.dependents.length - a.dependents.length).slice(0, 20).map((f) => ({
@@ -2181,13 +2787,13 @@ server.tool(
2181
2787
  "search_architecture",
2182
2788
  "Search architecture: file path search, impact analysis, critical component detection, orphan file detection",
2183
2789
  {
2184
- query: z2.string().optional().describe("Search query (required for path/affected modes, not needed for critical/orphans)"),
2185
- mode: z2.enum(["path", "affected", "critical", "orphans"]).default("path").describe(
2790
+ query: z3.string().optional().describe("Search query (required for path/affected modes, not needed for critical/orphans)"),
2791
+ mode: z3.enum(["path", "affected", "critical", "orphans"]).default("path").describe(
2186
2792
  "Search mode: path=search by path, affected=change impact, critical=key files, orphans=isolated files"
2187
2793
  ),
2188
- targetDir: z2.string().default("src").describe("Target directory path"),
2189
- projectRoot: z2.string().default(".").describe("Project root"),
2190
- limit: z2.number().int().min(1).max(50).optional().describe("Max results (default: 10)"),
2794
+ targetDir: z3.string().default("src").describe("Target directory path"),
2795
+ projectRoot: z3.string().default(".").describe("Project root"),
2796
+ limit: z3.number().int().min(1).max(50).optional().describe("Max results (default: 10)"),
2191
2797
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2192
2798
  },
2193
2799
  async ({ query, mode, targetDir, projectRoot, limit, language }) => {
@@ -2196,8 +2802,12 @@ server.tool(
2196
2802
  validatePath(projectRoot);
2197
2803
  let snapshot = await loadSnapshot(projectRoot);
2198
2804
  if (!snapshot) {
2199
- const graph2 = await analyzeProject(targetDir, { language });
2200
- snapshot = await saveSnapshot(projectRoot, graph2);
2805
+ const { graph: graph2, multiLayer } = await resolveGraphForMcp({
2806
+ targetDir,
2807
+ projectRoot,
2808
+ language
2809
+ });
2810
+ snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
2201
2811
  }
2202
2812
  const graph = snapshot.graph;
2203
2813
  const maxResults = limit ?? 10;