- 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
123 lines
5.2 KiB
TypeScript
123 lines
5.2 KiB
TypeScript
import { z } from 'zod';
|
|
import type { PoolClient } from 'pg'; // Import PoolClient type
|
|
|
|
import type { ToolContext } from './types.js';
|
|
import { handleSqlResponse } from './utils.js';
|
|
import type { SqlSuccessResponse } from '../types/index.js'; // Import the type
|
|
|
|
// Input schema
|
|
const ListStorageObjectsInputSchema = z.object({
|
|
bucket_id: z.string().describe('The ID of the bucket to list objects from.'),
|
|
limit: z.number().int().positive().optional().default(100).describe('Max number of objects to return'),
|
|
offset: z.number().int().nonnegative().optional().default(0).describe('Number of objects to skip'),
|
|
prefix: z.string().optional().describe('Filter objects by a path prefix (e.g., \'public/\')'),
|
|
});
|
|
type ListStorageObjectsInput = z.infer<typeof ListStorageObjectsInputSchema>;
|
|
|
|
// Output schema
|
|
const StorageObjectSchema = z.object({
|
|
id: z.string().uuid(),
|
|
name: z.string().nullable(), // Name can be null according to schema
|
|
bucket_id: z.string(),
|
|
owner: z.string().uuid().nullable(),
|
|
version: z.string().nullable(),
|
|
// Get mimetype directly from SQL extraction
|
|
mimetype: z.string().nullable(),
|
|
// size comes from metadata - use transform instead of pipe for Zod v4
|
|
size: z.union([z.string(), z.number(), z.null()]).transform((val) => {
|
|
if (val === null) return null;
|
|
const num = typeof val === 'number' ? val : parseInt(String(val), 10);
|
|
return isNaN(num) ? null : num;
|
|
}),
|
|
// Keep raw metadata as well
|
|
metadata: z.record(z.string(), z.any()).nullable(),
|
|
created_at: z.string().nullable(),
|
|
updated_at: z.string().nullable(),
|
|
last_accessed_at: z.string().nullable(),
|
|
});
|
|
const ListStorageObjectsOutputSchema = z.array(StorageObjectSchema);
|
|
type ListStorageObjectsOutput = z.infer<typeof ListStorageObjectsOutputSchema>;
|
|
|
|
// Static JSON schema for MCP
|
|
export const mcpInputSchema = {
|
|
type: 'object',
|
|
properties: {
|
|
bucket_id: { type: 'string', description: 'The ID of the bucket to list objects from.' },
|
|
limit: { type: 'number', description: 'Max number of objects to return', default: 100 },
|
|
offset: { type: 'number', description: 'Number of objects to skip', default: 0 },
|
|
prefix: { type: 'string', description: "Filter objects by a path prefix (e.g., 'public/')" },
|
|
},
|
|
required: ['bucket_id'],
|
|
};
|
|
|
|
// Tool definition
|
|
export const listStorageObjectsTool = {
|
|
name: 'list_storage_objects',
|
|
description: 'Lists objects within a specific storage bucket, optionally filtering by prefix.',
|
|
mcpInputSchema,
|
|
inputSchema: ListStorageObjectsInputSchema,
|
|
outputSchema: ListStorageObjectsOutputSchema,
|
|
|
|
execute: async (
|
|
input: ListStorageObjectsInput,
|
|
context: ToolContext
|
|
): Promise<ListStorageObjectsOutput> => {
|
|
const client = context.selfhostedClient;
|
|
const { bucket_id, limit, offset, prefix } = input;
|
|
|
|
console.error(`Listing objects for bucket ${bucket_id} (Prefix: ${prefix || 'N/A'})...`);
|
|
|
|
if (!client.isPgAvailable()) {
|
|
context.log('Direct database connection (DATABASE_URL) is required to list storage objects.', 'error');
|
|
throw new Error('Direct database connection (DATABASE_URL) is required to list storage objects.');
|
|
}
|
|
|
|
// Use a transaction to get access to the pg client for parameterized queries
|
|
const objects = await client.executeTransactionWithPg(async (pgClient: PoolClient) => {
|
|
// Build query with parameters
|
|
let sql = `
|
|
SELECT
|
|
id,
|
|
name,
|
|
bucket_id,
|
|
owner,
|
|
version,
|
|
metadata ->> 'mimetype' AS mimetype,
|
|
metadata ->> 'size' AS size, -- Extract size from metadata
|
|
metadata,
|
|
created_at::text,
|
|
updated_at::text,
|
|
last_accessed_at::text
|
|
FROM storage.objects
|
|
WHERE bucket_id = $1
|
|
`;
|
|
const params: (string | number)[] = [bucket_id];
|
|
let paramIndex = 2;
|
|
|
|
if (prefix) {
|
|
sql += ` AND name LIKE $${paramIndex++}`;
|
|
params.push(`${prefix}%`);
|
|
}
|
|
|
|
sql += ' ORDER BY name ASC NULLS FIRST';
|
|
sql += ` LIMIT $${paramIndex++}`;
|
|
params.push(limit);
|
|
sql += ` OFFSET $${paramIndex++}`;
|
|
params.push(offset);
|
|
sql += ';';
|
|
|
|
console.error('Executing parameterized SQL to list storage objects within transaction...');
|
|
const result = await pgClient.query(sql, params); // Raw pg result
|
|
|
|
// Explicitly pass result.rows, which matches the expected structure
|
|
// of SqlSuccessResponse (unknown[]) for handleSqlResponse.
|
|
return handleSqlResponse(result.rows as SqlSuccessResponse, ListStorageObjectsOutputSchema);
|
|
});
|
|
|
|
console.error(`Found ${objects.length} objects.`);
|
|
context.log(`Found ${objects.length} objects.`);
|
|
return objects;
|
|
},
|
|
};
|
|
|
|
export default listStorageObjectsTool;
|