mason-context 0.1.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.
@@ -0,0 +1,1206 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mcp/server.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+
8
+ // src/mcp/tools.ts
9
+ import fs5 from "fs/promises";
10
+ import path4 from "path";
11
+ import { execFile as execFile7 } from "child_process";
12
+ import { promisify as promisify7 } from "util";
13
+ import fg3 from "fast-glob";
14
+
15
+ // src/analyzers/git-history.ts
16
+ import { execFile } from "child_process";
17
+ import { promisify } from "util";
18
+
19
+ // src/analyzers/base.ts
20
+ import fs from "fs/promises";
21
+ import fg from "fast-glob";
22
+ var BaseAnalyzer = class {
23
+ async findFiles(patterns, root) {
24
+ return fg(patterns, {
25
+ cwd: root,
26
+ ignore: ["**/node_modules/**", "**/dist/**", "**/.git/**"],
27
+ absolute: true
28
+ });
29
+ }
30
+ async readFile(filePath) {
31
+ return fs.readFile(filePath, "utf-8");
32
+ }
33
+ createFinding(partial) {
34
+ return {
35
+ analyzer: this.name,
36
+ category: partial.category,
37
+ confidence: partial.confidence,
38
+ summary: partial.summary,
39
+ evidence: partial.evidence ?? [],
40
+ ruleCandidate: partial.ruleCandidate ?? null
41
+ };
42
+ }
43
+ createResult(findings, gaps, startTime) {
44
+ return {
45
+ analyzer: this.name,
46
+ findings,
47
+ gaps,
48
+ durationMs: Date.now() - startTime
49
+ };
50
+ }
51
+ };
52
+
53
+ // src/analyzers/git-history.ts
54
+ var exec = promisify(execFile);
55
+ var GitHistoryAnalyzer = class extends BaseAnalyzer {
56
+ name = "git-history";
57
+ async analyze(context) {
58
+ const startTime = Date.now();
59
+ const findings = [];
60
+ const gaps = [];
61
+ if (!context.gitAvailable) {
62
+ return this.createResult([], [], startTime);
63
+ }
64
+ const [staleFindings, staleGaps] = await this.findStaleDirectories(context);
65
+ findings.push(...staleFindings);
66
+ gaps.push(...staleGaps);
67
+ const hotFindings = await this.findHotFiles(context);
68
+ findings.push(...hotFindings);
69
+ const commitFindings = await this.analyzeCommitPatterns(context);
70
+ findings.push(...commitFindings);
71
+ return this.createResult(findings, gaps, startTime);
72
+ }
73
+ async git(args, cwd) {
74
+ try {
75
+ const { stdout } = await exec("git", args, { cwd, maxBuffer: 1e7 });
76
+ return stdout.trim();
77
+ } catch {
78
+ return "";
79
+ }
80
+ }
81
+ async findStaleDirectories(context) {
82
+ const findings = [];
83
+ const gaps = [];
84
+ const output = await this.git(
85
+ ["log", "--all", "--format=%ci", "--name-only", "--diff-filter=AMCR", "-n", "500"],
86
+ context.rootDir
87
+ );
88
+ if (!output) return [findings, gaps];
89
+ const dirLastTouch = /* @__PURE__ */ new Map();
90
+ let currentDate = null;
91
+ for (const line of output.split("\n")) {
92
+ if (!line) continue;
93
+ if (/^\d{4}-\d{2}-\d{2}/.test(line)) {
94
+ currentDate = new Date(line);
95
+ } else if (currentDate) {
96
+ const topDir = line.split("/")[0];
97
+ if (topDir && !topDir.startsWith(".") && !topDir.includes("node_modules")) {
98
+ const existing = dirLastTouch.get(topDir);
99
+ if (!existing || currentDate > existing) {
100
+ dirLastTouch.set(topDir, currentDate);
101
+ }
102
+ }
103
+ }
104
+ }
105
+ const sixMonthsAgo = /* @__PURE__ */ new Date();
106
+ sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6);
107
+ for (const [dir, lastTouch] of dirLastTouch) {
108
+ if (lastTouch < sixMonthsAgo) {
109
+ const monthsStale = Math.floor(
110
+ (Date.now() - lastTouch.getTime()) / (1e3 * 60 * 60 * 24 * 30)
111
+ );
112
+ findings.push(
113
+ this.createFinding({
114
+ category: "risk",
115
+ confidence: 0.7,
116
+ summary: `Directory "${dir}" hasn't been modified in ${monthsStale} months`,
117
+ evidence: [
118
+ { filePath: dir, detail: `Last commit: ${lastTouch.toISOString().split("T")[0]}` }
119
+ ],
120
+ ruleCandidate: `Do not refactor or modify files in "${dir}/" unless explicitly asked \u2014 this area has been stable for ${monthsStale} months and may be legacy code.`
121
+ })
122
+ );
123
+ gaps.push({
124
+ analyzer: this.name,
125
+ question: `Directory "${dir}" hasn't been touched in ${monthsStale} months. Is it deprecated, stable, or legacy?`,
126
+ context: `Last modified: ${lastTouch.toISOString().split("T")[0]}`,
127
+ answerKey: `stale-dir-${dir}`
128
+ });
129
+ }
130
+ }
131
+ return [findings, gaps];
132
+ }
133
+ async findHotFiles(context) {
134
+ const findings = [];
135
+ const output = await this.git(
136
+ ["log", "--since=3 months ago", "--format=", "--name-only"],
137
+ context.rootDir
138
+ );
139
+ if (!output) return findings;
140
+ const fileCounts = /* @__PURE__ */ new Map();
141
+ for (const line of output.split("\n")) {
142
+ if (!line || line.startsWith(".") || line.includes("node_modules")) continue;
143
+ fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
144
+ }
145
+ const sorted = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
146
+ if (sorted.length > 0 && sorted[0][1] >= 5) {
147
+ const hotFiles = sorted.filter(([, count]) => count >= 5);
148
+ if (hotFiles.length > 0) {
149
+ findings.push(
150
+ this.createFinding({
151
+ category: "risk",
152
+ confidence: 0.8,
153
+ summary: `${hotFiles.length} files changed frequently in the last 3 months`,
154
+ evidence: hotFiles.map(([file, count]) => ({
155
+ filePath: file,
156
+ detail: `${count} commits`
157
+ })),
158
+ ruleCandidate: `These files change frequently and are high-risk for conflicts: ${hotFiles.map(([f]) => f).join(", ")}. Take extra care when modifying them.`
159
+ })
160
+ );
161
+ }
162
+ }
163
+ return findings;
164
+ }
165
+ async analyzeCommitPatterns(context) {
166
+ const findings = [];
167
+ const output = await this.git(
168
+ ["log", "--format=%s", "-n", "100"],
169
+ context.rootDir
170
+ );
171
+ if (!output) return findings;
172
+ const messages = output.split("\n").filter(Boolean);
173
+ const conventionalPattern = /^(feat|fix|chore|docs|style|refactor|test|perf|ci|build|revert)(\(.+\))?:/;
174
+ const conventionalCount = messages.filter(
175
+ (m) => conventionalPattern.test(m)
176
+ ).length;
177
+ const conventionalRatio = conventionalCount / messages.length;
178
+ if (conventionalRatio > 0.5) {
179
+ findings.push(
180
+ this.createFinding({
181
+ category: "convention",
182
+ confidence: Math.min(conventionalRatio + 0.1, 1),
183
+ summary: `${Math.round(conventionalRatio * 100)}% of recent commits use conventional commit format`,
184
+ evidence: [
185
+ {
186
+ filePath: ".git",
187
+ detail: `${conventionalCount} of ${messages.length} commits match`
188
+ }
189
+ ],
190
+ ruleCandidate: "Use conventional commit format: type(scope): description (e.g., feat(auth): add login endpoint)"
191
+ })
192
+ );
193
+ }
194
+ const ticketPattern = /[A-Z]+-\d+|#\d+/;
195
+ const ticketCount = messages.filter((m) => ticketPattern.test(m)).length;
196
+ const ticketRatio = ticketCount / messages.length;
197
+ if (ticketRatio > 0.3) {
198
+ findings.push(
199
+ this.createFinding({
200
+ category: "convention",
201
+ confidence: ticketRatio,
202
+ summary: `${Math.round(ticketRatio * 100)}% of commits reference issue/ticket IDs`,
203
+ evidence: [
204
+ {
205
+ filePath: ".git",
206
+ detail: `${ticketCount} of ${messages.length} commits have ticket refs`
207
+ }
208
+ ],
209
+ ruleCandidate: "Include issue/ticket references in commit messages when applicable."
210
+ })
211
+ );
212
+ }
213
+ return findings;
214
+ }
215
+ };
216
+
217
+ // src/analyzers/index.ts
218
+ var analyzers = [new GitHistoryAnalyzer()];
219
+ async function runAll(context) {
220
+ return Promise.all(analyzers.map((a) => a.analyze(context)));
221
+ }
222
+
223
+ // src/utils/git.ts
224
+ import { execFile as execFile2 } from "child_process";
225
+ import { promisify as promisify2 } from "util";
226
+ var exec2 = promisify2(execFile2);
227
+ async function isGitRepo(dir) {
228
+ try {
229
+ await exec2("git", ["rev-parse", "--git-dir"], { cwd: dir });
230
+ return true;
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ // src/mcp/sampler.ts
237
+ import fs2 from "fs/promises";
238
+ import path from "path";
239
+ import { execFile as execFile3 } from "child_process";
240
+ import { promisify as promisify3 } from "util";
241
+ import fg2 from "fast-glob";
242
+ var exec3 = promisify3(execFile3);
243
+ var SOURCE_EXTENSIONS = [
244
+ "ts",
245
+ "tsx",
246
+ "js",
247
+ "jsx",
248
+ "mts",
249
+ "mjs",
250
+ "kt",
251
+ "kts",
252
+ "java",
253
+ "py",
254
+ "go",
255
+ "rs",
256
+ "swift",
257
+ "rb",
258
+ "cs",
259
+ "cpp",
260
+ "c",
261
+ "h",
262
+ "dart"
263
+ ];
264
+ var CONFIG_FILES = [
265
+ // Build & project config
266
+ "package.json",
267
+ "tsconfig.json",
268
+ "build.gradle.kts",
269
+ "build.gradle",
270
+ "settings.gradle.kts",
271
+ "settings.gradle",
272
+ "Cargo.toml",
273
+ "go.mod",
274
+ "pyproject.toml",
275
+ "Gemfile",
276
+ "*.csproj",
277
+ // Version catalogs & dependency locks
278
+ "gradle/libs.versions.toml",
279
+ // Code quality & formatting
280
+ ".editorconfig",
281
+ ".eslintrc.*",
282
+ "eslint.config.*",
283
+ ".prettierrc",
284
+ "rustfmt.toml",
285
+ ".swiftlint.yml",
286
+ // CI/CD
287
+ ".github/workflows/*.yml",
288
+ ".gitlab-ci.yml",
289
+ "Jenkinsfile",
290
+ // Containerization
291
+ "Dockerfile",
292
+ "docker-compose.yml",
293
+ "docker-compose.yaml"
294
+ ];
295
+ var ENTRY_POINT_PATTERNS = [
296
+ "src/main.*",
297
+ "src/index.*",
298
+ "src/app.*",
299
+ "main.*",
300
+ "index.*",
301
+ "app.*",
302
+ "App.*",
303
+ "**/Main.kt",
304
+ "**/Application.kt",
305
+ "**/main.py",
306
+ "**/main.go",
307
+ "**/main.rs",
308
+ "**/lib.rs",
309
+ "**/Program.cs"
310
+ ];
311
+ var ARCHITECTURAL_PATTERNS = [
312
+ // State/data flow
313
+ { glob: "**/*ViewModel.*", category: "state", reason: "viewmodel (state management)" },
314
+ { glob: "**/*Store.*", category: "state", reason: "store (state management)" },
315
+ { glob: "**/*Reducer.*", category: "state", reason: "reducer (state management)" },
316
+ // Data layer — interface
317
+ { glob: "**/*Repository.*", category: "data-interface", reason: "repository interface (data layer contract)" },
318
+ { glob: "**/*Dao.*", category: "data-interface", reason: "DAO (data access)" },
319
+ { glob: "**/*DataSource.*", category: "data-interface", reason: "data source" },
320
+ // Data layer — implementation (where actual patterns live: mappers, retry, IO dispatchers)
321
+ { glob: "**/*RepositoryImpl.*", category: "data-impl", reason: "repository implementation (data layer patterns)" },
322
+ { glob: "**/*ServiceImpl.*", category: "data-impl", reason: "service implementation" },
323
+ { glob: "**/*Impl.*", category: "data-impl", reason: "implementation (concrete patterns)" },
324
+ // Data transformation
325
+ { glob: "**/*Mapper.*", category: "transform", reason: "mapper (data transformation)" },
326
+ { glob: "**/*Converter.*", category: "transform", reason: "converter (data transformation)" },
327
+ { glob: "**/*Adapter.*", category: "transform", reason: "adapter (interface adaptation)" },
328
+ // Dependency injection / wiring
329
+ { glob: "**/*Module.*", category: "di", reason: "module (DI/wiring)" },
330
+ { glob: "**/*Provider.*", category: "di", reason: "provider (DI/wiring)" },
331
+ { glob: "**/*Container.*", category: "di", reason: "container (DI/wiring)" },
332
+ { glob: "**/*Factory.*", category: "di", reason: "factory (object creation)" },
333
+ // API / network
334
+ { glob: "**/*Service.*", category: "api", reason: "service (business/API layer)" },
335
+ { glob: "**/*Client.*", category: "api", reason: "client (API/network layer)" },
336
+ { glob: "**/*Api.*", category: "api", reason: "API interface definition" },
337
+ // Interface contracts / protocols
338
+ { glob: "**/*Interface.*", category: "contract", reason: "interface definition" },
339
+ { glob: "**/*Protocol.*", category: "contract", reason: "protocol definition" },
340
+ { glob: "**/*Trait.*", category: "contract", reason: "trait definition" },
341
+ // Routing / navigation
342
+ { glob: "**/*Router.*", category: "routing", reason: "router (navigation/routing)" },
343
+ { glob: "**/*Route.*", category: "routing", reason: "route definition" },
344
+ { glob: "**/*NavHost.*", category: "routing", reason: "navigation host" },
345
+ { glob: "**/*Controller.*", category: "routing", reason: "controller (request handling)" },
346
+ { glob: "**/*Handler.*", category: "routing", reason: "handler (request handling)" },
347
+ // Middleware / interceptors
348
+ { glob: "**/*Middleware.*", category: "middleware", reason: "middleware (request pipeline)" },
349
+ { glob: "**/*Interceptor.*", category: "middleware", reason: "interceptor (cross-cutting)" },
350
+ { glob: "**/*Plugin.*", category: "middleware", reason: "plugin (extensibility)" },
351
+ // Models / types
352
+ { glob: "**/*Model.*", category: "model", reason: "model (domain types)" },
353
+ { glob: "**/*Entity.*", category: "model", reason: "entity (persistence types)" },
354
+ { glob: "**/*Dto.*", category: "model", reason: "DTO (data transfer types)" },
355
+ { glob: "**/*Schema.*", category: "model", reason: "schema (data validation)" },
356
+ // Use cases / commands
357
+ { glob: "**/*UseCase.*", category: "usecase", reason: "use case (business logic)" },
358
+ { glob: "**/*Interactor.*", category: "usecase", reason: "interactor (business logic)" },
359
+ { glob: "**/*Command.*", category: "usecase", reason: "command (CQRS pattern)" }
360
+ ];
361
+ var IGNORE_PATTERNS = [
362
+ "**/node_modules/**",
363
+ "**/dist/**",
364
+ "**/build/**",
365
+ "**/.gradle/**",
366
+ "**/target/**",
367
+ "**/.git/**",
368
+ "**/vendor/**",
369
+ "**/__pycache__/**",
370
+ "**/venv/**",
371
+ "**/.venv/**",
372
+ "**/*.min.*",
373
+ "**/*.map",
374
+ "**/package-lock.json",
375
+ "**/yarn.lock",
376
+ "**/pnpm-lock.yaml",
377
+ "**/*.lock",
378
+ "**/*.generated.*",
379
+ "**/generated/**",
380
+ "**/R.java",
381
+ "**/BuildConfig.java"
382
+ ];
383
+ var PREVIEW_LINES = 60;
384
+ async function loadProjectConfig(rootDir) {
385
+ try {
386
+ const raw = await fs2.readFile(
387
+ path.join(rootDir, ".mason", "config.json"),
388
+ "utf-8"
389
+ );
390
+ return JSON.parse(raw);
391
+ } catch {
392
+ return {};
393
+ }
394
+ }
395
+ async function sampleFiles(rootDir, maxFiles = 25) {
396
+ const selected = /* @__PURE__ */ new Map();
397
+ const projectConfig = await loadProjectConfig(rootDir);
398
+ const ignorePatterns = [...IGNORE_PATTERNS, ...projectConfig.ignore ?? []];
399
+ for (const filePath of projectConfig.alwaysInclude ?? []) {
400
+ if (selected.size >= maxFiles) break;
401
+ selected.set(filePath, "always-include (project config)");
402
+ }
403
+ let configCount = 0;
404
+ for (const pattern of CONFIG_FILES) {
405
+ if (configCount >= 5) break;
406
+ const matches = await fg2(pattern, {
407
+ cwd: rootDir,
408
+ ignore: ignorePatterns,
409
+ deep: 3
410
+ });
411
+ for (const match of matches) {
412
+ if (configCount >= 5 || selected.size >= maxFiles) break;
413
+ selected.set(match, "config file");
414
+ configCount++;
415
+ }
416
+ }
417
+ const moduleBuildPatterns = [
418
+ // Gradle
419
+ "**/build.gradle.kts",
420
+ "**/build.gradle",
421
+ // Cargo workspace members
422
+ "**/Cargo.toml",
423
+ // Node workspaces
424
+ "**/package.json",
425
+ // Go sub-modules
426
+ "**/go.mod"
427
+ ];
428
+ let moduleBuildCount = 0;
429
+ for (const pattern of moduleBuildPatterns) {
430
+ const matches = await fg2(pattern, {
431
+ cwd: rootDir,
432
+ ignore: ignorePatterns,
433
+ deep: 4
434
+ });
435
+ const subMatches = matches.filter((m) => m.includes("/"));
436
+ for (const match of subMatches) {
437
+ if (moduleBuildCount >= 4 || selected.size >= maxFiles) break;
438
+ if (!selected.has(match)) {
439
+ selected.set(match, "module build file (reveals dependency graph)");
440
+ moduleBuildCount++;
441
+ }
442
+ }
443
+ if (moduleBuildCount >= 4) break;
444
+ }
445
+ let entryCount = 0;
446
+ for (const pattern of ENTRY_POINT_PATTERNS) {
447
+ if (entryCount >= 2) break;
448
+ const matches = await fg2(pattern, {
449
+ cwd: rootDir,
450
+ ignore: ignorePatterns,
451
+ deep: 5
452
+ });
453
+ for (const match of matches) {
454
+ if (entryCount >= 2 || selected.size >= maxFiles) break;
455
+ if (!selected.has(match)) {
456
+ selected.set(match, "entry point");
457
+ entryCount++;
458
+ }
459
+ }
460
+ }
461
+ try {
462
+ const { stdout } = await exec3(
463
+ "git",
464
+ ["log", "--since=3 months ago", "--format=", "--name-only"],
465
+ { cwd: rootDir, maxBuffer: 5e6 }
466
+ );
467
+ const fileCounts = /* @__PURE__ */ new Map();
468
+ for (const line of stdout.split("\n")) {
469
+ if (!line) continue;
470
+ if (line.includes("node_modules") || line.includes("/build/") || line.includes(".gradle") || line.includes("/generated/"))
471
+ continue;
472
+ const ext = path.extname(line).slice(1);
473
+ if (!SOURCE_EXTENSIONS.includes(ext)) continue;
474
+ fileCounts.set(line, (fileCounts.get(line) ?? 0) + 1);
475
+ }
476
+ const hotFiles = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
477
+ for (const [file, count] of hotFiles) {
478
+ if (selected.size >= maxFiles) break;
479
+ if (!selected.has(file)) {
480
+ selected.set(file, `frequently changed (${count} commits in 3 months)`);
481
+ }
482
+ }
483
+ } catch {
484
+ }
485
+ const seenCategories = /* @__PURE__ */ new Set();
486
+ let patternCount = 0;
487
+ for (const pattern of ARCHITECTURAL_PATTERNS) {
488
+ if (patternCount >= 8 || selected.size >= maxFiles) break;
489
+ if (seenCategories.has(pattern.category)) continue;
490
+ const matches = await fg2(pattern.glob, {
491
+ cwd: rootDir,
492
+ ignore: ignorePatterns
493
+ });
494
+ if (matches.length > 0) {
495
+ for (const match of matches) {
496
+ if (!selected.has(match)) {
497
+ selected.set(match, pattern.reason);
498
+ seenCategories.add(pattern.category);
499
+ patternCount++;
500
+ break;
501
+ }
502
+ }
503
+ }
504
+ }
505
+ for (const customGlob of projectConfig.patterns ?? []) {
506
+ if (selected.size >= maxFiles) break;
507
+ const matches = await fg2(customGlob, {
508
+ cwd: rootDir,
509
+ ignore: ignorePatterns
510
+ });
511
+ for (const match of matches) {
512
+ if (selected.size >= maxFiles) break;
513
+ if (!selected.has(match)) {
514
+ selected.set(match, "custom pattern (project config)");
515
+ break;
516
+ }
517
+ }
518
+ }
519
+ const testPatternGroups = [
520
+ // JS/TS tests
521
+ { patterns: ["**/*.test.*", "**/*.spec.*"], label: "JS/TS test" },
522
+ // JVM tests
523
+ { patterns: ["**/*Test.kt", "**/*Test.java"], label: "JVM test" },
524
+ // Python tests
525
+ { patterns: ["**/test_*.py", "**/*_test.py"], label: "Python test" },
526
+ // Go tests
527
+ { patterns: ["**/*_test.go"], label: "Go test" },
528
+ // Swift tests
529
+ { patterns: ["**/*Tests.swift", "**/*Test.swift"], label: "Swift test" },
530
+ // Rust tests
531
+ { patterns: ["**/*_test.rs"], label: "Rust test" }
532
+ ];
533
+ let testCount = 0;
534
+ for (const group of testPatternGroups) {
535
+ if (testCount >= 3 || selected.size >= maxFiles) break;
536
+ const testFiles = await fg2(group.patterns, {
537
+ cwd: rootDir,
538
+ ignore: ignorePatterns
539
+ });
540
+ if (testFiles.length > 0) {
541
+ for (const file of testFiles) {
542
+ if (!selected.has(file)) {
543
+ selected.set(file, `test example (${group.label})`);
544
+ testCount++;
545
+ break;
546
+ }
547
+ }
548
+ }
549
+ }
550
+ const sourceGlobs = SOURCE_EXTENSIONS.map((ext) => `**/*.${ext}`);
551
+ const allSourceFiles = await fg2(sourceGlobs, {
552
+ cwd: rootDir,
553
+ ignore: ignorePatterns
554
+ });
555
+ const dirRepresentatives = /* @__PURE__ */ new Map();
556
+ const boringFiles = /\.(gradle|gradle\.kts|json|toml|yaml|yml|xml|properties)$/;
557
+ for (const file of allSourceFiles) {
558
+ const topDir = file.split("/")[0];
559
+ if (!dirRepresentatives.has(topDir) && !boringFiles.test(file)) {
560
+ dirRepresentatives.set(topDir, file);
561
+ }
562
+ }
563
+ for (const [, file] of dirRepresentatives) {
564
+ if (selected.size >= maxFiles) break;
565
+ if (!selected.has(file)) {
566
+ selected.set(file, "directory representative");
567
+ }
568
+ }
569
+ const results = [];
570
+ for (const [filePath, reason] of selected) {
571
+ try {
572
+ const fullPath = path.join(rootDir, filePath);
573
+ const stat = await fs2.stat(fullPath);
574
+ if (stat.size > 1e5) continue;
575
+ const content = await fs2.readFile(fullPath, "utf-8");
576
+ const lines = content.split("\n");
577
+ const preview = lines.slice(0, PREVIEW_LINES).join("\n");
578
+ results.push({
579
+ path: filePath,
580
+ preview,
581
+ totalLines: lines.length,
582
+ sizeBytes: stat.size,
583
+ reason
584
+ });
585
+ } catch {
586
+ }
587
+ }
588
+ return results;
589
+ }
590
+ async function readFullFile(rootDir, filePath) {
591
+ try {
592
+ const fullPath = path.join(path.resolve(rootDir), filePath);
593
+ if (!fullPath.startsWith(path.resolve(rootDir))) return null;
594
+ const content = await fs2.readFile(fullPath, "utf-8");
595
+ return {
596
+ path: filePath,
597
+ content,
598
+ totalLines: content.split("\n").length
599
+ };
600
+ } catch {
601
+ return null;
602
+ }
603
+ }
604
+
605
+ // src/snapshot/snapshot.ts
606
+ import fs4 from "fs/promises";
607
+ import path3 from "path";
608
+ import { execFile as execFile6 } from "child_process";
609
+ import { promisify as promisify6 } from "util";
610
+
611
+ // src/llm/providers.ts
612
+ import { execFile as execFile5 } from "child_process";
613
+ import { promisify as promisify5 } from "util";
614
+
615
+ // src/llm/config.ts
616
+ import fs3 from "fs/promises";
617
+ import path2 from "path";
618
+ import os from "os";
619
+ import { execFile as execFile4 } from "child_process";
620
+ import { promisify as promisify4 } from "util";
621
+ var exec4 = promisify4(execFile4);
622
+ var CONFIG_DIR = path2.join(os.homedir(), ".mason");
623
+ var CONFIG_FILE = path2.join(CONFIG_DIR, "config.json");
624
+
625
+ // src/llm/providers.ts
626
+ var exec5 = promisify5(execFile5);
627
+
628
+ // src/snapshot/snapshot.ts
629
+ var exec6 = promisify6(execFile6);
630
+ function snapshotDir(rootDir) {
631
+ return path3.join(rootDir, ".mason");
632
+ }
633
+ function snapshotPath(rootDir) {
634
+ return path3.join(snapshotDir(rootDir), "snapshot.json");
635
+ }
636
+ async function loadSnapshot(rootDir) {
637
+ try {
638
+ const raw = await fs4.readFile(snapshotPath(rootDir), "utf-8");
639
+ const parsed = JSON.parse(raw);
640
+ if (parsed.version !== 2) return null;
641
+ return parsed;
642
+ } catch {
643
+ return null;
644
+ }
645
+ }
646
+ async function saveSnapshot(rootDir, snapshot) {
647
+ await fs4.mkdir(snapshotDir(rootDir), { recursive: true });
648
+ await fs4.writeFile(
649
+ snapshotPath(rootDir),
650
+ JSON.stringify(snapshot, null, 2),
651
+ "utf-8"
652
+ );
653
+ }
654
+ async function getCurrentGitHash(rootDir) {
655
+ try {
656
+ const { stdout } = await exec6("git", ["rev-parse", "HEAD"], {
657
+ cwd: rootDir
658
+ });
659
+ return stdout.trim();
660
+ } catch {
661
+ return "unknown";
662
+ }
663
+ }
664
+
665
+ // src/mcp/tools.ts
666
+ var exec7 = promisify7(execFile7);
667
+ var IGNORE = [
668
+ "**/node_modules/**",
669
+ "**/dist/**",
670
+ "**/build/**",
671
+ "**/.gradle/**",
672
+ "**/target/**",
673
+ "**/.git/**",
674
+ "**/vendor/**",
675
+ "**/__pycache__/**",
676
+ "**/venv/**",
677
+ "**/.venv/**",
678
+ "**/*.min.*",
679
+ "**/*.map"
680
+ ];
681
+ async function buildContext(dir) {
682
+ return {
683
+ rootDir: dir,
684
+ gitAvailable: await isGitRepo(dir)
685
+ };
686
+ }
687
+ async function analyzeProject(dir) {
688
+ const rootDir = path4.resolve(dir);
689
+ const context = await buildContext(rootDir);
690
+ const results = await runAll(context);
691
+ const projectSnapshot = await detectProjectSnapshot(rootDir);
692
+ const output = {
693
+ project: projectSnapshot,
694
+ analyzers: results.map((r) => ({
695
+ name: r.analyzer,
696
+ durationMs: r.durationMs,
697
+ findings: r.findings.map((f) => ({
698
+ category: f.category,
699
+ confidence: f.confidence,
700
+ summary: f.summary,
701
+ evidence: f.evidence,
702
+ suggestedRule: f.ruleCandidate
703
+ })),
704
+ gaps: r.gaps.map((g) => ({
705
+ question: g.question,
706
+ context: g.context
707
+ }))
708
+ }))
709
+ };
710
+ return JSON.stringify(output, null, 2);
711
+ }
712
+ async function detectProjectSnapshot(rootDir) {
713
+ const buildFiles = [
714
+ "package.json",
715
+ "tsconfig.json",
716
+ "build.gradle.kts",
717
+ "build.gradle",
718
+ "settings.gradle.kts",
719
+ "settings.gradle",
720
+ "gradle/libs.versions.toml",
721
+ "Cargo.toml",
722
+ "go.mod",
723
+ "go.sum",
724
+ "pyproject.toml",
725
+ "setup.py",
726
+ "requirements.txt",
727
+ "Pipfile",
728
+ "Gemfile",
729
+ "Package.swift",
730
+ "Makefile",
731
+ "CMakeLists.txt",
732
+ "Dockerfile",
733
+ "docker-compose.yml",
734
+ "docker-compose.yaml",
735
+ ".github/workflows",
736
+ ".gitlab-ci.yml",
737
+ "Jenkinsfile"
738
+ ];
739
+ const present = [];
740
+ for (const file of buildFiles) {
741
+ try {
742
+ await fs5.access(path4.join(rootDir, file));
743
+ present.push(file);
744
+ } catch {
745
+ }
746
+ }
747
+ const testDirs = [
748
+ "test",
749
+ "tests",
750
+ "__tests__",
751
+ "spec",
752
+ "src/test",
753
+ "src/tests",
754
+ "**/src/test",
755
+ "**/src/androidTest",
756
+ "**/src/iosTest"
757
+ ];
758
+ const testInfo = {};
759
+ for (const pattern of testDirs) {
760
+ const files = await fg3(`${pattern}/**/*`, {
761
+ cwd: rootDir,
762
+ ignore: IGNORE,
763
+ onlyFiles: true
764
+ });
765
+ if (files.length > 0) {
766
+ testInfo[pattern] = files.length;
767
+ }
768
+ }
769
+ const testFilePatterns = [
770
+ { pattern: "**/*.test.*", label: "*.test.*" },
771
+ { pattern: "**/*.spec.*", label: "*.spec.*" },
772
+ { pattern: "**/*Test.kt", label: "*Test.kt" },
773
+ { pattern: "**/*Test.java", label: "*Test.java" },
774
+ { pattern: "**/test_*.py", label: "test_*.py" },
775
+ { pattern: "**/*_test.go", label: "*_test.go" },
776
+ { pattern: "**/*Tests.swift", label: "*Tests.swift" },
777
+ { pattern: "**/*_test.rs", label: "*_test.rs" }
778
+ ];
779
+ for (const { pattern, label } of testFilePatterns) {
780
+ const files = await fg3(pattern, { cwd: rootDir, ignore: IGNORE });
781
+ if (files.length > 0) {
782
+ testInfo[label] = files.length;
783
+ }
784
+ }
785
+ const sourceFiles = await fg3("**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,c,dart}", {
786
+ cwd: rootDir,
787
+ ignore: IGNORE
788
+ });
789
+ const fileCounts = {};
790
+ for (const file of sourceFiles) {
791
+ const ext = path4.extname(file).slice(1);
792
+ fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
793
+ }
794
+ return {
795
+ configFilesPresent: present,
796
+ sourceFileCounts: fileCounts,
797
+ totalSourceFiles: sourceFiles.length,
798
+ testInfo: Object.keys(testInfo).length > 0 ? testInfo : void 0
799
+ };
800
+ }
801
+ async function getCodeSamples(dir, count = 15) {
802
+ const rootDir = path4.resolve(dir);
803
+ const samples = await sampleFiles(rootDir, count);
804
+ const output = {
805
+ note: "These are previews (first ~60 lines). Use get_file_content to read the full file if needed.",
806
+ files: samples.map((s) => ({
807
+ path: s.path,
808
+ reason: s.reason,
809
+ totalLines: s.totalLines,
810
+ sizeBytes: s.sizeBytes,
811
+ preview: s.preview
812
+ }))
813
+ };
814
+ return JSON.stringify(output, null, 2);
815
+ }
816
+ async function getFileContent(dir, filePath) {
817
+ const rootDir = path4.resolve(dir);
818
+ const result = await readFullFile(rootDir, filePath);
819
+ if (!result) {
820
+ return JSON.stringify({ error: `Could not read file: ${filePath}` });
821
+ }
822
+ return JSON.stringify(result, null, 2);
823
+ }
824
+ async function getProjectStructure(dir) {
825
+ const rootDir = path4.resolve(dir);
826
+ const allFiles = await fg3("**/*", {
827
+ cwd: rootDir,
828
+ ignore: IGNORE,
829
+ onlyFiles: true
830
+ });
831
+ const dirInfo = /* @__PURE__ */ new Map();
832
+ for (const file of allFiles) {
833
+ const parts = file.split("/");
834
+ for (let depth = 1; depth <= Math.min(parts.length, 2); depth++) {
835
+ const dirPath = parts.slice(0, depth).join("/");
836
+ if (!dirInfo.has(dirPath)) {
837
+ dirInfo.set(dirPath, { fileCount: 0, extensions: /* @__PURE__ */ new Map() });
838
+ }
839
+ const info = dirInfo.get(dirPath);
840
+ info.fileCount++;
841
+ const ext = path4.extname(file).slice(1);
842
+ if (ext) {
843
+ info.extensions.set(ext, (info.extensions.get(ext) ?? 0) + 1);
844
+ }
845
+ }
846
+ }
847
+ const directories = [...dirInfo.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([dirPath, info]) => {
848
+ const extensions = {};
849
+ for (const [ext, count] of info.extensions) {
850
+ extensions[ext] = count;
851
+ }
852
+ return { path: dirPath, fileCount: info.fileCount, extensions };
853
+ });
854
+ const topLevelFiles = allFiles.filter((f) => !f.includes("/"));
855
+ const output = {
856
+ totalFiles: allFiles.length,
857
+ topLevelFiles,
858
+ directories
859
+ };
860
+ return JSON.stringify(output, null, 2);
861
+ }
862
+ async function getTestMap(dir) {
863
+ const rootDir = path4.resolve(dir);
864
+ const testPatterns = [
865
+ "**/*.test.*",
866
+ "**/*.spec.*",
867
+ "**/*Test.kt",
868
+ "**/*Test.java",
869
+ "**/*Tests.kt",
870
+ "**/*Tests.java",
871
+ "**/test_*.py",
872
+ "**/*_test.py",
873
+ "**/*_test.go",
874
+ "**/*Tests.swift",
875
+ "**/*Test.swift",
876
+ "**/*_test.rs"
877
+ ];
878
+ const testFiles = await fg3(testPatterns, { cwd: rootDir, ignore: IGNORE });
879
+ const sourceFiles = await fg3(
880
+ "**/*.{ts,tsx,js,jsx,kt,kts,java,py,go,rs,swift,rb,cs,cpp,dart}",
881
+ { cwd: rootDir, ignore: IGNORE }
882
+ );
883
+ const sourceByBaseName = /* @__PURE__ */ new Map();
884
+ for (const file of sourceFiles) {
885
+ if (testFiles.includes(file)) continue;
886
+ const baseName = path4.basename(file).replace(/\.[^.]+$/, "");
887
+ const existing = sourceByBaseName.get(baseName) ?? [];
888
+ existing.push(file);
889
+ sourceByBaseName.set(baseName, existing);
890
+ }
891
+ const pairs = [];
892
+ const unmatched = [];
893
+ for (const testFile of testFiles) {
894
+ const testBaseName = path4.basename(testFile).replace(/\.[^.]+$/, "");
895
+ const sourceName = testBaseName.replace(/Test$|Tests$|Spec$|\.test$|\.spec$/, "").replace(/^test_|_test$/, "");
896
+ if (!sourceName) {
897
+ unmatched.push(testFile);
898
+ continue;
899
+ }
900
+ const candidates = sourceByBaseName.get(sourceName);
901
+ if (candidates && candidates.length > 0) {
902
+ const testDir = path4.dirname(testFile);
903
+ const bestMatch = candidates.reduce((best, candidate) => {
904
+ const candidateDir = path4.dirname(candidate);
905
+ const bestDir = path4.dirname(best);
906
+ const candidateOverlap = commonSegments(testDir, candidateDir);
907
+ const bestOverlap = commonSegments(testDir, bestDir);
908
+ return candidateOverlap > bestOverlap ? candidate : best;
909
+ });
910
+ pairs.push({
911
+ test: testFile,
912
+ source: bestMatch,
913
+ confidence: candidates.length === 1 ? "exact" : "best-guess"
914
+ });
915
+ } else {
916
+ unmatched.push(testFile);
917
+ }
918
+ }
919
+ const output = {
920
+ totalTestFiles: testFiles.length,
921
+ paired: pairs,
922
+ unmatched
923
+ };
924
+ return JSON.stringify(output, null, 2);
925
+ }
926
+ function commonSegments(pathA, pathB) {
927
+ const segsA = pathA.split("/");
928
+ const segsB = pathB.split("/");
929
+ let count = 0;
930
+ for (let i = 0; i < Math.min(segsA.length, segsB.length); i++) {
931
+ if (segsA[i] === segsB[i]) count++;
932
+ else break;
933
+ }
934
+ return count;
935
+ }
936
+ async function getSnapshot(dir) {
937
+ const rootDir = path4.resolve(dir);
938
+ const snapshot = await loadSnapshot(rootDir);
939
+ if (!snapshot) {
940
+ return JSON.stringify({
941
+ exists: false,
942
+ message: "No concept map found. Run 'mason snapshot' to create one, or call save_snapshot with features and flows."
943
+ });
944
+ }
945
+ const currentHash = await getCurrentGitHash(rootDir);
946
+ const isStale = snapshot.gitHash !== currentHash && snapshot.gitHash !== "unknown";
947
+ const output = {
948
+ exists: true,
949
+ createdAt: snapshot.createdAt,
950
+ updatedAt: snapshot.updatedAt,
951
+ featureCount: Object.keys(snapshot.features).length,
952
+ flowCount: Object.keys(snapshot.flows).length,
953
+ features: snapshot.features,
954
+ flows: snapshot.flows,
955
+ stale: isStale
956
+ };
957
+ if (isStale) {
958
+ output.message = "Snapshot is behind HEAD. Some features/flows may reference changed files. Run 'mason snapshot-update' or call save_snapshot to refresh.";
959
+ }
960
+ return JSON.stringify(output, null, 2);
961
+ }
962
+ async function fullAnalysis(dir) {
963
+ const rootDir = path4.resolve(dir);
964
+ const [analysis, structure, samples, testMap, snapshot] = await Promise.all([
965
+ analyzeProject(dir),
966
+ getProjectStructure(dir),
967
+ getCodeSamples(dir, 25),
968
+ getTestMap(dir),
969
+ loadSnapshot(rootDir)
970
+ ]);
971
+ const output = {
972
+ note: "Full project analysis. Code samples are previews (~60 lines). Use get_file_content to read any file in full.",
973
+ analysis: JSON.parse(analysis),
974
+ structure: JSON.parse(structure),
975
+ codeSamples: JSON.parse(samples),
976
+ testMap: JSON.parse(testMap)
977
+ };
978
+ if (snapshot) {
979
+ output.conceptMap = {
980
+ updatedAt: snapshot.updatedAt,
981
+ features: snapshot.features,
982
+ flows: snapshot.flows
983
+ };
984
+ output.note = "Full project analysis with concept map. The concept map shows which files implement each feature and how data flows through them. Use it to jump straight to relevant files instead of exploring. Use get_file_content to read specific files.";
985
+ }
986
+ return JSON.stringify(output, null, 2);
987
+ }
988
+ async function saveSnapshotData(dir, features, flows) {
989
+ const rootDir = path4.resolve(dir);
990
+ const gitHash = await getCurrentGitHash(rootDir);
991
+ const now = (/* @__PURE__ */ new Date()).toISOString();
992
+ const existing = await loadSnapshot(rootDir);
993
+ if (existing) {
994
+ existing.features = { ...existing.features, ...features };
995
+ existing.flows = { ...existing.flows, ...flows };
996
+ existing.updatedAt = now;
997
+ existing.gitHash = gitHash;
998
+ await saveSnapshot(rootDir, existing);
999
+ return JSON.stringify({
1000
+ status: "updated",
1001
+ features: Object.keys(existing.features).length,
1002
+ flows: Object.keys(existing.flows).length
1003
+ });
1004
+ }
1005
+ const snapshot = {
1006
+ version: 2,
1007
+ createdAt: now,
1008
+ updatedAt: now,
1009
+ gitHash,
1010
+ features,
1011
+ flows
1012
+ };
1013
+ await saveSnapshot(rootDir, snapshot);
1014
+ return JSON.stringify({
1015
+ status: "created",
1016
+ features: Object.keys(features).length,
1017
+ flows: Object.keys(flows).length
1018
+ });
1019
+ }
1020
+ async function configureProject(dir, config) {
1021
+ const rootDir = path4.resolve(dir);
1022
+ const configDir = path4.join(rootDir, ".mason");
1023
+ const configPath = path4.join(configDir, "config.json");
1024
+ let existing = {};
1025
+ try {
1026
+ const raw = await fs5.readFile(configPath, "utf-8");
1027
+ existing = JSON.parse(raw);
1028
+ } catch {
1029
+ }
1030
+ if (config.patterns) existing.patterns = config.patterns;
1031
+ if (config.alwaysInclude) existing.alwaysInclude = config.alwaysInclude;
1032
+ if (config.ignore) existing.ignore = config.ignore;
1033
+ await fs5.mkdir(configDir, { recursive: true });
1034
+ await fs5.writeFile(configPath, JSON.stringify(existing, null, 2), "utf-8");
1035
+ return JSON.stringify({
1036
+ status: "saved",
1037
+ path: configPath,
1038
+ config: existing
1039
+ });
1040
+ }
1041
+
1042
+ // src/mcp/server.ts
1043
+ function createMcpServer() {
1044
+ const server = new McpServer(
1045
+ {
1046
+ name: "mason",
1047
+ version: "0.1.0"
1048
+ },
1049
+ {
1050
+ instructions: "Mason is a context engineering tool. Always call get_snapshot before using Explore agents, Glob, or Grep to understand the codebase. The snapshot is a concept map that maps features and flows to their implementing files \u2014 it eliminates the need to search. This applies to ANY question about architecture, features, flows, how things work, cross-feature interactions, or bug investigation. Workflow: 1) Call get_snapshot first. 2) If no snapshot, call full_analysis instead. 3) Use get_file_content to read the files the snapshot points to. 4) Call save_snapshot to persist your understanding for future sessions. 5) Call write_claude_md for documentation."
1051
+ }
1052
+ );
1053
+ server.tool(
1054
+ "full_analysis",
1055
+ "Run a complete project analysis in one call. Returns git history stats, project structure with file counts, curated code sample previews (~60 lines each), and test-to-source file mapping. This is the recommended starting point \u2014 call this first, then use get_file_content to read specific files in full.",
1056
+ {
1057
+ dir: z.string().describe("Absolute path to the project root directory")
1058
+ },
1059
+ async ({ dir }) => {
1060
+ const result = await fullAnalysis(dir);
1061
+ return {
1062
+ content: [{ type: "text", text: result }]
1063
+ };
1064
+ }
1065
+ );
1066
+ server.tool(
1067
+ "analyze_project",
1068
+ "Run git history analysis on a codebase. Returns commit convention patterns, stale directories, and frequently changed files. These are aggregate stats across hundreds of commits that would be expensive to compute manually.",
1069
+ {
1070
+ dir: z.string().describe("Absolute path to the project root directory")
1071
+ },
1072
+ async ({ dir }) => {
1073
+ const result = await analyzeProject(dir);
1074
+ return {
1075
+ content: [{ type: "text", text: result }]
1076
+ };
1077
+ }
1078
+ );
1079
+ server.tool(
1080
+ "get_code_samples",
1081
+ "Get previews (first ~60 lines) of representative source files from the codebase. Includes entry points, config files, hot files (frequently changed), test examples, and one file per directory for breadth. Use get_file_content to read the full content of any file that looks interesting.",
1082
+ {
1083
+ dir: z.string().describe("Absolute path to the project root directory"),
1084
+ count: z.number().optional().default(15).describe("Maximum number of files to sample (default: 15)")
1085
+ },
1086
+ async ({ dir, count }) => {
1087
+ const result = await getCodeSamples(dir, count);
1088
+ return {
1089
+ content: [{ type: "text", text: result }]
1090
+ };
1091
+ }
1092
+ );
1093
+ server.tool(
1094
+ "get_file_content",
1095
+ "Read the full content of a specific file. Use this after get_code_samples to drill into files you want to understand fully.",
1096
+ {
1097
+ dir: z.string().describe("Absolute path to the project root directory"),
1098
+ file_path: z.string().describe("Relative path to the file within the project (e.g., 'src/main.ts')")
1099
+ },
1100
+ async ({ dir, file_path }) => {
1101
+ const result = await getFileContent(dir, file_path);
1102
+ return {
1103
+ content: [{ type: "text", text: result }]
1104
+ };
1105
+ }
1106
+ );
1107
+ server.tool(
1108
+ "get_project_structure",
1109
+ "Get the directory structure of a project with file counts and extension breakdown per directory. Shows top-level files and annotated directory listing up to 2 levels deep. Useful for understanding project layout before diving into code.",
1110
+ {
1111
+ dir: z.string().describe("Absolute path to the project root directory")
1112
+ },
1113
+ async ({ dir }) => {
1114
+ const result = await getProjectStructure(dir);
1115
+ return {
1116
+ content: [{ type: "text", text: result }]
1117
+ };
1118
+ }
1119
+ );
1120
+ server.tool(
1121
+ "get_test_map",
1122
+ "Map test files to their corresponding source files by name matching. Shows which source files have tests and which don't. Useful for understanding test coverage patterns and test organization conventions.",
1123
+ {
1124
+ dir: z.string().describe("Absolute path to the project root directory")
1125
+ },
1126
+ async ({ dir }) => {
1127
+ const result = await getTestMap(dir);
1128
+ return {
1129
+ content: [{ type: "text", text: result }]
1130
+ };
1131
+ }
1132
+ );
1133
+ server.tool(
1134
+ "get_snapshot",
1135
+ "Get the project's concept map \u2014 a lookup table from features and flows to the files that implement them. Use this to jump straight to relevant files instead of exploring. Example: 'home screen' \u2192 [HomeScreen.kt, HomeViewModel.kt, HomeModule.kt]. If stale, run 'mason snapshot-update' to refresh.",
1136
+ {
1137
+ dir: z.string().describe("Absolute path to the project root directory")
1138
+ },
1139
+ async ({ dir }) => {
1140
+ const result = await getSnapshot(dir);
1141
+ return {
1142
+ content: [{ type: "text", text: result }]
1143
+ };
1144
+ }
1145
+ );
1146
+ server.tool(
1147
+ "save_snapshot",
1148
+ "Save a concept-to-files map as a persistent project snapshot. Maps feature names and data flows to the files that implement them. Persists across conversations \u2014 future sessions can call get_snapshot to instantly find relevant files. No API key needed \u2014 you are the LLM generating the map.",
1149
+ {
1150
+ dir: z.string().describe("Absolute path to the project root directory"),
1151
+ features: z.record(
1152
+ z.object({
1153
+ description: z.string().describe("One-line description of the feature"),
1154
+ files: z.array(z.string()).describe("File paths that implement this feature"),
1155
+ tests: z.array(z.string()).optional().describe("Test file paths for this feature")
1156
+ })
1157
+ ).describe("Map of feature names to their implementing files"),
1158
+ flows: z.record(
1159
+ z.object({
1160
+ description: z.string().describe("One-line description of the flow"),
1161
+ chain: z.array(z.string()).describe("Ordered list of file paths showing data/call flow")
1162
+ })
1163
+ ).describe("Map of flow names to ordered file chains")
1164
+ },
1165
+ async ({ dir, features, flows }) => {
1166
+ const result = await saveSnapshotData(dir, features, flows);
1167
+ return {
1168
+ content: [{ type: "text", text: result }]
1169
+ };
1170
+ }
1171
+ );
1172
+ server.tool(
1173
+ "configure_project",
1174
+ "Configure Mason for this project. Add custom file patterns to sample, files to always include, or paths to ignore. Saved to .mason/config.json. Use this when the default architectural patterns miss important files in the project.",
1175
+ {
1176
+ dir: z.string().describe("Absolute path to the project root directory"),
1177
+ patterns: z.array(z.string()).optional().describe("Custom glob patterns for architecturally important files (e.g., '**/*Gateway.*', '**/*Bloc.*')"),
1178
+ alwaysInclude: z.array(z.string()).optional().describe("Specific file paths to always include in samples (e.g., 'src/core/config.ts')"),
1179
+ ignore: z.array(z.string()).optional().describe("Additional glob patterns to ignore (e.g., '**/fixtures/**')")
1180
+ },
1181
+ async ({ dir, patterns, alwaysInclude, ignore }) => {
1182
+ const result = await configureProject(dir, {
1183
+ patterns,
1184
+ alwaysInclude,
1185
+ ignore
1186
+ });
1187
+ return {
1188
+ content: [{ type: "text", text: result }]
1189
+ };
1190
+ }
1191
+ );
1192
+ return server;
1193
+ }
1194
+ async function startMcpServer() {
1195
+ const server = createMcpServer();
1196
+ const transport = new StdioServerTransport();
1197
+ await server.connect(transport);
1198
+ }
1199
+
1200
+ // bin/mason-mcp.ts
1201
+ startMcpServer().catch((err) => {
1202
+ process.stderr.write(`Mason MCP server error: ${err}
1203
+ `);
1204
+ process.exit(1);
1205
+ });
1206
+ //# sourceMappingURL=mason-mcp.js.map