@tenantegroup/ai-rules-mcp 1.0.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.
Files changed (67) hide show
  1. package/INSTALLATION.md +52 -0
  2. package/README.md +57 -0
  3. package/USAGE.md +46 -0
  4. package/package.json +57 -0
  5. package/rules/cloudflare/api-services.md +80 -0
  6. package/rules/cloudflare/cicd-deployment.md +56 -0
  7. package/rules/cloudflare/database-orm.md +28 -0
  8. package/rules/cloudflare/edge-parity.md +24 -0
  9. package/rules/cloudflare/kv-usage.md +31 -0
  10. package/rules/cloudflare/logging-observability.md +66 -0
  11. package/rules/cloudflare/performance.md +44 -0
  12. package/rules/cloudflare/realtime-background.md +58 -0
  13. package/rules/cloudflare/security.md +162 -0
  14. package/rules/cloudflare/seeding.md +27 -0
  15. package/rules/cloudflare/workflows.md +593 -0
  16. package/rules/dotnet/api.md +26 -0
  17. package/rules/dotnet/architecture.md +27 -0
  18. package/rules/dotnet/cli.md +26 -0
  19. package/rules/dotnet/configuration.md +26 -0
  20. package/rules/dotnet/logging.md +25 -0
  21. package/rules/dotnet/maui.md +26 -0
  22. package/rules/dotnet/mvvm.md +26 -0
  23. package/rules/dotnet/packaging.md +24 -0
  24. package/rules/dotnet/project-structure.md +26 -0
  25. package/rules/dotnet/sqlite.md +29 -0
  26. package/rules/dotnet/testing.md +24 -0
  27. package/rules/flutter/api.md +29 -0
  28. package/rules/flutter/architecture.md +34 -0
  29. package/rules/flutter/auth.md +27 -0
  30. package/rules/flutter/configuration.md +24 -0
  31. package/rules/flutter/database.md +30 -0
  32. package/rules/flutter/logging.md +27 -0
  33. package/rules/flutter/navigation.md +28 -0
  34. package/rules/flutter/offline-sync.md +26 -0
  35. package/rules/flutter/platform.md +30 -0
  36. package/rules/flutter/project-structure.md +32 -0
  37. package/rules/flutter/riverpod.md +32 -0
  38. package/rules/flutter/testing.md +31 -0
  39. package/rules/nuxt/architecture-principles.md +31 -0
  40. package/rules/nuxt/authentication.md +35 -0
  41. package/rules/nuxt/code-quality.md +71 -0
  42. package/rules/nuxt/configuration.md +31 -0
  43. package/rules/nuxt/core-directives.md +12 -0
  44. package/rules/nuxt/project-initialization.md +53 -0
  45. package/rules/nuxt/project-structure.md +44 -0
  46. package/rules/nuxt/testing.md +48 -0
  47. package/src/index.js +757 -0
  48. package/templates/cloudflare/compile-context.js +43 -0
  49. package/templates/cloudflare/hooks/post-checkout +5 -0
  50. package/templates/cloudflare/hooks/pre-commit +14 -0
  51. package/templates/cloudflare/install-hooks.js +34 -0
  52. package/templates/cloudflare/validate-code.js +57 -0
  53. package/templates/dotnet/compile-context.js +43 -0
  54. package/templates/dotnet/hooks/post-checkout +5 -0
  55. package/templates/dotnet/hooks/pre-commit +14 -0
  56. package/templates/dotnet/install-hooks.js +34 -0
  57. package/templates/dotnet/validate-code.js +84 -0
  58. package/templates/flutter/compile-context.js +43 -0
  59. package/templates/flutter/hooks/post-checkout +5 -0
  60. package/templates/flutter/hooks/pre-commit +14 -0
  61. package/templates/flutter/install-hooks.js +34 -0
  62. package/templates/flutter/validate-code.js +64 -0
  63. package/templates/nuxt/compile-context.js +43 -0
  64. package/templates/nuxt/hooks/post-checkout +5 -0
  65. package/templates/nuxt/hooks/pre-commit +14 -0
  66. package/templates/nuxt/install-hooks.js +34 -0
  67. package/templates/nuxt/validate-code.js +57 -0
