Optimized agents rules subfolders, dependencies updates (#355)

* Update AGENTS.md and CLAUDE.md for improved clarity and structure
* Added MCP Server
* Added missing triggers to tables that should have used them
* Updated all dependencies
* Fixed rare bug in React present in the Admin layout which prevents navigating to pages (sometimes...)
This commit is contained in:
Giancarlo Buomprisco
2025-09-17 11:36:02 +08:00
committed by GitHub
parent 9fae142f2d
commit 533dfba5b9
83 changed files with 9223 additions and 2974 deletions

1
packages/mcp-server/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build

View File

@@ -0,0 +1,58 @@
# Makerkit MCP Server
The Makerkit MCP Server provides tools to AI Agents for working with the codebase.
## Build MCP Server
Run the command:
```bash
pnpm --filter "@kit/mcp-server" build
```
The command will build the MCP Server at `packages/mcp-server/build/index.js`.
## Adding MCP Servers to AI Coding tools
Before getting started, retrieve the absolute path to the `index.js` file created above. You can normally do this in your IDE by right-clicking the `index.js` file and selecting `Copy Path`.
I will reference this as `<full-path>` in the steps below: please replace it with the full path to your `index.js`.
### Claude Code
Run the command below:
```bash
claude mcp add makerkit node <full-path>
```
Restart Claude Code. If no errors appear, the MCP should be correctly configured.
### Codex
Open the Codex YAML config and add the following:
```
[mcp_servers.makerkit]
command = "node"
args = ["<full-path>"]
```
### Cursor
Open the `mcp.json` config in Cursor and add the following config:
```json
{
"mcpServers": {
"makerkit": {
"command": "node",
"args": ["<full-path>"]
}
}
}
```
## Additional MCP Servers
I strongly suggest using [the Postgres MCP Server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/postgres) that allows AI Agents to understand the structure of your Database.

View File

@@ -0,0 +1,3 @@
import eslintConfigBase from '@kit/eslint-config/base.js';
export default eslintConfigBase;

View File

@@ -0,0 +1,31 @@
{
"name": "@kit/mcp-server",
"private": true,
"version": "0.1.0",
"main": "./build/index.js",
"bin": {
"makerkit-mcp-server": "./build/index.js"
},
"typesVersions": {
"*": {
"*": [
"src/*"
]
}
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
"build": "tsc && chmod 755 build/index.js",
"mcp": "node build/index.js"
},
"devDependencies": {
"@kit/eslint-config": "workspace:*",
"@kit/prettier-config": "workspace:*",
"@kit/tsconfig": "workspace:*",
"@modelcontextprotocol/sdk": "1.18.0",
"@types/node": "^24.5.0",
"zod": "^3.25.74"
},
"prettier": "@kit/prettier-config"
}

View File

@@ -0,0 +1,31 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { registerComponentsTools } from './tools/components';
import { registerDatabaseTools } from './tools/database';
import { registerGetMigrationsTools } from './tools/migrations';
import { registerScriptsTools } from './tools/scripts';
// Create server instance
const server = new McpServer({
name: 'makerkit',
version: '1.0.0',
capabilities: {},
});
registerGetMigrationsTools(server);
registerDatabaseTools(server);
registerComponentsTools(server);
registerScriptsTools(server);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Makerkit MCP Server running on stdio');
}
main().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});

View File

View File

