MCP/Rules Improvements + MCP Prompts (#357)
- Use ESM for building the MCP Server - Added own Postgres dependency to MCP Server for querying tables and other entities in MCP - Vastly improved AI Agent rules - Added MCP Prompts for reviewing code and planning features - Minor refactoring
This commit is contained in:
committed by
GitHub
parent
f85035bd01
commit
9712e2354b
@@ -1,8 +1,17 @@
|
||||
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { type McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||
import { readFile, readdir, stat } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import postgres from 'postgres';
|
||||
import { z } from 'zod';
|
||||
|
||||
const DATABASE_URL =
|
||||
process.env.DATABASE_URL ||
|
||||
'postgresql://postgres:postgres@127.0.0.1:54322/postgres';
|
||||
|
||||
const sql = postgres(DATABASE_URL, {
|
||||
prepare: false,
|
||||
});
|
||||
|
||||
interface DatabaseFunction {
|
||||
name: string;
|
||||
parameters: Array<{
|
||||
@@ -30,6 +39,58 @@ interface SchemaFile {
|
||||
topic: string;
|
||||
}
|
||||
|
||||
interface ProjectTable {
|
||||
name: string;
|
||||
schema: string;
|
||||
sourceFile: string;
|
||||
topic: string;
|
||||
}
|
||||
|
||||
interface TableColumn {
|
||||
name: string;
|
||||
type: string;
|
||||
nullable: boolean;
|
||||
defaultValue?: string;
|
||||
isPrimaryKey: boolean;
|
||||
isForeignKey: boolean;
|
||||
referencedTable?: string;
|
||||
referencedColumn?: string;
|
||||
}
|
||||
|
||||
interface TableIndex {
|
||||
name: string;
|
||||
columns: string[];
|
||||
unique: boolean;
|
||||
type: string;
|
||||
definition: string;
|
||||
}
|
||||
|
||||
interface TableForeignKey {
|
||||
name: string;
|
||||
columns: string[];
|
||||
referencedTable: string;
|
||||
referencedColumns: string[];
|
||||
onDelete?: string;
|
||||
onUpdate?: string;
|
||||
}
|
||||
|
||||
interface TableInfo {
|
||||
name: string;
|
||||
schema: string;
|
||||
sourceFile: string;
|
||||
topic: string;
|
||||
columns: TableColumn[];
|
||||
foreignKeys: TableForeignKey[];
|
||||
indexes: TableIndex[];
|
||||
createStatement?: string;
|
||||
}
|
||||
|
||||
interface EnumInfo {
|
||||
name: string;
|
||||
values: string[];
|
||||
sourceFile: string;
|
||||
}
|
||||
|
||||
export class DatabaseTool {
|
||||
static async getSchemaFiles(): Promise<SchemaFile[]> {
|
||||
const schemasPath = join(
|
||||
@@ -95,7 +156,21 @@ export class DatabaseTool {
|
||||
functionName: string,
|
||||
): Promise<DatabaseFunction> {
|
||||
const functions = await this.getFunctions();
|
||||
const func = functions.find((f) => f.name === functionName);
|
||||
|
||||
// Extract just the function name if schema prefix is provided (e.g., "public.has_permission" -> "has_permission")
|
||||
const nameParts = functionName.split('.');
|
||||
const cleanFunctionName = nameParts[nameParts.length - 1];
|
||||
const providedSchema = nameParts.length > 1 ? nameParts[0] : 'public';
|
||||
|
||||
// Try to find by exact name first, then by cleaned name and schema
|
||||
let func = functions.find((f) => f.name === functionName);
|
||||
|
||||
if (!func) {
|
||||
// Match by function name and schema (defaulting to public if no schema provided)
|
||||
func = functions.find(
|
||||
(f) => f.name === cleanFunctionName && f.schema === providedSchema,
|
||||
);
|
||||
}
|
||||
|
||||
if (!func) {
|
||||
throw new Error(`Function "${functionName}" not found`);
|
||||
@@ -108,12 +183,43 @@ export class DatabaseTool {
|
||||
const allFunctions = await this.getFunctions();
|
||||
const searchTerm = query.toLowerCase();
|
||||
|
||||
// Extract schema and function name from search query if provided
|
||||
const nameParts = query.split('.');
|
||||
const cleanSearchTerm = nameParts[nameParts.length - 1].toLowerCase();
|
||||
|
||||
const searchSchema =
|
||||
nameParts.length > 1 ? nameParts[0].toLowerCase() : null;
|
||||
|
||||
return allFunctions.filter((func) => {
|
||||
const matchesName = func.name.toLowerCase().includes(cleanSearchTerm);
|
||||
const matchesFullName = func.name.toLowerCase().includes(searchTerm);
|
||||
|
||||
const matchesSchema = searchSchema
|
||||
? func.schema.toLowerCase() === searchSchema
|
||||
: true;
|
||||
|
||||
const matchesDescription = func.description
|
||||
.toLowerCase()
|
||||
.includes(searchTerm);
|
||||
|
||||
const matchesPurpose = func.purpose.toLowerCase().includes(searchTerm);
|
||||
|
||||
const matchesReturnType = func.returnType
|
||||
.toLowerCase()
|
||||
.includes(searchTerm);
|
||||
|
||||
// If schema is specified in query, must match both name and schema
|
||||
if (searchSchema) {
|
||||
return (matchesName || matchesFullName) && matchesSchema;
|
||||
}
|
||||
|
||||
// Otherwise, match on any field
|
||||
return (
|
||||
func.name.toLowerCase().includes(searchTerm) ||
|
||||
func.description.toLowerCase().includes(searchTerm) ||
|
||||
func.purpose.toLowerCase().includes(searchTerm) ||
|
||||
func.returnType.toLowerCase().includes(searchTerm)
|
||||
matchesName ||
|
||||
matchesFullName ||
|
||||
matchesDescription ||
|
||||
matchesPurpose ||
|
||||
matchesReturnType
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -158,6 +264,262 @@ export class DatabaseTool {
|
||||
);
|
||||
}
|
||||
|
||||
static async getAllProjectTables(): Promise<ProjectTable[]> {
|
||||
const schemaFiles = await this.getSchemaFiles();
|
||||
const tables: ProjectTable[] = [];
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return tables;
|
||||
}
|
||||
|
||||
static async getAllEnums(): Promise<Record<string, EnumInfo>> {
|
||||
try {
|
||||
// Try to get live enums from database first
|
||||
const liveEnums = await this.getEnumsFromDB();
|
||||
if (Object.keys(liveEnums).length > 0) {
|
||||
return liveEnums;
|
||||
}
|
||||
|
||||
// Fallback to schema files
|
||||
const enumContent = await this.getSchemaContent('01-enums.sql');
|
||||
return this.parseEnums(enumContent);
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
static async getTableInfo(
|
||||
schema: string,
|
||||
tableName: string,
|
||||
): Promise<TableInfo> {
|
||||
const schemaFiles = await this.getSchemaFiles();
|
||||
|
||||
for (const file of schemaFiles) {
|
||||
const content = await readFile(file.path, 'utf8');
|
||||
const tableDefinition = this.extractTableDefinition(
|
||||
content,
|
||||
schema,
|
||||
tableName,
|
||||
);
|
||||
|
||||
if (tableDefinition) {
|
||||
// Enhance with live database info
|
||||
const liveColumns = await this.getTableColumnsFromDB(schema, tableName);
|
||||
const liveForeignKeys = await this.getTableForeignKeysFromDB(
|
||||
schema,
|
||||
tableName,
|
||||
);
|
||||
const liveIndexes = await this.getTableIndexesFromDB(schema, tableName);
|
||||
|
||||
return {
|
||||
name: tableName,
|
||||
schema: schema,
|
||||
sourceFile: file.name,
|
||||
topic: file.topic,
|
||||
columns:
|
||||
liveColumns.length > 0
|
||||
? liveColumns
|
||||
: this.parseColumns(tableDefinition),
|
||||
foreignKeys:
|
||||
liveForeignKeys.length > 0
|
||||
? liveForeignKeys
|
||||
: this.parseForeignKeys(tableDefinition),
|
||||
indexes:
|
||||
liveIndexes.length > 0
|
||||
? liveIndexes
|
||||
: this.parseIndexes(content, tableName),
|
||||
createStatement: tableDefinition,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Table ${schema}.${tableName} not found in schema files`);
|
||||
}
|
||||
|
||||
static async getTableColumnsFromDB(
|
||||
schema: string,
|
||||
tableName: string,
|
||||
): Promise<TableColumn[]> {
|
||||
try {
|
||||
const columns = await sql`
|
||||
SELECT
|
||||
c.column_name,
|
||||
c.data_type,
|
||||
c.is_nullable,
|
||||
c.column_default,
|
||||
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key,
|
||||
CASE WHEN fk.column_name IS NOT NULL THEN true ELSE false END as is_foreign_key,
|
||||
fk.foreign_table_name as referenced_table,
|
||||
fk.foreign_column_name as referenced_column
|
||||
FROM information_schema.columns c
|
||||
LEFT JOIN (
|
||||
SELECT ku.table_name, ku.column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage ku
|
||||
ON tc.constraint_name = ku.constraint_name
|
||||
AND tc.table_schema = ku.table_schema
|
||||
WHERE tc.constraint_type = 'PRIMARY KEY'
|
||||
AND tc.table_schema = ${schema}
|
||||
) pk ON c.table_name = pk.table_name AND c.column_name = pk.column_name
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
ku.table_name,
|
||||
ku.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage ku
|
||||
ON tc.constraint_name = ku.constraint_name
|
||||
AND tc.table_schema = ku.table_schema
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.table_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = ${schema}
|
||||
) fk ON c.table_name = fk.table_name AND c.column_name = fk.column_name
|
||||
WHERE c.table_schema = ${schema}
|
||||
AND c.table_name = ${tableName}
|
||||
ORDER BY c.ordinal_position
|
||||
`;
|
||||
|
||||
return columns.map((col) => ({
|
||||
name: col.column_name,
|
||||
type: col.data_type,
|
||||
nullable: col.is_nullable === 'YES',
|
||||
defaultValue: col.column_default,
|
||||
isPrimaryKey: col.is_primary_key,
|
||||
isForeignKey: col.is_foreign_key,
|
||||
referencedTable: col.referenced_table,
|
||||
referencedColumn: col.referenced_column,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async getTableForeignKeysFromDB(
|
||||
schema: string,
|
||||
tableName: string,
|
||||
): Promise<TableForeignKey[]> {
|
||||
try {
|
||||
const foreignKeys = await sql`
|
||||
SELECT
|
||||
tc.constraint_name,
|
||||
string_agg(kcu.column_name, ',' ORDER BY kcu.ordinal_position) as columns,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
string_agg(ccu.column_name, ',' ORDER BY kcu.ordinal_position) as foreign_columns,
|
||||
rc.delete_rule,
|
||||
rc.update_rule
|
||||
FROM information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.table_schema
|
||||
JOIN information_schema.referential_constraints rc
|
||||
ON tc.constraint_name = rc.constraint_name
|
||||
AND tc.table_schema = rc.constraint_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_schema = ${schema}
|
||||
AND tc.table_name = ${tableName}
|
||||
GROUP BY tc.constraint_name, ccu.table_name, rc.delete_rule, rc.update_rule
|
||||
`;
|
||||
|
||||
return foreignKeys.map((fk: any) => ({
|
||||
name: fk.constraint_name,
|
||||
columns: fk.columns.split(','),
|
||||
referencedTable: fk.foreign_table_name,
|
||||
referencedColumns: fk.foreign_columns.split(','),
|
||||
onDelete: fk.delete_rule,
|
||||
onUpdate: fk.update_rule,
|
||||
}));
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async getTableIndexesFromDB(
|
||||
schema: string,
|
||||
tableName: string,
|
||||
): Promise<TableIndex[]> {
|
||||
try {
|
||||
const indexes = await sql`
|
||||
SELECT
|
||||
i.indexname,
|
||||
i.indexdef,
|
||||
ix.indisunique as is_unique,
|
||||
string_agg(a.attname, ',' ORDER BY a.attnum) as columns
|
||||
FROM pg_indexes i
|
||||
JOIN pg_class c ON c.relname = i.tablename
|
||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||
JOIN pg_index ix ON ix.indexrelid = (
|
||||
SELECT oid FROM pg_class WHERE relname = i.indexname
|
||||
)
|
||||
JOIN pg_attribute a ON a.attrelid = c.oid
|
||||
AND a.attnum = ANY(ix.indkey)
|
||||
WHERE n.nspname = ${schema}
|
||||
AND i.tablename = ${tableName}
|
||||
AND i.indexname NOT LIKE '%_pkey'
|
||||
GROUP BY i.indexname, i.indexdef, ix.indisunique
|
||||
ORDER BY i.indexname
|
||||
`;
|
||||
|
||||
return indexes.map((idx) => ({
|
||||
name: idx.indexname,
|
||||
columns: idx.columns.split(','),
|
||||
unique: idx.is_unique,
|
||||
type: 'btree', // Default, could be enhanced
|
||||
definition: idx.indexdef,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
static async getEnumsFromDB(): Promise<Record<string, EnumInfo>> {
|
||||
try {
|
||||
const enums = await sql`
|
||||
SELECT
|
||||
t.typname as enum_name,
|
||||
array_agg(e.enumlabel ORDER BY e.enumsortorder) as enum_values
|
||||
FROM pg_type t
|
||||
JOIN pg_enum e ON t.oid = e.enumtypid
|
||||
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||
WHERE n.nspname = 'public'
|
||||
GROUP BY t.typname
|
||||
ORDER BY t.typname
|
||||
`;
|
||||
|
||||
const result: Record<string, EnumInfo> = {};
|
||||
for (const enumData of enums) {
|
||||
result[enumData.enum_name] = {
|
||||
name: enumData.enum_name,
|
||||
values: enumData.enum_values,
|
||||
sourceFile: 'database', // Live from DB
|
||||
};
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private static extractFunctionsFromContent(
|
||||
content: string,
|
||||
sourceFile: string,
|
||||
@@ -328,6 +690,32 @@ export class DatabaseTool {
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
private static extractFunctionNames(content: string): string[] {
|
||||
const functions: string[] = [];
|
||||
const functionRegex =
|
||||
@@ -361,6 +749,176 @@ export class DatabaseTool {
|
||||
return [...new Set(dependencies)]; // Remove duplicates
|
||||
}
|
||||
|
||||
private static extractTableDefinition(
|
||||
content: string,
|
||||
schema: string,
|
||||
tableName: string,
|
||||
): string | null {
|
||||
const tableRegex = new RegExp(
|
||||
`create\\s+table\\s+(?:if\\s+not\\s+exists\\s+)?(?:${schema}\\.)?${tableName}\\s*\\([^;]*?\\);`,
|
||||
'gis',
|
||||
);
|
||||
const match = content.match(tableRegex);
|
||||
return match ? match[0] : null;
|
||||
}
|
||||
|
||||
private static parseColumns(tableDefinition: string): TableColumn[] {
|
||||
const columns: TableColumn[] = [];
|
||||
|
||||
// Extract the content between parentheses
|
||||
const contentMatch = tableDefinition.match(/\(([\s\S]*)\)/);
|
||||
if (!contentMatch) return columns;
|
||||
|
||||
const content = contentMatch[1];
|
||||
|
||||
// Split by commas, but be careful of nested structures
|
||||
const lines = content
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line);
|
||||
|
||||
for (const line of lines) {
|
||||
if (
|
||||
line.startsWith('constraint') ||
|
||||
line.startsWith('primary key') ||
|
||||
line.startsWith('foreign key')
|
||||
) {
|
||||
continue; // Skip constraint definitions
|
||||
}
|
||||
|
||||
// Parse column definition: name type [constraints]
|
||||
const columnMatch = line.match(
|
||||
/^([a-zA-Z_][a-zA-Z0-9_]*)\s+([^,\s]+)(?:\s+(.*))?/,
|
||||
);
|
||||
if (columnMatch) {
|
||||
const [, name, type, constraints = ''] = columnMatch;
|
||||
|
||||
columns.push({
|
||||
name,
|
||||
type: type.replace(/,$/, ''), // Remove trailing comma
|
||||
nullable: !constraints.includes('not null'),
|
||||
defaultValue: this.extractDefault(constraints),
|
||||
isPrimaryKey: constraints.includes('primary key'),
|
||||
isForeignKey: constraints.includes('references'),
|
||||
referencedTable: this.extractReferencedTable(constraints),
|
||||
referencedColumn: this.extractReferencedColumn(constraints),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
private static extractDefault(constraints: string): string | undefined {
|
||||
const defaultMatch = constraints.match(/default\s+([^,\s]+)/i);
|
||||
return defaultMatch ? defaultMatch[1] : undefined;
|
||||
}
|
||||
|
||||
private static extractReferencedTable(
|
||||
constraints: string,
|
||||
): string | undefined {
|
||||
const refMatch = constraints.match(
|
||||
/references\s+([a-zA-Z_][a-zA-Z0-9_]*)/i,
|
||||
);
|
||||
return refMatch ? refMatch[1] : undefined;
|
||||
}
|
||||
|
||||
private static extractReferencedColumn(
|
||||
constraints: string,
|
||||
): string | undefined {
|
||||
const refMatch = constraints.match(
|
||||
/references\s+[a-zA-Z_][a-zA-Z0-9_]*\s*\(([^)]+)\)/i,
|
||||
);
|
||||
return refMatch ? refMatch[1].trim() : undefined;
|
||||
}
|
||||
|
||||
private static parseForeignKeys(tableDefinition: string): TableForeignKey[] {
|
||||
const foreignKeys: TableForeignKey[] = [];
|
||||
|
||||
// Match foreign key constraints
|
||||
const fkRegex =
|
||||
/foreign\s+key\s*\(([^)]+)\)\s*references\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(([^)]+)\)(?:\s+on\s+delete\s+([a-z\s]+))?(?:\s+on\s+update\s+([a-z\s]+))?/gi;
|
||||
|
||||
let match;
|
||||
while ((match = fkRegex.exec(tableDefinition)) !== null) {
|
||||
const [
|
||||
,
|
||||
columns,
|
||||
referencedTable,
|
||||
referencedColumns,
|
||||
onDelete,
|
||||
onUpdate,
|
||||
] = match;
|
||||
|
||||
foreignKeys.push({
|
||||
name: `fk_${referencedTable}_${columns.replace(/\s/g, '')}`,
|
||||
columns: columns.split(',').map((col) => col.trim()),
|
||||
referencedTable,
|
||||
referencedColumns: referencedColumns
|
||||
.split(',')
|
||||
.map((col) => col.trim()),
|
||||
onDelete: onDelete?.trim(),
|
||||
onUpdate: onUpdate?.trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return foreignKeys;
|
||||
}
|
||||
|
||||
private static parseIndexes(
|
||||
content: string,
|
||||
tableName: string,
|
||||
): TableIndex[] {
|
||||
const indexes: TableIndex[] = [];
|
||||
|
||||
// Match CREATE INDEX statements
|
||||
const indexRegex = new RegExp(
|
||||
`create\\s+(?:unique\\s+)?index\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s+on\\s+(?:public\\.)?${tableName}\\s*\\(([^)]+)\\)`,
|
||||
'gi',
|
||||
);
|
||||
|
||||
let match;
|
||||
while ((match = indexRegex.exec(content)) !== null) {
|
||||
const [fullMatch, indexName, columns] = match;
|
||||
|
||||
indexes.push({
|
||||
name: indexName,
|
||||
columns: columns.split(',').map((col) => col.trim()),
|
||||
unique: fullMatch.toLowerCase().includes('unique'),
|
||||
type: 'btree', // Default type
|
||||
definition: fullMatch,
|
||||
});
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
private static parseEnums(content: string): Record<string, EnumInfo> {
|
||||
const enums: Record<string, EnumInfo> = {};
|
||||
|
||||
// Match CREATE TYPE ... AS ENUM
|
||||
const enumRegex =
|
||||
/create\s+type\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+as\s+enum\s*\(([^)]+)\)/gi;
|
||||
|
||||
let match;
|
||||
while ((match = enumRegex.exec(content)) !== null) {
|
||||
const [, enumName, values] = match;
|
||||
|
||||
const enumValues = values
|
||||
.split(',')
|
||||
.map((value) => value.trim().replace(/['"]/g, ''))
|
||||
.filter((value) => value);
|
||||
|
||||
enums[enumName] = {
|
||||
name: enumName,
|
||||
values: enumValues,
|
||||
sourceFile: '01-enums.sql',
|
||||
};
|
||||
}
|
||||
|
||||
return enums;
|
||||
}
|
||||
|
||||
private static determineTopic(fileName: string, content: string): string {
|
||||
// Map file names to topics
|
||||
const fileTopicMap: Record<string, string> = {
|
||||
@@ -426,6 +984,14 @@ export function registerDatabaseTools(server: McpServer) {
|
||||
createSearchFunctionsTool(server);
|
||||
}
|
||||
|
||||
export function registerDatabaseResources(server: McpServer) {
|
||||
createDatabaseSummaryTool(server);
|
||||
createDatabaseTablesListTool(server);
|
||||
createGetTableInfoTool(server);
|
||||
createGetEnumInfoTool(server);
|
||||
createGetAllEnumsTool(server);
|
||||
}
|
||||
|
||||
function createGetSchemaFilesTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'get_schema_files',
|
||||
@@ -704,3 +1270,192 @@ function createGetSchemaBySectionTool(server: McpServer) {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createDatabaseSummaryTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'get_database_summary',
|
||||
'📊 Get comprehensive database overview with tables, enums, and functions',
|
||||
async () => {
|
||||
const tables = await DatabaseTool.getAllProjectTables();
|
||||
const enums = await DatabaseTool.getAllEnums();
|
||||
const functions = await DatabaseTool.getFunctions();
|
||||
|
||||
const summary = {
|
||||
overview: {
|
||||
totalTables: tables.length,
|
||||
totalEnums: Object.keys(enums).length,
|
||||
totalFunctions: functions.length,
|
||||
},
|
||||
tables: tables.map((t) => ({
|
||||
name: t.name,
|
||||
schema: t.schema,
|
||||
topic: t.topic,
|
||||
sourceFile: t.sourceFile,
|
||||
})),
|
||||
enums: Object.entries(enums).map(([name, info]) => ({
|
||||
name,
|
||||
values: info.values,
|
||||
sourceFile: info.sourceFile,
|
||||
})),
|
||||
functions: functions.map((f) => ({
|
||||
name: f.name,
|
||||
schema: f.schema,
|
||||
purpose: f.purpose,
|
||||
sourceFile: f.sourceFile,
|
||||
})),
|
||||
tablesByTopic: tables.reduce(
|
||||
(acc, table) => {
|
||||
if (!acc[table.topic]) acc[table.topic] = [];
|
||||
acc[table.topic].push(table.name);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string[]>,
|
||||
),
|
||||
};
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `📊 DATABASE OVERVIEW\n\n${JSON.stringify(summary, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createDatabaseTablesListTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'get_database_tables',
|
||||
'📋 Get list of all project-defined database tables',
|
||||
async () => {
|
||||
const tables = await DatabaseTool.getAllProjectTables();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `📋 PROJECT TABLES\n\n${JSON.stringify(tables, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createGetTableInfoTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'get_table_info',
|
||||
'🗂️ Get detailed table schema with columns, foreign keys, and indexes',
|
||||
{
|
||||
state: z.object({
|
||||
schema: z.string().default('public'),
|
||||
tableName: z.string(),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
try {
|
||||
const tableInfo = await DatabaseTool.getTableInfo(
|
||||
state.schema,
|
||||
state.tableName,
|
||||
);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `🗂️ TABLE: ${state.schema}.${state.tableName}\n\n${JSON.stringify(tableInfo, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `❌ Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createGetEnumInfoTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'get_enum_info',
|
||||
'🏷️ Get enum type definition with all possible values',
|
||||
{
|
||||
state: z.object({
|
||||
enumName: z.string(),
|
||||
}),
|
||||
},
|
||||
async ({ state }) => {
|
||||
try {
|
||||
const enums = await DatabaseTool.getAllEnums();
|
||||
const enumInfo = enums[state.enumName];
|
||||
|
||||
if (!enumInfo) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `❌ Enum "${state.enumName}" not found. Available enums: ${Object.keys(enums).join(', ')}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `🏷️ ENUM: ${state.enumName}\n\n${JSON.stringify(enumInfo, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `❌ Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function createGetAllEnumsTool(server: McpServer) {
|
||||
return server.tool(
|
||||
'get_all_enums',
|
||||
'🏷️ Get all enum types and their values',
|
||||
async () => {
|
||||
try {
|
||||
const enums = await DatabaseTool.getAllEnums();
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `🏷️ ALL ENUMS\n\n${JSON.stringify(enums, null, 2)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `❌ Error: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user