Skip to content

Commit cebaed9

Browse files
committed
feat: provide rules interface
1 parent 0da9d4e commit cebaed9

File tree

5 files changed

+213
-59
lines changed

5 files changed

+213
-59
lines changed

.github/workflows/test-deploy.yml

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,6 @@ jobs:
6969
- name: Install root dependencies
7070
run: npm ci
7171

72-
- name: Generate preparedRules.json
73-
run: npm run generate-rules # Assuming direct execution works
74-
7572
- name: Build Astro site
7673
env:
7774
PUBLIC_ENV_NAME: ${{ secrets.PUBLIC_ENV_NAME }}
@@ -80,8 +77,17 @@ jobs:
8077
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
8178
run: npm run build # This should create the dist/ directory
8279

83-
- name: Copy preparedRules.json to worker directory
84-
run: cp src/data/preparedRules.json mcp-server/src/preparedRules.json # Adjust target path if needed
80+
- name: Deploy to Cloudflare Pages
81+
id: deployment
82+
uses: cloudflare/wrangler-action@v3
83+
with:
84+
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} # Secret needed for Cloudflare API
85+
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} # Secret needed for Cloudflare Account ID
86+
command: pages deploy dist --project-name='ai-rules-builder' # Use the command input
87+
gitHubToken: ${{ secrets.GITHUB_TOKEN }} # Optional: Adds commit details to deployments
88+
89+
- name: Generate preparedRules.json
90+
run: npm run generate-rules # Assuming direct execution works
8591

8692
- name: Install worker dependencies
8793
run: cd mcp-server && npm ci
@@ -91,12 +97,3 @@ jobs:
9197
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
9298
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
9399
run: cd mcp-server && npx wrangler deploy # Assumes wrangler.jsonc is configured
94-
95-
- name: Deploy to Cloudflare Pages
96-
id: deployment
97-
uses: cloudflare/wrangler-action@v3
98-
with:
99-
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} # Secret needed for Cloudflare API
100-
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} # Secret needed for Cloudflare Account ID
101-
command: pages deploy dist --project-name='ai-rules-builder' # Use the command input
102-
gitHubToken: ${{ secrets.GITHUB_TOKEN }} # Optional: Adds commit details to deployments
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import preparedRulesData from '../preparedRules.json';
2+
// Import type from the tools directory
3+
import type { HierarchyNode } from '../tools/rulesTools';
4+
5+
interface PreparedRules {
6+
hierarchy: HierarchyNode[];
7+
rules: Record<string, string[]>;
8+
}
9+
10+
// Type assertion for JSON import.
11+
// Ensure 'resolveJsonModule' is true in your tsconfig.json
12+
const preparedRules = preparedRulesData as PreparedRules;
13+
14+
/**
15+
* Returns the hierarchical structure of available rules.
16+
*/
17+
export function getRuleHierarchy(): HierarchyNode[] {
18+
// Add basic check in case the json is malformed or empty
19+
if (!preparedRules || !preparedRules.hierarchy) {
20+
console.error('Error: preparedRules.json missing or hierarchy property not found.');
21+
return [];
22+
}
23+
return preparedRules.hierarchy;
24+
}
25+
26+
/**
27+
* Returns the rules for a specific library identifier.
28+
* @param libraryIdentifier The unique identifier for the library (e.g., 'REACT', 'NEXT_JS').
29+
* @returns An array of rules strings, or undefined if the library identifier is not found.
30+
*/
31+
export function getRulesForLibrary(libraryIdentifier: string): string[] | undefined {
32+
// Add basic check
33+
if (!preparedRules || !preparedRules.rules) {
34+
console.error('Error: preparedRules.json missing or rules property not found.');
35+
return undefined;
36+
}
37+
return preparedRules.rules[libraryIdentifier];
38+
}

mcp-server/src/index.ts

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,70 @@
11
import { McpAgent } from "agents/mcp";
2+
// Only import McpServer if helper types are not available/exported
23
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3-
import { z } from "zod";
4+
// Import the rules tools
5+
import { listAvailableRulesTool, getRuleContentTool } from "./tools/rulesTools";
6+
import { z } from 'zod';
47

