Claude sub-agents, PRD, MCP improvements (#359)
1. Added Claude Code sub-agents 2. Added PRD tool to MCP Server 3. Added MCP Server UI to Dev Tools 4. Improved MCP Server Database Tool 5. Updated dependencies
This commit is contained in:
committed by
GitHub
parent
02e2502dcc
commit
2b8572baaa
@@ -7,22 +7,21 @@
|
||||
"bin": {
|
||||
"makerkit-mcp-server": "./build/index.js"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
"exports": {
|
||||
"./database": "./src/tools/database.ts",
|
||||
"./components": "./src/tools/components.ts",
|
||||
"./migrations": "./src/tools/migrations.ts",
|
||||
"./prd-manager": "./src/tools/prd-manager.ts",
|
||||
"./prompts": "./src/tools/prompts.ts",
|
||||
"./scripts": "./src/tools/scripts.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
|
||||
"build": "tsc && chmod 755 build/index.js",
|
||||
"build:watch": "tsc --watch",
|
||||
"mcp": "node build/index.js"
|
||||
"build": "tsc",
|
||||
"build:watch": "tsc --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@modelcontextprotocol/sdk": "1.18.1",
|
||||
@@ -30,5 +29,12 @@
|
||||
"postgres": "3.4.7",
|
||||
"zod": "^3.25.74"
|
||||
},
|
||||
"prettier": "@kit/prettier-config"
|
||||
"prettier": "@kit/prettier-config",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
registerDatabaseTools,
|
||||
} from './tools/database';
|
||||
import { registerGetMigrationsTools } from './tools/migrations';
|
||||
import { registerPRDTools } from './tools/prd-manager';
|
||||
import { registerPromptsSystem } from './tools/prompts';
|
||||
import { registerScriptsTools } from './tools/scripts';
|
||||
|
||||
@@ -24,6 +25,7 @@ async function main() {
|
||||
registerDatabaseResources(server);
|
||||
registerComponentsTools(server);
|
||||
registerScriptsTools(server);
|
||||
registerPRDTools(server);
|
||||
registerPromptsSystem(server);
|
||||
|
||||
await server.connect(transport);
|
||||
|
||||
@@ -92,14 +92,25 @@ interface EnumInfo {
|
||||
}
|
||||
|
||||
export class DatabaseTool {
|
||||
private static _ROOT_PATH = process.cwd();
|
||||
|
||||
static get ROOT_PATH(): string {
|
||||
return this._ROOT_PATH;
|
||||
}
|
||||
|
||||
static set ROOT_PATH(path: string) {
|
||||
this._ROOT_PATH = path;
|
||||
}
|
||||
|
||||
static async getSchemaFiles(): Promise<SchemaFile[]> {
|
||||
const schemasPath = join(
|
||||
process.cwd(),
|
||||
DatabaseTool.ROOT_PATH,
|
||||
'apps',
|
||||
'web',
|
||||
'supabase',
|
||||
'schemas',
|
||||
);
|
||||
|
||||
const files = await readdir(schemasPath);
|
||||
|
||||
const schemaFiles: SchemaFile[] = [];
|
||||
@@ -113,10 +124,10 @@ export class DatabaseTool {
|
||||
const sectionMatch = content.match(/\* Section: ([^\n*]+)/);
|
||||
const descriptionMatch = content.match(/\* ([^*\n]+)\n \* We create/);
|
||||
|
||||
// Extract tables and functions from content
|
||||
const tables = this.extractTables(content);
|
||||
const functions = this.extractFunctionNames(content);
|
||||
const dependencies = this.extractDependencies(content);
|
||||
// Extract tables and functions from content using simple regex (for schema file metadata only)
|
||||
const tables = this.extractTablesRegex(content);
|
||||
const functions = this.extractFunctionNamesRegex(content);
|
||||
const dependencies = this.extractDependenciesRegex(content);
|
||||
const topic = this.determineTopic(file, content);
|
||||
|
||||
schemaFiles.push({
|
||||
@@ -137,6 +148,54 @@ export class DatabaseTool {
|
||||
}
|
||||
|
||||
static async getFunctions(): Promise<DatabaseFunction[]> {
|
||||
try {
|
||||
// Query the database directly for function information
|
||||
const functions = await sql`
|
||||
SELECT
|
||||
p.proname as function_name,
|
||||
n.nspname as schema_name,
|
||||
pg_get_function_result(p.oid) as return_type,
|
||||
pg_get_function_arguments(p.oid) as parameters,
|
||||
CASE p.prosecdef WHEN true THEN 'definer' ELSE 'invoker' END as security_level,
|
||||
l.lanname as language,
|
||||
obj_description(p.oid, 'pg_proc') as description
|
||||
FROM pg_proc p
|
||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
||||
LEFT JOIN pg_language l ON p.prolang = l.oid
|
||||
WHERE n.nspname IN ('public', 'kit')
|
||||
AND p.prokind = 'f' -- Only functions, not procedures
|
||||
ORDER BY n.nspname, p.proname
|
||||
`;
|
||||
|
||||
// Get schema files to map functions to source files
|
||||
const schemaFiles = await this.getSchemaFiles();
|
||||
const fileMapping = this.createFunctionFileMapping(schemaFiles);
|
||||
|
||||
return functions.map((func) => ({
|
||||
name: func.function_name,
|
||||
schema: func.schema_name,
|
||||
returnType: func.return_type || 'unknown',
|
||||
parameters: this.parsePostgresParameters(func.parameters || ''),
|
||||
securityLevel: func.security_level as 'definer' | 'invoker',
|
||||
description: func.description || 'No description available',
|
||||
purpose: this.extractPurpose(
|
||||
func.description || '',
|
||||
func.function_name,
|
||||
),
|
||||
sourceFile:
|
||||
fileMapping[`${func.schema_name}.${func.function_name}`] || 'unknown',
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error querying database functions, falling back to file parsing:',
|
||||
error.message,
|
||||
);
|
||||
// Fallback to file-based extraction if database query fails
|
||||
return this.getFunctionsFromFiles();
|
||||
}
|
||||
}
|
||||
|
||||
private static async getFunctionsFromFiles(): Promise<DatabaseFunction[]> {
|
||||
const schemaFiles = await this.getSchemaFiles();
|
||||
const functions: DatabaseFunction[] = [];
|
||||
|
||||
@@ -152,6 +211,70 @@ export class DatabaseTool {
|
||||
return functions.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
private static createFunctionFileMapping(
|
||||
schemaFiles: SchemaFile[],
|
||||
): Record<string, string> {
|
||||
const mapping: Record<string, string> = {};
|
||||
|
||||
for (const file of schemaFiles) {
|
||||
for (const functionName of file.functions) {
|
||||
// Map both public.functionName and functionName to the file
|
||||
mapping[`public.${functionName}`] = file.name;
|
||||
mapping[`kit.${functionName}`] = file.name;
|
||||
mapping[functionName] = file.name;
|
||||
}
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private static parsePostgresParameters(paramString: string): Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
defaultValue?: string;
|
||||
}> {
|
||||
if (!paramString.trim()) return [];
|
||||
|
||||
const parameters: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
defaultValue?: string;
|
||||
}> = [];
|
||||
|
||||
// PostgreSQL function arguments format: "name type, name type DEFAULT value"
|
||||
const params = paramString
|
||||
.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p);
|
||||
|
||||
for (const param of params) {
|
||||
// Match pattern: "name type" or "name type DEFAULT value"
|
||||
const match = param.match(
|
||||
/^(?:(?:IN|OUT|INOUT)\s+)?([a-zA-Z_][a-zA-Z0-9_]*)\s+([^=\s]+)(?:\s+DEFAULT\s+(.+))?$/i,
|
||||
);
|
||||
|
||||
if (match) {
|
||||
const [, name, type, defaultValue] = match;
|
||||
parameters.push({
|
||||
name: name.trim(),
|
||||
type: type.trim(),
|
||||
defaultValue: defaultValue?.trim(),
|
||||
});
|
||||
} else if (param.includes(' ')) {
|
||||
// Fallback for unnamed parameters
|
||||
const parts = param.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
parameters.push({
|
||||
name: parts[0] || 'unnamed',
|
||||
type: parts.slice(1).join(' ').trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
static async getFunctionDetails(
|
||||
functionName: string,
|
||||
): Promise<DatabaseFunction> {
|
||||
@@ -226,12 +349,13 @@ export class DatabaseTool {
|
||||
|
||||
static async getSchemaContent(fileName: string): Promise<string> {
|
||||
const schemasPath = join(
|
||||
process.cwd(),
|
||||
DatabaseTool.ROOT_PATH,
|
||||
'apps',
|
||||
'web',
|
||||
'supabase',
|
||||
'schemas',
|
||||
);
|
||||
|
||||
const filePath = join(schemasPath, fileName);
|
||||
|
||||
try {
|
||||
@@ -265,24 +389,61 @@ export class DatabaseTool {
|
||||
}
|
||||
|
||||
static async getAllProjectTables(): Promise<ProjectTable[]> {
|
||||
// Query database directly for table information
|
||||
const tables = await sql`
|
||||
SELECT
|
||||
t.table_name,
|
||||
t.table_schema,
|
||||
obj_description(c.oid, 'pg_class') as description
|
||||
FROM information_schema.tables t
|
||||
LEFT JOIN pg_class c ON c.relname = t.table_name
|
||||
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace AND n.nspname = t.table_schema
|
||||
WHERE t.table_schema IN ('public', 'kit')
|
||||
AND t.table_type = 'BASE TABLE'
|
||||
ORDER BY t.table_schema, t.table_name
|
||||
`;
|
||||
|
||||
// Get schema files to map tables to source files
|
||||
const schemaFiles = await this.getSchemaFiles();
|
||||
const tables: ProjectTable[] = [];
|
||||
const fileMapping = this.createTableFileMapping(schemaFiles);
|
||||
|
||||
return tables.map((table: any) => ({
|
||||
name: table.table_name,
|
||||
schema: table.table_schema,
|
||||
sourceFile:
|
||||
fileMapping[`${table.table_schema}.${table.table_name}`] ||
|
||||
fileMapping[table.table_name] ||
|
||||
'database',
|
||||
topic: this.getTableTopic(table.table_name, schemaFiles),
|
||||
}));
|
||||
}
|
||||
|
||||
private static createTableFileMapping(
|
||||
schemaFiles: SchemaFile[],
|
||||
): Record<string, string> {
|
||||
const mapping: Record<string, string> = {};
|
||||
|
||||
for (const file of schemaFiles) {
|
||||
const content = await readFile(file.path, 'utf8');
|
||||
const extractedTables = this.extractTablesWithSchema(content);
|
||||
|
||||
for (const table of extractedTables) {
|
||||
tables.push({
|
||||
name: table.name,
|
||||
schema: table.schema || 'public',
|
||||
sourceFile: file.name,
|
||||
topic: file.topic,
|
||||
});
|
||||
for (const tableName of file.tables) {
|
||||
mapping[`public.${tableName}`] = file.name;
|
||||
mapping[`kit.${tableName}`] = file.name;
|
||||
mapping[tableName] = file.name;
|
||||
}
|
||||
}
|
||||
|
||||
return tables;
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private static getTableTopic(
|
||||
tableName: string,
|
||||
schemaFiles: SchemaFile[],
|
||||
): string {
|
||||
for (const file of schemaFiles) {
|
||||
if (file.tables.includes(tableName)) {
|
||||
return file.topic;
|
||||
}
|
||||
}
|
||||
return 'general';
|
||||
}
|
||||
|
||||
static async getAllEnums(): Promise<Record<string, EnumInfo>> {
|
||||
@@ -675,78 +836,59 @@ export class DatabaseTool {
|
||||
return `Custom database function: ${description}`;
|
||||
}
|
||||
|
||||
private static extractTables(content: string): string[] {
|
||||
const tables: string[] = [];
|
||||
const tableRegex =
|
||||
/create\s+table\s+(?:if\s+not\s+exists\s+)?(?:public\.)?([a-zA-Z_][a-zA-Z0-9_]*)/gi;
|
||||
let match;
|
||||
|
||||
while ((match = tableRegex.exec(content)) !== null) {
|
||||
if (match[1]) {
|
||||
tables.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(tables)]; // Remove duplicates
|
||||
}
|
||||
|
||||
private static extractTablesWithSchema(content: string): Array<{
|
||||
name: string;
|
||||
schema: string;
|
||||
}> {
|
||||
const tables: Array<{ name: string; schema: string }> = [];
|
||||
const tableRegex =
|
||||
/create\s+table\s+(?:if\s+not\s+exists\s+)?(?:([a-zA-Z_][a-zA-Z0-9_]*)\.)?([a-zA-Z_][a-zA-Z0-9_]*)/gi;
|
||||
let match;
|
||||
|
||||
while ((match = tableRegex.exec(content)) !== null) {
|
||||
if (match[2]) {
|
||||
tables.push({
|
||||
schema: match[1] || 'public',
|
||||
name: match[2],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tables.filter(
|
||||
(table, index, arr) =>
|
||||
arr.findIndex(
|
||||
(t) => t.name === table.name && t.schema === table.schema,
|
||||
) === index,
|
||||
// Fallback regex methods (simplified and more reliable)
|
||||
private static extractTablesRegex(content: string): string[] {
|
||||
const tableMatches = content.match(
|
||||
/create\s+table\s+(?:if\s+not\s+exists\s+)?(?:public\.)?([a-zA-Z_][a-zA-Z0-9_]*)/gi,
|
||||
);
|
||||
if (!tableMatches) return [];
|
||||
|
||||
return [
|
||||
...new Set(
|
||||
tableMatches
|
||||
.map((match) => {
|
||||
const nameMatch = match.match(/([a-zA-Z_][a-zA-Z0-9_]*)$/i);
|
||||
return nameMatch ? nameMatch[1] : '';
|
||||
})
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private static extractFunctionNames(content: string): string[] {
|
||||
const functions: string[] = [];
|
||||
const functionRegex =
|
||||
/create\s+(?:or\s+replace\s+)?function\s+(?:public\.)?([a-zA-Z_][a-zA-Z0-9_]*)/gi;
|
||||
let match;
|
||||
private static extractFunctionNamesRegex(content: string): string[] {
|
||||
const functionMatches = content.match(
|
||||
/create\s+(?:or\s+replace\s+)?function\s+(?:public\.)?([a-zA-Z_][a-zA-Z0-9_]*)/gi,
|
||||
);
|
||||
if (!functionMatches) return [];
|
||||
|
||||
while ((match = functionRegex.exec(content)) !== null) {
|
||||
if (match[1]) {
|
||||
functions.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(functions)]; // Remove duplicates
|
||||
return [
|
||||
...new Set(
|
||||
functionMatches
|
||||
.map((match) => {
|
||||
const nameMatch = match.match(/([a-zA-Z_][a-zA-Z0-9_]*)$/i);
|
||||
return nameMatch ? nameMatch[1] : '';
|
||||
})
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private static extractDependencies(content: string): string[] {
|
||||
const dependencies: string[] = [];
|
||||
private static extractDependenciesRegex(content: string): string[] {
|
||||
const refMatches = content.match(
|
||||
/references\s+(?:public\.)?([a-zA-Z_][a-zA-Z0-9_]*)/gi,
|
||||
);
|
||||
if (!refMatches) return [];
|
||||
|
||||
// Look for references to other tables
|
||||
const referencesRegex =
|
||||
/references\s+(?:public\.)?([a-zA-Z_][a-zA-Z0-9_]*)/gi;
|
||||
let match;
|
||||
|
||||
while ((match = referencesRegex.exec(content)) !== null) {
|
||||
if (match[1] && match[1] !== 'users') {
|
||||
// Exclude auth.users as it's external
|
||||
dependencies.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(dependencies)]; // Remove duplicates
|
||||
return [
|
||||
...new Set(
|
||||
refMatches
|
||||
.map((match) => {
|
||||
const nameMatch = match.match(/([a-zA-Z_][a-zA-Z0-9_]*)$/i);
|
||||
return nameMatch && nameMatch[1] !== 'users' ? nameMatch[1] : '';
|
||||
})
|
||||
.filter(Boolean),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private static extractTableDefinition(
|
||||
|
||||
@@ -20,11 +20,11 @@ export class MigrationsTool {
|
||||
}
|
||||
|
||||
static CreateMigration(name: string) {
|
||||
return promisify(exec)(`pnpm --filter web supabase migration new ${name}`);
|
||||
return promisify(exec)(`pnpm --filter web supabase migrations new ${name}`);
|
||||
}
|
||||
|
||||
static Diff() {
|
||||
return promisify(exec)(`supabase migration diff`);
|
||||
return promisify(exec)(`supabase db diff`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
985
packages/mcp-server/src/tools/prd-manager.ts
Normal file
985
packages/mcp-server/src/tools/prd-manager.ts
Normal file
@@ -0,0 +1,985 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { z } from 'zod';
|
||||
|
||||
// Custom phase for organizing user stories
|
||||
interface CustomPhase {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string; // Tailwind color class
|
||||
order: number;
|
||||
userStoryIds: string[];
|
||||
}
|
||||
|
||||
// Business-focused user story following ChatPRD best practices
|
||||
interface UserStory {
|
||||
id: string;
|
||||
title: string;
|
||||
userStory: string;
|
||||
businessValue: string;
|
||||
acceptanceCriteria: string[];
|
||||
status:
|
||||
| 'not_started'
|
||||
| 'research'
|
||||
| 'in_progress'
|
||||
| 'review'
|
||||
| 'completed'
|
||||
| 'blocked';
|
||||
priority: 'P0' | 'P1' | 'P2' | 'P3';
|
||||
estimatedComplexity: 'XS' | 'S' | 'M' | 'L' | 'XL';
|
||||
dependencies: string[];
|
||||
notes?: string;
|
||||
completedAt?: string;
|
||||
}
|
||||
|
||||
// Structured PRD following ChatPRD format
|
||||
interface StructuredPRD {
|
||||
introduction: {
|
||||
title: string;
|
||||
overview: string;
|
||||
lastUpdated: string;
|
||||
};
|
||||
|
||||
problemStatement: {
|
||||
problem: string;
|
||||
marketOpportunity: string;
|
||||
targetUsers: string[];
|
||||
};
|
||||
|
||||
solutionOverview: {
|
||||
description: string;
|
||||
keyFeatures: string[];
|
||||
successMetrics: string[];
|
||||
};
|
||||
|
||||
userStories: UserStory[];
|
||||
customPhases?: CustomPhase[];
|
||||
|
||||
technicalRequirements: {
|
||||
constraints: string[];
|
||||
integrationNeeds: string[];
|
||||
complianceRequirements: string[];
|
||||
};
|
||||
|
||||
acceptanceCriteria: {
|
||||
global: string[];
|
||||
qualityStandards: string[];
|
||||
};
|
||||
|
||||
constraints: {
|
||||
timeline: string;
|
||||
budget?: string;
|
||||
resources: string[];
|
||||
nonNegotiables: string[];
|
||||
};
|
||||
|
||||
metadata: {
|
||||
version: string;
|
||||
created: string;
|
||||
lastUpdated: string;
|
||||
approver: string;
|
||||
};
|
||||
|
||||
progress: {
|
||||
overall: number;
|
||||
completed: number;
|
||||
total: number;
|
||||
blocked: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class PRDManager {
|
||||
private static ROOT_PATH = process.cwd();
|
||||
|
||||
private static get PRDS_DIR() {
|
||||
return join(this.ROOT_PATH, '.prds');
|
||||
}
|
||||
|
||||
static setRootPath(path: string) {
|
||||
this.ROOT_PATH = path;
|
||||
}
|
||||
|
||||
static async ensurePRDsDirectory(): Promise<void> {
|
||||
try {
|
||||
await mkdir(this.PRDS_DIR, { recursive: true });
|
||||
} catch {
|
||||
// Directory exists
|
||||
}
|
||||
}
|
||||
|
||||
static async createStructuredPRD(
|
||||
title: string,
|
||||
overview: string,
|
||||
problemStatement: string,
|
||||
marketOpportunity: string,
|
||||
targetUsers: string[],
|
||||
solutionDescription: string,
|
||||
keyFeatures: string[],
|
||||
successMetrics: string[],
|
||||
): Promise<string> {
|
||||
await this.ensurePRDsDirectory();
|
||||
|
||||
const filename = `${title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}.json`;
|
||||
const now = new Date().toISOString().split('T')[0];
|
||||
|
||||
const prd: StructuredPRD = {
|
||||
introduction: {
|
||||
title,
|
||||
overview,
|
||||
lastUpdated: now,
|
||||
},
|
||||
problemStatement: {
|
||||
problem: problemStatement,
|
||||
marketOpportunity,
|
||||
targetUsers,
|
||||
},
|
||||
solutionOverview: {
|
||||
description: solutionDescription,
|
||||
keyFeatures,
|
||||
successMetrics,
|
||||
},
|
||||
userStories: [],
|
||||
technicalRequirements: {
|
||||
constraints: [],
|
||||
integrationNeeds: [],
|
||||
complianceRequirements: [],
|
||||
},
|
||||
acceptanceCriteria: {
|
||||
global: [],
|
||||
qualityStandards: [],
|
||||
},
|
||||
constraints: {
|
||||
timeline: '',
|
||||
resources: [],
|
||||
nonNegotiables: [],
|
||||
},
|
||||
metadata: {
|
||||
version: '1.0',
|
||||
created: now,
|
||||
lastUpdated: now,
|
||||
approver: '',
|
||||
},
|
||||
progress: {
|
||||
overall: 0,
|
||||
completed: 0,
|
||||
total: 0,
|
||||
blocked: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const filePath = join(this.PRDS_DIR, filename);
|
||||
await writeFile(filePath, JSON.stringify(prd, null, 2), 'utf8');
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
static async addUserStory(
|
||||
filename: string,
|
||||
userType: string,
|
||||
action: string,
|
||||
benefit: string,
|
||||
acceptanceCriteria: string[],
|
||||
priority: UserStory['priority'] = 'P2',
|
||||
): Promise<string> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
|
||||
const userStory = `As a ${userType}, I want to ${action} so that ${benefit}`;
|
||||
const title = this.extractTitleFromAction(action);
|
||||
const complexity = this.assessComplexity(acceptanceCriteria);
|
||||
|
||||
const storyNumber = prd.userStories.length + 1;
|
||||
const storyId = `US${storyNumber.toString().padStart(3, '0')}`;
|
||||
|
||||
const newStory: UserStory = {
|
||||
id: storyId,
|
||||
title,
|
||||
userStory,
|
||||
businessValue: benefit,
|
||||
acceptanceCriteria,
|
||||
status: 'not_started',
|
||||
priority,
|
||||
estimatedComplexity: complexity,
|
||||
dependencies: [],
|
||||
};
|
||||
|
||||
prd.userStories.push(newStory);
|
||||
this.updateProgress(prd);
|
||||
|
||||
await this.savePRD(filename, prd);
|
||||
|
||||
return `User story ${storyId} added: "${title}"`;
|
||||
}
|
||||
|
||||
static async updateStoryStatus(
|
||||
filename: string,
|
||||
storyId: string,
|
||||
status: UserStory['status'],
|
||||
notes?: string,
|
||||
): Promise<string> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
const story = prd.userStories.find((s) => s.id === storyId);
|
||||
|
||||
if (!story) {
|
||||
throw new Error(`Story ${storyId} not found`);
|
||||
}
|
||||
|
||||
story.status = status;
|
||||
if (notes) {
|
||||
story.notes = notes;
|
||||
}
|
||||
if (status === 'completed') {
|
||||
story.completedAt = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
this.updateProgress(prd);
|
||||
await this.savePRD(filename, prd);
|
||||
|
||||
return `Story "${story.title}" updated to ${status}. Progress: ${prd.progress.overall}%`;
|
||||
}
|
||||
|
||||
static async exportAsMarkdown(filename: string): Promise<string> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
const content = this.formatPRDMarkdown(prd);
|
||||
|
||||
const markdownFile = filename.replace('.json', '.md');
|
||||
const markdownPath = join(this.PRDS_DIR, markdownFile);
|
||||
await writeFile(markdownPath, content, 'utf8');
|
||||
|
||||
return markdownFile;
|
||||
}
|
||||
|
||||
static async generateImplementationPrompts(
|
||||
filename: string,
|
||||
): Promise<string[]> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
const prompts: string[] = [];
|
||||
|
||||
prompts.push(
|
||||
`Implement "${prd.introduction.title}" based on the PRD. ` +
|
||||
`Goal: ${prd.solutionOverview.description}. ` +
|
||||
`Key features: ${prd.solutionOverview.keyFeatures.join(', ')}. ` +
|
||||
`You must research and decide all technical implementation details.`,
|
||||
);
|
||||
|
||||
const readyStories = prd.userStories.filter(
|
||||
(s) => s.status === 'not_started',
|
||||
);
|
||||
readyStories.slice(0, 3).forEach((story) => {
|
||||
prompts.push(
|
||||
`Implement ${story.id}: "${story.userStory}". ` +
|
||||
`Business value: ${story.businessValue}. ` +
|
||||
`Acceptance criteria: ${story.acceptanceCriteria.join(' | ')}. ` +
|
||||
`Research technical approach and implement.`,
|
||||
);
|
||||
});
|
||||
|
||||
return prompts;
|
||||
}
|
||||
|
||||
static async getImprovementSuggestions(filename: string): Promise<string[]> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
const suggestions: string[] = [];
|
||||
|
||||
if (prd.userStories.length === 0) {
|
||||
suggestions.push('Add user stories to define specific functionality');
|
||||
}
|
||||
|
||||
if (prd.solutionOverview.successMetrics.length === 0) {
|
||||
suggestions.push('Define success metrics to measure progress');
|
||||
}
|
||||
|
||||
if (prd.acceptanceCriteria.global.length === 0) {
|
||||
suggestions.push('Add global acceptance criteria for quality standards');
|
||||
}
|
||||
|
||||
const vagueStories = prd.userStories.filter(
|
||||
(s) => s.acceptanceCriteria.length < 2,
|
||||
);
|
||||
if (vagueStories.length > 0) {
|
||||
suggestions.push(
|
||||
`${vagueStories.length} stories need more detailed acceptance criteria`,
|
||||
);
|
||||
}
|
||||
|
||||
const blockedStories = prd.userStories.filter(
|
||||
(s) => s.status === 'blocked',
|
||||
);
|
||||
if (blockedStories.length > 0) {
|
||||
suggestions.push(
|
||||
`${blockedStories.length} stories are blocked and need attention`,
|
||||
);
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
static async listPRDs(): Promise<string[]> {
|
||||
await this.ensurePRDsDirectory();
|
||||
|
||||
try {
|
||||
const files = await readdir(this.PRDS_DIR);
|
||||
|
||||
return files.filter((file) => file.endsWith('.json'));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async getPRDContent(filename: string): Promise<string> {
|
||||
const filePath = join(this.PRDS_DIR, filename);
|
||||
try {
|
||||
return await readFile(filePath, 'utf8');
|
||||
} catch {
|
||||
throw new Error(`PRD file "${filename}" not found`);
|
||||
}
|
||||
}
|
||||
|
||||
static async getProjectStatus(filename: string): Promise<{
|
||||
progress: number;
|
||||
summary: string;
|
||||
nextSteps: string[];
|
||||
blockers: UserStory[];
|
||||
}> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
|
||||
const blockers = prd.userStories.filter((s) => s.status === 'blocked');
|
||||
const inProgress = prd.userStories.filter(
|
||||
(s) => s.status === 'in_progress',
|
||||
);
|
||||
const nextPending = prd.userStories
|
||||
.filter((s) => s.status === 'not_started')
|
||||
.slice(0, 3);
|
||||
|
||||
const nextSteps = [
|
||||
...inProgress.map((s) => `Continue: ${s.title}`),
|
||||
...nextPending.map((s) => `Start: ${s.title}`),
|
||||
];
|
||||
|
||||
const summary = `${prd.progress.completed}/${prd.progress.total} stories completed (${prd.progress.overall}%). Total stories: ${prd.userStories.length}`;
|
||||
|
||||
return {
|
||||
progress: prd.progress.overall,
|
||||
summary,
|
||||
nextSteps,
|
||||
blockers,
|
||||
};
|
||||
}
|
||||
|
||||
// Custom Phase Management
|
||||
static async createCustomPhase(
|
||||
filename: string,
|
||||
name: string,
|
||||
description: string,
|
||||
color: string,
|
||||
): Promise<string> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
|
||||
// Initialize customPhases if it doesn't exist
|
||||
if (!prd.customPhases) {
|
||||
prd.customPhases = [];
|
||||
}
|
||||
|
||||
// Check for unique name
|
||||
if (prd.customPhases.some((p) => p.name === name)) {
|
||||
throw new Error(`Phase with name "${name}" already exists`);
|
||||
}
|
||||
|
||||
const phaseId = `PHASE${(prd.customPhases.length + 1).toString().padStart(3, '0')}`;
|
||||
const order = prd.customPhases.length;
|
||||
|
||||
const newPhase: CustomPhase = {
|
||||
id: phaseId,
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
order,
|
||||
userStoryIds: [],
|
||||
};
|
||||
|
||||
prd.customPhases.push(newPhase);
|
||||
await this.savePRD(filename, prd);
|
||||
|
||||
return `Custom phase "${name}" created with ID ${phaseId}`;
|
||||
}
|
||||
|
||||
static async updateCustomPhase(
|
||||
filename: string,
|
||||
phaseId: string,
|
||||
updates: Partial<Pick<CustomPhase, 'name' | 'description' | 'color'>>,
|
||||
): Promise<string> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
|
||||
if (!prd.customPhases) {
|
||||
throw new Error('No custom phases found in this PRD');
|
||||
}
|
||||
|
||||
const phase = prd.customPhases.find((p) => p.id === phaseId);
|
||||
if (!phase) {
|
||||
throw new Error(`Phase ${phaseId} not found`);
|
||||
}
|
||||
|
||||
// Check for unique name if updating name
|
||||
if (updates.name && updates.name !== phase.name) {
|
||||
if (
|
||||
prd.customPhases.some(
|
||||
(p) => p.name === updates.name && p.id !== phaseId,
|
||||
)
|
||||
) {
|
||||
throw new Error(`Phase with name "${updates.name}" already exists`);
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(phase, updates);
|
||||
await this.savePRD(filename, prd);
|
||||
|
||||
return `Phase "${phase.name}" updated successfully`;
|
||||
}
|
||||
|
||||
static async deleteCustomPhase(
|
||||
filename: string,
|
||||
phaseId: string,
|
||||
reassignToPhaseId?: string,
|
||||
): Promise<string> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
|
||||
if (!prd.customPhases) {
|
||||
throw new Error('No custom phases found in this PRD');
|
||||
}
|
||||
|
||||
const phaseIndex = prd.customPhases.findIndex((p) => p.id === phaseId);
|
||||
if (phaseIndex === -1) {
|
||||
throw new Error(`Phase ${phaseId} not found`);
|
||||
}
|
||||
|
||||
const phase = prd.customPhases[phaseIndex];
|
||||
|
||||
// Handle story reassignment
|
||||
if (phase.userStoryIds.length > 0) {
|
||||
if (reassignToPhaseId) {
|
||||
const targetPhase = prd.customPhases.find(
|
||||
(p) => p.id === reassignToPhaseId,
|
||||
);
|
||||
if (!targetPhase) {
|
||||
throw new Error(
|
||||
`Target phase ${reassignToPhaseId} not found for reassignment`,
|
||||
);
|
||||
}
|
||||
targetPhase.userStoryIds.push(...phase.userStoryIds);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Phase "${phase.name}" contains ${phase.userStoryIds.length} user stories. Provide reassignToPhaseId or move stories first.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
prd.customPhases.splice(phaseIndex, 1);
|
||||
await this.savePRD(filename, prd);
|
||||
|
||||
return `Phase "${phase.name}" deleted successfully`;
|
||||
}
|
||||
|
||||
static async assignStoryToPhase(
|
||||
filename: string,
|
||||
storyId: string,
|
||||
phaseId: string,
|
||||
): Promise<string> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
|
||||
if (!prd.customPhases) {
|
||||
throw new Error('No custom phases found in this PRD');
|
||||
}
|
||||
|
||||
const story = prd.userStories.find((s) => s.id === storyId);
|
||||
if (!story) {
|
||||
throw new Error(`Story ${storyId} not found`);
|
||||
}
|
||||
|
||||
const targetPhase = prd.customPhases.find((p) => p.id === phaseId);
|
||||
if (!targetPhase) {
|
||||
throw new Error(`Phase ${phaseId} not found`);
|
||||
}
|
||||
|
||||
// Remove story from all phases first
|
||||
prd.customPhases.forEach((phase) => {
|
||||
phase.userStoryIds = phase.userStoryIds.filter((id) => id !== storyId);
|
||||
});
|
||||
|
||||
// Add to target phase
|
||||
if (!targetPhase.userStoryIds.includes(storyId)) {
|
||||
targetPhase.userStoryIds.push(storyId);
|
||||
}
|
||||
|
||||
await this.savePRD(filename, prd);
|
||||
|
||||
return `Story "${story.title}" assigned to phase "${targetPhase.name}"`;
|
||||
}
|
||||
|
||||
static async getCustomPhases(filename: string): Promise<CustomPhase[]> {
|
||||
const prd = await this.loadPRD(filename);
|
||||
return prd.customPhases || [];
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private static async loadPRD(filename: string): Promise<StructuredPRD> {
|
||||
const filePath = join(this.PRDS_DIR, filename);
|
||||
try {
|
||||
const content = await readFile(filePath, 'utf8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
throw new Error(`PRD file "${filename}" not found`);
|
||||
}
|
||||
}
|
||||
|
||||
private static async savePRD(
|
||||
filename: string,
|
||||
prd: StructuredPRD,
|
||||
): Promise<void> {
|
||||
prd.metadata.lastUpdated = new Date().toISOString().split('T')[0];
|
||||
const filePath = join(this.PRDS_DIR, filename);
|
||||
await writeFile(filePath, JSON.stringify(prd, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
private static extractTitleFromAction(action: string): string {
|
||||
const cleaned = action.trim().toLowerCase();
|
||||
const words = cleaned.split(' ').slice(0, 4);
|
||||
return words
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
private static assessComplexity(
|
||||
criteria: string[],
|
||||
): UserStory['estimatedComplexity'] {
|
||||
const count = criteria.length;
|
||||
if (count <= 2) return 'XS';
|
||||
if (count <= 3) return 'S';
|
||||
if (count <= 5) return 'M';
|
||||
if (count <= 8) return 'L';
|
||||
return 'XL';
|
||||
}
|
||||
|
||||
private static updateProgress(prd: StructuredPRD): void {
|
||||
const completed = prd.userStories.filter(
|
||||
(s) => s.status === 'completed',
|
||||
).length;
|
||||
const blocked = prd.userStories.filter(
|
||||
(s) => s.status === 'blocked',
|
||||
).length;
|
||||
|
||||
prd.progress.completed = completed;
|
||||
prd.progress.total = prd.userStories.length;
|
||||
prd.progress.blocked = blocked;
|
||||
prd.progress.overall =
|
||||
prd.userStories.length > 0
|
||||
? Math.round((completed / prd.userStories.length) * 100)
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static formatPRDMarkdown(prd: StructuredPRD): string {
|
||||
let content = `# ${prd.introduction.title}\n\n`;
|
||||
|
||||
content += `## Introduction\n\n`;
|
||||
content += `${prd.introduction.overview}\n\n`;
|
||||
content += `**Last Updated:** ${prd.introduction.lastUpdated}\n`;
|
||||
content += `**Version:** ${prd.metadata.version}\n\n`;
|
||||
|
||||
content += `## Problem Statement\n\n`;
|
||||
content += `${prd.problemStatement.problem}\n\n`;
|
||||
content += `### Market Opportunity\n${prd.problemStatement.marketOpportunity}\n\n`;
|
||||
content += `### Target Users\n`;
|
||||
prd.problemStatement.targetUsers.forEach((user) => {
|
||||
content += `- ${user}\n`;
|
||||
});
|
||||
|
||||
content += `\n## Solution Overview\n\n`;
|
||||
content += `${prd.solutionOverview.description}\n\n`;
|
||||
content += `### Key Features\n`;
|
||||
prd.solutionOverview.keyFeatures.forEach((feature) => {
|
||||
content += `- ${feature}\n`;
|
||||
});
|
||||
content += `\n### Success Metrics\n`;
|
||||
prd.solutionOverview.successMetrics.forEach((metric) => {
|
||||
content += `- ${metric}\n`;
|
||||
});
|
||||
|
||||
content += `\n## User Stories\n\n`;
|
||||
|
||||
const priorities: UserStory['priority'][] = ['P0', 'P1', 'P2', 'P3'];
|
||||
priorities.forEach((priority) => {
|
||||
const stories = prd.userStories.filter((s) => s.priority === priority);
|
||||
if (stories.length > 0) {
|
||||
content += `### Priority ${priority}\n\n`;
|
||||
|
||||
stories.forEach((story) => {
|
||||
const statusIcon = this.getStatusIcon(story.status);
|
||||
content += `#### ${story.id}: ${story.title} ${statusIcon} [${story.estimatedComplexity}]\n\n`;
|
||||
content += `**User Story:** ${story.userStory}\n\n`;
|
||||
content += `**Business Value:** ${story.businessValue}\n\n`;
|
||||
content += `**Acceptance Criteria:**\n`;
|
||||
story.acceptanceCriteria.forEach((criterion) => {
|
||||
content += `- ${criterion}\n`;
|
||||
});
|
||||
|
||||
if (story.dependencies.length > 0) {
|
||||
content += `\n**Dependencies:** ${story.dependencies.join(', ')}\n`;
|
||||
}
|
||||
|
||||
content += '\n';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
content += `\n## Progress\n\n`;
|
||||
content += `**Overall:** ${prd.progress.overall}% (${prd.progress.completed}/${prd.progress.total} stories)\n`;
|
||||
if (prd.progress.blocked > 0) {
|
||||
content += `**Blocked:** ${prd.progress.blocked} stories need attention\n`;
|
||||
}
|
||||
|
||||
content += `\n---\n\n`;
|
||||
content += `*Approver: ${prd.metadata.approver || 'TBD'}*\n`;
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
private static getStatusIcon(status: UserStory['status']): string {
|
||||
const icons = {
|
||||
not_started: '⏳',
|
||||
research: '🔍',
|
||||
in_progress: '🚧',
|
||||
review: '👀',
|
||||
completed: '✅',
|
||||
blocked: '🚫',
|
||||
};
|
||||
return icons[status];
|
||||
}
|
||||
}
|
||||
|
||||
// MCP Server Tool Registration
|
||||
export function registerPRDTools(server: McpServer) {
|
||||
createListPRDsTool(server);
|
||||
createGetPRDTool(server);
|
||||
createCreatePRDTool(server);
|
||||
createAddUserStoryTool(server);
|
||||
createUpdateStoryStatusTool(server);
|
||||
createExportMarkdownTool(server);
|
||||
createGetImplementationPromptsTool(server);
|
||||
createGetImprovementSuggestionsTool(server);
|
||||
createGetProjectStatusTool(server);
|
||||
}
|
||||
|
||||
function createListPRDsTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'list_prds',
|
||||
'List all Product Requirements Documents',
|
||||
async () => {
|
||||
const prds = await PRDManager.listPRDs();
|
||||
|
||||
if (prds.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'No PRD files found in .prds folder',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const prdList = prds.map((prd) => `- ${prd}`).join('\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Found ${prds.length} PRD files:\n\n${prdList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createGetPRDTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'get_prd',
|
||||
'Get the contents of a specific PRD file',
|
||||
{
|
||||
state: z.object({
|
||||
filename: z.string(),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
const content = await PRDManager.getPRDContent(state.filename);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: content,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createCreatePRDTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'create_prd',
|
||||
'Create a new structured PRD following ChatPRD best practices',
|
||||
{
|
||||
state: z.object({
|
||||
title: z.string(),
|
||||
overview: z.string(),
|
||||
problemStatement: z.string(),
|
||||
marketOpportunity: z.string(),
|
||||
targetUsers: z.array(z.string()),
|
||||
solutionDescription: z.string(),
|
||||
keyFeatures: z.array(z.string()),
|
||||
successMetrics: z.array(z.string()),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
const filename = await PRDManager.createStructuredPRD(
|
||||
state.title,
|
||||
state.overview,
|
||||
state.problemStatement,
|
||||
state.marketOpportunity,
|
||||
state.targetUsers,
|
||||
state.solutionDescription,
|
||||
state.keyFeatures,
|
||||
state.successMetrics,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `PRD created successfully: ${filename}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createAddUserStoryTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'add_user_story',
|
||||
'Add a new user story to an existing PRD',
|
||||
{
|
||||
state: z.object({
|
||||
filename: z.string(),
|
||||
userType: z.string(),
|
||||
action: z.string(),
|
||||
benefit: z.string(),
|
||||
acceptanceCriteria: z.array(z.string()),
|
||||
priority: z.enum(['P0', 'P1', 'P2', 'P3']).default('P2'),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
const result = await PRDManager.addUserStory(
|
||||
state.filename,
|
||||
state.userType,
|
||||
state.action,
|
||||
state.benefit,
|
||||
state.acceptanceCriteria,
|
||||
state.priority,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createUpdateStoryStatusTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'update_story_status',
|
||||
'Update the status of a specific user story',
|
||||
{
|
||||
state: z.object({
|
||||
filename: z.string(),
|
||||
storyId: z.string(),
|
||||
status: z.enum([
|
||||
'not_started',
|
||||
'research',
|
||||
'in_progress',
|
||||
'review',
|
||||
'completed',
|
||||
'blocked',
|
||||
]),
|
||||
notes: z.string().optional(),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
const result = await PRDManager.updateStoryStatus(
|
||||
state.filename,
|
||||
state.storyId,
|
||||
state.status,
|
||||
state.notes,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createExportMarkdownTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'export_prd_markdown',
|
||||
'Export PRD as markdown for visualization and sharing',
|
||||
{
|
||||
state: z.object({
|
||||
filename: z.string(),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
const markdownFile = await PRDManager.exportAsMarkdown(state.filename);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `PRD exported as markdown: ${markdownFile}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createGetImplementationPromptsTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'get_implementation_prompts',
|
||||
'Generate Claude Code implementation prompts from PRD',
|
||||
{
|
||||
state: z.object({
|
||||
filename: z.string(),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
const prompts = await PRDManager.generateImplementationPrompts(
|
||||
state.filename,
|
||||
);
|
||||
|
||||
if (prompts.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'No implementation prompts available. Add user stories first.',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const promptsList = prompts.map((p, i) => `${i + 1}. ${p}`).join('\n\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Implementation prompts:\n\n${promptsList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createGetImprovementSuggestionsTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'get_improvement_suggestions',
|
||||
'Get AI-powered suggestions to improve the PRD',
|
||||
{
|
||||
state: z.object({
|
||||
filename: z.string(),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
const suggestions = await PRDManager.getImprovementSuggestions(
|
||||
state.filename,
|
||||
);
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'PRD looks good! No specific improvements suggested at this time.',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const suggestionsList = suggestions.map((s) => `- ${s}`).join('\n');
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Improvement suggestions:\n\n${suggestionsList}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createGetProjectStatusTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'get_project_status',
|
||||
'Get comprehensive status overview of the PRD project',
|
||||
{
|
||||
state: z.object({
|
||||
filename: z.string(),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
const status = await PRDManager.getProjectStatus(state.filename);
|
||||
|
||||
let result = `**Project Status**\n\n`;
|
||||
result += `${status.summary}\n\n`;
|
||||
|
||||
if (status.nextSteps.length > 0) {
|
||||
result += `**Next Steps:**\n`;
|
||||
status.nextSteps.forEach((step) => {
|
||||
result += `- ${step}\n`;
|
||||
});
|
||||
result += '\n';
|
||||
}
|
||||
|
||||
if (status.blockers.length > 0) {
|
||||
result += `**Blockers:**\n`;
|
||||
status.blockers.forEach((blocker) => {
|
||||
result += `- ${blocker.title}: ${blocker.notes || 'No details provided'}\n`;
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -124,250 +124,6 @@ export class PromptsManager {
|
||||
'Performance analysis of database queries',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'component_implementation',
|
||||
title: 'Component Implementation Guide',
|
||||
description:
|
||||
'Generate implementation guidance for creating new UI components',
|
||||
category: 'development',
|
||||
arguments: [
|
||||
{
|
||||
name: 'component_description',
|
||||
description: 'Description of the component to implement',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'component_type',
|
||||
description: 'Type of component to create',
|
||||
required: true,
|
||||
type: 'enum',
|
||||
options: ['shadcn', 'makerkit', 'page', 'form', 'table', 'modal'],
|
||||
},
|
||||
{
|
||||
name: 'features',
|
||||
description: 'Specific features or functionality needed',
|
||||
required: false,
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
template: `Help me implement a {{component_type}} component: {{component_description}}
|
||||
|
||||
{{#if features}}
|
||||
**Required Features:**
|
||||
{{features}}
|
||||
{{/if}}
|
||||
|
||||
**Please provide:**
|
||||
1. **Component Design:** Architecture and structure recommendations
|
||||
2. **Code Implementation:** Full TypeScript/React code with proper typing
|
||||
3. **Styling Approach:** Tailwind CSS classes and variants (use CVA if applicable)
|
||||
4. **Props Interface:** Complete TypeScript interface definition
|
||||
5. **Usage Examples:** How to use the component in different scenarios
|
||||
6. **Testing Strategy:** Unit tests and accessibility considerations
|
||||
7. **Makerkit Integration:** How this fits with existing patterns
|
||||
|
||||
**Makerkit Implementation Requirements:**
|
||||
|
||||
**TypeScript Standards:**
|
||||
- Strict TypeScript with no 'any' types
|
||||
- Use implicit type inference unless impossible
|
||||
- Proper error handling with typed errors
|
||||
- Clean code without unnecessary comments
|
||||
|
||||
**Component Architecture:**
|
||||
- Functional components with proper 'use client' directive
|
||||
- Use existing @kit/ui components (shadcn + makerkit customs)
|
||||
- Follow established patterns: enhanced-data-table, if, trans, page
|
||||
- Implement proper conditional rendering with <If> component
|
||||
- Display loading indicators with LoadingSpinner component where appropriate
|
||||
- Encapsulate repeated blocks of code into reusable local components
|
||||
|
||||
**Styling & UI Standards:**
|
||||
- Tailwind CSS 4 with CVA (Class Variance Authority) for variants
|
||||
- Responsive design with mobile-first approach
|
||||
- Proper accessibility with ARIA attributes and data-test for E2E
|
||||
- Use shadcn components as base, extend with makerkit patterns
|
||||
|
||||
**State & Data Management:**
|
||||
- Single state objects over multiple useState
|
||||
- Server-side data fetching with RSC preferred
|
||||
- Supabase client integration with proper error handling
|
||||
- Account-based data access with proper RLS policies
|
||||
|
||||
**File Structure:**
|
||||
- Follow monorepo structure: packages/features/* for feature packages
|
||||
- Use established naming conventions and folder organization
|
||||
- Import from @kit/* packages appropriately`,
|
||||
examples: [
|
||||
'Create a data table component with sorting and filtering',
|
||||
'Build a multi-step form component',
|
||||
'Design a notification center component',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'architecture_guidance',
|
||||
title: 'Architecture Guidance',
|
||||
description: 'Provide architectural recommendations for complex features',
|
||||
category: 'architecture',
|
||||
arguments: [
|
||||
{
|
||||
name: 'feature_scope',
|
||||
description: 'Description of the feature or system to architect',
|
||||
required: true,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'scale_requirements',
|
||||
description: 'Expected scale and performance requirements',
|
||||
required: false,
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'constraints',
|
||||
description: 'Technical constraints or requirements',
|
||||
required: false,
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
template: `Provide architectural guidance for: {{feature_scope}}
|
||||
|
||||
{{#if scale_requirements}}
|
||||
**Scale Requirements:** {{scale_requirements}}
|
||||
{{/if}}
|
||||
|
||||
{{#if constraints}}
|
||||
**Constraints:** {{constraints}}
|
||||
{{/if}}
|
||||
|
||||
**Please provide:**
|
||||
1. **Architecture Overview:** High-level system design and components
|
||||
2. **Data Architecture:** Database design and data flow patterns
|
||||
3. **API Design:** RESTful endpoints and GraphQL considerations
|
||||
4. **State Management:** Client-side state architecture
|
||||
5. **Security Architecture:** Authentication, authorization, and data protection
|
||||
6. **Performance Strategy:** Caching, optimization, and scaling approaches
|
||||
7. **Integration Patterns:** How this fits with existing Makerkit architecture
|
||||
|
||||
**Makerkit Architecture Standards:**
|
||||
|
||||
**Multi-Tenant Patterns:**
|
||||
- Account-based data isolation with proper foreign key relationships
|
||||
- Personal vs Team account architecture (auth.users.id = accounts.id for personal)
|
||||
- Role-based access control with roles, memberships, and permissions tables
|
||||
- RLS policies that enforce account boundaries at database level
|
||||
|
||||
**Technology Stack Integration:**
|
||||
- Next.js 15 App Router with React Server Components
|
||||
- Supabase for database, auth, storage, and real-time features
|
||||
- TypeScript strict mode with no 'any' types
|
||||
- Tailwind CSS 4 with shadcn/ui and custom Makerkit components
|
||||
- Turborepo monorepo with proper package organization
|
||||
|
||||
**Performance & Security:**
|
||||
- Server-side data fetching preferred over client-side
|
||||
- Proper error boundaries and graceful error handling
|
||||
- Account-level data access patterns with efficient queries
|
||||
- Use of existing database functions for complex operations
|
||||
|
||||
**Code Organization:**
|
||||
- For simplicity, place feature directly in the application (apps/web) unless you're asked to create a separate package for it
|
||||
- Shared utilities in packages/* (ui, auth, billing, etc.)
|
||||
- Consistent naming conventions and file structure
|
||||
- Proper import patterns from @kit/* packages`,
|
||||
examples: [
|
||||
'Design a real-time notification system',
|
||||
'Architect a file upload and processing system',
|
||||
'Design a reporting and analytics feature',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'makerkit_feature_implementation',
|
||||
title: 'Makerkit Feature Implementation Guide',
|
||||
description:
|
||||
'Complete guide for implementing new features following Makerkit patterns',
|
||||
category: 'development',
|
||||
arguments: [
|
||||
{
|
||||
name: 'feature_name',
|
||||
description: 'Name of the feature to implement',
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
name: 'feature_type',
|
||||
description: 'Type of feature being implemented',
|
||||
required: true,
|
||||
type: 'enum',
|
||||
options: [
|
||||
'billing',
|
||||
'auth',
|
||||
'team-management',
|
||||
'data-management',
|
||||
'api',
|
||||
'ui-component',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'user_stories',
|
||||
description: 'User stories or requirements for the feature',
|
||||
required: false,
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
template: `Implement a {{feature_type}} feature: {{feature_name}}
|
||||
|
||||
{{#if user_stories}}
|
||||
**User Requirements:**
|
||||
{{user_stories}}
|
||||
{{/if}}
|
||||
|
||||
**Please provide a complete Makerkit implementation including:**
|
||||
|
||||
**1. Database Design:**
|
||||
- Schema changes following multi-tenant patterns
|
||||
- RLS policies for account-based access control
|
||||
- Database functions if needed (SECURITY DEFINER/INVOKER)
|
||||
- Proper foreign key relationships with account_id
|
||||
- Schema uses constraints/triggers where required for data integrity and business rules
|
||||
- Schema prevents invalid data from being inserted or updated
|
||||
|
||||
**2. Backend Implementation:**
|
||||
- Server Actions or API routes following Next.js 15 patterns
|
||||
- Proper error handling with typed responses
|
||||
- Integration with existing Supabase auth and database
|
||||
- Account-level data access patterns
|
||||
- Redirect using Server Actions/API Routes instead of client-side navigation
|
||||
|
||||
**3. Frontend Components:**
|
||||
- React Server Components where possible
|
||||
- Use of @kit/ui components (shadcn + makerkit)
|
||||
- Small, composable, explicit, reusable, well-named components
|
||||
- Proper TypeScript interfaces and types
|
||||
- Single state objects over multiple useState
|
||||
- Conditional rendering with <If> component
|
||||
|
||||
**4. Package Organization:**
|
||||
- If reusable, create feature package in packages/features/{{feature_name}}
|
||||
- Proper exports and package.json configuration
|
||||
- Integration with existing packages (@kit/auth, @kit/ui, etc.)
|
||||
|
||||
**5. Code Quality:**
|
||||
- TypeScript strict mode with no 'any' types
|
||||
- Proper error boundaries and handling
|
||||
- Follow established file structure and naming conventions
|
||||
|
||||
**Makerkit Standards:**
|
||||
- Multi-tenant architecture with account-based access
|
||||
- Use existing database functions where applicable
|
||||
- Follow monorepo patterns and package organization
|
||||
- Implement proper security and performance best practices`,
|
||||
examples: [
|
||||
'Implement team collaboration features',
|
||||
'Build a subscription management system',
|
||||
'Create a file sharing feature with permissions',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'supabase_rls_policy_design',
|
||||
title: 'Supabase RLS Policy Design',
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext"
|
||||
},
|
||||
"files": ["src/index.ts"],
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user