archtracker-mcp 0.4.3 → 0.6.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,61 +1843,540 @@ function formatAnalysisReport(graph, options = {}) {
1761
1843
  return lines.join("\n");
1762
1844
  }
1763
1845
 
1764
- // src/storage/snapshot.ts
1765
- import { mkdir, writeFile, readFile as readFile2, access } from "fs/promises";
1766
- import { join as join4 } from "path";
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
+
2229
+ // src/storage/layers.ts
2230
+ import { readFile as readFile2, writeFile, mkdir } from "fs/promises";
2231
+ import { join as join5 } from "path";
1767
2232
  import { z } from "zod";
2233
+ var ARCHTRACKER_DIR = ".archtracker";
2234
+ var LAYERS_FILE = "layers.json";
2235
+ var LayerDefinitionSchema = z.object({
2236
+ name: z.string().min(1).regex(
2237
+ /^[a-zA-Z0-9_-]+$/,
2238
+ "Layer name must be alphanumeric (hyphens/underscores allowed)"
2239
+ ),
2240
+ targetDir: z.string().min(1),
2241
+ language: z.enum(LANGUAGE_IDS).optional(),
2242
+ exclude: z.array(z.string()).optional(),
2243
+ color: z.string().optional(),
2244
+ description: z.string().optional()
2245
+ });
2246
+ var CrossLayerConnectionSchema = z.object({
2247
+ fromLayer: z.string(),
2248
+ fromFile: z.string(),
2249
+ toLayer: z.string(),
2250
+ toFile: z.string(),
2251
+ type: z.enum(["api-call", "event", "data-flow", "manual"]),
2252
+ label: z.string().optional()
2253
+ });
2254
+ var LayerConfigSchema = z.object({
2255
+ version: z.literal("1.0"),
2256
+ layers: z.array(LayerDefinitionSchema).min(1).refine(
2257
+ (layers) => {
2258
+ const names = layers.map((l) => l.name);
2259
+ return new Set(names).size === names.length;
2260
+ },
2261
+ { message: "Layer names must be unique" }
2262
+ ),
2263
+ connections: z.array(CrossLayerConnectionSchema).optional()
2264
+ });
2265
+ async function loadLayerConfig(projectRoot) {
2266
+ const filePath = join5(projectRoot, ARCHTRACKER_DIR, LAYERS_FILE);
2267
+ let raw;
2268
+ try {
2269
+ raw = await readFile2(filePath, "utf-8");
2270
+ } catch (error) {
2271
+ if (isNodeError(error) && error.code === "ENOENT") {
2272
+ return null;
2273
+ }
2274
+ throw new Error(`Failed to read ${filePath}`);
2275
+ }
2276
+ let parsed;
2277
+ try {
2278
+ parsed = JSON.parse(raw);
2279
+ } catch {
2280
+ throw new Error(`Invalid JSON in ${filePath}`);
2281
+ }
2282
+ const result = LayerConfigSchema.safeParse(parsed);
2283
+ if (!result.success) {
2284
+ const issues = result.error.issues.map((i) => ` ${i.path.join(".")}: ${i.message}`).slice(0, 5).join("\n");
2285
+ throw new Error(`layers.json validation failed:
2286
+ ${issues}`);
2287
+ }
2288
+ return result.data;
2289
+ }
2290
+ function isNodeError(error) {
2291
+ return error instanceof Error && "code" in error;
2292
+ }
2293
+
2294
+ // src/analyzer/resolve.ts
2295
+ async function resolveGraph(opts) {
2296
+ const layerConfig = await loadLayerConfig(opts.projectRoot);
2297
+ if (layerConfig) {
2298
+ const multi = await analyzeMultiLayer(opts.projectRoot, layerConfig.layers);
2299
+ const autoConnections = detectCrossLayerConnections(multi.layers, layerConfig.layers);
2300
+ const manualConnections = layerConfig.connections ?? [];
2301
+ const manualKeys = new Set(manualConnections.map(
2302
+ (c) => `${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`
2303
+ ));
2304
+ const merged = [
2305
+ ...manualConnections,
2306
+ ...autoConnections.filter(
2307
+ (c) => !manualKeys.has(`${c.fromLayer}/${c.fromFile}\u2192${c.toLayer}/${c.toFile}`)
2308
+ )
2309
+ ];
2310
+ return {
2311
+ graph: multi.merged,
2312
+ multiLayer: multi,
2313
+ layerMetadata: multi.layerMetadata,
2314
+ crossLayerEdges: merged
2315
+ };
2316
+ }
2317
+ const graph = await analyzeProject(opts.targetDir, {
2318
+ exclude: opts.exclude,
2319
+ language: opts.language
2320
+ });
2321
+ return { graph };
2322
+ }
2323
+
2324
+ // src/storage/snapshot.ts
2325
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile3, access } from "fs/promises";
2326
+ import { join as join6 } from "path";
2327
+ import { z as z2 } from "zod";
1768
2328
 
