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:
Giancarlo Buomprisco
2025-09-25 12:03:53 +08:00
committed by GitHub
parent 02e2502dcc
commit 2b8572baaa
62 changed files with 5661 additions and 1231 deletions

View File

@@ -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(