ai-readme-mcp 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.
package/dist/index.js ADDED
@@ -0,0 +1,1073 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { zodToJsonSchema } from "zod-to-json-schema";
11
+
12
+ // src/tools/discover.ts
13
+ import { z } from "zod";
14
+
15
+ // src/core/scanner.ts
16
+ import { glob } from "glob";
17
+ import { readFile } from "fs/promises";
18
+ import { dirname, join } from "path";
19
+ var AIReadmeScanner = class {
20
+ projectRoot;
21
+ options;
22
+ constructor(projectRoot, options) {
23
+ this.projectRoot = projectRoot;
24
+ this.options = {
25
+ excludePatterns: options?.excludePatterns || [
26
+ "**/node_modules/**",
27
+ "**/.git/**",
28
+ "**/dist/**",
29
+ "**/build/**",
30
+ "**/.next/**",
31
+ "**/coverage/**"
32
+ ],
33
+ cacheContent: options?.cacheContent ?? true,
34
+ readmeFilename: options?.readmeFilename || "AI_README.md"
35
+ };
36
+ }
37
+ /**
38
+ * Scan the project directory for AI_README.md files
39
+ */
40
+ async scan() {
41
+ const pattern = `**/${this.options.readmeFilename}`;
42
+ const ignore = this.options.excludePatterns;
43
+ const files = await glob(pattern, {
44
+ cwd: this.projectRoot,
45
+ ignore,
46
+ absolute: false,
47
+ nodir: true
48
+ });
49
+ const readmes = [];
50
+ for (const file of files) {
51
+ const entry = await this.createReadmeEntry(file);
52
+ readmes.push(entry);
53
+ }
54
+ readmes.sort((a, b) => a.level - b.level);
55
+ return {
56
+ projectRoot: this.projectRoot,
57
+ readmes,
58
+ lastUpdated: /* @__PURE__ */ new Date()
59
+ };
60
+ }
61
+ /**
62
+ * Create a ReadmeEntry from a file path
63
+ */
64
+ async createReadmeEntry(filePath) {
65
+ const normalizedPath = filePath.replace(/\\/g, "/");
66
+ const dir = dirname(normalizedPath);
67
+ const level = dir === "." ? 0 : dir.split("/").length;
68
+ const scope = dir === "." ? "root" : dir.replace(/\//g, "-");
69
+ const patterns = this.generatePatterns(dir);
70
+ let content;
71
+ if (this.options.cacheContent) {
72
+ try {
73
+ const fullPath = join(this.projectRoot, filePath);
74
+ content = await readFile(fullPath, "utf-8");
75
+ } catch (error) {
76
+ console.error(`Failed to read ${filePath}:`, error);
77
+ }
78
+ }
79
+ return {
80
+ path: normalizedPath,
81
+ scope,
82
+ level,
83
+ patterns,
84
+ content
85
+ };
86
+ }
87
+ /**
88
+ * Generate glob patterns that this README covers
89
+ */
90
+ generatePatterns(dir) {
91
+ if (dir === ".") {
92
+ return ["**/*"];
93
+ }
94
+ return [
95
+ `${dir}/**/*`,
96
+ // All files in this directory and subdirectories
97
+ `${dir}/*`
98
+ // Direct children
99
+ ];
100
+ }
101
+ /**
102
+ * Refresh the scan (re-scan the project)
103
+ */
104
+ async refresh() {
105
+ return this.scan();
106
+ }
107
+ /**
108
+ * Get the project root directory
109
+ */
110
+ getProjectRoot() {
111
+ return this.projectRoot;
112
+ }
113
+ /**
114
+ * Get current scanner options
115
+ */
116
+ getOptions() {
117
+ return { ...this.options };
118
+ }
119
+ };
120
+
121
+ // src/tools/discover.ts
122
+ var discoverSchema = z.object({
123
+ projectRoot: z.string().describe("The root directory of the project"),
124
+ excludePatterns: z.array(z.string()).optional().describe("Glob patterns to exclude (e.g., ['node_modules/**', '.git/**'])")
125
+ });
126
+ async function discoverAIReadmes(input) {
127
+ const { projectRoot, excludePatterns } = input;
128
+ const scanner = new AIReadmeScanner(projectRoot, {
129
+ excludePatterns,
130
+ cacheContent: false
131
+ // Don't cache content in discovery phase
132
+ });
133
+ const index = await scanner.scan();
134
+ return {
135
+ projectRoot: index.projectRoot,
136
+ totalFound: index.readmes.length,
137
+ readmeFiles: index.readmes.map((readme) => ({
138
+ path: readme.path,
139
+ scope: readme.scope,
140
+ level: readme.level,
141
+ patterns: readme.patterns
142
+ })),
143
+ lastUpdated: index.lastUpdated.toISOString()
144
+ };
145
+ }
146
+
147
+ // src/tools/getContext.ts
148
+ import { z as z2 } from "zod";
149
+
150
+ // src/core/router.ts
151
+ import { minimatch } from "minimatch";
152
+ import { dirname as dirname2, join as join2 } from "path";
153
+ import { readFile as readFile2 } from "fs/promises";
154
+ var ContextRouter = class {
155
+ index;
156
+ constructor(index) {
157
+ this.index = index;
158
+ }
159
+ /**
160
+ * Get relevant AI_README contexts for a specific file path
161
+ * @param filePath - The file path relative to project root
162
+ * @param includeRoot - Whether to include root-level README (default: true)
163
+ */
164
+ async getContextForFile(filePath, includeRoot = true) {
165
+ const contexts = [];
166
+ for (const readme of this.index.readmes) {
167
+ const match = this.matchesPath(filePath, readme);
168
+ if (!match) continue;
169
+ if (!includeRoot && readme.level === 0) continue;
170
+ const distance = this.calculateDistance(filePath, readme);
171
+ const content = await this.getReadmeContent(readme);
172
+ const relevance = this.determineRelevance(filePath, readme);
173
+ contexts.push({
174
+ path: readme.path,
175
+ content,
176
+ relevance,
177
+ distance
178
+ });
179
+ }
180
+ contexts.sort((a, b) => {
181
+ if (a.distance !== b.distance) {
182
+ return a.distance - b.distance;
183
+ }
184
+ const relevanceOrder = { direct: 0, parent: 1, root: 2 };
185
+ return relevanceOrder[a.relevance] - relevanceOrder[b.relevance];
186
+ });
187
+ return contexts;
188
+ }
189
+ /**
190
+ * Get contexts for multiple files
191
+ */
192
+ async getContextForFiles(filePaths) {
193
+ const results = /* @__PURE__ */ new Map();
194
+ for (const filePath of filePaths) {
195
+ const contexts = await this.getContextForFile(filePath);
196
+ results.set(filePath, contexts);
197
+ }
198
+ return results;
199
+ }
200
+ /**
201
+ * Check if a file path matches a README's patterns
202
+ */
203
+ matchesPath(filePath, readme) {
204
+ return readme.patterns.some((pattern) => {
205
+ return minimatch(filePath, pattern, { dot: true });
206
+ });
207
+ }
208
+ /**
209
+ * Calculate the directory distance between a file and a README
210
+ */
211
+ calculateDistance(filePath, readme) {
212
+ const fileDir = dirname2(filePath);
213
+ const readmeDir = dirname2(readme.path);
214
+ if (readmeDir === ".") {
215
+ return fileDir === "." ? 0 : fileDir.split("/").length;
216
+ }
217
+ if (fileDir === readmeDir) {
218
+ return 0;
219
+ }
220
+ if (fileDir.startsWith(readmeDir + "/")) {
221
+ const subPath = fileDir.slice(readmeDir.length + 1);
222
+ return subPath.split("/").length;
223
+ }
224
+ const fileParts = fileDir.split("/");
225
+ const readmeParts = readmeDir.split("/");
226
+ let commonDepth = 0;
227
+ for (let i = 0; i < Math.min(fileParts.length, readmeParts.length); i++) {
228
+ if (fileParts[i] === readmeParts[i]) {
229
+ commonDepth++;
230
+ } else {
231
+ break;
232
+ }
233
+ }
234
+ return fileParts.length - commonDepth + (readmeParts.length - commonDepth);
235
+ }
236
+ /**
237
+ * Determine the relevance type of a README for a file
238
+ */
239
+ determineRelevance(filePath, readme) {
240
+ const fileDir = dirname2(filePath);
241
+ const readmeDir = dirname2(readme.path);
242
+ if (readmeDir === ".") {
243
+ return "root";
244
+ }
245
+ if (fileDir === readmeDir) {
246
+ return "direct";
247
+ }
248
+ if (fileDir.startsWith(readmeDir + "/")) {
249
+ return "parent";
250
+ }
251
+ return "parent";
252
+ }
253
+ /**
254
+ * Get the content of a README (from cache or file system)
255
+ */
256
+ async getReadmeContent(readme) {
257
+ if (readme.content) {
258
+ return readme.content;
259
+ }
260
+ try {
261
+ const fullPath = join2(this.index.projectRoot, readme.path);
262
+ return await readFile2(fullPath, "utf-8");
263
+ } catch (error) {
264
+ console.error(`Failed to read ${readme.path}:`, error);
265
+ return `[Error: Could not read ${readme.path}]`;
266
+ }
267
+ }
268
+ /**
269
+ * Update the index (useful after re-scanning)
270
+ */
271
+ updateIndex(index) {
272
+ this.index = index;
273
+ }
274
+ /**
275
+ * Get the current index
276
+ */
277
+ getIndex() {
278
+ return this.index;
279
+ }
280
+ };
281
+
282
+ // src/tools/getContext.ts
283
+ var getContextSchema = z2.object({
284
+ projectRoot: z2.string().describe("The root directory of the project"),
285
+ filePath: z2.string().describe("The file path to get context for (relative to project root)"),
286
+ includeRoot: z2.boolean().optional().default(true).describe("Whether to include root-level AI_README (default: true)"),
287
+ excludePatterns: z2.array(z2.string()).optional().describe("Glob patterns to exclude when scanning")
288
+ });
289
+ async function getContextForFile(input) {
290
+ const { projectRoot, filePath, includeRoot, excludePatterns } = input;
291
+ const scanner = new AIReadmeScanner(projectRoot, {
292
+ excludePatterns,
293
+ cacheContent: true
294
+ // Cache content for context retrieval
295
+ });
296
+ const index = await scanner.scan();
297
+ const router = new ContextRouter(index);
298
+ const contexts = await router.getContextForFile(filePath, includeRoot);
299
+ const formattedContexts = contexts.map((ctx) => ({
300
+ path: ctx.path,
301
+ relevance: ctx.relevance,
302
+ distance: ctx.distance,
303
+ content: ctx.content
304
+ }));
305
+ let promptText = `## \u{1F4DA} Project Context for: ${filePath}
306
+
307
+ `;
308
+ for (const ctx of formattedContexts) {
309
+ if (ctx.relevance === "root") {
310
+ promptText += `### Root Conventions (${ctx.path})
311
+
312
+ `;
313
+ } else if (ctx.relevance === "direct") {
314
+ promptText += `### Direct Module Conventions (${ctx.path})
315
+
316
+ `;
317
+ } else {
318
+ promptText += `### Parent Module Conventions (${ctx.path})
319
+
320
+ `;
321
+ }
322
+ promptText += ctx.content + "\n\n";
323
+ }
324
+ promptText += `---
325
+ **Important Reminders:**
326
+ `;
327
+ promptText += `- Follow the above conventions when making changes
328
+ `;
329
+ promptText += `- If your changes affect architecture or conventions, consider updating the relevant AI_README
330
+ `;
331
+ return {
332
+ filePath,
333
+ totalContexts: contexts.length,
334
+ contexts: formattedContexts,
335
+ formattedPrompt: promptText
336
+ };
337
+ }
338
+
339
+ // src/tools/update.ts
340
+ import { z as z3 } from "zod";
341
+ import { dirname as dirname3 } from "path";
342
+
343
+ // src/core/updater.ts
344
+ import { readFile as readFile3, writeFile } from "fs/promises";
345
+ import { existsSync } from "fs";
346
+ var ReadmeUpdater = class {
347
+ constructor() {
348
+ }
349
+ /**
350
+ * Update a README file with given operations
351
+ *
352
+ * @param readmePath - Path to the AI_README.md file
353
+ * @param operations - List of update operations
354
+ * @returns Update result with changes
355
+ */
356
+ async update(readmePath, operations) {
357
+ try {
358
+ if (!existsSync(readmePath)) {
359
+ return {
360
+ success: false,
361
+ error: `File not found: ${readmePath}`,
362
+ changes: []
363
+ };
364
+ }
365
+ const content = await readFile3(readmePath, "utf-8");
366
+ let updatedContent = content;
367
+ const changes = [];
368
+ for (const operation of operations) {
369
+ const result = await this.applyOperation(updatedContent, operation);
370
+ updatedContent = result.content;
371
+ changes.push(result.change);
372
+ }
373
+ await writeFile(readmePath, updatedContent, "utf-8");
374
+ return {
375
+ success: true,
376
+ changes
377
+ };
378
+ } catch (error) {
379
+ return {
380
+ success: false,
381
+ error: error instanceof Error ? error.message : String(error),
382
+ changes: []
383
+ };
384
+ }
385
+ }
386
+ /**
387
+ * Apply a single update operation to content
388
+ *
389
+ * @param content - Current content
390
+ * @param operation - Operation to apply
391
+ * @returns Updated content and change info
392
+ */
393
+ async applyOperation(content, operation) {
394
+ const lines = content.split("\n");
395
+ let updatedLines = [...lines];
396
+ let linesAdded = 0;
397
+ let linesRemoved = 0;
398
+ switch (operation.type) {
399
+ case "append": {
400
+ updatedLines.push("", operation.content);
401
+ linesAdded = operation.content.split("\n").length + 1;
402
+ break;
403
+ }
404
+ case "prepend": {
405
+ updatedLines.unshift(operation.content, "");
406
+ linesAdded = operation.content.split("\n").length + 1;
407
+ break;
408
+ }
409
+ case "replace": {
410
+ if (!operation.searchText) {
411
+ throw new Error("searchText is required for replace operation");
412
+ }
413
+ const originalContent = updatedLines.join("\n");
414
+ const newContent = originalContent.replace(
415
+ operation.searchText,
416
+ operation.content
417
+ );
418
+ if (originalContent === newContent) {
419
+ throw new Error(`Text not found: ${operation.searchText}`);
420
+ }
421
+ updatedLines = newContent.split("\n");
422
+ linesRemoved = operation.searchText.split("\n").length;
423
+ linesAdded = operation.content.split("\n").length;
424
+ break;
425
+ }
426
+ case "insert-after": {
427
+ if (!operation.section) {
428
+ throw new Error("section is required for insert-after operation");
429
+ }
430
+ const sectionIndex = this.findSectionIndex(updatedLines, operation.section);
431
+ if (sectionIndex === -1) {
432
+ throw new Error(`Section not found: ${operation.section}`);
433
+ }
434
+ const insertIndex = this.findSectionEnd(updatedLines, sectionIndex);
435
+ updatedLines.splice(insertIndex, 0, "", operation.content);
436
+ linesAdded = operation.content.split("\n").length + 1;
437
+ break;
438
+ }
439
+ case "insert-before": {
440
+ if (!operation.section) {
441
+ throw new Error("section is required for insert-before operation");
442
+ }
443
+ const sectionIndex = this.findSectionIndex(updatedLines, operation.section);
444
+ if (sectionIndex === -1) {
445
+ throw new Error(`Section not found: ${operation.section}`);
446
+ }
447
+ updatedLines.splice(sectionIndex, 0, operation.content, "");
448
+ linesAdded = operation.content.split("\n").length + 1;
449
+ break;
450
+ }
451
+ default:
452
+ throw new Error(`Unknown operation type: ${operation.type}`);
453
+ }
454
+ return {
455
+ content: updatedLines.join("\n"),
456
+ change: {
457
+ operation: operation.type,
458
+ section: operation.section,
459
+ linesAdded,
460
+ linesRemoved
461
+ }
462
+ };
463
+ }
464
+ /**
465
+ * Find the index of a section heading
466
+ *
467
+ * @param lines - Content lines
468
+ * @param section - Section heading (e.g., "## Coding Conventions")
469
+ * @returns Index of the section, or -1 if not found
470
+ */
471
+ findSectionIndex(lines, section) {
472
+ return lines.findIndex((line) => line.trim() === section.trim());
473
+ }
474
+ /**
475
+ * Find the end of a section (before the next section of same or higher level)
476
+ *
477
+ * @param lines - Content lines
478
+ * @param startIndex - Index of the section heading
479
+ * @returns Index where the section ends
480
+ */
481
+ findSectionEnd(lines, startIndex) {
482
+ const startLine = lines[startIndex];
483
+ if (!startLine) {
484
+ return lines.length;
485
+ }
486
+ const sectionLevel = this.getSectionLevel(startLine);
487
+ for (let i = startIndex + 1; i < lines.length; i++) {
488
+ const line = lines[i];
489
+ if (!line) continue;
490
+ const trimmedLine = line.trim();
491
+ if (trimmedLine.startsWith("#")) {
492
+ const level = this.getSectionLevel(trimmedLine);
493
+ if (level <= sectionLevel) {
494
+ return i;
495
+ }
496
+ }
497
+ }
498
+ return lines.length;
499
+ }
500
+ /**
501
+ * Get the heading level of a markdown section
502
+ *
503
+ * @param line - Line containing a heading
504
+ * @returns Heading level (1-6)
505
+ */
506
+ getSectionLevel(line) {
507
+ const match = line.match(/^(#{1,6})\s/);
508
+ return match && match[1] ? match[1].length : 0;
509
+ }
510
+ };
511
+
512
+ // src/core/validator.ts
513
+ import { readFile as readFile4 } from "fs/promises";
514
+ import { existsSync as existsSync2 } from "fs";
515
+ import { join as join3 } from "path";
516
+
517
+ // src/types/index.ts
518
+ var DEFAULT_VALIDATION_CONFIG = {
519
+ maxTokens: 400,
520
+ rules: {
521
+ requireH1: true,
522
+ requireSections: [],
523
+ allowCodeBlocks: false,
524
+ maxLineLength: 100
525
+ },
526
+ tokenLimits: {
527
+ excellent: 200,
528
+ good: 400,
529
+ warning: 600,
530
+ error: 1e3
531
+ }
532
+ };
533
+
534
+ // src/core/validator.ts
535
+ var ReadmeValidator = class {
536
+ config;
537
+ constructor(config) {
538
+ this.config = this.mergeConfig(config || {});
539
+ }
540
+ /**
541
+ * Merge user config with default config
542
+ */
543
+ mergeConfig(userConfig) {
544
+ return {
545
+ maxTokens: userConfig.maxTokens ?? DEFAULT_VALIDATION_CONFIG.maxTokens,
546
+ rules: {
547
+ ...DEFAULT_VALIDATION_CONFIG.rules,
548
+ ...userConfig.rules || {}
549
+ },
550
+ tokenLimits: {
551
+ ...DEFAULT_VALIDATION_CONFIG.tokenLimits,
552
+ ...userConfig.tokenLimits || {}
553
+ }
554
+ };
555
+ }
556
+ /**
557
+ * Validate a single AI_README.md file
558
+ *
559
+ * @param readmePath - Path to the README file
560
+ * @returns Validation result
561
+ */
562
+ async validate(readmePath) {
563
+ const issues = [];
564
+ if (!existsSync2(readmePath)) {
565
+ return {
566
+ valid: false,
567
+ filePath: readmePath,
568
+ issues: [
569
+ {
570
+ type: "error",
571
+ rule: "structure",
572
+ message: `File not found: ${readmePath}`
573
+ }
574
+ ]
575
+ };
576
+ }
577
+ const content = await readFile4(readmePath, "utf-8");
578
+ if (content.trim().length === 0) {
579
+ issues.push({
580
+ type: "error",
581
+ rule: "empty-content",
582
+ message: "README file is empty",
583
+ suggestion: "Add content to the README file"
584
+ });
585
+ }
586
+ const lines = content.split("\n");
587
+ const tokens = this.estimateTokens(content);
588
+ const characters = content.length;
589
+ this.validateTokenCount(tokens, issues);
590
+ this.validateStructure(content, lines, issues);
591
+ this.validateLineLength(lines, issues);
592
+ this.validateCodeBlocks(content, issues);
593
+ const score = this.calculateScore(issues, tokens);
594
+ return {
595
+ valid: !issues.some((i) => i.type === "error"),
596
+ filePath: readmePath,
597
+ issues,
598
+ score,
599
+ stats: {
600
+ tokens,
601
+ lines: lines.length,
602
+ characters
603
+ }
604
+ };
605
+ }
606
+ /**
607
+ * Estimate token count (simple word-based estimation)
608
+ * Formula: words * 1.3 (approximate token-to-word ratio)
609
+ */
610
+ estimateTokens(content) {
611
+ const words = content.split(/\s+/).filter((w) => w.length > 0).length;
612
+ return Math.round(words * 1.3);
613
+ }
614
+ /**
615
+ * Validate token count against limits
616
+ */
617
+ validateTokenCount(tokens, issues) {
618
+ const { tokenLimits, maxTokens } = this.config;
619
+ if (tokens > tokenLimits.error) {
620
+ issues.push({
621
+ type: "error",
622
+ rule: "token-count",
623
+ message: `README is too long (${tokens} tokens). Maximum recommended: ${maxTokens} tokens.`,
624
+ suggestion: "Remove unnecessary content, use bullet points instead of paragraphs, and avoid code examples."
625
+ });
626
+ } else if (tokens > tokenLimits.warning) {
627
+ issues.push({
628
+ type: "warning",
629
+ rule: "token-count",
630
+ message: `README is quite long (${tokens} tokens). Consider keeping it under ${tokenLimits.good} tokens.`,
631
+ suggestion: "Simplify content and remove redundant information."
632
+ });
633
+ } else if (tokens > tokenLimits.good) {
634
+ issues.push({
635
+ type: "info",
636
+ rule: "token-count",
637
+ message: `README length is acceptable (${tokens} tokens).`
638
+ });
639
+ }
640
+ }
641
+ /**
642
+ * Validate README structure
643
+ */
644
+ validateStructure(_content, lines, issues) {
645
+ if (this.config.rules.requireH1) {
646
+ const hasH1 = lines.some((line) => line.trim().match(/^#\s+[^#]/));
647
+ if (!hasH1) {
648
+ issues.push({
649
+ type: "error",
650
+ rule: "require-h1",
651
+ message: "README must have a H1 heading (# Title)",
652
+ suggestion: "Add a title at the beginning of the file: # Project Name"
653
+ });
654
+ }
655
+ }
656
+ if (this.config.rules.requireSections && this.config.rules.requireSections.length > 0) {
657
+ for (const section of this.config.rules.requireSections) {
658
+ const hasSection = lines.some((line) => line.trim() === section);
659
+ if (!hasSection) {
660
+ issues.push({
661
+ type: "warning",
662
+ rule: "require-sections",
663
+ message: `Missing required section: ${section}`,
664
+ suggestion: `Add section: ${section}`
665
+ });
666
+ }
667
+ }
668
+ }
669
+ }
670
+ /**
671
+ * Validate line length
672
+ */
673
+ validateLineLength(lines, issues) {
674
+ const { maxLineLength } = this.config.rules;
675
+ const longLines = lines.map((line, index) => ({ line, index })).filter(({ line }) => line.length > maxLineLength);
676
+ if (longLines.length > 3) {
677
+ issues.push({
678
+ type: "info",
679
+ rule: "line-length",
680
+ message: `${longLines.length} lines exceed ${maxLineLength} characters`,
681
+ suggestion: "Consider breaking long lines for better readability"
682
+ });
683
+ }
684
+ }
685
+ /**
686
+ * Validate code blocks
687
+ */
688
+ validateCodeBlocks(content, issues) {
689
+ if (!this.config.rules.allowCodeBlocks) {
690
+ const codeBlockCount = (content.match(/```/g) || []).length / 2;
691
+ if (codeBlockCount > 0) {
692
+ issues.push({
693
+ type: "warning",
694
+ rule: "code-blocks",
695
+ message: `Found ${codeBlockCount} code blocks. Code examples consume many tokens.`,
696
+ suggestion: "Remove code examples or move them to separate documentation."
697
+ });
698
+ }
699
+ }
700
+ }
701
+ /**
702
+ * Calculate quality score (0-100)
703
+ */
704
+ calculateScore(issues, tokens) {
705
+ let score = 100;
706
+ for (const issue of issues) {
707
+ if (issue.type === "error") score -= 20;
708
+ else if (issue.type === "warning") score -= 10;
709
+ else if (issue.type === "info") score -= 2;
710
+ }
711
+ const { tokenLimits } = this.config;
712
+ if (tokens > tokenLimits.error) score -= 30;
713
+ else if (tokens > tokenLimits.warning) score -= 15;
714
+ else if (tokens < tokenLimits.excellent) score += 10;
715
+ return Math.max(0, Math.min(100, score));
716
+ }
717
+ /**
718
+ * Load validation config from .aireadme.config.json
719
+ *
720
+ * @param projectRoot - Project root directory
721
+ * @returns Validation config or null if not found
722
+ */
723
+ static async loadConfig(projectRoot) {
724
+ const configPath = join3(projectRoot, ".aireadme.config.json");
725
+ if (!existsSync2(configPath)) {
726
+ return null;
727
+ }
728
+ try {
729
+ const content = await readFile4(configPath, "utf-8");
730
+ const config = JSON.parse(content);
731
+ return config.validation || config;
732
+ } catch (error) {
733
+ console.error(`Failed to load config from ${configPath}:`, error);
734
+ return null;
735
+ }
736
+ }
737
+ };
738
+
739
+ // src/tools/update.ts
740
+ var updateOperationSchema = z3.object({
741
+ type: z3.enum(["replace", "append", "prepend", "insert-after", "insert-before"]).describe(
742
+ "Type of update operation"
743
+ ),
744
+ section: z3.string().optional().describe(
745
+ 'Section heading to target (e.g., "## Coding Conventions")'
746
+ ),
747
+ searchText: z3.string().optional().describe(
748
+ "Text to search for (required for replace operation)"
749
+ ),
750
+ content: z3.string().describe("Content to add or replace")
751
+ });
752
+ var updateSchema = z3.object({
753
+ readmePath: z3.string().describe("Path to the AI_README.md file to update"),
754
+ operations: z3.array(updateOperationSchema).describe(
755
+ "List of update operations to perform"
756
+ )
757
+ });
758
+ async function updateAIReadme(input) {
759
+ const { readmePath, operations } = input;
760
+ const updater = new ReadmeUpdater();
761
+ const result = await updater.update(readmePath, operations);
762
+ if (!result.success) {
763
+ return {
764
+ success: false,
765
+ readmePath,
766
+ error: result.error,
767
+ summary: `Failed to update ${readmePath}: ${result.error}`
768
+ };
769
+ }
770
+ try {
771
+ const projectRoot = dirname3(dirname3(readmePath));
772
+ const config = await ReadmeValidator.loadConfig(projectRoot);
773
+ const validator = new ReadmeValidator(config || void 0);
774
+ const validation = await validator.validate(readmePath);
775
+ const warnings = validation.issues.filter((i) => i.type === "warning" || i.type === "error").map((i) => `[${i.type.toUpperCase()}] ${i.message}`);
776
+ return {
777
+ success: true,
778
+ readmePath,
779
+ changes: result.changes,
780
+ summary: `Successfully updated ${readmePath} with ${result.changes.length} operation(s). Use 'git diff' to review changes.`,
781
+ validation: {
782
+ valid: validation.valid,
783
+ score: validation.score,
784
+ warnings: warnings.length > 0 ? warnings : void 0,
785
+ stats: validation.stats
786
+ }
787
+ };
788
+ } catch (validationError) {
789
+ return {
790
+ success: true,
791
+ readmePath,
792
+ changes: result.changes,
793
+ summary: `Successfully updated ${readmePath} with ${result.changes.length} operation(s). Use 'git diff' to review changes.`,
794
+ validation: {
795
+ valid: false,
796
+ error: validationError instanceof Error ? validationError.message : "Validation failed"
797
+ }
798
+ };
799
+ }
800
+ }
801
+
802
+ // src/tools/validate.ts
803
+ import { z as z4 } from "zod";
804
+ var validateSchema = z4.object({
805
+ projectRoot: z4.string().describe("The root directory of the project"),
806
+ excludePatterns: z4.array(z4.string()).optional().describe(
807
+ 'Glob patterns to exclude (e.g., ["node_modules/**", ".git/**"])'
808
+ ),
809
+ config: z4.object({
810
+ maxTokens: z4.number().optional(),
811
+ rules: z4.object({
812
+ requireH1: z4.boolean().optional(),
813
+ requireSections: z4.array(z4.string()).optional(),
814
+ allowCodeBlocks: z4.boolean().optional(),
815
+ maxLineLength: z4.number().optional()
816
+ }).optional(),
817
+ tokenLimits: z4.object({
818
+ excellent: z4.number().optional(),
819
+ good: z4.number().optional(),
820
+ warning: z4.number().optional(),
821
+ error: z4.number().optional()
822
+ }).optional()
823
+ }).optional().describe("Custom validation configuration (optional, uses defaults if not provided)")
824
+ });
825
+ async function validateAIReadmes(input) {
826
+ const { projectRoot, excludePatterns, config: userConfig } = input;
827
+ try {
828
+ let config = userConfig;
829
+ if (!config) {
830
+ const fileConfig = await ReadmeValidator.loadConfig(projectRoot);
831
+ if (fileConfig) {
832
+ config = fileConfig;
833
+ }
834
+ }
835
+ const validator = new ReadmeValidator(config);
836
+ const scanner = new AIReadmeScanner(projectRoot, {
837
+ excludePatterns: excludePatterns || [],
838
+ cacheContent: false
839
+ });
840
+ const index = await scanner.scan();
841
+ const results = [];
842
+ for (const readme of index.readmes) {
843
+ const result = await validator.validate(readme.path);
844
+ results.push(result);
845
+ }
846
+ const totalFiles = results.length;
847
+ const validFiles = results.filter((r) => r.valid).length;
848
+ const totalIssues = results.reduce((sum, r) => sum + r.issues.length, 0);
849
+ const averageScore = totalFiles > 0 ? Math.round(results.reduce((sum, r) => sum + (r.score || 0), 0) / totalFiles) : 0;
850
+ const issuesBySeverity = {
851
+ error: 0,
852
+ warning: 0,
853
+ info: 0
854
+ };
855
+ for (const result of results) {
856
+ for (const issue of result.issues) {
857
+ issuesBySeverity[issue.type]++;
858
+ }
859
+ }
860
+ return {
861
+ success: true,
862
+ projectRoot,
863
+ summary: {
864
+ totalFiles,
865
+ validFiles,
866
+ invalidFiles: totalFiles - validFiles,
867
+ totalIssues,
868
+ averageScore,
869
+ issuesBySeverity
870
+ },
871
+ results,
872
+ message: totalIssues === 0 ? `All ${totalFiles} README files passed validation! Average score: ${averageScore}/100` : `Found ${totalIssues} issues across ${totalFiles} README files. ${validFiles} files passed validation.`
873
+ };
874
+ } catch (error) {
875
+ return {
876
+ success: false,
877
+ projectRoot,
878
+ error: error instanceof Error ? error.message : String(error),
879
+ message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
880
+ };
881
+ }
882
+ }
883
+
884
+ // src/tools/init.ts
885
+ import { z as z5 } from "zod";
886
+ import { readFile as readFile5, writeFile as writeFile2 } from "fs/promises";
887
+ import { join as join4, dirname as dirname4 } from "path";
888
+ import { existsSync as existsSync3 } from "fs";
889
+ import { fileURLToPath } from "url";
890
+ var initSchema = z5.object({
891
+ targetPath: z5.string().describe("Directory where AI_README.md will be created"),
892
+ projectName: z5.string().optional().describe("Project name to use in the template (optional)"),
893
+ overwrite: z5.boolean().optional().describe("Whether to overwrite existing AI_README.md (default: false)")
894
+ });
895
+ async function initAIReadme(input) {
896
+ const { targetPath, projectName, overwrite = false } = input;
897
+ try {
898
+ if (!existsSync3(targetPath)) {
899
+ return {
900
+ success: false,
901
+ error: `Target directory does not exist: ${targetPath}`,
902
+ message: `Failed to create AI_README.md: Directory not found`
903
+ };
904
+ }
905
+ const readmePath = join4(targetPath, "AI_README.md");
906
+ if (existsSync3(readmePath) && !overwrite) {
907
+ return {
908
+ success: false,
909
+ error: "AI_README.md already exists",
910
+ message: `AI_README.md already exists at ${readmePath}. Use overwrite: true to replace it.`,
911
+ existingPath: readmePath
912
+ };
913
+ }
914
+ const __filename2 = fileURLToPath(import.meta.url);
915
+ const __dirname2 = dirname4(__filename2);
916
+ const templatePath = join4(__dirname2, "..", "..", "docs", "templates", "basic.md");
917
+ if (!existsSync3(templatePath)) {
918
+ return {
919
+ success: false,
920
+ error: `Template file not found: ${templatePath}`,
921
+ message: "Template file is missing. Please check installation."
922
+ };
923
+ }
924
+ let content = await readFile5(templatePath, "utf-8");
925
+ const finalProjectName = projectName || "Project Name";
926
+ content = content.replace(/\{\{PROJECT_NAME\}\}/g, finalProjectName);
927
+ await writeFile2(readmePath, content, "utf-8");
928
+ return {
929
+ success: true,
930
+ readmePath,
931
+ projectName: finalProjectName,
932
+ message: `Successfully created AI_README.md at ${readmePath}. Edit the file to customize it for your project.`
933
+ };
934
+ } catch (error) {
935
+ return {
936
+ success: false,
937
+ error: error instanceof Error ? error.message : String(error),
938
+ message: `Failed to create AI_README.md: ${error instanceof Error ? error.message : String(error)}`
939
+ };
940
+ }
941
+ }
942
+
943
+ // src/index.ts
944
+ var server = new Server(
945
+ {
946
+ name: "ai-readme-mcp",
947
+ version: "0.1.0"
948
+ },
949
+ {
950
+ capabilities: {
951
+ tools: {}
952
+ }
953
+ }
954
+ );
955
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
956
+ return {
957
+ tools: [
958
+ {
959
+ name: "discover_ai_readmes",
960
+ description: "Scan the project and discover all AI_README.md files. Returns an index of all README files with their paths, scopes, and coverage patterns.",
961
+ inputSchema: zodToJsonSchema(discoverSchema)
962
+ },
963
+ {
964
+ name: "get_context_for_file",
965
+ description: "Get relevant AI_README context for a specific file path. Returns formatted context from relevant README files to help understand project conventions.",
966
+ inputSchema: zodToJsonSchema(getContextSchema)
967
+ },
968
+ {
969
+ name: "update_ai_readme",
970
+ description: "Update an AI_README.md file with specified operations (append, prepend, replace, insert-after, insert-before). Auto-validates after update. Changes are written directly; use Git for version control.",
971
+ inputSchema: zodToJsonSchema(updateSchema)
972
+ },
973
+ {
974
+ name: "validate_ai_readmes",
975
+ description: "Validate all AI_README.md files in a project. Checks token count, structure, and content quality. Returns validation results with suggestions for improvement.",
976
+ inputSchema: zodToJsonSchema(validateSchema)
977
+ },
978
+ {
979
+ name: "init_ai_readme",
980
+ description: "Initialize a new AI_README.md file from a template. Creates a basic structure with common sections to help you get started quickly.",
981
+ inputSchema: zodToJsonSchema(initSchema)
982
+ }
983
+ ]
984
+ };
985
+ });
986
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
987
+ const { name, arguments: args } = request.params;
988
+ try {
989
+ if (name === "discover_ai_readmes") {
990
+ const input = discoverSchema.parse(args);
991
+ const result = await discoverAIReadmes(input);
992
+ return {
993
+ content: [
994
+ {
995
+ type: "text",
996
+ text: JSON.stringify(result, null, 2)
997
+ }
998
+ ]
999
+ };
1000
+ }
1001
+ if (name === "get_context_for_file") {
1002
+ const input = getContextSchema.parse(args);
1003
+ const result = await getContextForFile(input);
1004
+ return {
1005
+ content: [
1006
+ {
1007
+ type: "text",
1008
+ text: result.formattedPrompt
1009
+ }
1010
+ ]
1011
+ };
1012
+ }
1013
+ if (name === "update_ai_readme") {
1014
+ const input = updateSchema.parse(args);
1015
+ const result = await updateAIReadme(input);
1016
+ return {
1017
+ content: [
1018
+ {
1019
+ type: "text",
1020
+ text: JSON.stringify(result, null, 2)
1021
+ }
1022
+ ]
1023
+ };
1024
+ }
1025
+ if (name === "validate_ai_readmes") {
1026
+ const input = validateSchema.parse(args);
1027
+ const result = await validateAIReadmes(input);
1028
+ return {
1029
+ content: [
1030
+ {
1031
+ type: "text",
1032
+ text: JSON.stringify(result, null, 2)
1033
+ }
1034
+ ]
1035
+ };
1036
+ }
1037
+ if (name === "init_ai_readme") {
1038
+ const input = initSchema.parse(args);
1039
+ const result = await initAIReadme(input);
1040
+ return {
1041
+ content: [
1042
+ {
1043
+ type: "text",
1044
+ text: JSON.stringify(result, null, 2)
1045
+ }
1046
+ ]
1047
+ };
1048
+ }
1049
+ throw new Error(`Unknown tool: ${name}`);
1050
+ } catch (error) {
1051
+ const errorMessage = error instanceof Error ? error.message : String(error);
1052
+ return {
1053
+ content: [
1054
+ {
1055
+ type: "text",
1056
+ text: `Error: ${errorMessage}`
1057
+ }
1058
+ ],
1059
+ isError: true
1060
+ };
1061
+ }
1062
+ });
1063
+ async function main() {
1064
+ const transport = new StdioServerTransport();
1065
+ await server.connect(transport);
1066
+ console.error("AI_README MCP Server started");
1067
+ console.error("Available tools: discover_ai_readmes, get_context_for_file, update_ai_readme, validate_ai_readmes, init_ai_readme");
1068
+ }
1069
+ main().catch((error) => {
1070
+ console.error("Fatal error:", error);
1071
+ process.exit(1);
1072
+ });
1073
+ //# sourceMappingURL=index.js.map