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:
committed by
GitHub
parent
9fae142f2d
commit
533dfba5b9
1
packages/mcp-server/.gitignore
vendored
Normal file
1
packages/mcp-server/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
58
packages/mcp-server/README.md
Normal file
58
packages/mcp-server/README.md
Normal 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.
|
||||
3
packages/mcp-server/eslint.config.mjs
Normal file
3
packages/mcp-server/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import eslintConfigBase from '@kit/eslint-config/base.js';
|
||||
|
||||
export default eslintConfigBase;
|
||||
31
packages/mcp-server/package.json
Normal file
31
packages/mcp-server/package.json
Normal 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"
|
||||
}
|
||||
31
packages/mcp-server/src/index.ts
Normal file
31
packages/mcp-server/src/index.ts
Normal 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);
|
||||
});
|
||||
0
packages/mcp-server/src/server.ts
Normal file
0
packages/mcp-server/src/server.ts
Normal file
493
packages/mcp-server/src/tools/components.ts
Normal file
493
packages/mcp-server/src/tools/components.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
706
packages/mcp-server/src/tools/database.ts
Normal file
706
packages/mcp-server/src/tools/database.ts
Normal 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()}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
122
packages/mcp-server/src/tools/migrations.ts
Normal file
122
packages/mcp-server/src/tools/migrations.ts
Normal 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')}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
323
packages/mcp-server/src/tools/scripts.ts
Normal file
323
packages/mcp-server/src/tools/scripts.ts
Normal 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
391
packages/mcp-server/test.ts
Normal 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`,
|
||||
);
|
||||
14
packages/mcp-server/tsconfig.json
Normal file
14
packages/mcp-server/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user