@@ -0,0 +1,493 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { z } from 'zod';
interface ComponentInfo {
name: string;
exportPath: string;
filePath: string;
category: 'shadcn' | 'makerkit' | 'utils';
description: string;
}
export class ComponentsTool {
static async getComponents(): Promise<ComponentInfo[]> {
const packageJsonPath = join(
process.cwd(),
'packages',
'ui',
'package.json',
);
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
const components: ComponentInfo[] = [];
for (const [exportName, filePath] of Object.entries(packageJson.exports)) {
if (typeof filePath === 'string' && filePath.endsWith('.tsx')) {
const category = this.determineCategory(filePath);
const description = await this.generateDescription(
exportName,
filePath,
category,
);
components.push({
name: exportName.replace('./', ''),
exportPath: exportName,
filePath: filePath,
category,
description,
});
}
}
return components.sort((a, b) => a.name.localeCompare(b.name));
}
static async searchComponents(query: string): Promise<ComponentInfo[]> {
const allComponents = await this.getComponents();
const searchTerm = query.toLowerCase();
return allComponents.filter((component) => {
return (
component.name.toLowerCase().includes(searchTerm) ||
component.description.toLowerCase().includes(searchTerm) ||
component.category.toLowerCase().includes(searchTerm)
);
});
}
static async getComponentProps(componentName: string): Promise<{
componentName: string;
props: Array<{
name: string;
type: string;
optional: boolean;
description?: string;
}>;
interfaces: string[];
variants?: Record<string, string[]>;
}> {
const content = await this.getComponentContent(componentName);
return {
componentName,
props: this.extractProps(content),
interfaces: this.extractInterfaces(content),
variants: this.extractVariants(content),
};
}
private static extractProps(content: string): Array<{
name: string;
type: string;
optional: boolean;
description?: string;
}> {
const props: Array<{
name: string;
type: string;
optional: boolean;
description?: string;
}> = [];
// Look for interface definitions that end with "Props"
const interfaceRegex =
/interface\s+(\w*Props)\s*(?:extends[^{]*?)?\s*{([^}]*)}/gs;
let match;
while ((match = interfaceRegex.exec(content)) !== null) {
const interfaceBody = match[2];
const propLines = interfaceBody
.split('\n')
.map((line) => line.trim())
.filter((line) => line);
for (const line of propLines) {
// Skip comments and empty lines
if (
line.startsWith('//') ||
line.startsWith('*') ||
!line.includes(':')
)
continue;
// Extract prop name and type
const propMatch = line.match(/(\w+)(\?)?\s*:\s*([^;,]+)/);
if (propMatch) {
const [, name, optional, type] = propMatch;
props.push({
name,
type: type.trim(),
optional: Boolean(optional),
});
}
}
}
return props;
}
private static extractInterfaces(content: string): string[] {
const interfaces: string[] = [];
const interfaceRegex = /(?:export\s+)?interface\s+(\w+)/g;
let match;
while ((match = interfaceRegex.exec(content)) !== null) {
interfaces.push(match[1]);
}
return interfaces;
}
private static extractVariants(
content: string,
): Record<string, string[]> | undefined {
// Look for CVA (class-variance-authority) variants
const cvaRegex = /cva\s*\([^,]*,\s*{[^}]*variants:\s*{([^}]*)}/s;
const match = cvaRegex.exec(content);
if (!match) return undefined;
const variantsSection = match[1];
const variants: Record<string, string[]> = {};
// Extract each variant category
const variantRegex = /(\w+):\s*{([^}]*)}/g;
let variantMatch;
while ((variantMatch = variantRegex.exec(variantsSection)) !== null) {
const [, variantName, variantOptions] = variantMatch;
const options: string[] = [];
// Extract option names
const optionRegex = /(\w+):/g;
let optionMatch;
while ((optionMatch = optionRegex.exec(variantOptions)) !== null) {
options.push(optionMatch[1]);
}
if (options.length > 0) {
variants[variantName] = options;
}
}
return Object.keys(variants).length > 0 ? variants : undefined;
}
static async getComponentContent(componentName: string): Promise<string> {
const packageJsonPath = join(
process.cwd(),
'packages',
'ui',
'package.json',
);
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
const exportPath = `./${componentName}`;
const filePath = packageJson.exports[exportPath];
if (!filePath) {
throw new Error(`Component "${componentName}" not found in exports`);
}
const fullPath = join(process.cwd(), 'packages', 'ui', filePath);
return readFile(fullPath, 'utf8');
}
private static determineCategory(
filePath: string,
): 'shadcn' | 'makerkit' | 'utils' {
if (filePath.includes('/shadcn/')) return 'shadcn';
if (filePath.includes('/makerkit/')) return 'makerkit';
return 'utils';
}
private static async generateDescription(
exportName: string,
_filePath: string,
category: 'shadcn' | 'makerkit' | 'utils',
): Promise<string> {
const componentName = exportName.replace('./', '');
if (category === 'shadcn') {
return this.getShadcnDescription(componentName);
} else if (category === 'makerkit') {
return this.getMakerkitDescription(componentName);
} else {
return this.getUtilsDescription(componentName);
}
}
private static getShadcnDescription(componentName: string): string {
const descriptions: Record<string, string> = {
accordion:
'A vertically stacked set of interactive headings that each reveal a section of content',
'alert-dialog':
'A modal dialog that interrupts the user with important content and expects a response',
alert:
'Displays a callout for user attention with different severity levels',
avatar: 'An image element with a fallback for representing the user',
badge: 'A small status descriptor for UI elements',
breadcrumb:
'Displays the path to the current resource using a hierarchy of links',
button: 'Displays a button or a component that looks like a button',
calendar:
'A date field component that allows users to enter and edit date',
card: 'Displays a card with header, content, and footer',
chart: 'A collection of chart components built on top of Recharts',
checkbox:
'A control that allows the user to toggle between checked and not checked',
collapsible: 'An interactive component which can be expanded/collapsed',
command: 'A fast, composable, unstyled command menu for React',
'data-table': 'A powerful table component built on top of TanStack Table',
dialog:
'A window overlaid on either the primary window or another dialog window',
'dropdown-menu': 'Displays a menu to the user triggered by a button',
form: 'Building forms with validation and error handling',
heading: 'Typography component for displaying headings',
input:
'Displays a form input field or a component that looks like an input field',
'input-otp':
'Accessible one-time password component with copy paste functionality',
label: 'Renders an accessible label associated with controls',
'navigation-menu': 'A collection of links for navigating websites',
popover: 'Displays rich content in a portal, triggered by a button',
progress:
'Displays an indicator showing the completion progress of a task',
'radio-group':
'A set of checkable buttons where no more than one can be checked at a time',
'scroll-area':
'Augments native scroll functionality for custom, cross-browser styling',
select: 'Displays a list of options for the user to pick from',
separator: 'Visually or semantically separates content',
sheet:
'Extends the Dialog component to display content that complements the main content of the screen',
sidebar: 'A collapsible sidebar component with navigation',
skeleton: 'Use to show a placeholder while content is loading',
slider:
'An input where the user selects a value from within a given range',
sonner: 'An opinionated toast component for React',
switch:
'A control that allows the user to toggle between checked and not checked',
table: 'A responsive table component',
tabs: 'A set of layered sections of content - known as tab panels - that are displayed one at a time',
textarea:
'Displays a form textarea or a component that looks like a textarea',
tooltip:
'A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it',
};
return (
descriptions[componentName] || `Shadcn UI component: ${componentName}`
);
}
private static getMakerkitDescription(componentName: string): string {
const descriptions: Record<string, string> = {
if: 'Conditional rendering component that shows children only when condition is true',
trans:
'Internationalization component for translating text with interpolation support',
sidebar:
'Application sidebar component with navigation and collapsible functionality',
'bordered-navigation-menu':
'Navigation menu component with bordered styling',
spinner: 'Loading spinner component with customizable size and styling',
page: 'Page layout component that provides consistent structure and styling',
'image-uploader':
'Component for uploading and displaying images with drag-and-drop support',
'global-loader':
'Global loading indicator component for application-wide loading states',
'loading-overlay':
'Overlay component that shows loading state over content',
'profile-avatar':
'User profile avatar component with fallback and customization options',
'enhanced-data-table':
'Enhanced data table component with sorting, filtering, and pagination (best table component)',
'language-selector':
'Component for selecting application language/locale',
stepper: 'Step-by-step navigation component for multi-step processes',
'card-button': 'Clickable card component that acts as a button',
'multi-step-form':
'Multi-step form component with validation and navigation',
'app-breadcrumbs': 'Application breadcrumb navigation component',
'empty-state':
'Component for displaying empty states with customizable content',
marketing: 'Collection of marketing-focused components and layouts',
'file-uploader':
'File upload component with drag-and-drop and preview functionality',
};
return (
descriptions[componentName] ||
`Makerkit custom component: ${componentName}`
);
}
private static getUtilsDescription(componentName: string): string {
const descriptions: Record<string, string> = {
utils:
'Utility functions for styling, class management, and common operations',
'navigation-schema': 'Schema and types for navigation configuration',
};
return descriptions[componentName] || `Utility module: ${componentName}`;
}
}
export function registerComponentsTools(server: McpServer) {
createGetComponentsTool(server);
createGetComponentContentTool(server);
createComponentsSearchTool(server);
createGetComponentPropsTool(server);
}
function createGetComponentsTool(server: McpServer) {
return server.tool(
'get_components',
'Get all available UI components from the @kit/ui package with descriptions',
async () => {
const components = await ComponentsTool.getComponents();
const componentsList = components
.map(
(component) =>
`${component.name} (${component.category}): ${component.description}`,
)
.join('\n');
return {
content: [
{
type: 'text',
text: componentsList,
},
],
};
},
);
}
function createGetComponentContentTool(server: McpServer) {
return server.tool(
'get_component_content',
'Get the source code content of a specific UI component',
{
state: z.object({
componentName: z.string(),
}),
},
async ({ state }) => {
const content = await ComponentsTool.getComponentContent(
state.componentName,
);
return {
content: [
{
type: 'text',
text: content,
},
],
};
},
);
}
function createComponentsSearchTool(server: McpServer) {
return server.tool(
'components_search',
'Search UI components by keyword in name, description, or category',
{
state: z.object({
query: z.string(),
}),
},
async ({ state }) => {
const components = await ComponentsTool.searchComponents(state.query);
if (components.length === 0) {
return {
content: [
{
type: 'text',
text: `No components found matching "${state.query}"`,
},
],
};
}
const componentsList = components
.map(
(component) =>
`${component.name} (${component.category}): ${component.description}`,
)
.join('\n');
return {
content: [
{
type: 'text',
text: `Found ${components.length} components matching "${state.query}":\n\n${componentsList}`,
},
],
};
},
);
}
function createGetComponentPropsTool(server: McpServer) {
return server.tool(
'get_component_props',
'Extract component props, interfaces, and variants from a UI component',
{
state: z.object({
componentName: z.string(),
}),
},
async ({ state }) => {
const propsInfo = await ComponentsTool.getComponentProps(
state.componentName,
);
let result = `Component: ${propsInfo.componentName}\n\n`;
if (propsInfo.interfaces.length > 0) {
result += `Interfaces: ${propsInfo.interfaces.join(', ')}\n\n`;
}
if (propsInfo.props.length > 0) {
result += `Props:\n`;
propsInfo.props.forEach((prop) => {
const optional = prop.optional ? '?' : '';
result += ` - ${prop.name}${optional}: ${prop.type}\n`;
});
result += '\n';
}
if (propsInfo.variants) {
result += `Variants (CVA):\n`;
Object.entries(propsInfo.variants).forEach(([variantName, options]) => {
result += ` - ${variantName}: ${options.join(' | ')}\n`;
});
result += '\n';
}
if (propsInfo.props.length === 0 && !propsInfo.variants) {
result +=
'No props or variants found. This might be a simple component or utility.';
}
return {
content: [
{
type: 'text',
text: result,
},
],
};
},
);
}

