feat: migrate to docker-compose with selfhosted-supabase-mcp
- Replace legacy directory structure (api/, db/, functions/, logs/, pooler/) with single docker-compose.yml based self-hosted setup - Add selfhosted-supabase-mcp TypeScript MCP server for database management - Add .dockerignore for Docker build context - Update .gitignore to exclude .env files, volumes/, backups, logs
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import { z } from 'zod';
|
||||
import { handleSqlResponse, executeSqlWithFallback } from './utils.js';
|
||||
import type { ToolContext } from './types.js';
|
||||
|
||||
// Schema for bucket configuration
|
||||
const BucketConfigSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
public: z.boolean(),
|
||||
file_size_limit: z.number().nullable(),
|
||||
allowed_mime_types: z.array(z.string()).nullable(),
|
||||
avif_autodetection: z.boolean().nullable(),
|
||||
owner: z.string().nullable(),
|
||||
created_at: z.string().nullable(),
|
||||
updated_at: z.string().nullable(),
|
||||
});
|
||||
|
||||
// Schema for output
|
||||
const GetStorageConfigOutputSchema = z.object({
|
||||
buckets: z.array(BucketConfigSchema),
|
||||
global_config: z.object({
|
||||
max_file_size_limit: z.number().nullable(),
|
||||
bucket_count: z.number(),
|
||||
}),
|
||||
});
|
||||
type GetStorageConfigOutput = z.infer<typeof GetStorageConfigOutputSchema>;
|
||||
|
||||
// Input schema
|
||||
const GetStorageConfigInputSchema = z.object({
|
||||
bucket_id: z.string().optional().describe('Optional bucket ID to get config for a specific bucket. If omitted, returns all buckets.'),
|
||||
});
|
||||
type GetStorageConfigInput = z.infer<typeof GetStorageConfigInputSchema>;
|
||||
|
||||
// Static JSON Schema for MCP capabilities
|
||||
const mcpInputSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
bucket_id: {
|
||||
type: 'string',
|
||||
description: 'Optional bucket ID to get config for a specific bucket. If omitted, returns all buckets.',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
};
|
||||
|
||||
// Tool definition
|
||||
export const getStorageConfigTool = {
|
||||
name: 'get_storage_config',
|
||||
description: 'Gets storage configuration for Supabase Storage buckets. Returns bucket settings including file size limits, allowed MIME types, and public/private status.',
|
||||
inputSchema: GetStorageConfigInputSchema,
|
||||
mcpInputSchema: mcpInputSchema,
|
||||
outputSchema: GetStorageConfigOutputSchema,
|
||||
execute: async (input: GetStorageConfigInput, context: ToolContext): Promise<GetStorageConfigOutput> => {
|
||||
const client = context.selfhostedClient;
|
||||
const { bucket_id } = input;
|
||||
|
||||
// Check if storage schema exists
|
||||
const checkSchemaSql = `
|
||||
SELECT EXISTS (
|
||||
SELECT 1 FROM pg_catalog.pg_namespace WHERE nspname = 'storage'
|
||||
) AS exists
|
||||
`;
|
||||
|
||||
const schemaCheckResult = await executeSqlWithFallback(client, checkSchemaSql, true);
|
||||
|
||||
if (!Array.isArray(schemaCheckResult) || schemaCheckResult.length === 0 || !schemaCheckResult[0]?.exists) {
|
||||
context.log('storage schema not found - Storage may not be configured', 'info');
|
||||
return {
|
||||
buckets: [],
|
||||
global_config: {
|
||||
max_file_size_limit: null,
|
||||
bucket_count: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Build query for buckets
|
||||
let bucketQuery = `
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
public,
|
||||
file_size_limit,
|
||||
allowed_mime_types,
|
||||
avif_autodetection,
|
||||
owner::text,
|
||||
created_at::text,
|
||||
updated_at::text
|
||||
FROM storage.buckets
|
||||
`;
|
||||
|
||||
if (bucket_id) {
|
||||
// Escape single quotes
|
||||
const escapedBucketId = bucket_id.replace(/'/g, "''");
|
||||
bucketQuery += ` WHERE id = '${escapedBucketId}'`;
|
||||
}
|
||||
|
||||
bucketQuery += ' ORDER BY name';
|
||||
|
||||
const bucketsResult = await executeSqlWithFallback(client, bucketQuery, true);
|
||||
|
||||
const bucketsSchema = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
public: z.boolean(),
|
||||
file_size_limit: z.number().nullable(),
|
||||
allowed_mime_types: z.array(z.string()).nullable(),
|
||||
avif_autodetection: z.boolean().nullable(),
|
||||
owner: z.string().nullable(),
|
||||
created_at: z.string().nullable(),
|
||||
updated_at: z.string().nullable(),
|
||||
})
|
||||
);
|
||||
|
||||
const buckets = handleSqlResponse(bucketsResult, bucketsSchema);
|
||||
|
||||
// Get global stats
|
||||
const statsQuery = `
|
||||
SELECT
|
||||
MAX(file_size_limit) as max_file_size_limit,
|
||||
COUNT(*) as bucket_count
|
||||
FROM storage.buckets
|
||||
`;
|
||||
|
||||
const statsResult = await executeSqlWithFallback(client, statsQuery, true);
|
||||
|
||||
let globalConfig = {
|
||||
max_file_size_limit: null as number | null,
|
||||
bucket_count: buckets.length,
|
||||
};
|
||||
|
||||
if (Array.isArray(statsResult) && statsResult.length > 0) {
|
||||
const maxLimit = statsResult[0]?.max_file_size_limit;
|
||||
globalConfig = {
|
||||
max_file_size_limit: typeof maxLimit === 'number' ? maxLimit : null,
|
||||
bucket_count: Number(statsResult[0]?.bucket_count) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
buckets,
|
||||
global_config: globalConfig,
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user