package/src/index.js ADDED
@@ -0,0 +1,757 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListResourcesRequestSchema,
8
+ ListToolsRequestSchema,
9
+ ReadResourceRequestSchema,
10
+ } from "@modelcontextprotocol/sdk/types.js";
11
+ import fs from "fs/promises";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { z } from "zod";
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+ const RULES_DIR = path.join(__dirname, "..", "rules");
20
+
21
+ // Tool Input Schemas using Zod
22
+ const GetRelevantRulesSchema = z.object({
23
+ task_description: z.string().describe("Description of what you're trying to accomplish"),
24
+ file_types: z.array(z.string()).optional().describe("File types involved (e.g., ['cs', 'dart', 'vue', 'ts'])"),
25
+ keywords: z.array(z.string()).optional().describe("Keywords related to the task (e.g., ['mvvm', 'riverpod', 'drizzle', 'workflows'])"),
26
+ include_all: z.boolean().default(false).describe("Include all rules (use sparingly, high token cost)"),
27
+ });
28
+
29
+ const GetRuleByNameSchema = z.object({
30
+ rule_name: z.string().describe("Rule path or name (e.g., 'flutter/riverpod' or 'cloudflare/workflows')"),
31
+ });
32
+
33
+ const SearchRuleContentSchema = z.object({
34
+ query: z.string().describe("The search term or phrase to find within rule contents"),
35
+ case_sensitive: z.boolean().default(false).describe("Whether the search should be case-sensitive"),
36
+ });
37
+
38
+ const InstallRulesSchema = z.object({
39
+ target_dir: z.string().describe("The absolute path to the project root where rules should be installed"),
40
+ stack: z.enum(["cloudflare", "nuxt", "flutter", "dotnet"]).optional().describe("Optional stack selector. If omitted, target directory is auto-detected."),
41
+ });
42
+
43
+ function toInputSchema(zodSchema) {
44
+ const schema = z.toJSONSchema(zodSchema);
45
+ delete schema.$schema;
46
+ if (!schema.type) schema.type = "object";
47
+ return schema;
48
+ }
49
+
50
+ const RULE_METADATA = {
51
+ // Cloudflare Backend Rules
52
+ "cloudflare/edge-parity.md": {
53
+ stack: "cloudflare",
54
+ keywords: ["cloudflare", "worker", "edge", "wrangler", "miniflare", "runtime", "fetch", "env"],
55
+ categories: ["backend", "runtime"],
56
+ },
57
+ "cloudflare/database-orm.md": {
58
+ stack: "cloudflare",
59
+ keywords: ["drizzle", "orm", "d1", "database", "sqlite", "query", "schema", "migration"],
60
+ categories: ["backend", "database"],
61
+ },
62
+ "cloudflare/seeding.md": {
63
+ stack: "cloudflare",
64
+ keywords: ["seed", "seeding", "test data", "fixtures"],
65
+ categories: ["backend", "database"],
66
+ },
67
+ "cloudflare/kv-usage.md": {
68
+ stack: "cloudflare",
69
+ keywords: ["kv", "cache", "key-value", "namespaces"],
70
+ categories: ["backend", "storage"],
71
+ },
72
+ "cloudflare/workflows.md": {
73
+ stack: "cloudflare",
74
+ keywords: ["workflow", "workflows", "saga", "compensation", "durable", "step"],
75
+ categories: ["backend", "orchestration"],
76
+ },
77
+ "cloudflare/realtime-background.md": {
78
+ stack: "cloudflare",
79
+ keywords: ["durable object", "do", "queue", "queues", "background", "realtime"],
80
+ categories: ["backend", "concurrency"],
81
+ },
82
+ "cloudflare/security.md": {
83
+ stack: "cloudflare",
84
+ keywords: ["security", "auth", "validation", "headers", "cors", "leak"],
85
+ categories: ["backend", "security"],
86
+ },
87
+ "cloudflare/performance.md": {
88
+ stack: "cloudflare",
89
+ keywords: ["performance", "speed", "latency", "cache-control", "stream", "optimize"],
90
+ categories: ["backend", "performance"],
91
+ },
92
+ "cloudflare/logging-observability.md": {
93
+ stack: "cloudflare",
94
+ keywords: ["logging", "log", "correlation", "observability", "sentry", "axiom"],
95
+ categories: ["backend", "logging"],
96
+ },
97
+ "cloudflare/cicd-deployment.md": {
98
+ stack: "cloudflare",
99
+ keywords: ["cicd", "github actions", "deploy", "deployment", "wrangler deploy"],
100
+ categories: ["backend", "deployment"],
101
+ },
102
+ "cloudflare/api-services.md": {
103
+ stack: "cloudflare",
104
+ keywords: ["api", "nitro", "service", "route", "handler", "endpoint"],
105
+ categories: ["backend", "api"],
106
+ },
107
+
108
+ // Nuxt Frontend Rules
109
+ "nuxt/core-directives.md": {
110
+ stack: "nuxt",
111
+ keywords: ["nuxt", "vue", "core", "directive", "rules"],
112
+ categories: ["frontend", "core"],
113
+ always: true,
114
+ },
115
+ "nuxt/architecture-principles.md": {
116
+ stack: "nuxt",
117
+ keywords: ["architecture", "nuxt", "structure", "vue", "pattern"],
118
+ categories: ["frontend", "architecture"],
119
+ },
120
+ "nuxt/project-structure.md": {
121
+ stack: "nuxt",
122
+ keywords: ["structure", "directory", "layout", "pages", "components", "server"],
123
+ categories: ["frontend", "structure"],
124
+ },
125
+ "nuxt/configuration.md": {
126
+ stack: "nuxt",
127
+ keywords: ["configuration", "nuxt.config", "wrangler.toml", "settings"],
128
+ categories: ["frontend", "config"],
129
+ },
130
+ "nuxt/authentication.md": {
131
+ stack: "nuxt",
132
+ keywords: ["auth", "login", "session", "jwt", "cookie", "oauth"],
133
+ categories: ["frontend", "security"],
134
+ },
135
+ "nuxt/testing.md": {
136
+ stack: "nuxt",
137
+ keywords: ["testing", "vitest", "unit test", "mock", "e2e"],
138
+ categories: ["frontend", "testing"],
139
+ },
140
+ "nuxt/project-initialization.md": {
141
+ stack: "nuxt",
142
+ keywords: ["init", "scaffold", "new project", "checklist"],
143
+ categories: ["frontend", "setup"],
144
+ },
145
+ "nuxt/code-quality.md": {
146
+ stack: "nuxt",
147
+ keywords: ["typescript", "eslint", "quality", "vue", "composition api"],
148
+ categories: ["frontend", "quality"],
149
+ },
150
+
151
+ // Flutter Client Rules
152
+ "flutter/architecture.md": {
153
+ stack: "flutter",
154
+ keywords: ["architecture", "layer", "domain", "data", "presentation", "result", "apperror"],
155
+ categories: ["flutter", "architecture"],
156
+ always: true,
157
+ },
158
+ "flutter/project-structure.md": {
159
+ stack: "flutter",
160
+ keywords: ["structure", "directory", "pubspec", "flutter", "layout"],
161
+ categories: ["flutter", "structure"],
162
+ },
163
+ "flutter/riverpod.md": {
164
+ stack: "flutter",
165
+ keywords: ["riverpod", "provider", "notifier", "asyncvalue", "state", "keepalive"],
166
+ categories: ["flutter", "state-management"],
167
+ },
168
+ "flutter/navigation.md": {
169
+ stack: "flutter",
170
+ keywords: ["navigation", "gorouter", "route", "shellroute", "guard"],
171
+ categories: ["flutter", "navigation"],
172
+ },
173
+ "flutter/api.md": {
174
+ stack: "flutter",
175
+ keywords: ["api", "retrofit", "dio", "http", "client", "interceptor"],
176
+ categories: ["flutter", "api"],
177
+ },
178
+ "flutter/database.md": {
179
+ stack: "flutter",
180
+ keywords: ["database", "drift", "sqlite", "dao", "table", "migration"],
181
+ categories: ["flutter", "database"],
182
+ },
183
+ "flutter/auth.md": {
184
+ stack: "flutter",
185
+ keywords: ["auth", "token", "secure storage", "login", "refresh"],
186
+ categories: ["flutter", "security"],
187
+ },
188
+ "flutter/offline-sync.md": {
189
+ stack: "flutter",
190
+ keywords: ["offline", "sync", "connectivity", "conflict", "background"],
191
+ categories: ["flutter", "sync"],
192
+ },
193
+ "flutter/logging.md": {
194
+ stack: "flutter",
195
+ keywords: ["logging", "logger", "applogger", "print"],
196
+ categories: ["flutter", "logging"],
197
+ },
198
+ "flutter/testing.md": {
199
+ stack: "flutter",
200
+ keywords: ["testing", "test", "mocktail", "providercontainer", "unit"],
201
+ categories: ["flutter", "testing"],
202
+ },
203
+ "flutter/configuration.md": {
204
+ stack: "flutter",
205
+ keywords: ["configuration", "config", "dart-define", "env"],
206
+ categories: ["flutter", "config"],
207
+ },
208
+ "flutter/platform.md": {
209
+ stack: "flutter",
210
+ keywords: ["platform", "ios", "android", "macos", "windows", "target", "build"],
211
+ categories: ["flutter", "platform"],
212
+ },
213
+
214
+ // dotNET Client Rules
215
+ "dotnet/architecture.md": {
216
+ stack: "dotnet",
217
+ keywords: ["architecture", "maui", "mvvm", "di", "dependency injection", "services"],
218
+ categories: ["dotnet", "architecture"],
219
+ always: true,
220
+ },
221
+ "dotnet/project-structure.md": {
222
+ stack: "dotnet",
223
+ keywords: ["structure", "directory", "layout", "csproj", "sln"],
224
+ categories: ["dotnet", "structure"],
225
+ },
226
+ "dotnet/maui.md": {
227
+ stack: "dotnet",
228
+ keywords: ["maui", "xaml", "shell", "navigation", "ui", "binding"],
229
+ categories: ["dotnet", "maui"],
230
+ },
231
+ "dotnet/mvvm.md": {
232
+ stack: "dotnet",
233
+ keywords: ["mvvm", "viewmodel", "observableproperty", "relaycommand", "communitytoolkit"],
234
+ categories: ["dotnet", "mvvm"],
235
+ },
236
+ "dotnet/sqlite.md": {
237
+ stack: "dotnet",
238
+ keywords: ["sqlite", "sqlite-net", "database", "repository", "migration"],
239
+ categories: ["dotnet", "database"],
240
+ },
241
+ "dotnet/api.md": {
242
+ stack: "dotnet",
243
+ keywords: ["api", "refit", "http", "dto", "retry"],
244
+ categories: ["dotnet", "api"],
245
+ },
246
+ "dotnet/logging.md": {
247
+ stack: "dotnet",
248
+ keywords: ["logging", "serilog", "diagnostics", "error"],
249
+ categories: ["dotnet", "logging"],
250
+ },
251
+ "dotnet/cli.md": {
252
+ stack: "dotnet",
253
+ keywords: ["cli", "spectre", "command", "console"],
254
+ categories: ["dotnet", "cli"],
255
+ },
256
+ "dotnet/configuration.md": {
257
+ stack: "dotnet",
258
+ keywords: ["configuration", "secrets", "securestorage", "appsettings"],
259
+ categories: ["dotnet", "config"],
260
+ },
261
+ "dotnet/packaging.md": {
262
+ stack: "dotnet",
263
+ keywords: ["packaging", "release", "msix", "winget", "brew"],
264
+ categories: ["dotnet", "packaging"],
265
+ },
266
+ "dotnet/testing.md": {
267
+ stack: "dotnet",
268
+ keywords: ["testing", "test", "mock", "unit", "xunit"],
269
+ categories: ["dotnet", "testing"],
270
+ }
271
+ };
272
+
273
+ class AIRulesServer {
274
+ constructor() {
275
+ this.server = new Server(
276
+ {
277
+ name: "@tenantegroup/ai-rules-mcp",
278
+ version: "1.0.0",
279
+ },
280
+ {
281
+ capabilities: {
282
+ resources: {},
283
+ tools: {},
284
+ },
285
+ }
286
+ );
287
+
288
+ this.setupHandlers();
289
+ this.setupErrorHandling();
290
+ }
291
+
292
+ setupErrorHandling() {
293
+ this.server.onerror = (error) => {
294
+ console.error("[MCP Error]", error);
295
+ };
296
+
297
+ process.on("SIGINT", async () => {
298
+ await this.server.close();
299
+ process.exit(0);
300
+ });
301
+ }
302
+
303
+ setupHandlers() {
304
+ // Resources - lists all available rules
305
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
306
+ const resources = [];
307
+ for (const [filename, metadata] of Object.entries(RULE_METADATA)) {
308
+ resources.push({
309
+ uri: `rule:///${filename}`,
310
+ name: filename.replace(".md", "").replace(/^\d+-/, "").replace(/-/g, " "),
311
+ description: `Stack: ${metadata.stack} | Categories: ${metadata.categories.join(", ")}`,
312
+ mimeType: "text/markdown",
313
+ });
314
+ }
315
+ return { resources };
316
+ });
317
+
318
+ // Read Resource
319
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
320
+ const uri = request.params.uri;
321
+ const filename = uri.replace("rule:///", "");
322
+ const filePath = path.join(RULES_DIR, filename);
323
+
324
+ if (!RULE_METADATA[filename]) {
325
+ throw new Error(`Rule not found in metadata registry: ${filename}`);
326
+ }
327
+
328
+ try {
329
+ const content = await fs.readFile(filePath, "utf-8");
330
+ return {
331
+ contents: [
332
+ {
333
+ uri,
334
+ mimeType: "text/markdown",
335
+ text: content,
336
+ },
337
+ ],
338
+ };
339
+ } catch (err) {
340
+ throw new Error(`Failed to read rule file: ${filename} | ${err.message}`);
341
+ }
342
+ });
343
+
344
+ // List Tools
345
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
346
+ return {
347
+ tools: [
348
+ {
349
+ name: "get_relevant_rules",
350
+ description: "Get rules relevant to the current task. Dynamically detects technology stack and auto-filters unrelated rules to optimize token budget.",
351
+ inputSchema: toInputSchema(GetRelevantRulesSchema),
352
+ },
353
+ {
354
+ name: "get_rule_by_name",
355
+ description: "Get a specific rule file by stack-relative path (e.g. 'flutter/riverpod' or 'cloudflare/workflows').",
356
+ inputSchema: toInputSchema(GetRuleByNameSchema),
357
+ },
358
+ {
359
+ name: "search_rule_content",
360
+ description: "Search for specific terms or patterns across the actual content of all rule files.",
361
+ inputSchema: toInputSchema(SearchRuleContentSchema),
362
+ },
363
+ {
364
+ name: "list_all_rules",
365
+ description: "List all available rules grouped by stack.",
366
+ inputSchema: { type: "object", properties: {} },
367
+ },
368
+ {
369
+ name: "install_rules_to_project",
370
+ description: "Auto-detects the project stack and copies standard .ai rules and git commit validator hooks to a local project directory.",
371
+ inputSchema: toInputSchema(InstallRulesSchema),
372
+ },
373
+ ],
374
+ };
375
+ });
376
+
377
+ // Call Tool
378
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
379
+ const { name, arguments: args } = request.params;
380
+
381
+ try {
382
+ switch (name) {
383
+ case "get_relevant_rules": {
384
+ const parsed = GetRelevantRulesSchema.parse(args);
385
+ return await this.getRelevantRules(parsed);
386
+ }
387
+ case "get_rule_by_name": {
388
+ const parsed = GetRuleByNameSchema.parse(args);
389
+ return await this.getRuleByName(parsed);
390
+ }
391
+ case "search_rule_content": {
392
+ const parsed = SearchRuleContentSchema.parse(args || {});
393
+ return await this.searchRuleContent(parsed);
394
+ }
395
+ case "list_all_rules":
396
+ return await this.listAllRules();
397
+ case "install_rules_to_project": {
398
+ const parsed = InstallRulesSchema.parse(args);
399
+ return await this.installRulesToProject(parsed);
400
+ }
401
+ default:
402
+ throw new Error(`Unknown tool: ${name}`);
403
+ }
404
+ } catch (error) {
405
+ if (error instanceof z.ZodError) {
406
+ return {
407
+ isError: true,
408
+ content: [{ type: "text", text: `Invalid input: ${error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')}` }],
409
+ };
410
+ }
411
+ return {
412
+ isError: true,
413
+ content: [{ type: "text", text: `Error: ${error.message}` }],
414
+ };
415
+ }
416
+ });
417
+ }
418
+
419
+ async getRelevantRules(args) {
420
+ const { task_description = "", file_types = [], keywords = [], include_all = false } = args || {};
421
+
422
+ if (include_all) {
423
+ return await this.getAllRules();
424
+ }
425
+
426
+ const safeTaskDesc = (task_description || "").toLowerCase();
427
+ const safeFileTypes = (file_types || []).map(f => f.toLowerCase());
428
+ const safeKeywords = (keywords || []).map(k => k.toLowerCase());
429
+ const context = [safeTaskDesc, ...safeFileTypes, ...safeKeywords].join(" ");
430
+
431
+ // 1. Detect target stacks
432
+ const targetStacks = new Set();
433
+
434
+ // dotNET checks
435
+ if (safeFileTypes.some(ft => ["cs", "xaml"].includes(ft)) ||
436
+ /\b(maui|dotnet|c#|refit|wpf|spectre\.console|communitytoolkit|viewmodel)\b/i.test(context)) {
437
+ targetStacks.add("dotnet");
438
+ }
439
+
440
+ // Flutter checks
441
+ if (safeFileTypes.includes("dart") ||
442
+ /\b(flutter|dart|riverpod|gorouter|drift|retrofit\.dart|mocktail)\b/i.test(context)) {
443
+ targetStacks.add("flutter");
444
+ }
445
+
446
+ // Nuxt checks
447
+ if (safeFileTypes.some(ft => ["vue", "html"].includes(ft)) ||
448
+ /\b(nuxt|vue|pinia|vitest)\b/i.test(context)) {
449
+ targetStacks.add("nuxt");
450
+ }
451
+
452
+ // Cloudflare checks
453
+ if (/\b(cloudflare|worker|wrangler|miniflare|drizzle|d1|kv|r2|durable object|workflows|queue|saga)\b/i.test(context)) {
454
+ targetStacks.add("cloudflare");
455
+ }
456
+
457
+ // Special behavior: If Nuxt is selected, it's very common to use Cloudflare as backend too
458
+ if (targetStacks.has("nuxt") && !targetStacks.has("dotnet") && !targetStacks.has("flutter")) {
459
+ targetStacks.add("cloudflare");
460
+ }
461
+
462
+ // If nothing detected, search all rules (fallback)
463
+ const restrictToStacks = targetStacks.size > 0;
464
+
465
+ // 2. Score rules
466
+ const scoredRules = [];
467
+ for (const [filename, metadata] of Object.entries(RULE_METADATA)) {
468
+ // If we detected specific stacks, strictly exclude files from other stacks
469
+ if (restrictToStacks && !targetStacks.has(metadata.stack)) {
470
+ continue;
471
+ }
472
+
473
+ let score = 0;
474
+
475
+ // Rules with 'always: true' within target stacks are automatically selected
476
+ if (metadata.always) {
477
+ score += 1000;
478
+ }
479
+
480
+ // Keyword matches
481
+ for (const kw of metadata.keywords) {
482
+ if (context.includes(kw.toLowerCase())) {
483
+ score += 15;
484
+ }
485
+ }
486
+
487
+ // Category matches
488
+ for (const cat of metadata.categories) {
489
+ if (context.includes(cat.toLowerCase())) {
490
+ score += 5;
491
+ }
492
+ }
493
+
494
+ if (score > 0) {
495
+ scoredRules.push({ filename, score });
496
+ }
497
+ }
498
+
499
+ // Sort rules by score descending
500
+ scoredRules.sort((a, b) => b.score - a.score);
501
+
502
+ if (scoredRules.length === 0) {
503
+ return {
504
+ content: [{ type: "text", text: "No relevant rules found matching the description or stack context." }],
505
+ };
506
+ }
507
+
508
+ // Read files
509
+ const ruleContents = await Promise.all(
510
+ scoredRules.map(async (item) => {
511
+ const filePath = path.join(RULES_DIR, item.filename);
512
+ const content = await fs.readFile(filePath, "utf-8");
513
+ return `\n\n## Rule: ${item.filename}\n${content}`;
514
+ })
515
+ );
516
+
517
+ const summary = `Selected ${scoredRules.length} relevant rules (Filter Stacks: ${[...targetStacks].join(", ") || "All"}): ${scoredRules.map(r => r.filename).join(", ")}`;
518
+
519
+ return {
520
+ content: [
521
+ {
522
+ type: "text",
523
+ text: `${summary}\n\n---\n${ruleContents.join("\n")}`,
524
+ },
525
+ ],
526
+ };
527
+ }
528
+
529
+ async getRuleByName(args) {
530
+ const { rule_name } = args;
531
+
532
+ // Support either 'flutter/riverpod' or 'riverpod'
533
+ let matchedFilename = null;
534
+ const lowerName = rule_name.toLowerCase().replace(".md", "");
535
+
536
+ if (RULE_METADATA[lowerName + ".md"]) {
537
+ matchedFilename = lowerName + ".md";
538
+ } else {
539
+ // Search registry keys
540
+ for (const key of Object.keys(RULE_METADATA)) {
541
+ if (key.toLowerCase().endsWith(`/${lowerName}.md`) || key.toLowerCase() === `${lowerName}.md`) {
542
+ matchedFilename = key;
543
+ break;
544
+ }
545
+ }
546
+ }
547
+
548
+ if (!matchedFilename) {
549
+ return {
550
+ content: [{ type: "text", text: `Rule not found in registry: "${rule_name}"` }],
551
+ };
552
+ }
553
+
554
+ try {
555
+ const filePath = path.join(RULES_DIR, matchedFilename);
556
+ const content = await fs.readFile(filePath, "utf-8");
557
+ return {
558
+ content: [{ type: "text", text: `## ${matchedFilename}\n\n${content}` }],
559
+ };
560
+ } catch (err) {
561
+ return {
562
+ isError: true,
563
+ content: [{ type: "text", text: `Failed to read rule file: ${err.message}` }],
564
+ };
565
+ }
566
+ }
567
+
568
+ async searchRuleContent(args) {
569
+ const { query, case_sensitive } = args;
570
+ const regex = new RegExp(query, case_sensitive ? "g" : "gi");
571
+ const matches = [];
572
+
573
+ for (const filename of Object.keys(RULE_METADATA)) {
574
+ try {
575
+ const filePath = path.join(RULES_DIR, filename);
576
+ const content = await fs.readFile(filePath, "utf-8");
577
+ if (regex.test(content)) {
578
+ const lines = content.split("\n");
579
+ const previews = lines
580
+ .filter(l => {
581
+ regex.lastIndex = 0;
582
+ return regex.test(l);
583
+ })
584
+ .map(l => l.trim())
585
+ .slice(0, 2);
586
+
587
+ matches.push({ filename, preview: previews.join("\n") });
588
+ }
589
+ } catch (err) {
590
+ // ignore
591
+ }
592
+ }
593
+
594
+ if (matches.length === 0) {
595
+ return {
596
+ content: [{ type: "text", text: `No rules found containing term: "${query}"` }],
597
+ };
598
+ }
599
+
600
+ const output = matches
601
+ .map(m => `### ${m.filename}\n${m.preview}\n...`)
602
+ .join("\n\n");
603
+
604
+ return {
605
+ content: [{ type: "text", text: `Found "${query}" in ${matches.length} rule files:\n\n${output}` }],
606
+ };
607
+ }
608
+
609
+ async listAllRules() {
610
+ const grouped = {};
611
+ for (const [filename, metadata] of Object.entries(RULE_METADATA)) {
612
+ const stack = metadata.stack;
613
+ if (!grouped[stack]) grouped[stack] = [];
614
+ grouped[stack].push(filename.split("/")[1].replace(".md", ""));
615
+ }
616
+
617
+ let output = "# Available AI Rules\n\n";
618
+ for (const [stack, list] of Object.entries(grouped)) {
619
+ output += `### ${stack.toUpperCase()} STACK\n`;
620
+ output += list.map(item => `- ${item}`).join("\n") + "\n\n";
621
+ }
622
+
623
+ return {
624
+ content: [{ type: "text", text: output }],
625
+ };
626
+ }
627
+
628
+ async installRulesToProject(args) {
629
+ const { target_dir, stack } = args;
630
+ const targetRulesDir = path.join(target_dir, ".ai", "rules");
631
+ const targetScriptsDir = path.join(target_dir, ".ai", "scripts");
632
+ const targetHooksDir = path.join(target_dir, ".ai", "hooks");
633
+
634
+ let detectedStack = stack;
635
+ if (!detectedStack) {
636
+ detectedStack = await this.autoDetectStack(target_dir);
637
+ }
638
+
639
+ if (!detectedStack) {
640
+ return {
641
+ isError: true,
642
+ content: [{ type: "text", text: "Could not auto-detect stack. Please specify 'stack' parameter explicitly (cloudflare, nuxt, flutter, dotnet)." }],
643
+ };
644
+ }
645
+
646
+ try {
647
+ // 1. Create target directories
648
+ await fs.mkdir(targetRulesDir, { recursive: true });
649
+
650
+ // 2. Select rule files to copy
651
+ const ruleFilesToCopy = Object.keys(RULE_METADATA).filter(filename => {
652
+ const meta = RULE_METADATA[filename];
653
+ return meta.stack === detectedStack;
654
+ });
655
+
656
+ // 3. Copy files
657
+ for (const filename of ruleFilesToCopy) {
658
+ const srcPath = path.join(RULES_DIR, filename);
659
+ // Copy to flat folder in target project
660
+ const basename = filename.split("/")[1];
661
+ const destPath = path.join(targetRulesDir, basename);
662
+ await fs.copyFile(srcPath, destPath);
663
+ }
664
+
665
+ // 4. Scaffold stack-specific compilation and validation tooling if templates exist
666
+ const templatesDir = path.join(__dirname, "..", "templates", detectedStack);
667
+ let copiedScriptsCount = 0;
668
+
669
+ try {
670
+ const stat = await fs.stat(templatesDir);
671
+ if (stat.isDirectory()) {
672
+ await fs.mkdir(targetScriptsDir, { recursive: true });
673
+
674
+ // Copy scripts
675
+ const scriptFiles = await fs.readdir(templatesDir);
676
+ for (const sfile of scriptFiles) {
677
+ const srcPath = path.join(templatesDir, sfile);
678
+ const destPath = path.join(targetScriptsDir, sfile);
679
+ const scriptStat = await fs.stat(srcPath);
680
+
681
+ if (scriptStat.isFile()) {
682
+ await fs.copyFile(srcPath, destPath);
683
+ copiedScriptsCount++;
684
+ } else if (sfile === "hooks") {
685
+ // Copy hook sub-directory
686
+ await fs.mkdir(targetHooksDir, { recursive: true });
687
+ const hookFiles = await fs.readdir(srcPath);
688
+ for (const hfile of hookFiles) {
689
+ await fs.copyFile(path.join(srcPath, hfile), path.join(targetHooksDir, hfile));
690
+ }
691
+ }
692
+ }
693
+ }
694
+ } catch (err) {
695
+ // No templates for this stack, ignore script copying
696
+ }
697
+
698
+ return {
699
+ content: [
700
+ {
701
+ type: "text",
702
+ text: `Successfully scaffolded project in ${target_dir}!\n` +
703
+ `- Detected Stack: ${detectedStack}\n` +
704
+ `- Copied ${ruleFilesToCopy.length} rule files to .ai/rules/\n` +
705
+ (copiedScriptsCount > 0 ? `- Scaffolded validation scripts to .ai/scripts/ and git hooks to .ai/hooks/\n` : "") +
706
+ `\nNext steps:\n` +
707
+ (detectedStack === "nuxt" || detectedStack === "cloudflare" ? `1. Run 'node .ai/scripts/install-hooks.js' to activate git pre-commit checks.\n2. Run 'node .ai/scripts/compile-context.js' to initialize CLAUDE.md / GEMINI.md context.` : "1. Setup pre-commit linting in your IDE or git configuration.")
708
+ },
709
+ ],
710
+ };
711
+ } catch (err) {
712
+ return {
713
+ isError: true,
714
+ content: [{ type: "text", text: `Error scaffolding rules: ${err.message}` }],
715
+ };
716
+ }
717
+ }
718
+
719
+ async autoDetectStack(targetDir) {
720
+ try {
721
+ const files = await fs.readdir(targetDir);
722
+ if (files.includes("pubspec.yaml")) return "flutter";
723
+ if (files.includes("package.json")) {
724
+ if (files.includes("nuxt.config.ts") || files.includes("nuxt.config.js")) {
725
+ return "nuxt";
726
+ }
727
+ return "cloudflare";
728
+ }
729
+ const hasDotnet = files.some(f => f.endsWith(".csproj") || f.endsWith(".sln"));
730
+ if (hasDotnet) return "dotnet";
731
+ } catch (e) {
732
+ // ignore
733
+ }
734
+ return null;
735
+ }
736
+
737
+ async getAllRules() {
738
+ const ruleContents = [];
739
+ for (const filename of Object.keys(RULE_METADATA)) {
740
+ const filePath = path.join(RULES_DIR, filename);
741
+ const content = await fs.readFile(filePath, "utf-8");
742
+ ruleContents.push(`\n\n## Rule: ${filename}\n${content}`);
743
+ }
744
+ return {
745
+ content: [{ type: "text", text: ruleContents.join("\n") }],
746
+ };
747
+ }
748
+
749
+ async run() {
750
+ const transport = new StdioServerTransport();
751
+ await this.server.connect(transport);
752
+ console.error("Unified AI Rules MCP Server running on stdio");
753
+ }
754
+ }
755
+
756
+ const server = new AIRulesServer();
757
+ server.run().catch(console.error);