View File

@@ -0,0 +1,706 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { readFile, readdir, stat } from 'node:fs/promises';
import { join } from 'node:path';
import { z } from 'zod';
interface DatabaseFunction {
name: string;
parameters: Array<{
name: string;
type: string;
defaultValue?: string;
}>;
returnType: string;
description: string;
purpose: string;
securityLevel: 'definer' | 'invoker';
schema: string;
sourceFile: string;
}
interface SchemaFile {
name: string;
path: string;
description: string;
section: string;
lastModified: Date;
tables: string[];
functions: string[];
dependencies: string[];
topic: string;
}
export class DatabaseTool {
static async getSchemaFiles(): Promise<SchemaFile[]> {
const schemasPath = join(
process.cwd(),
'apps',
'web',
'supabase',
'schemas',
);
const files = await readdir(schemasPath);
const schemaFiles: SchemaFile[] = [];
for (const file of files.filter((f) => f.endsWith('.sql'))) {
const filePath = join(schemasPath, file);
const content = await readFile(filePath, 'utf8');
const stats = await stat(filePath);
// Extract section and description from the file header
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);
const topic = this.determineTopic(file, content);
schemaFiles.push({
name: file,
path: filePath,
section: sectionMatch?.[1]?.trim() || 'Unknown',
description:
descriptionMatch?.[1]?.trim() || 'No description available',
lastModified: stats.mtime,
tables,
functions,
dependencies,
topic,
});
}
return schemaFiles.sort((a, b) => a.name.localeCompare(b.name));
}
static async getFunctions(): Promise<DatabaseFunction[]> {
const schemaFiles = await this.getSchemaFiles();
const functions: DatabaseFunction[] = [];
for (const schemaFile of schemaFiles) {
const content = await readFile(schemaFile.path, 'utf8');
const fileFunctions = this.extractFunctionsFromContent(
content,
schemaFile.name,
);
functions.push(...fileFunctions);
}
return functions.sort((a, b) => a.name.localeCompare(b.name));
}
static async getFunctionDetails(
functionName: string,
): Promise<DatabaseFunction> {
const functions = await this.getFunctions();
const func = functions.find((f) => f.name === functionName);
if (!func) {
throw new Error(`Function "${functionName}" not found`);
}
return func;
}
static async searchFunctions(query: string): Promise<DatabaseFunction[]> {
const allFunctions = await this.getFunctions();
const searchTerm = query.toLowerCase();
return allFunctions.filter((func) => {
return (
func.name.toLowerCase().includes(searchTerm) ||
func.description.toLowerCase().includes(searchTerm) ||
func.purpose.toLowerCase().includes(searchTerm) ||
func.returnType.toLowerCase().includes(searchTerm)
);
});
}
static async getSchemaContent(fileName: string): Promise<string> {
const schemasPath = join(
process.cwd(),
'apps',
'web',
'supabase',
'schemas',
);
const filePath = join(schemasPath, fileName);
try {
return await readFile(filePath, 'utf8');
} catch (error) {
throw new Error(`Schema file "${fileName}" not found`);
}
}
static async getSchemasByTopic(topic: string): Promise<SchemaFile[]> {
const allSchemas = await this.getSchemaFiles();
const searchTerm = topic.toLowerCase();
return allSchemas.filter((schema) => {
return (
schema.topic.toLowerCase().includes(searchTerm) ||
schema.section.toLowerCase().includes(searchTerm) ||
schema.description.toLowerCase().includes(searchTerm) ||
schema.name.toLowerCase().includes(searchTerm)
);
});
}
static async getSchemaBySection(section: string): Promise<SchemaFile | null> {
const allSchemas = await this.getSchemaFiles();
return (
allSchemas.find(
(schema) => schema.section.toLowerCase() === section.toLowerCase(),
) || null
);
}
private static extractFunctionsFromContent(
content: string,
sourceFile: string,
): DatabaseFunction[] {
const functions: DatabaseFunction[] = [];
// Updated regex to capture function definitions with optional "or replace"
const functionRegex =
/create\s+(?:or\s+replace\s+)?function\s+([a-zA-Z_][a-zA-Z0-9_.]*)\s*\(([^)]*)\)\s*returns?\s+([^;\n]+)(?:\s+language\s+\w+)?(?:\s+security\s+(definer|invoker))?[^$]*?\$\$([^$]*)\$\$/gi;
let match;
while ((match = functionRegex.exec(content)) !== null) {
const [, fullName, params, returnType, securityLevel, body] = match;
if (!fullName || !returnType) continue;
// Extract schema and function name
const nameParts = fullName.split('.');
const functionName = nameParts[nameParts.length - 1];
const schema = nameParts.length > 1 ? nameParts[0] : 'public';
// Parse parameters
const parameters = this.parseParameters(params || '');
// Extract description and purpose from comments before function
const functionIndex = match.index || 0;
const beforeFunction = content.substring(
Math.max(0, functionIndex - 500),
functionIndex,
);
const description = this.extractDescription(beforeFunction, body || '');
const purpose = this.extractPurpose(description, functionName);
functions.push({
name: functionName,
parameters,
returnType: returnType.trim(),
description,
purpose,
securityLevel: (securityLevel as 'definer' | 'invoker') || 'invoker',
schema,
sourceFile,
});
}
return functions;
}
private static parseParameters(paramString: string): Array<{
name: string;
type: string;
defaultValue?: string;
}> {
if (!paramString.trim()) return [];
const parameters: Array<{
name: string;
type: string;
defaultValue?: string;
}> = [];
// Split by comma, but be careful of nested types
const params = paramString.split(',');
for (const param of params) {
const cleaned = param.trim();
if (!cleaned) continue;
// Match parameter pattern: name type [default value]
const paramMatch = cleaned.match(
/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s+([^=\s]+)(?:\s+default\s+(.+))?\s*$/i,
);
if (paramMatch) {
const [, name, type, defaultValue] = paramMatch;
if (name && type) {
parameters.push({
name: name.trim(),
type: type.trim(),
defaultValue: defaultValue?.trim(),
});
}
}
}
return parameters;
}
private static extractDescription(
beforeFunction: string,
body: string,
): string {
// Look for comments before the function
const commentMatch = beforeFunction.match(/--\s*(.+?)(?:\n|$)/);
if (commentMatch?.[1]) {
return commentMatch[1].trim();
}
// Look for comments inside the function body
const bodyCommentMatch = body.match(/--\s*(.+?)(?:\n|$)/);
if (bodyCommentMatch?.[1]) {
return bodyCommentMatch[1].trim();
}
return 'No description available';
}
private static extractPurpose(
description: string,
functionName: string,
): string {
// Map function names to purposes
const purposeMap: Record<string, string> = {
create_nonce:
'Create one-time authentication tokens for secure operations',
verify_nonce: 'Verify and consume one-time tokens for authentication',
is_mfa_compliant:
'Check if user has completed multi-factor authentication',
team_account_workspace:
'Load comprehensive team account data with permissions',
has_role_on_account: 'Check if user has access to a specific account',
has_permission: 'Verify user permissions for specific account operations',
get_user_billing_account: 'Retrieve billing account information for user',
create_team_account: 'Create new team account with proper permissions',
invite_user_to_account: 'Send invitation to join team account',
accept_invitation: 'Process and accept team invitation',
transfer_account_ownership: 'Transfer account ownership between users',
delete_account: 'Safely delete account and associated data',
};
if (purposeMap[functionName]) {
return purposeMap[functionName];
}
// Analyze function name for purpose hints
if (functionName.includes('create'))
return 'Create database records with validation';
if (functionName.includes('delete') || functionName.includes('remove'))
return 'Delete records with proper authorization';
if (functionName.includes('update') || functionName.includes('modify'))
return 'Update existing records with validation';
if (functionName.includes('get') || functionName.includes('fetch'))
return 'Retrieve data with access control';
if (functionName.includes('verify') || functionName.includes('validate'))
return 'Validate data or permissions';
if (functionName.includes('check') || functionName.includes('is_'))
return 'Check conditions or permissions';
if (functionName.includes('invite'))
return 'Handle user invitations and access';
if (functionName.includes('transfer'))
return 'Transfer ownership or data between entities';
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 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;
while ((match = functionRegex.exec(content)) !== null) {
if (match[1]) {
functions.push(match[1]);
}
}
return [...new Set(functions)]; // Remove duplicates
}
private static extractDependencies(content: string): string[] {
const dependencies: string[] = [];
// 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
}
private static determineTopic(fileName: string, content: string): string {
// Map file names to topics
const fileTopicMap: Record<string, string> = {
'00-privileges.sql': 'security',
'01-enums.sql': 'types',
'02-config.sql': 'configuration',
'03-accounts.sql': 'accounts',
'04-roles.sql': 'permissions',
'05-memberships.sql': 'teams',
'06-roles-permissions.sql': 'permissions',
'07-invitations.sql': 'teams',
'08-billing-customers.sql': 'billing',
'09-subscriptions.sql': 'billing',
'10-orders.sql': 'billing',
'11-notifications.sql': 'notifications',
'12-one-time-tokens.sql': 'auth',
'13-mfa.sql': 'auth',
'14-super-admin.sql': 'admin',
'15-account-views.sql': 'accounts',
'16-storage.sql': 'storage',
'17-roles-seed.sql': 'permissions',
};
if (fileTopicMap[fileName]) {
return fileTopicMap[fileName];
}
// Analyze content for topic hints
const contentLower = content.toLowerCase();
if (contentLower.includes('account') && contentLower.includes('team'))
return 'accounts';
if (
contentLower.includes('subscription') ||
contentLower.includes('billing')
)
return 'billing';
if (
contentLower.includes('auth') ||
contentLower.includes('mfa') ||
contentLower.includes('token')
)
return 'auth';
if (contentLower.includes('permission') || contentLower.includes('role'))
return 'permissions';
if (contentLower.includes('notification') || contentLower.includes('email'))
return 'notifications';
if (contentLower.includes('storage') || contentLower.includes('bucket'))
return 'storage';
if (contentLower.includes('admin') || contentLower.includes('super'))
return 'admin';
return 'general';
}
}
export function registerDatabaseTools(server: McpServer) {
createGetSchemaFilesTool(server);
createGetSchemaContentTool(server);
createGetSchemasByTopicTool(server);
createGetSchemaBySectionTool(server);
createGetFunctionsTool(server);
createGetFunctionDetailsTool(server);
createSearchFunctionsTool(server);
}
function createGetSchemaFilesTool(server: McpServer) {
return server.tool(
'get_schema_files',
'🔥 DATABASE SCHEMA FILES (SOURCE OF TRUTH - ALWAYS CURRENT) - Use these over migrations!',
async () => {
const schemaFiles = await DatabaseTool.getSchemaFiles();
const filesList = schemaFiles
.map((file) => {
const tablesInfo =
file.tables.length > 0
? ` | Tables: ${file.tables.join(', ')}`
: '';
const functionsInfo =
file.functions.length > 0
? ` | Functions: ${file.functions.join(', ')}`
: '';
return `${file.name} (${file.topic}): ${file.section} - ${file.description}${tablesInfo}${functionsInfo}`;
})
.join('\n');
return {
content: [
{
type: 'text',
text: `🔥 DATABASE SCHEMA FILES (ALWAYS UP TO DATE)\n\nThese files represent the current database state. Use these instead of migrations for current schema understanding.\n\n${filesList}`,
},
],
};
},
);
}
function createGetFunctionsTool(server: McpServer) {
return server.tool(
'get_database_functions',
'Get all database functions with descriptions and usage guidance',
async () => {
const functions = await DatabaseTool.getFunctions();
const functionsList = functions
.map((func) => {
const security =
func.securityLevel === 'definer' ? ' [SECURITY DEFINER]' : '';
const params = func.parameters
.map((p) => {
const defaultVal = p.defaultValue ? ` = ${p.defaultValue}` : '';
return `${p.name}: ${p.type}${defaultVal}`;
})
.join(', ');
return `${func.name}(${params}) <20> ${func.returnType}${security}\n Purpose: ${func.purpose}\n Source: ${func.sourceFile}`;
})
.join('\n\n');
return {
content: [
{
type: 'text',
text: `Database Functions:\n\n${functionsList}`,
},
],
};
},
);
}
function createGetFunctionDetailsTool(server: McpServer) {
return server.tool(
'get_function_details',
'Get detailed information about a specific database function',
{
state: z.object({
functionName: z.string(),
}),
},
async ({ state }) => {
const func = await DatabaseTool.getFunctionDetails(state.functionName);
const params =
func.parameters.length > 0
? func.parameters
.map((p) => {
const defaultVal = p.defaultValue
? ` (default: ${p.defaultValue})`
: '';
return ` - ${p.name}: ${p.type}${defaultVal}`;
})
.join('\n')
: ' No parameters';
const securityNote =
func.securityLevel === 'definer'
? '\n<> SECURITY DEFINER: This function runs with elevated privileges and bypasses RLS.'
: '\n SECURITY INVOKER: This function inherits caller permissions and respects RLS.';
return {
content: [
{
type: 'text',
text: `Function: ${func.schema}.${func.name}
Purpose: ${func.purpose}
Description: ${func.description}
Return Type: ${func.returnType}
Security Level: ${func.securityLevel}${securityNote}
Parameters:
${params}
Source File: ${func.sourceFile}`,
},
],
};
},
);
}
function createSearchFunctionsTool(server: McpServer) {
return server.tool(
'search_database_functions',
'Search database functions by name, description, or purpose',
{
state: z.object({
query: z.string(),
}),
},
async ({ state }) => {
const functions = await DatabaseTool.searchFunctions(state.query);
if (functions.length === 0) {
return {
content: [
{
type: 'text',
text: `No database functions found matching "${state.query}"`,
},
],
};
}
const functionsList = functions
.map((func) => {
const security = func.securityLevel === 'definer' ? ' [DEFINER]' : '';
return `${func.name}${security}: ${func.purpose}`;
})
.join('\n');
return {
content: [
{
type: 'text',
text: `Found ${functions.length} functions matching "${state.query}":\n\n${functionsList}`,
},
],
};
},
);
}
function createGetSchemaContentTool(server: McpServer) {
return server.tool(
'get_schema_content',
'📋 Get raw schema file content (CURRENT DATABASE STATE) - Source of truth for database structure',
{
state: z.object({
fileName: z.string(),
}),
},
async ({ state }) => {
const content = await DatabaseTool.getSchemaContent(state.fileName);
return {
content: [
{
type: 'text',
text: `📋 SCHEMA FILE: ${state.fileName} (CURRENT STATE)\n\n${content}`,
},
],
};
},
);
}
function createGetSchemasByTopicTool(server: McpServer) {
return server.tool(
'get_schemas_by_topic',
'🎯 Find schema files by topic (accounts, auth, billing, permissions, etc.) - Fastest way to find relevant schemas',
{
state: z.object({
topic: z.string(),
}),
},
async ({ state }) => {
const schemas = await DatabaseTool.getSchemasByTopic(state.topic);
if (schemas.length === 0) {
return {
content: [
{
type: 'text',
text: `No schema files found for topic "${state.topic}". Available topics: accounts, auth, billing, permissions, teams, notifications, storage, admin, security, types, configuration.`,
},
],
};
}
const schemasList = schemas
.map((schema) => {
const tablesInfo =
schema.tables.length > 0
? `\n Tables: ${schema.tables.join(', ')}`
: '';
const functionsInfo =
schema.functions.length > 0
? `\n Functions: ${schema.functions.join(', ')}`
: '';
return `${schema.name}: ${schema.description}${tablesInfo}${functionsInfo}`;
})
.join('\n\n');
return {
content: [
{
type: 'text',
text: `🎯 SCHEMAS FOR TOPIC: "${state.topic}"\n\n${schemasList}`,
},
],
};
},
);
}
function createGetSchemaBySectionTool(server: McpServer) {
return server.tool(
'get_schema_by_section',
'📂 Get specific schema by section name (Accounts, Permissions, etc.) - Direct access to schema sections',
{
state: z.object({
section: z.string(),
}),
},
async ({ state }) => {
const schema = await DatabaseTool.getSchemaBySection(state.section);
if (!schema) {
return {
content: [
{
type: 'text',
text: `No schema found for section "${state.section}". Use get_schema_files to see available sections.`,
},
],
};
}
const tablesInfo =
schema.tables.length > 0 ? `\nTables: ${schema.tables.join(', ')}` : '';
const functionsInfo =
schema.functions.length > 0
? `\nFunctions: ${schema.functions.join(', ')}`
: '';
const dependenciesInfo =
schema.dependencies.length > 0
? `\nDependencies: ${schema.dependencies.join(', ')}`
: '';
return {
content: [
{
type: 'text',
text: `📂 SCHEMA SECTION: ${schema.section}\n\nFile: ${schema.name}\nTopic: ${schema.topic}\nDescription: ${schema.description}${tablesInfo}${functionsInfo}${dependenciesInfo}\n\nLast Modified: ${schema.lastModified.toISOString()}`,
},
],
};
},
);
}