58
// Define our MCP agent with tools
69
export class MyMCP extends McpAgent {
710
server = new McpServer({
8-
name: "Authless Calculator",
11+
name: "MCP Rules Server",
912
version: "1.0.0",
1013
});
1114

1215
async init() {
13-
// Simple addition tool
16+
// Register listAvailableRulesTool
1417
this.server.tool(
15-
"add",
16-
{ a: z.number(), b: z.number() },
17-
async ({ a, b }) => ({
18-
content: [{ type: "text", text: String(a + b) }],
19-
})
18+
listAvailableRulesTool.name,
19+
listAvailableRulesTool.description,
20+
// Callback function - use 'any' for extra due to type export issues
21+
async (/* extra: any */) => { // extra is likely unused here
22+
const result = await listAvailableRulesTool.execute();
23+
// Attempt to return as text JSON, based on error messages hinting available types
24+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
25+
}
2026
);
2127

22-
// Calculator tool with multiple operations
28+
// Register getRuleContentTool
29+
const inputSchemaShape = getRuleContentTool.inputSchema instanceof z.ZodObject
30+
? getRuleContentTool.inputSchema.shape
31+
: {};
32+
2333
this.server.tool(
24-
"calculate",
25-
{
26-
operation: z.enum(["add", "subtract", "multiply", "divide"]),
27-
a: z.number(),
28-
b: z.number(),
29-
},
30-
async ({ operation, a, b }) => {
31-
let result: number;
32-
switch (operation) {
33-
case "add":
34-
result = a + b;
35-
break;
36-
case "subtract":
37-
result = a - b;
38-
break;
39-
case "multiply":
40-
result = a * b;
41-
break;
42-
case "divide":
43-
if (b === 0)
44-
return {
45-
content: [
46-
{
47-
type: "text",
48-
text: "Error: Cannot divide by zero",
49-
},
50-
],
51-
};
52-
result = a / b;
53-
break;
54-
}
55-
return { content: [{ type: "text", text: String(result) }] };
56-
}
34+
getRuleContentTool.name,
35+
inputSchemaShape,
36+
// Callback function - use 'any' for args and extra due to type issues
37+
async (args: unknown /*, extra: any */) => { // extra is likely unused
38+
// Manual validation inside if args type is 'any'
39+
const parsedArgs = getRuleContentTool.inputSchema.safeParse(args);
40+
if (!parsedArgs.success) {
41+
// Handle invalid input from SDK side
42+
return { content: [{ type: 'text', text: `Invalid input: ${parsedArgs.error.message}`}], isError: true };
43+
}
44+
const result = await getRuleContentTool.execute(parsedArgs.data);
45+
// Return as text JSON
46+
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
47+
}
5748
);
5849
}
5950
}
6051

