@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.
- package/INSTALLATION.md +52 -0
- package/README.md +57 -0
- package/USAGE.md +46 -0
- package/package.json +57 -0
- package/rules/cloudflare/api-services.md +80 -0
- package/rules/cloudflare/cicd-deployment.md +56 -0
- package/rules/cloudflare/database-orm.md +28 -0
- package/rules/cloudflare/edge-parity.md +24 -0
- package/rules/cloudflare/kv-usage.md +31 -0
- package/rules/cloudflare/logging-observability.md +66 -0
- package/rules/cloudflare/performance.md +44 -0
- package/rules/cloudflare/realtime-background.md +58 -0
- package/rules/cloudflare/security.md +162 -0
- package/rules/cloudflare/seeding.md +27 -0
- package/rules/cloudflare/workflows.md +593 -0
- package/rules/dotnet/api.md +26 -0
- package/rules/dotnet/architecture.md +27 -0
- package/rules/dotnet/cli.md +26 -0
- package/rules/dotnet/configuration.md +26 -0
- package/rules/dotnet/logging.md +25 -0
- package/rules/dotnet/maui.md +26 -0
- package/rules/dotnet/mvvm.md +26 -0
- package/rules/dotnet/packaging.md +24 -0
- package/rules/dotnet/project-structure.md +26 -0
- package/rules/dotnet/sqlite.md +29 -0
- package/rules/dotnet/testing.md +24 -0
- package/rules/flutter/api.md +29 -0
- package/rules/flutter/architecture.md +34 -0
- package/rules/flutter/auth.md +27 -0
- package/rules/flutter/configuration.md +24 -0
- package/rules/flutter/database.md +30 -0
- package/rules/flutter/logging.md +27 -0
- package/rules/flutter/navigation.md +28 -0
- package/rules/flutter/offline-sync.md +26 -0
- package/rules/flutter/platform.md +30 -0
- package/rules/flutter/project-structure.md +32 -0
- package/rules/flutter/riverpod.md +32 -0
- package/rules/flutter/testing.md +31 -0
- package/rules/nuxt/architecture-principles.md +31 -0
- package/rules/nuxt/authentication.md +35 -0
- package/rules/nuxt/code-quality.md +71 -0
- package/rules/nuxt/configuration.md +31 -0
- package/rules/nuxt/core-directives.md +12 -0
- package/rules/nuxt/project-initialization.md +53 -0
- package/rules/nuxt/project-structure.md +44 -0
- package/rules/nuxt/testing.md +48 -0
- package/src/index.js +757 -0
- package/templates/cloudflare/compile-context.js +43 -0
- package/templates/cloudflare/hooks/post-checkout +5 -0
- package/templates/cloudflare/hooks/pre-commit +14 -0
- package/templates/cloudflare/install-hooks.js +34 -0
- package/templates/cloudflare/validate-code.js +57 -0
- package/templates/dotnet/compile-context.js +43 -0
- package/templates/dotnet/hooks/post-checkout +5 -0
- package/templates/dotnet/hooks/pre-commit +14 -0
- package/templates/dotnet/install-hooks.js +34 -0
- package/templates/dotnet/validate-code.js +84 -0
- package/templates/flutter/compile-context.js +43 -0
- package/templates/flutter/hooks/post-checkout +5 -0
- package/templates/flutter/hooks/pre-commit +14 -0
- package/templates/flutter/install-hooks.js +34 -0
- package/templates/flutter/validate-code.js +64 -0
- package/templates/nuxt/compile-context.js +43 -0
- package/templates/nuxt/hooks/post-checkout +5 -0
- package/templates/nuxt/hooks/pre-commit +14 -0
- package/templates/nuxt/install-hooks.js +34 -0
- 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);
|