@aiready/mcp-server 0.6.0 → 0.6.2
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/.turbo/turbo-build.log +5 -5
- package/.turbo/turbo-format-check.log +2 -2
- package/.turbo/turbo-format.log +14 -0
- package/.turbo/turbo-lint$colon$fix.log +6 -0
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test$colon$coverage.log +132 -0
- package/.turbo/turbo-test.log +70 -45
- package/.turbo/turbo-type-check.log +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +598 -231
- package/package.json +22 -19
- package/src/__tests__/schema-sync.test.ts +54 -0
- package/src/__tests__/server.test.ts +28 -1
- package/src/index.ts +34 -327
- package/src/prompts/index.ts +76 -0
- package/src/resources/index.ts +94 -0
- package/src/state-store.ts +111 -0
- package/src/tools/best-practices.ts +167 -0
- package/src/tools/context-budget.ts +76 -0
- package/src/tools/index.ts +236 -0
- package/tsconfig.json +2 -1
- package/.smithery/shttp/manifest.json +0 -73
- package/.smithery/shttp/module.js +0 -270910
- package/.smithery/shttp/module.js.map +0 -7
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -6
- package/coverage/coverage-final.json +0 -1
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -101
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ListResourcesRequestSchema,
|
|
3
|
+
ReadResourceRequestSchema,
|
|
4
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { StateStore } from '../state-store.js';
|
|
6
|
+
|
|
7
|
+
export function registerResourceHandlers(server: any, stateStore: StateStore) {
|
|
8
|
+
// List available resources
|
|
9
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
10
|
+
return {
|
|
11
|
+
resources: [
|
|
12
|
+
{
|
|
13
|
+
uri: 'aiready://project/summary',
|
|
14
|
+
name: 'AIReady Project Summary',
|
|
15
|
+
description: 'Quick top-level AI-readiness summary.',
|
|
16
|
+
mimeType: 'text/markdown',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
uri: 'aiready://project/issues',
|
|
20
|
+
name: 'AIReady Critical Issues',
|
|
21
|
+
description: 'List of top 10 critical readiness issues.',
|
|
22
|
+
mimeType: 'application/json',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
uri: 'aiready://project/graph',
|
|
26
|
+
name: 'AIReady Codebase Graph',
|
|
27
|
+
description: 'Force-directed graph data for visualization.',
|
|
28
|
+
mimeType: 'application/json',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
uri: 'aiready://project/roadmap',
|
|
32
|
+
name: 'AIReady Readiness Roadmap',
|
|
33
|
+
description: 'A prioritized plan to reach elite readiness.',
|
|
34
|
+
mimeType: 'text/markdown',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Read resource content
|
|
41
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request: any) => {
|
|
42
|
+
const { uri } = request.params;
|
|
43
|
+
|
|
44
|
+
if (uri === 'aiready://project/summary') {
|
|
45
|
+
return {
|
|
46
|
+
contents: [
|
|
47
|
+
{
|
|
48
|
+
uri,
|
|
49
|
+
mimeType: 'text/markdown',
|
|
50
|
+
text: stateStore.getSummaryMarkdown(),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (uri === 'aiready://project/issues') {
|
|
57
|
+
return {
|
|
58
|
+
contents: [
|
|
59
|
+
{
|
|
60
|
+
uri,
|
|
61
|
+
mimeType: 'application/json',
|
|
62
|
+
text: stateStore.getIssuesJson(),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (uri === 'aiready://project/graph') {
|
|
69
|
+
return {
|
|
70
|
+
contents: [
|
|
71
|
+
{
|
|
72
|
+
uri,
|
|
73
|
+
mimeType: 'application/json',
|
|
74
|
+
text: stateStore.getGraphJson(),
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (uri === 'aiready://project/roadmap') {
|
|
81
|
+
return {
|
|
82
|
+
contents: [
|
|
83
|
+
{
|
|
84
|
+
uri,
|
|
85
|
+
mimeType: 'text/markdown',
|
|
86
|
+
text: stateStore.getRoadmapMarkdown(),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { UnifiedReport } from '@aiready/core';
|
|
2
|
+
|
|
3
|
+
export class StateStore {
|
|
4
|
+
private lastResults: UnifiedReport | null = null;
|
|
5
|
+
private lastScanTimestamp: string | null = null;
|
|
6
|
+
|
|
7
|
+
updateLastResults(results: UnifiedReport) {
|
|
8
|
+
this.lastResults = results;
|
|
9
|
+
this.lastScanTimestamp = new Date().toISOString();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getLastResults(): UnifiedReport | null {
|
|
13
|
+
return this.lastResults;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getSummaryMarkdown(): string {
|
|
17
|
+
if (!this.lastResults) {
|
|
18
|
+
return '# AIReady Summary\n\nNo scan has been run yet. Run an AIReady scan tool to see results here.';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { score, summary } = this.lastResults;
|
|
22
|
+
const grade = this.calculateGrade(score);
|
|
23
|
+
|
|
24
|
+
return `# AIReady Summary
|
|
25
|
+
|
|
26
|
+
Project Score: **${score}/100 (${grade})**
|
|
27
|
+
Last Scan: ${this.lastScanTimestamp}
|
|
28
|
+
|
|
29
|
+
## Issue Breakdown
|
|
30
|
+
- Critical: ${summary.criticalIssues}
|
|
31
|
+
- Major: ${summary.majorIssues}
|
|
32
|
+
- Total Issues: ${summary.totalIssues}
|
|
33
|
+
- Files Analyzed: ${summary.totalFiles}
|
|
34
|
+
|
|
35
|
+
Run the \`aiready-mcp\` tool for a detailed analysis.`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getIssuesJson(): string {
|
|
39
|
+
if (!this.lastResults) {
|
|
40
|
+
return JSON.stringify({
|
|
41
|
+
message: 'No issues found. Please run a scan first.',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Return top 10 issues
|
|
46
|
+
const topIssues = this.lastResults.issues.slice(0, 10);
|
|
47
|
+
return JSON.stringify(topIssues, null, 2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getGraphJson(): string {
|
|
51
|
+
if (!this.lastResults || !this.lastResults.metadata.graph) {
|
|
52
|
+
return JSON.stringify({
|
|
53
|
+
message:
|
|
54
|
+
'Graph data not available. Run a scan with graph analysis enabled.',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return JSON.stringify(this.lastResults.metadata.graph, null, 2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
getRoadmapMarkdown(): string {
|
|
61
|
+
if (!this.lastResults) {
|
|
62
|
+
return '# AIReady Roadmap\n\nNo scan has been run yet. Run an AIReady scan to generate a prioritized roadmap.';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { score, issues } = this.lastResults;
|
|
66
|
+
const grade = this.calculateGrade(score);
|
|
67
|
+
|
|
68
|
+
let roadmap = `# AIReady Roadmap\n\n`;
|
|
69
|
+
roadmap += `Current Score: **${score}/100 (${grade})**\n\n`;
|
|
70
|
+
|
|
71
|
+
roadmap += `## Phase 1: High-Impact Fixes (Readiness Score 90+)\n`;
|
|
72
|
+
const criticalIssues = issues
|
|
73
|
+
.filter((i: any) => i.severity === 'critical')
|
|
74
|
+
.slice(0, 3);
|
|
75
|
+
if (criticalIssues.length > 0) {
|
|
76
|
+
criticalIssues.forEach((i: any) => {
|
|
77
|
+
roadmap += ` - [ ] **Fix ${i.type}** in \`${i.location.file}:L${i.location.line}\`: ${i.message}\n`;
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
roadmap += ` - ✅ All critical issues resolved!\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
roadmap += `\n## Phase 2: Structural Optimization (Efficiency)\n`;
|
|
84
|
+
const majorIssues = issues
|
|
85
|
+
.filter((i: any) => i.severity === 'major')
|
|
86
|
+
.slice(0, 3);
|
|
87
|
+
if (majorIssues.length > 0) {
|
|
88
|
+
majorIssues.forEach((i: any) => {
|
|
89
|
+
roadmap += ` - [ ] **Address ${i.type}**: ${i.message} (\`${i.location.file}\`)\n`;
|
|
90
|
+
});
|
|
91
|
+
} else {
|
|
92
|
+
roadmap += ` - ✅ All major structural issues resolved!\n`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
roadmap += `\n## Phase 3: Continuous AI Excellence\n`;
|
|
96
|
+
roadmap += ` - [ ] Implement \`mcp-server\` in CI/CD pipeline.\n`;
|
|
97
|
+
roadmap += ` - [ ] Achieve consistency score > 95 across all naming patterns.\n`;
|
|
98
|
+
|
|
99
|
+
return roadmap;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private calculateGrade(score: number): string {
|
|
103
|
+
if (score >= 90) return 'A';
|
|
104
|
+
if (score >= 80) return 'B';
|
|
105
|
+
if (score >= 70) return 'C';
|
|
106
|
+
if (score >= 60) return 'D';
|
|
107
|
+
return 'F';
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const stateStore = new StateStore();
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
// Resolve skills path: go up to src, then to package root, then to monorepo root, then into skills
|
|
9
|
+
// Actually, it's easier to use a relative path if they are in the same monorepo
|
|
10
|
+
const SKILLS_AGENTS_MD_PATH = path.resolve(
|
|
11
|
+
__dirname,
|
|
12
|
+
'../../skills/aiready-best-practices/AGENTS.md'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
export const BestPracticesArgsSchema = z.object({
|
|
16
|
+
category: z
|
|
17
|
+
.string()
|
|
18
|
+
.describe(
|
|
19
|
+
'Category of best practices (e.g., patterns, context, consistency, signal, grounding)'
|
|
20
|
+
),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const ComplianceArgsSchema = z.object({
|
|
24
|
+
file_path: z.string().describe('Absolute path to the file to check'),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export async function handleGetBestPractices(
|
|
28
|
+
args: z.infer<typeof BestPracticesArgsSchema>
|
|
29
|
+
) {
|
|
30
|
+
const { category } = args;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const content = await fs.promises.readFile(SKILLS_AGENTS_MD_PATH, 'utf-8');
|
|
34
|
+
|
|
35
|
+
// Simple parsing to extract the relevant section based on ## <Number>. <Category>
|
|
36
|
+
// We'll search for the heading that starts with the number and contains the category name in parentheses
|
|
37
|
+
// e.g., "## 1. Pattern Detection (patterns)"
|
|
38
|
+
const lines = content.split('\n');
|
|
39
|
+
let inSection = false;
|
|
40
|
+
const sectionContent: string[] = [];
|
|
41
|
+
|
|
42
|
+
for (const line of lines) {
|
|
43
|
+
if (
|
|
44
|
+
line.startsWith('## ') &&
|
|
45
|
+
line.toLowerCase().includes(`(${category.toLowerCase()})`)
|
|
46
|
+
) {
|
|
47
|
+
inSection = true;
|
|
48
|
+
sectionContent.push(line);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
inSection &&
|
|
54
|
+
line.startsWith('## ') &&
|
|
55
|
+
!line.toLowerCase().includes(`(${category.toLowerCase()})`)
|
|
56
|
+
) {
|
|
57
|
+
break; // Next main section
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (inSection) {
|
|
61
|
+
sectionContent.push(line);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (sectionContent.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `Category "${category}" not found in AIReady Best Practices.`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: 'text', text: sectionContent.join('\n') }],
|
|
79
|
+
};
|
|
80
|
+
} catch (error: any) {
|
|
81
|
+
return {
|
|
82
|
+
content: [
|
|
83
|
+
{
|
|
84
|
+
type: 'text',
|
|
85
|
+
text: `Error reading best practices: ${error.message}`,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
isError: true,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function handleCheckCompliance(
|
|
94
|
+
args: z.infer<typeof ComplianceArgsSchema>
|
|
95
|
+
) {
|
|
96
|
+
const { file_path } = args;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const content = await fs.promises.readFile(file_path, 'utf-8');
|
|
100
|
+
const issues: string[] = [];
|
|
101
|
+
|
|
102
|
+
// Lightweight checks based on AGENTS.md rules
|
|
103
|
+
|
|
104
|
+
// 1. File Length (>500 lines)
|
|
105
|
+
const lineCount = content.split('\n').length;
|
|
106
|
+
if (lineCount > 500) {
|
|
107
|
+
issues.push(
|
|
108
|
+
`⚠️ **Context Optimization (2.3)**: File has ${lineCount} lines. Large files waste context. Consider splitting into smaller modules.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. Boolean Trap Parameters
|
|
113
|
+
if (
|
|
114
|
+
content.includes('true, false') ||
|
|
115
|
+
content.includes('false, true') ||
|
|
116
|
+
content.includes('true, true')
|
|
117
|
+
) {
|
|
118
|
+
issues.push(
|
|
119
|
+
`🚩 **AI Signal Clarity (4.1)**: Detected potential positional boolean traps. Prefer named options objects for clarity.`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 3. Magic Literals
|
|
124
|
+
const magicNumberRegex = /[^A-Z_a-z][0-9]{2,}[^A-Z_a-z0-9]/g;
|
|
125
|
+
if (magicNumberRegex.test(content)) {
|
|
126
|
+
issues.push(
|
|
127
|
+
`🚩 **AI Signal Clarity (4.3)**: Detected potential magic literals (raw numbers). Use named constants/enums for business rules.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 4. Entropy
|
|
132
|
+
const entropyRegex = /\b(data|info|handle|obj|item)\b/gi;
|
|
133
|
+
const matches = content.match(entropyRegex);
|
|
134
|
+
if (matches && matches.length > 5) {
|
|
135
|
+
issues.push(
|
|
136
|
+
`🚩 **AI Signal Clarity (4.2)**: High-entropy names detected (${[...new Set(matches.map((m) => m.toLowerCase()))].join(', ')}). Use specific domain names instead.`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (issues.length === 0) {
|
|
141
|
+
return {
|
|
142
|
+
content: [
|
|
143
|
+
{
|
|
144
|
+
type: 'text',
|
|
145
|
+
text: `✅ File "${path.basename(file_path)}" is compliant with lightweight AIReady Best Practices.`,
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
content: [
|
|
153
|
+
{
|
|
154
|
+
type: 'text',
|
|
155
|
+
text: `AIReady Compliance Report for "${path.basename(file_path)}":\n\n${issues.join('\n')}`,
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
return {
|
|
161
|
+
content: [
|
|
162
|
+
{ type: 'text', text: `Error checking compliance: ${error.message}` },
|
|
163
|
+
],
|
|
164
|
+
isError: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export const ContextBudgetArgsSchema = z.object({
|
|
6
|
+
file_path: z.string().describe('Absolute path to the file to analyze'),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export interface TokenEstimation {
|
|
10
|
+
tokens: number;
|
|
11
|
+
budgetPercentage: number;
|
|
12
|
+
tier: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function handleAnalyzeContextBudget(
|
|
16
|
+
args: z.infer<typeof ContextBudgetArgsSchema>
|
|
17
|
+
) {
|
|
18
|
+
const { file_path } = args;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const content = await fs.promises.readFile(file_path, 'utf-8');
|
|
22
|
+
|
|
23
|
+
// Token estimation (approx 4 chars/token)
|
|
24
|
+
const charCount = content.length;
|
|
25
|
+
const tokens = Math.ceil(charCount / 4);
|
|
26
|
+
|
|
27
|
+
// Tiers according to common context windows
|
|
28
|
+
const tiers = [
|
|
29
|
+
{ name: '8k (GPT-4/Turbo)', threshold: 8000 },
|
|
30
|
+
{ name: '32k (GPT-4o/Mini)', threshold: 32000 },
|
|
31
|
+
{ name: '128k (GPT-4o/Claude 3)', threshold: 128000 },
|
|
32
|
+
{ name: '200k (Claude 3.5 Sonnet)', threshold: 200000 },
|
|
33
|
+
{ name: '1M (Gemini 1.5 Pro)', threshold: 1000000 },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
let result = `# Context Budget for "${path.basename(file_path)}"\n\n`;
|
|
37
|
+
result += `**Estimated Tokens:** ${tokens.toLocaleString()}\n\n`;
|
|
38
|
+
|
|
39
|
+
result += `### Context Usage By Tier\n`;
|
|
40
|
+
tiers.forEach((tier) => {
|
|
41
|
+
const percentage = (tokens / tier.threshold) * 100;
|
|
42
|
+
const barLength = Math.min(Math.ceil(percentage / 5), 20);
|
|
43
|
+
const bar =
|
|
44
|
+
'█'.repeat(barLength) + '░'.repeat(Math.max(0, 20 - barLength));
|
|
45
|
+
result += ` - **${tier.name}:** ${bar} ${percentage.toFixed(1)}%\n`;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
result += `\n### Recommendations\n`;
|
|
49
|
+
|
|
50
|
+
if (tokens > 5000) {
|
|
51
|
+
result += `⚠️ **High Context Usage:** This file alone takes up a significant chunk of smaller context windows. Consider refactoring to extract logical sub-modules.\n`;
|
|
52
|
+
} else {
|
|
53
|
+
result += `✅ **Optimal Size:** This file is well-sized for AI context windows.\n`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check for large imports
|
|
57
|
+
const importCount = (content.match(/^import /gm) || []).length;
|
|
58
|
+
if (importCount > 20) {
|
|
59
|
+
result += `⚠️ **High Dependency Load:** ${importCount} imports found. AI will need to load many dependent files, multiplying the context budget needed. Use barrel exports (index.ts) to flatten the dependency tree.\n`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: 'text', text: result }],
|
|
64
|
+
};
|
|
65
|
+
} catch (error: any) {
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: `Error analyzing context budget: ${error.message}`,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
isError: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ToolRegistry, ToolName } from '@aiready/core';
|
|
3
|
+
import {
|
|
4
|
+
handleGetBestPractices,
|
|
5
|
+
handleCheckCompliance,
|
|
6
|
+
BestPracticesArgsSchema,
|
|
7
|
+
ComplianceArgsSchema,
|
|
8
|
+
} from './best-practices.js';
|
|
9
|
+
import {
|
|
10
|
+
handleAnalyzeContextBudget,
|
|
11
|
+
ContextBudgetArgsSchema,
|
|
12
|
+
} from './context-budget.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Zod schemas for tool arguments
|
|
16
|
+
*/
|
|
17
|
+
export const AnalysisArgsSchema = z.object({
|
|
18
|
+
path: z.string().describe('Path to the directory to analyze'),
|
|
19
|
+
summary_only: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe(
|
|
23
|
+
'If true, returns only the summary and skips the detailed issue list. Best for large projects to save context.'
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const RemediationArgsSchema = z.object({
|
|
28
|
+
issue_id: z.string().describe('The unique ID of the issue to fix'),
|
|
29
|
+
file_path: z.string().describe('The path to the file containing the issue'),
|
|
30
|
+
context: z.string().describe('The content of the file or surrounding code'),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
BestPracticesArgsSchema,
|
|
35
|
+
ComplianceArgsSchema,
|
|
36
|
+
ContextBudgetArgsSchema,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mapping between tool names and @aiready/ package names.
|
|
41
|
+
* Used for dynamic registration on-demand to minimize initial context budget.
|
|
42
|
+
*/
|
|
43
|
+
export const TOOL_PACKAGE_MAP: Record<string, string> = {
|
|
44
|
+
[ToolName.PatternDetect]: '@aiready/pattern-detect',
|
|
45
|
+
[ToolName.ContextAnalyzer]: '@aiready/context-analyzer',
|
|
46
|
+
[ToolName.NamingConsistency]: '@aiready/consistency',
|
|
47
|
+
[ToolName.AiSignalClarity]: '@aiready/ai-signal-clarity',
|
|
48
|
+
[ToolName.AgentGrounding]: '@aiready/agent-grounding',
|
|
49
|
+
[ToolName.TestabilityIndex]: '@aiready/testability',
|
|
50
|
+
[ToolName.DocDrift]: '@aiready/doc-drift',
|
|
51
|
+
[ToolName.DependencyHealth]: '@aiready/deps',
|
|
52
|
+
[ToolName.ChangeAmplification]: '@aiready/change-amplification',
|
|
53
|
+
[ToolName.ContractEnforcement]: '@aiready/contract-enforcement',
|
|
54
|
+
// New tools from core
|
|
55
|
+
[ToolName.CognitiveLoad]: '@aiready/cognitive-load',
|
|
56
|
+
[ToolName.PatternEntropy]: '@aiready/pattern-entropy',
|
|
57
|
+
[ToolName.ConceptCohesion]: '@aiready/concept-cohesion',
|
|
58
|
+
[ToolName.SemanticDistance]: '@aiready/semantic-distance',
|
|
59
|
+
// Aliases
|
|
60
|
+
patterns: '@aiready/pattern-detect',
|
|
61
|
+
duplicates: '@aiready/pattern-detect',
|
|
62
|
+
context: '@aiready/context-analyzer',
|
|
63
|
+
fragmentation: '@aiready/context-analyzer',
|
|
64
|
+
consistency: '@aiready/consistency',
|
|
65
|
+
'ai-signal': '@aiready/ai-signal-clarity',
|
|
66
|
+
grounding: '@aiready/agent-grounding',
|
|
67
|
+
testability: '@aiready/testability',
|
|
68
|
+
'deps-health': '@aiready/deps',
|
|
69
|
+
'change-amp': '@aiready/change-amplification',
|
|
70
|
+
'contract-enforce': '@aiready/contract-enforcement',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* List of tools to advertise to the client
|
|
75
|
+
*/
|
|
76
|
+
export const ADVERTISED_TOOLS = [
|
|
77
|
+
ToolName.PatternDetect,
|
|
78
|
+
ToolName.ContextAnalyzer,
|
|
79
|
+
ToolName.NamingConsistency,
|
|
80
|
+
ToolName.AiSignalClarity,
|
|
81
|
+
ToolName.AgentGrounding,
|
|
82
|
+
ToolName.TestabilityIndex,
|
|
83
|
+
ToolName.DocDrift,
|
|
84
|
+
ToolName.DependencyHealth,
|
|
85
|
+
ToolName.ChangeAmplification,
|
|
86
|
+
ToolName.ContractEnforcement,
|
|
87
|
+
ToolName.CognitiveLoad,
|
|
88
|
+
ToolName.PatternEntropy,
|
|
89
|
+
ToolName.ConceptCohesion,
|
|
90
|
+
ToolName.SemanticDistance,
|
|
91
|
+
'get_best_practices',
|
|
92
|
+
'check_best_practice_compliance',
|
|
93
|
+
'analyze_context_budget',
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
handleGetBestPractices,
|
|
98
|
+
handleCheckCompliance,
|
|
99
|
+
handleAnalyzeContextBudget,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export async function handleAnalysis(
|
|
103
|
+
name: string,
|
|
104
|
+
args: any,
|
|
105
|
+
stateStore?: any
|
|
106
|
+
) {
|
|
107
|
+
const parsedArgs = AnalysisArgsSchema.safeParse(args);
|
|
108
|
+
if (!parsedArgs.success) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Invalid arguments for ${name}: ${parsedArgs.error.message}`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const { path: rootDir, summary_only } = parsedArgs.data;
|
|
114
|
+
|
|
115
|
+
let provider = ToolRegistry.find(name);
|
|
116
|
+
|
|
117
|
+
// Dynamic loading if not already registered
|
|
118
|
+
if (!provider) {
|
|
119
|
+
const packageName =
|
|
120
|
+
TOOL_PACKAGE_MAP[name] ??
|
|
121
|
+
(name.startsWith('@aiready/') ? name : `@aiready/${name}`);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
console.error(
|
|
125
|
+
`[MCP] Dynamically loading ${packageName} for tool ${name}`
|
|
126
|
+
);
|
|
127
|
+
await import(packageName);
|
|
128
|
+
provider = ToolRegistry.find(name);
|
|
129
|
+
} catch (importError: unknown) {
|
|
130
|
+
const importErrorMessage =
|
|
131
|
+
importError instanceof Error
|
|
132
|
+
? importError.message
|
|
133
|
+
: String(importError);
|
|
134
|
+
const error = new Error(
|
|
135
|
+
`Tool ${name} not found and failed to load package ${packageName}: ${importErrorMessage}`
|
|
136
|
+
);
|
|
137
|
+
(error as any).cause = importError;
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!provider) {
|
|
143
|
+
throw new Error(`Tool ${name} not found after attempting to load`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.error(
|
|
147
|
+
`[MCP] Executing ${name} on ${rootDir}${summary_only ? ' (summary only)' : ''}`
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const results = await provider.analyze({
|
|
151
|
+
rootDir,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Update state store if provided
|
|
155
|
+
if (stateStore) {
|
|
156
|
+
stateStore.updateLastResults(results);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Format results for the agent
|
|
160
|
+
if (summary_only) {
|
|
161
|
+
const summary = results.summary;
|
|
162
|
+
return {
|
|
163
|
+
summary: `## Issue Breakdown
|
|
164
|
+
- Critical: ${summary.criticalIssues}
|
|
165
|
+
- Major: ${summary.majorIssues}
|
|
166
|
+
- Total Issues: ${summary.totalIssues}
|
|
167
|
+
- Files Analyzed: ${summary.totalFiles}`,
|
|
168
|
+
metadata: results.metadata,
|
|
169
|
+
notice:
|
|
170
|
+
'Detailed issues were omitted (summary_only: true). Run without summary_only for full details.',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return results;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function handleRemediation(
|
|
178
|
+
args: z.infer<typeof RemediationArgsSchema>
|
|
179
|
+
) {
|
|
180
|
+
const apiKey = process.env.AIREADY_API_KEY;
|
|
181
|
+
const serverUrl =
|
|
182
|
+
process.env.AIREADY_PLATFORM_URL || 'https://platform.getaiready.dev';
|
|
183
|
+
|
|
184
|
+
if (!apiKey) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
'AIREADY_API_KEY is not set. Remediation requires an active subscription.'
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.error(`[MCP] Requesting remediation for ${args.issue_id}...`);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetch(`${serverUrl}/api/v1/remediate`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: {
|
|
196
|
+
'Content-Type': 'application/json',
|
|
197
|
+
'X-API-KEY': apiKey,
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
issueId: args.issue_id,
|
|
201
|
+
filePath: args.file_path,
|
|
202
|
+
context: args.context,
|
|
203
|
+
agent: 'mcp-server',
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
if (!response.ok) {
|
|
208
|
+
const errorData = await response.json().catch(() => ({}));
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Platform Error: ${errorData.message || response.statusText}`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const data = await response.json();
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: 'text',
|
|
220
|
+
text: `Recommended Fix (Diff):\n\n${data.diff}\n\nRationale:\n${data.rationale}`,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
};
|
|
224
|
+
} catch (error: unknown) {
|
|
225
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
226
|
+
return {
|
|
227
|
+
content: [
|
|
228
|
+
{
|
|
229
|
+
type: 'text',
|
|
230
|
+
text: `Failed to get remediation: ${errorMessage}. Please visit the dashboard to fix manually.`,
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
isError: true,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|