52+
// Define more specific types for Env and ExecutionContext if known for the environment
53+
// Example for Cloudflare Workers:
54+
// interface Env { /* ... bindings ... */ }
55+
// interface ExecutionContext { waitUntil(promise: Promise<any>): void; passThroughOnException(): void; }
56+
6157
export default {
6258
fetch(request: Request, env: Env, ctx: ExecutionContext) {
6359
const url = new URL(request.url);
6460

6561
if (url.pathname === "/sse" || url.pathname === "/sse/message") {
66-
// @ts-ignore
62+
// @ts-expect-error - env is not typed
6763
return MyMCP.serveSSE("/sse").fetch(request, env, ctx);
6864
}
6965

7066
if (url.pathname === "/mcp") {
71-
// @ts-ignore
67+
// @ts-expect-error - env is not typed
7268
return MyMCP.serve("/mcp").fetch(request, env, ctx);
7369
}
7470

mcp-server/src/tools/rulesTools.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { z } from 'zod';
2+
import { getRuleHierarchy, getRulesForLibrary } from '../data/rulesProvider';
3+
4+
// Define recursive schema for the hierarchy structure
5+
export interface HierarchyNode {
6+
id: string;
7+
name: string;
8+
children?: HierarchyNode[];
9+
}
10+
11+
const hierarchyNodeSchema: z.ZodType<HierarchyNode> = z.lazy(() =>
12+
z.object({
13+
id: z.string(),
14+
name: z.string(),
15+
children: z.array(hierarchyNodeSchema).optional(),
16+
})
17+
);
18+
19+
// New schema for the listAvailableRulesTool output
20+
const libraryInfoSchema = z.object({
21+
identifier: z.string(),
22+
name: z.string(),
23+
stack: z.array(z.string()), // e.g., ["Frontend", "React"]
24+
});
25+
26+
const listAvailableRulesOutputSchema = z.object({
27+
availableLibraries: z.array(libraryInfoSchema),
28+
reminder: z.string(),
29+
});
30+
31+
const getRuleContentInputSchema = z.object({ libraryIdentifier: z.string() });
32+
const getRuleContentOutputSchema = z.union([
33+
z.object({ rules: z.array(z.string()) }),
34+
z.object({ error: z.string() }),
35+
]);
36+
37+
// Helper function to find leaf nodes (libraries)
38+
type LibraryInfo = z.infer<typeof libraryInfoSchema>;
39+
40+
function findLibraries(nodes: HierarchyNode[], currentStack: string[] = []): LibraryInfo[] {
41+
let libraries: LibraryInfo[] = [];
42+
if (!nodes) return libraries;
43+
44+
nodes.forEach(node => {
45+
const newStack = [...currentStack, node.name];
46+
if (!node.children || node.children.length === 0) {
47+
// This is a leaf node (a library)
48+
// We only add it if it seems like a rule identifier (heuristic: contains '_')
49+
// or if it's explicitly marked somehow (adjust logic if needed based on preparedRules.json structure)
50+
// For now, let's assume all leaf nodes with actual rules in preparedRules.json are valid identifiers.
51+
// A better approach might be to cross-reference with preparedRules.rules keys if necessary.
52+
libraries.push({
53+
identifier: node.id,
54+
name: node.name,
55+
stack: currentStack, // Stack leading *to* this node's parent category
56+
});
57+
} else {
58+
// Recurse into children
59+
libraries = libraries.concat(findLibraries(node.children, newStack));
60+
}
61+
});
62+
return libraries;
63+
}
64+
65+
// Updated listAvailableRulesTool
66+
export const listAvailableRulesTool = {
67+
name: 'listAvailableRules',
68+
description: 'Lists available AI library identifiers and their stacks, with instructions on how to get rules.',
69+
inputSchema: z.object({}).optional(),
70+
outputSchema: listAvailableRulesOutputSchema, // Use the new output schema
71+
async execute(): Promise<z.infer<typeof listAvailableRulesOutputSchema>> {
72+
const hierarchy = getRuleHierarchy();
73+
const availableLibraries = findLibraries(hierarchy);
74+
75+
const result = {
76+
availableLibraries: availableLibraries,
77+
reminder: "Use the 'getRuleContent' tool with one of the 'identifier' values (e.g., 'REACT_CODING_STANDARDS') to get specific rules."
78+
};
79+
80+
// Validate the final output structure
81+
const validation = listAvailableRulesOutputSchema.safeParse(result);
82+
if (!validation.success) {
83+
console.error('Output validation failed for listAvailableRules:', validation.error);
84+
// Fallback or throw error
85+
throw new Error('Internal server error: Failed to prepare available libraries list.');
86+
}
87+
return validation.data;
88+
},
89+
};
90+
91+
export const getRuleContentTool = {
92+
name: 'getRuleContent',
93+
description: 'Gets the AI rules for a specific library identifier.',
94+
inputSchema: getRuleContentInputSchema,
95+
outputSchema: getRuleContentOutputSchema,
96+
async execute(input: z.infer<typeof getRuleContentInputSchema>): Promise<z.infer<typeof getRuleContentOutputSchema>> {
97+
const rules = getRulesForLibrary(input.libraryIdentifier);
98+
let result: z.infer<typeof getRuleContentOutputSchema>;
99+
if (rules) {
100+
result = { rules };
101+
} else {
102+
result = { error: `Rules not found for library identifier: ${input.libraryIdentifier}` };
103+
}
104+
// Validate output before returning
105+
const validation = getRuleContentOutputSchema.safeParse(result);
106+
if (!validation.success) {
107+
console.error(`Output validation failed for getRuleContent (input: ${input.libraryIdentifier}):`, validation.error);
108+
// Even if rules were found, if they don't match schema, it's an internal error
109+
throw new Error('Internal server error: Failed to prepare rule content.');
110+
}
111+
return validation.data;
112+
},
113+
};
114+
115+
// Export all tools in a map for easy lookup
116+
export const tools = {
117+
[listAvailableRulesTool.name]: listAvailableRulesTool,
118+
[getRuleContentTool.name]: getRuleContentTool,
119+
};
120+
121+
export type Tool = typeof listAvailableRulesTool | typeof getRuleContentTool;
122+
export type ToolName = keyof typeof tools;

scripts/generate-rules-json.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { fileURLToPath } from 'url';
55
// Assuming these paths are correct relative to the script location
66
const dataDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src/data');
77
const i18nDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../src/i18n');
8-
const outputFilePath = path.join(dataDir, 'preparedRules.json');
8+
const mcpServerSourceDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../mcp-server/src');
9+
const outputFilePath = path.join(mcpServerSourceDir, 'preparedRules.json');
910

1011
interface TranslationObject {
1112
[key: string]: string;

0 commit comments

Comments
 (0)