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/LICENSE +21 -0
- package/README.md +496 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1073 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
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
|