View File

@@ -0,0 +1,122 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { exec } from 'node:child_process';
import { readFile, readdir } from 'node:fs/promises';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { z } from 'zod';
export class MigrationsTool {
static GetMigrations() {
return readdir(
join(process.cwd(), 'apps', 'web', 'supabase', 'migrations'),
);
}
static getMigrationContent(path: string) {
return readFile(
join(process.cwd(), 'apps', 'web', 'supabase', 'migrations', path),
'utf8',
);
}
static CreateMigration(path: string) {
return promisify(exec)(`supabase migration new ${path}`);
}
static Diff() {
return promisify(exec)(`supabase migration diff`);
}
}
export function registerGetMigrationsTools(server: McpServer) {
createGetMigrationsTool(server);
createGetMigrationContentTool(server);
createCreateMigrationTool(server);
createDiffMigrationTool(server);
}
function createDiffMigrationTool(server: McpServer) {
return server.tool(
'diff_migrations',
'Compare differences between the declarative schemas and the applied migrations in Supabase',
async () => {
const { stdout } = await MigrationsTool.Diff();
return {
content: [
{
type: 'text',
text: stdout,
},
],
};
},
);
}
function createCreateMigrationTool(server: McpServer) {
return server.tool(
'create_migration',
'Create a new Supabase Postgres migration file',
{
state: z.object({
name: z.string(),
}),
},
async ({ state }) => {
const { stdout } = await MigrationsTool.CreateMigration(state.name);
return {
content: [
{
type: 'text',
text: stdout,
},
],
};
},
);
}
function createGetMigrationContentTool(server: McpServer) {
return server.tool(
'get_migration_content',
'📜 Get migration file content (HISTORICAL) - For current state use get_schema_content instead',
{
state: z.object({
path: z.string(),
}),
},
async ({ state }) => {
const content = await MigrationsTool.getMigrationContent(state.path);
return {
content: [
{
type: 'text',
text: `📜 MIGRATION FILE: ${state.path} (HISTORICAL)\n\nNote: This shows historical changes. For current database state, use get_schema_content instead.\n\n${content}`,
},
],
};
},
);
}
function createGetMigrationsTool(server: McpServer) {
return server.tool(
'get_migrations',
'📜 Get migration files (HISTORICAL CHANGES) - Use schema files for current state instead',
async () => {
const migrations = await MigrationsTool.GetMigrations();
return {
content: [
{
type: 'text',
text: `📜 MIGRATION FILES (HISTORICAL CHANGES)\n\nNote: For current database state, use get_schema_files instead. Migrations show historical changes.\n\n${migrations.join('\n')}`,
},
],
};
},
);
}

