Windsurf Supabase MCP Server

import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { processSql, renderHttp } from '@supabase/sql-to-rest'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { version } from '../package.json'; import { ensureNoTrailingSlash, ensureTrailingSlash } from './util.js'; export type PostgrestMcpServerOptions = { apiUrl: string; apiKey?: string; schema: string; }; export default class PostgrestMcpServer extends Server { readonly #apiUrl: string; readonly #apiKey?: string; readonly #schema: string; readonly #tools = { postgrestRequest: { description: 'Performs HTTP request against the PostgREST API', parameters: z.object({ method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']), path: z.string(), body: z.record(z.unknown()).optional(), }), execute: async <Body>({ method, path, body, }: { method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; path: string; body?: Body; }) => { // Ensure path starts with / const cleanPath = path.startsWith('/') ? path : `/${path}`; const url = new URL(`${this.#apiUrl}${cleanPath}`); console.log('Making request to:', url.toString()); console.log('Headers:', this.#getHeaders(method)); const response = await fetch(url, { method, headers: this.#getHeaders(method), body: body ? JSON.stringify(body) : undefined, }); const result = await response.json(); console.log('Response:', result); return result; }, }, sqlToRest: { description: 'Converts SQL query to a PostgREST API request (method, path)', parameters: z.object({ sql: z.string(), }), execute: async ({ sql }: { sql: string }) => { const statement = await processSql(sql); const request = await renderHttp(statement); return { method: request.method, path: request.fullPath, }; }, }, }; constructor(options: PostgrestMcpServerOptions) { super( { name: 'supabase/postgrest', version, }, { capabilities: { resources: {}, tools: { query: true, insert: true, update: true, delete: true }, }, } ); this.#apiUrl = ensureNoTrailingSlash(options.apiUrl); this.#apiKey = options.apiKey; this.#schema = options.schema; this.setRequestHandler(ListResourcesRequestSchema, async () => { const openApiSpec = await this.#fetchOpenApiSpec(); const resources = Object.keys(openApiSpec.paths) .filter((path) => path !== '/') .map((path) => { const name = path.split('/').pop(); const pathValue = openApiSpec.paths[path]; const description = pathValue.get?.summary; return { uri: new URL(`${path}/spec`, `postgrest://${this.#schema}`).href, name: `"${name}" OpenAPI path spec`, description, mimeType: 'application/json', }; }); return { resources, }; }); this.setRequestHandler(ReadResourceRequestSchema, async (request) => { const openApiSpec = await this.#fetchOpenApiSpec(); const resourceUrl = new URL(request.params.uri); const pathComponents = resourceUrl.pathname.split('/'); const specLiteral = pathComponents.pop(); const pathName = pathComponents.pop(); if (specLiteral !== 'spec') { throw new Error('invalid resource uri'); } const pathSpec = openApiSpec.paths[`/${pathName}`]; if (!pathSpec) { throw new Error('path not found'); } return { contents: [ { uri: request.params.uri, mimeType: 'application/json', text: JSON.stringify(pathSpec), }, ], }; }); this.setRequestHandler(ListToolsRequestSchema, async () => { const tools = Object.entries(this.#tools).map( ([name, { description, parameters }]) => { return { name, description, inputSchema: zodToJsonSchema(parameters), }; } ); return { tools, }; }); this.setRequestHandler(CallToolRequestSchema, async (request) => { const tools = this.#tools; const toolName = request.params.name as keyof typeof tools; if (!(toolName in this.#tools)) { throw new Error('tool not found'); } const tool = this.#tools[toolName]; const args = tool.parameters.parse(request.params.arguments); if (!args) { throw new Error('missing arguments'); } const result = await tool.execute(args as any); return { content: [ { type: 'text', text: JSON.stringify(result), }, ], }; }); } async #fetchOpenApiSpec() { const response = await fetch(ensureTrailingSlash(this.#apiUrl), { headers: this.#getHeaders(), }); return (await response.json()) as any; } #getHeaders(method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' = 'GET') { const schemaHeader = method === 'GET' ? 'accept-profile' : 'content-profile'; const headers: HeadersInit = { 'content-type': 'application/json', prefer: 'return=representation', [schemaHeader]: this.#schema, }; if (this.#apiKey) { headers['apikey'] = this.#apiKey; headers['Authorization'] = `Bearer ${this.#apiKey}`; } return headers; } }