1769
2329
  // src/types/schema.ts
1770
- var SCHEMA_VERSION = "1.0";
2330
+ var SCHEMA_VERSION = "1.1";
1771
2331
 
1772
2332
  // src/storage/snapshot.ts
1773
- var ARCHTRACKER_DIR = ".archtracker";
2333
+ var ARCHTRACKER_DIR2 = ".archtracker";
1774
2334
  var SNAPSHOT_FILE = "snapshot.json";
1775
- var FileNodeSchema = z.object({
1776
- path: z.string(),
1777
- exists: z.boolean(),
1778
- dependencies: z.array(z.string()),
1779
- dependents: z.array(z.string())
2335
+ var FileNodeSchema = z2.object({
2336
+ path: z2.string(),
2337
+ exists: z2.boolean(),
2338
+ dependencies: z2.array(z2.string()),
2339
+ dependents: z2.array(z2.string())
1780
2340
  });
1781
- var DependencyGraphSchema = z.object({
1782
- rootDir: z.string(),
1783
- files: z.record(z.string(), FileNodeSchema),
1784
- edges: z.array(z.object({
1785
- source: z.string(),
1786
- target: z.string(),
1787
- type: z.enum(["static", "dynamic", "type-only"])
2341
+ var DependencyGraphSchema = z2.object({
2342
+ rootDir: z2.string(),
2343
+ files: z2.record(z2.string(), FileNodeSchema),
2344
+ edges: z2.array(z2.object({
2345
+ source: z2.string(),
2346
+ target: z2.string(),
2347
+ type: z2.enum(["static", "dynamic", "type-only"])
1788
2348
  })),
1789
- circularDependencies: z.array(z.object({ cycle: z.array(z.string()) })),
1790
- totalFiles: z.number(),
1791
- totalEdges: z.number()
2349
+ circularDependencies: z2.array(z2.object({ cycle: z2.array(z2.string()) })),
2350
+ totalFiles: z2.number(),
2351
+ totalEdges: z2.number()
1792
2352
  });
1793
- var SnapshotSchema = z.object({
1794
- version: z.literal(SCHEMA_VERSION),
1795
- timestamp: z.string(),
1796
- rootDir: z.string(),
2353
+ var SnapshotSchema = z2.object({
2354
+ version: z2.enum([SCHEMA_VERSION, "1.0"]),
2355
+ timestamp: z2.string(),
2356
+ rootDir: z2.string(),
1797
2357
  graph: DependencyGraphSchema
1798
2358
  });
1799
- async function saveSnapshot(projectRoot, graph) {
1800
- const dirPath = join4(projectRoot, ARCHTRACKER_DIR);
1801
- const filePath = join4(dirPath, SNAPSHOT_FILE);
2359
+ async function saveSnapshot(projectRoot, graph, multiLayer) {
2360
+ const dirPath = join6(projectRoot, ARCHTRACKER_DIR2);
2361
+ const filePath = join6(dirPath, SNAPSHOT_FILE);
1802
2362
  const snapshot = {
1803
2363
  version: SCHEMA_VERSION,
1804
2364
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1805
2365
  rootDir: graph.rootDir,
1806
- graph
2366
+ graph,
2367
+ ...multiLayer ? { multiLayer } : {}
1807
2368
  };
1808
- await mkdir(dirPath, { recursive: true });
1809
- await writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
2369
+ await mkdir2(dirPath, { recursive: true });
2370
+ await writeFile2(filePath, JSON.stringify(snapshot, null, 2), "utf-8");
1810
2371
  return snapshot;
1811
2372
  }
1812
2373
  async function loadSnapshot(projectRoot) {
1813
- const filePath = join4(projectRoot, ARCHTRACKER_DIR, SNAPSHOT_FILE);
2374
+ const filePath = join6(projectRoot, ARCHTRACKER_DIR2, SNAPSHOT_FILE);
1814
2375
  let raw;
1815
2376
  try {
1816
- raw = await readFile2(filePath, "utf-8");
2377
+ raw = await readFile3(filePath, "utf-8");
1817
2378
  } catch (error) {
1818
- if (isNodeError(error) && error.code === "ENOENT") {
2379
+ if (isNodeError2(error) && error.code === "ENOENT") {
1819
2380
  return null;
1820
2381
  }
1821
2382
  throw new StorageError(
@@ -1846,7 +2407,7 @@ var StorageError = class extends Error {
1846
2407
  this.name = "StorageError";
1847
2408
  }
1848
2409
  };
1849
- function isNodeError(error) {
2410
+ function isNodeError2(error) {
1850
2411
  return error instanceof Error && "code" in error;
1851
2412
  }
1852
2413
 
@@ -1948,10 +2509,10 @@ function arraysEqual(a, b) {
1948
2509
  }
1949
2510
 
1950
2511
  // src/utils/path-guard.ts
1951
- import { resolve as resolve4 } from "path";
2512
+ import { resolve as resolve5 } from "path";
1952
2513
  function validatePath(inputPath, boundary) {
1953
- const resolved = resolve4(inputPath);
1954
- const root = boundary ? resolve4(boundary) : process.cwd();
2514
+ const resolved = resolve5(inputPath);
2515
+ const root = boundary ? resolve5(boundary) : process.cwd();
1955
2516
  if (!resolved.startsWith(root)) {
1956
2517
  throw new PathTraversalError(
1957
2518
  t("pathGuard.traversal", { input: inputPath, resolved, boundary: root })
@@ -1967,14 +2528,14 @@ var PathTraversalError = class extends Error {
1967
2528
  };
1968
2529
 
1969
2530
  // src/utils/version.ts
1970
- import { readFileSync as readFileSync2 } from "fs";
1971
- import { join as join5, dirname as dirname2 } from "path";
2531
+ import { readFileSync as readFileSync3 } from "fs";
2532
+ import { join as join7, dirname as dirname2 } from "path";
1972
2533
  import { fileURLToPath } from "url";
1973
2534
  function loadVersion() {
1974
2535
  let dir = dirname2(fileURLToPath(import.meta.url));
1975
2536
  for (let i = 0; i < 5; i++) {
1976
2537
  try {
1977
- const pkg = JSON.parse(readFileSync2(join5(dir, "package.json"), "utf-8"));
2538
+ const pkg = JSON.parse(readFileSync3(join7(dir, "package.json"), "utf-8"));
1978
2539
  return pkg.version;
1979
2540
  } catch {
1980
2541
  dir = dirname2(dir);
@@ -1989,34 +2550,58 @@ var server = new McpServer({
1989
2550
  name: "archtracker",
1990
2551
  version: VERSION
1991
2552
  });
1992
- var languageEnum = z2.enum(LANGUAGE_IDS);
2553
+ var languageEnum = z3.enum(LANGUAGE_IDS);
1993
2554
  var LANG_DISPLAY = {
1994
2555
  javascript: "JS/TS",
1995
2556
  "c-cpp": "C/C++",
1996
2557
  "c-sharp": "C#"
1997
2558
  };
1998
2559
  var languageList = LANGUAGE_IDS.map((id) => LANG_DISPLAY[id] ?? id.charAt(0).toUpperCase() + id.slice(1)).join(", ");
2560
+ async function resolveGraphMcp(opts) {
2561
+ return resolveGraph({
2562
+ targetDir: opts.targetDir,
2563
+ projectRoot: opts.projectRoot,
2564
+ exclude: opts.exclude,
2565
+ language: opts.language
2566
+ });
2567
+ }
2568
+ function formatLayerSummary(metadata) {
2569
+ return metadata.map(
2570
+ (m) => ` [${m.name}] ${m.fileCount} files, ${m.edgeCount} edges (${m.language})`
2571
+ ).join("\n");
2572
+ }
1999
2573
  server.tool(
2000
2574
  "generate_map",
2001
- `Analyze dependency graph of a directory and return file import/export structure as JSON. Supports ${languageList}.`,
2575
+ `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
2576
  {
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)"),
2577
+ 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."),
2578
+ projectRoot: z3.string().default(".").describe("Project root (where .archtracker/ is located)"),
2579
+ exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude (e.g. ['test', 'mock'])"),
2006
2580
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2007
2581
  },
2008
- async ({ targetDir, exclude, maxDepth, language }) => {
2582
+ async ({ targetDir, projectRoot, exclude, language }) => {
2009
2583
  try {
2010
2584
  validatePath(targetDir);
2011
- const graph = await analyzeProject(targetDir, { exclude, maxDepth, language });
2585
+ validatePath(projectRoot);
2586
+ const { graph, layerMetadata, crossLayerEdges } = await resolveGraphMcp({
2587
+ targetDir,
2588
+ projectRoot,
2589
+ exclude,
2590
+ language
2591
+ });
2012
2592
  const summary = [
2013
2593
  t("mcp.analyzeComplete", { files: graph.totalFiles, edges: graph.totalEdges }),
2014
- graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone")
2594
+ graph.circularDependencies.length > 0 ? t("mcp.circularFound", { count: graph.circularDependencies.length }) : t("mcp.circularNone"),
2595
+ ...layerMetadata ? ["\nLayers:\n" + formatLayerSummary(layerMetadata)] : [],
2596
+ ...crossLayerEdges?.length ? [`
2597
+ Cross-layer connections: ${crossLayerEdges.length}`] : []
2015
2598
  ].join("\n");
2599
+ const result = { ...graph };
2600
+ if (crossLayerEdges?.length) result.crossLayerConnections = crossLayerEdges;
2016
2601
  return {
2017
2602
  content: [
2018
2603
  { type: "text", text: summary },
2019
- { type: "text", text: JSON.stringify(graph, null, 2) }
2604
+ { type: "text", text: JSON.stringify(result, null, 2) }
2020
2605
  ]
2021
2606
  };
2022
2607
  } catch (error) {
@@ -2026,26 +2611,42 @@ server.tool(
2026
2611
  );
2027
2612
  server.tool(
2028
2613
  "analyze_existing_architecture",
2029
- `Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${LANGUAGE_IDS.length} languages.`,
2614
+ `Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${languageList}.`,
2030
2615
  {
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)"),
2616
+ targetDir: z3.string().default("src").describe("Target directory path (default: src)"),
2617
+ exclude: z3.array(z3.string()).optional().describe("Array of regex patterns to exclude"),
2618
+ topN: z3.number().int().min(1).max(50).optional().describe("Number of top items to show in each section (default: 10)"),
2619
+ saveSnapshot: z3.boolean().optional().describe("Also save a snapshot after analysis (default: false)"),
2620
+ projectRoot: z3.string().default(".").describe("Project root (where .archtracker/ is located)"),
2036
2621
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2037
2622
  },
2038
2623
  async ({ targetDir, exclude, topN, saveSnapshot: doSave, projectRoot, language }) => {
2039
2624
  try {
2040
2625
  validatePath(targetDir);
2041
- const graph = await analyzeProject(targetDir, { exclude, language });
2626
+ validatePath(projectRoot);
2627
+ const { graph, multiLayer, layerMetadata, crossLayerEdges } = await resolveGraphMcp({
2628
+ targetDir,
2629
+ projectRoot,
2630
+ exclude,
2631
+ language
2632
+ });
2042
2633
  const report = formatAnalysisReport(graph, { topN: topN ?? 10 });
2043
2634
  const content = [
2044
2635
  { type: "text", text: report }
2045
2636
  ];
2637
+ if (layerMetadata) {
2638
+ content.push({ type: "text", text: "\nLayers:\n" + formatLayerSummary(layerMetadata) });
2639
+ }
2640
+ if (crossLayerEdges?.length) {
2641
+ const crossSummary = crossLayerEdges.map(
2642
+ (c) => ` ${c.fromLayer}/${c.fromFile} \u2192 ${c.toLayer}/${c.toFile} [${c.type}] ${c.label ?? ""}`
2643
+ ).join("\n");
2644
+ content.push({ type: "text", text: `
2645
+ Cross-layer connections (${crossLayerEdges.length}):
2646
+ ${crossSummary}` });
2647
+ }
2046
2648
  if (doSave) {
2047
- validatePath(projectRoot);
2048
- await saveSnapshot(projectRoot, graph);
2649
+ await saveSnapshot(projectRoot, graph, multiLayer);
2049
2650
  content.push({ type: "text", text: t("analyze.snapshotSaved") });
2050
2651
  }
2051
2652
  return { content };
@@ -2058,22 +2659,27 @@ server.tool(
2058
2659
  "save_architecture_snapshot",
2059
2660
  "Save the current dependency graph as a snapshot to .archtracker/snapshot.json",
2060
2661
  {
2061
- targetDir: z2.string().default("src").describe("Target directory path"),
2062
- projectRoot: z2.string().default(".").describe("Project root (where .archtracker is placed)"),
2662
+ targetDir: z3.string().default("src").describe("Target directory path"),
2663
+ projectRoot: z3.string().default(".").describe("Project root (where .archtracker is placed)"),
2063
2664
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2064
2665
  },
2065
2666
  async ({ targetDir, projectRoot, language }) => {
2066
2667
  try {
2067
2668
  validatePath(targetDir);
2068
2669
  validatePath(projectRoot);
2069
- const graph = await analyzeProject(targetDir, { language });
2070
- const snapshot = await saveSnapshot(projectRoot, graph);
2670
+ const { graph, multiLayer, layerMetadata } = await resolveGraphMcp({
2671
+ targetDir,
2672
+ projectRoot,
2673
+ language
2674
+ });
2675
+ const snapshot = await saveSnapshot(projectRoot, graph, multiLayer);
2071
2676
  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
2677
  const report = [
2073
2678
  t("mcp.snapshotSaved"),
2074
2679
  t("cli.timestamp", { ts: snapshot.timestamp }),
2075
2680
  t("cli.fileCount", { count: graph.totalFiles }),
2076
2681
  t("cli.edgeCount", { count: graph.totalEdges }),
2682
+ ...layerMetadata ? ["", "Layers:", formatLayerSummary(layerMetadata)] : [],
2077
2683
  "",
2078
2684
  t("cli.keyComponents"),
2079
2685
  ...keyComponents
@@ -2088,8 +2694,8 @@ server.tool(
2088
2694
  "check_architecture_diff",
2089
2695
  "Compare saved snapshot with current code dependencies and warn about files that may need updates",
2090
2696
  {
2091
- targetDir: z2.string().default("src").describe("Target directory path"),
2092
- projectRoot: z2.string().default(".").describe("Project root (where .archtracker is placed)"),
2697
+ targetDir: z3.string().default("src").describe("Target directory path"),
2698
+ projectRoot: z3.string().default(".").describe("Project root (where .archtracker is placed)"),
2093
2699
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2094
2700
  },
2095
2701
  async ({ targetDir, projectRoot, language }) => {
@@ -2098,8 +2704,12 @@ server.tool(
2098
2704
  validatePath(projectRoot);
2099
2705
  const existingSnapshot = await loadSnapshot(projectRoot);
2100
2706
  if (!existingSnapshot) {
2101
- const graph = await analyzeProject(targetDir, { language });
2102
- await saveSnapshot(projectRoot, graph);
2707
+ const { graph, multiLayer } = await resolveGraphMcp({
2708
+ targetDir,
2709
+ projectRoot,
2710
+ language
2711
+ });
2712
+ await saveSnapshot(projectRoot, graph, multiLayer);
2103
2713
  return {
2104
2714
  content: [
2105
2715
  {
@@ -2113,7 +2723,11 @@ server.tool(
2113
2723
  ]
2114
2724
  };
2115
2725
  }
2116
- const currentGraph = await analyzeProject(targetDir, { language });
2726
+ const { graph: currentGraph } = await resolveGraphMcp({
2727
+ targetDir,
2728
+ projectRoot,
2729
+ language
2730
+ });
2117
2731
  const diff = computeDiff(existingSnapshot.graph, currentGraph);
2118
2732
  const report = formatDiffReport(diff);
2119
2733
  return { content: [{ type: "text", text: report }] };
@@ -2126,16 +2740,22 @@ server.tool(
2126
2740
  "get_current_context",
2127
2741
  "Get current valid file paths and architecture summary for AI session initialization",
2128
2742
  {
2129
- targetDir: z2.string().default("src").describe("Target directory path"),
2130
- projectRoot: z2.string().default(".").describe("Project root"),
2743
+ targetDir: z3.string().default("src").describe("Target directory path"),
2744
+ projectRoot: z3.string().default(".").describe("Project root"),
2131
2745
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2132
2746
  },
2133
2747
  async ({ targetDir, projectRoot, language }) => {
2134
2748
  try {
2749
+ validatePath(targetDir);
2750
+ validatePath(projectRoot);
2135
2751
  let snapshot = await loadSnapshot(projectRoot);
2136
2752
  if (!snapshot) {
2137
- const graph2 = await analyzeProject(targetDir, { language });
2138
- snapshot = await saveSnapshot(projectRoot, graph2);
2753
+ const { graph: graph2, multiLayer } = await resolveGraphMcp({
2754
+ targetDir,
2755
+ projectRoot,
2756
+ language
2757
+ });
2758
+ snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
2139
2759
  }
2140
2760
  const graph = snapshot.graph;
2141
2761
  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 +2801,13 @@ server.tool(
2181
2801
  "search_architecture",
2182
2802
  "Search architecture: file path search, impact analysis, critical component detection, orphan file detection",
2183
2803
  {
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(
2804
+ query: z3.string().optional().describe("Search query (required for path/affected modes, not needed for critical/orphans)"),
2805
+ mode: z3.enum(["path", "affected", "critical", "orphans"]).default("path").describe(
2186
2806
  "Search mode: path=search by path, affected=change impact, critical=key files, orphans=isolated files"
2187
2807
  ),
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)"),
2808
+ targetDir: z3.string().default("src").describe("Target directory path"),
2809
+ projectRoot: z3.string().default(".").describe("Project root"),
2810
+ limit: z3.number().int().min(1).max(50).optional().describe("Max results (default: 10)"),
2191
2811
  language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
2192
2812
  },
2193
2813
  async ({ query, mode, targetDir, projectRoot, limit, language }) => {
@@ -2196,8 +2816,12 @@ server.tool(
2196
2816
  validatePath(projectRoot);
2197
2817
  let snapshot = await loadSnapshot(projectRoot);
2198
2818
  if (!snapshot) {
2199
- const graph2 = await analyzeProject(targetDir, { language });
2200
- snapshot = await saveSnapshot(projectRoot, graph2);
2819
+ const { graph: graph2, multiLayer } = await resolveGraphMcp({
2820
+ targetDir,
2821
+ projectRoot,
2822
+ language
2823
+ });
2824
+ snapshot = await saveSnapshot(projectRoot, graph2, multiLayer);
2201
2825
  }
2202
2826
  const graph = snapshot.graph;
2203
2827
  const maxResults = limit ?? 10;