View File

@@ -0,0 +1,323 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { z } from 'zod';
interface ScriptInfo {
name: string;
command: string;
category:
| 'development'
| 'build'
| 'testing'
| 'linting'
| 'database'
| 'maintenance'
| 'environment';
description: string;
usage: string;
importance: 'critical' | 'high' | 'medium' | 'low';
healthcheck?: boolean;
}
export class ScriptsTool {
static async getScripts(): Promise<ScriptInfo[]> {
const packageJsonPath = join(process.cwd(), 'package.json');
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
const scripts: ScriptInfo[] = [];
for (const [scriptName, command] of Object.entries(packageJson.scripts)) {
if (typeof command === 'string') {
const scriptInfo = this.getScriptInfo(scriptName, command);
scripts.push(scriptInfo);
}
}
return scripts.sort((a, b) => {
const importanceOrder = { critical: 0, high: 1, medium: 2, low: 3 };
return importanceOrder[a.importance] - importanceOrder[b.importance];
});
}
static async getScriptDetails(scriptName: string): Promise<ScriptInfo> {
const packageJsonPath = join(process.cwd(), 'package.json');
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
const command = packageJson.scripts[scriptName];
if (!command) {
throw new Error(`Script "${scriptName}" not found`);
}
return this.getScriptInfo(scriptName, command);
}
private static getScriptInfo(
scriptName: string,
command: string,
): ScriptInfo {
const scriptDescriptions: Record<
string,
Omit<ScriptInfo, 'name' | 'command'>
> = {
dev: {
category: 'development',
description:
'Start development servers for all applications in parallel with hot reloading',
usage: 'Run this to start developing. Opens web app on port 3000.',
importance: 'medium',
},
build: {
category: 'build',
description:
'Build all applications and packages for production deployment',
usage:
'Use before deploying to production. Ensures all code compiles correctly.',
importance: 'medium',
},
typecheck: {
category: 'linting',
description:
'Run TypeScript compiler to check for type errors across all packages',
usage:
'CRITICAL: Run after writing code to ensure type safety. Must pass before commits.',
importance: 'critical',
healthcheck: true,
},
lint: {
category: 'linting',
description:
'Run ESLint to check code quality and enforce coding standards',
usage:
'CRITICAL: Run after writing code to ensure code quality. Must pass before commits.',
importance: 'medium',
healthcheck: true,
},
'lint:fix': {
category: 'linting',
description:
'Run ESLint with auto-fix to automatically resolve fixable issues',
usage:
'Use to automatically fix linting issues. Run before manual fixes.',
importance: 'high',
healthcheck: true,
},
format: {
category: 'linting',
description: 'Check code formatting with Prettier across all files',
usage: 'Verify code follows consistent formatting standards.',
importance: 'high',
},
'format:fix': {
category: 'linting',
description:
'Auto-format all code with Prettier to ensure consistent styling',
usage: 'Use to automatically format code. Run before commits.',
importance: 'high',
healthcheck: true,
},
test: {
category: 'testing',
description: 'Run all test suites across the monorepo',
usage: 'Execute to verify functionality. Should pass before commits.',
importance: 'high',
healthcheck: true,
},
'supabase:web:start': {
category: 'database',
description: 'Start local Supabase instance for development',
usage: 'Required for local development with database access.',
importance: 'critical',
},
'supabase:web:stop': {
category: 'database',
description: 'Stop the local Supabase instance',
usage: 'Use when done developing to free up resources.',
importance: 'medium',
},
'supabase:web:reset': {
category: 'database',
description: 'Reset local database to latest schema and seed data',
usage: 'Use when database state is corrupted or needs fresh start.',
importance: 'high',
},
'supabase:web:typegen': {
category: 'database',
description: 'Generate TypeScript types from Supabase database schema',
usage: 'Run after database schema changes to update types.',
importance: 'high',
},
'supabase:web:test': {
category: 'testing',
description: 'Run Supabase-specific tests',
usage: 'Test database functions, RLS policies, and migrations.',
importance: 'high',
},
clean: {
category: 'maintenance',
description: 'Remove all generated files and dependencies',
usage:
'Use when build artifacts are corrupted. Requires reinstall after.',
importance: 'medium',
},
'clean:workspaces': {
category: 'maintenance',
description: 'Clean all workspace packages using Turbo',
usage: 'Lighter cleanup that preserves node_modules.',
importance: 'medium',
},
'stripe:listen': {
category: 'development',
description: 'Start Stripe webhook listener for local development',
usage: 'Required when testing payment workflows locally.',
importance: 'medium',
},
'env:generate': {
category: 'environment',
description: 'Generate environment variable templates',
usage: 'Creates .env templates for new environments.',
importance: 'low',
},
'env:validate': {
category: 'environment',
description: 'Validate environment variables against schema',
usage: 'Ensures all required environment variables are properly set.',
importance: 'medium',
},
update: {
category: 'maintenance',
description: 'Update all dependencies across the monorepo',
usage: 'Keep dependencies current. Test thoroughly after updating.',
importance: 'low',
},
'syncpack:list': {
category: 'maintenance',
description: 'List dependency version mismatches across packages',
usage: 'Identify inconsistent package versions in monorepo.',
importance: 'low',
},
'syncpack:fix': {
category: 'maintenance',
description: 'Fix dependency version mismatches across packages',
usage: 'Automatically align package versions across workspaces.',
importance: 'low',
},
};
const scriptInfo = scriptDescriptions[scriptName] || {
category: 'maintenance' as const,
description: `Custom script: ${scriptName}`,
usage: 'See package.json for command details.',
importance: 'low' as const,
};
return {
name: scriptName,
command,
...scriptInfo,
};
}
static getHealthcheckScripts(): ScriptInfo[] {
const allScripts = ['typecheck', 'lint', 'lint:fix', 'format:fix', 'test'];
return allScripts.map((scriptName) =>
this.getScriptInfo(scriptName, `[healthcheck] ${scriptName}`),
);
}
}
export function registerScriptsTools(server: McpServer) {
createGetScriptsTool(server);
createGetScriptDetailsTool(server);
createGetHealthcheckScriptsTool(server);
}
function createGetScriptsTool(server: McpServer) {
return server.tool(
'get_scripts',
'Get all available npm/pnpm scripts with descriptions and usage guidance',
async () => {
const scripts = await ScriptsTool.getScripts();
const scriptsList = scripts
.map((script) => {
const healthcheck = script.healthcheck ? ' [HEALTHCHECK]' : '';
return `${script.name} (${script.category})${healthcheck}: ${script.description}\n Usage: ${script.usage}`;
})
.join('\n\n');
return {
content: [
{
type: 'text',
text: `Available Scripts (sorted by importance):\n\n${scriptsList}`,
},
],
};
},
);
}
function createGetScriptDetailsTool(server: McpServer) {
return server.tool(
'get_script_details',
'Get detailed information about a specific script',
{
state: z.object({
scriptName: z.string(),
}),
},
async ({ state }) => {
const script = await ScriptsTool.getScriptDetails(state.scriptName);
const healthcheck = script.healthcheck
? '\n<> HEALTHCHECK SCRIPT: This script should be run after writing code to ensure quality.'
: '';
return {
content: [
{
type: 'text',
text: `Script: ${script.name}
Command: ${script.command}
Category: ${script.category}
Importance: ${script.importance}
Description: ${script.description}
Usage: ${script.usage}${healthcheck}`,
},
],
};
},
);
}
function createGetHealthcheckScriptsTool(server: McpServer) {
return server.tool(
'get_healthcheck_scripts',
'Get critical scripts that should be run after writing code (typecheck, lint, format, test)',
async () => {
const scripts = await ScriptsTool.getScripts();
const healthcheckScripts = scripts.filter((script) => script.healthcheck);
const scriptsList = healthcheckScripts
.map((script) => `pnpm ${script.name}: ${script.usage}`)
.join('\n');
return {
content: [
{
type: 'text',
text: `<<3C> CODE HEALTHCHECK SCRIPTS
These scripts MUST be run after writing code to ensure quality:
${scriptsList}
<EFBFBD> IMPORTANT: Always run these scripts before considering your work complete. They catch type errors, code quality issues, and ensure consistent formatting.`,
},
],
};
},
);
}

391
packages/mcp-server/test.ts Normal file
View File

@@ -0,0 +1,391 @@
import { ComponentsTool } from './src/tools/components';
import { DatabaseTool } from './src/tools/database';
import { MigrationsTool } from './src/tools/migrations';
import { ScriptsTool } from './src/tools/scripts';
console.log('=== Testing MigrationsTool ===');
console.log(await MigrationsTool.GetMigrations());
console.log(
await MigrationsTool.getMigrationContent('20240319163440_roles-seed.sql'),
);
console.log('\n=== Testing ComponentsTool ===');
console.log('\n--- Getting all components ---');
const components = await ComponentsTool.getComponents();
console.log(`Found ${components.length} components:`);
components.slice(0, 5).forEach((component) => {
console.log(
`- ${component.name} (${component.category}): ${component.description}`,
);
});
console.log('...');
console.log('\n--- Testing component content retrieval ---');
try {
const buttonContent = await ComponentsTool.getComponentContent('button');
console.log('Button component content length:', buttonContent.length);
console.log('First 200 characters:', buttonContent.substring(0, 200));
} catch (error) {
console.error('Error getting button component:', error);
}
console.log('\n--- Testing component filtering by category ---');
const shadcnComponents = components.filter((c) => c.category === 'shadcn');
const makerkitComponents = components.filter((c) => c.category === 'makerkit');
const utilsComponents = components.filter((c) => c.category === 'utils');
console.log(`Shadcn components: ${shadcnComponents.length}`);
console.log(`Makerkit components: ${makerkitComponents.length}`);
console.log(`Utils components: ${utilsComponents.length}`);
console.log('\n--- Sample components by category ---');
console.log(
'Shadcn:',
shadcnComponents
.slice(0, 3)
.map((c) => c.name)
.join(', '),
);
console.log(
'Makerkit:',
makerkitComponents
.slice(0, 3)
.map((c) => c.name)
.join(', '),
);
console.log('Utils:', utilsComponents.map((c) => c.name).join(', '));
console.log('\n--- Testing error handling ---');
try {
await ComponentsTool.getComponentContent('non-existent-component');
} catch (error) {
console.log(
'Expected error for non-existent component:',
error instanceof Error ? error.message : String(error),
);
}
console.log('\n=== Testing ScriptsTool ===');
console.log('\n--- Getting all scripts ---');
const scripts = await ScriptsTool.getScripts();
console.log(`Found ${scripts.length} scripts:`);
console.log('\n--- Critical and High importance scripts ---');
const importantScripts = scripts.filter(
(s) => s.importance === 'critical' || s.importance === 'high',
);
importantScripts.forEach((script) => {
const healthcheck = script.healthcheck ? ' [HEALTHCHECK]' : '';
console.log(
`- ${script.name} (${script.importance})${healthcheck}: ${script.description}`,
);
});
console.log('\n--- Healthcheck scripts (code quality) ---');
const healthcheckScripts = scripts.filter((s) => s.healthcheck);
console.log('Scripts that should be run after writing code:');
healthcheckScripts.forEach((script) => {
console.log(`- pnpm ${script.name}: ${script.usage}`);
});
console.log('\n--- Scripts by category ---');
const categories = [...new Set(scripts.map((s) => s.category))];
categories.forEach((category) => {
const categoryScripts = scripts.filter((s) => s.category === category);
console.log(`${category}: ${categoryScripts.map((s) => s.name).join(', ')}`);
});
console.log('\n--- Testing script details ---');
try {
const typecheckDetails = await ScriptsTool.getScriptDetails('typecheck');
console.log('Typecheck script details:');
console.log(` Command: ${typecheckDetails.command}`);
console.log(` Importance: ${typecheckDetails.importance}`);
console.log(` Healthcheck: ${typecheckDetails.healthcheck}`);
console.log(` Usage: ${typecheckDetails.usage}`);
} catch (error) {
console.error('Error getting typecheck details:', error);
}
console.log('\n--- Testing error handling for scripts ---');
try {
await ScriptsTool.getScriptDetails('non-existent-script');
} catch (error) {
console.log(
'Expected error for non-existent script:',
error instanceof Error ? error.message : String(error),
);
}
console.log('\n=== Testing New ComponentsTool Features ===');
console.log('\n--- Testing component search ---');
const buttonSearchResults = await ComponentsTool.searchComponents('button');
console.log(`Search for "button": ${buttonSearchResults.length} results`);
buttonSearchResults.forEach((component) => {
console.log(` - ${component.name}: ${component.description}`);
});
console.log('\n--- Testing search by category ---');
const shadcnSearchResults = await ComponentsTool.searchComponents('shadcn');
console.log(
`Search for "shadcn": ${shadcnSearchResults.length} results (showing first 3)`,
);
shadcnSearchResults.slice(0, 3).forEach((component) => {
console.log(` - ${component.name}`);
});
console.log('\n--- Testing search by description keyword ---');
const formSearchResults = await ComponentsTool.searchComponents('form');
console.log(`Search for "form": ${formSearchResults.length} results`);
formSearchResults.forEach((component) => {
console.log(` - ${component.name}: ${component.description}`);
});
console.log('\n--- Testing component props extraction ---');
try {
console.log('\n--- Button component props ---');
const buttonProps = await ComponentsTool.getComponentProps('button');
console.log(`Component: ${buttonProps.componentName}`);
console.log(`Interfaces: ${buttonProps.interfaces.join(', ')}`);
console.log(`Props (${buttonProps.props.length}):`);
buttonProps.props.forEach((prop) => {
const optional = prop.optional ? '?' : '';
console.log(` - ${prop.name}${optional}: ${prop.type}`);
});
if (buttonProps.variants) {
console.log('Variants:');
Object.entries(buttonProps.variants).forEach(([variantName, options]) => {
console.log(` - ${variantName}: ${options.join(' | ')}`);
});
}
} catch (error) {
console.error('Error getting button props:', error);
}
console.log('\n--- Testing simpler component props ---');
try {
const ifProps = await ComponentsTool.getComponentProps('if');
console.log(`Component: ${ifProps.componentName}`);
console.log(`Interfaces: ${ifProps.interfaces.join(', ')}`);
console.log(`Props count: ${ifProps.props.length}`);
if (ifProps.props.length > 0) {
ifProps.props.forEach((prop) => {
const optional = prop.optional ? '?' : '';
console.log(` - ${prop.name}${optional}: ${prop.type}`);
});
}
} catch (error) {
console.error('Error getting if component props:', error);
}
console.log('\n--- Testing search with no results ---');
const noResults = await ComponentsTool.searchComponents('xyz123nonexistent');
console.log(`Search for non-existent: ${noResults.length} results`);
console.log('\n--- Testing props extraction error handling ---');
try {
await ComponentsTool.getComponentProps('non-existent-component');
} catch (error) {
console.log(
'Expected error for non-existent component props:',
error instanceof Error ? error.message : String(error),
);
}
console.log('\n=== Testing DatabaseTool ===');
console.log('\n--- Getting schema files ---');
const schemaFiles = await DatabaseTool.getSchemaFiles();
console.log(`Found ${schemaFiles.length} schema files:`);
schemaFiles.slice(0, 5).forEach((file) => {
console.log(` - ${file.name}: ${file.section}`);
});
console.log('\n--- Getting database functions ---');
const dbFunctions = await DatabaseTool.getFunctions();
console.log(`Found ${dbFunctions.length} database functions:`);
dbFunctions.forEach((func) => {
const security = func.securityLevel === 'definer' ? ' [DEFINER]' : '';
console.log(` - ${func.name}${security}: ${func.purpose}`);
});
console.log('\n--- Testing function search ---');
const authFunctions = await DatabaseTool.searchFunctions('auth');
console.log(`Functions related to "auth": ${authFunctions.length}`);
authFunctions.forEach((func) => {
console.log(` - ${func.name}: ${func.purpose}`);
});
console.log('\n--- Testing function search by security ---');
const definerFunctions = await DatabaseTool.searchFunctions('definer');
console.log(`Functions with security definer: ${definerFunctions.length}`);
definerFunctions.forEach((func) => {
console.log(` - ${func.name}: ${func.purpose}`);
});
console.log('\n--- Testing function details ---');
if (dbFunctions.length > 0) {
try {
const firstFunction = dbFunctions[0];
if (firstFunction) {
const functionDetails = await DatabaseTool.getFunctionDetails(
firstFunction.name,
);
console.log(`Details for ${functionDetails.name}:`);
console.log(` Purpose: ${functionDetails.purpose}`);
console.log(` Return Type: ${functionDetails.returnType}`);
console.log(` Security: ${functionDetails.securityLevel}`);
console.log(` Parameters: ${functionDetails.parameters.length}`);
functionDetails.parameters.forEach((param) => {
const defaultVal = param.defaultValue
? ` (default: ${param.defaultValue})`
: '';
console.log(` - ${param.name}: ${param.type}${defaultVal}`);
});
}
} catch (error) {
console.error('Error getting function details:', error);
}
}
console.log('\n--- Testing function search with no results ---');
const noFunctionResults =
await DatabaseTool.searchFunctions('xyz123nonexistent');
console.log(
`Search for non-existent function: ${noFunctionResults.length} results`,
);
console.log('\n--- Testing function details error handling ---');
try {
await DatabaseTool.getFunctionDetails('non-existent-function');
} catch (error) {
console.log(
'Expected error for non-existent function:',
error instanceof Error ? error.message : String(error),
);
}
console.log('\n=== Testing Enhanced DatabaseTool Features ===');
console.log('\n--- Testing direct schema content access ---');
try {
const accountsSchemaContent =
await DatabaseTool.getSchemaContent('03-accounts.sql');
console.log('Accounts schema content length:', accountsSchemaContent.length);
console.log('First 200 characters:', accountsSchemaContent.substring(0, 200));
} catch (error) {
console.error(
'Error getting accounts schema content:',
error instanceof Error ? error.message : String(error),
);
}
console.log('\n--- Testing schema search by topic ---');
const authSchemas = await DatabaseTool.getSchemasByTopic('auth');
console.log(`Schemas related to "auth": ${authSchemas.length}`);
authSchemas.forEach((schema) => {
console.log(` - ${schema.name} (${schema.topic}): ${schema.section}`);
if (schema.functions.length > 0) {
console.log(` Functions: ${schema.functions.join(', ')}`);
}
});
console.log('\n--- Testing schema search by topic - billing ---');
const billingSchemas = await DatabaseTool.getSchemasByTopic('billing');
console.log(`Schemas related to "billing": ${billingSchemas.length}`);
billingSchemas.forEach((schema) => {
console.log(` - ${schema.name}: ${schema.description}`);
if (schema.tables.length > 0) {
console.log(` Tables: ${schema.tables.join(', ')}`);
}
});
console.log('\n--- Testing schema search by topic - accounts ---');
const accountSchemas = await DatabaseTool.getSchemasByTopic('accounts');
console.log(`Schemas related to "accounts": ${accountSchemas.length}`);
accountSchemas.forEach((schema) => {
console.log(` - ${schema.name}: ${schema.description}`);
if (schema.dependencies.length > 0) {
console.log(` Dependencies: ${schema.dependencies.join(', ')}`);
}
});
console.log('\n--- Testing schema by section lookup ---');
try {
const accountsSection = await DatabaseTool.getSchemaBySection('Accounts');
if (accountsSection) {
console.log(`Found section: ${accountsSection.section}`);
console.log(`File: ${accountsSection.name}`);
console.log(`Topic: ${accountsSection.topic}`);
console.log(`Tables: ${accountsSection.tables.join(', ')}`);
console.log(`Last modified: ${accountsSection.lastModified.toISOString()}`);
}
} catch (error) {
console.error('Error getting accounts section:', error);
}
console.log('\n--- Testing enhanced schema metadata ---');
const enhancedSchemas = await DatabaseTool.getSchemaFiles();
console.log(`Total schemas with metadata: ${enhancedSchemas.length}`);
// Show schemas with the most tables
const schemasWithTables = enhancedSchemas.filter((s) => s.tables.length > 0);
console.log(`Schemas with tables: ${schemasWithTables.length}`);
schemasWithTables.slice(0, 3).forEach((schema) => {
console.log(
` - ${schema.name}: ${schema.tables.length} tables (${schema.tables.join(', ')})`,
);
});
// Show schemas with functions
const schemasWithFunctions = enhancedSchemas.filter(
(s) => s.functions.length > 0,
);
console.log(`Schemas with functions: ${schemasWithFunctions.length}`);
schemasWithFunctions.slice(0, 3).forEach((schema) => {
console.log(
` - ${schema.name}: ${schema.functions.length} functions (${schema.functions.join(', ')})`,
);
});
// Show topic distribution
const topicCounts = enhancedSchemas.reduce(
(acc, schema) => {
acc[schema.topic] = (acc[schema.topic] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
console.log('\n--- Topic distribution ---');
Object.entries(topicCounts).forEach(([topic, count]) => {
console.log(` - ${topic}: ${count} files`);
});
console.log('\n--- Testing error handling for enhanced features ---');
try {
await DatabaseTool.getSchemaContent('non-existent-schema.sql');
} catch (error) {
console.log(
'Expected error for non-existent schema:',
error instanceof Error ? error.message : String(error),
);
}
try {
const nonExistentSection =
await DatabaseTool.getSchemaBySection('NonExistentSection');
console.log('Non-existent section result:', nonExistentSection);
} catch (error) {
console.error('Unexpected error for non-existent section:', error);
}
const emptyTopicResults =
await DatabaseTool.getSchemasByTopic('xyz123nonexistent');
console.log(
`Search for non-existent topic: ${emptyTopicResults.length} results`,
);

View File

@@ -0,0 +1,14 @@
{
"extends": "@kit/tsconfig/base.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"outDir": "./build",
"noEmit": false,
"strict": false,
"target": "ES2022",
"module": "commonjs",
"moduleResolution": "node"
},
"files": ["src/index.ts"],
"exclude": ["node_modules"]
}