fix(local-mcp): database operations (#38965)

* fix(local-mcp): return values from database operations

* fix(local-mcp): pg-meta status code forwarding
This commit is contained in:
Greg Richardson
2025-09-23 16:41:36 -06:00
committed by GitHub
parent 43dc048155
commit 081a75350e
7 changed files with 144 additions and 41 deletions

View File

@@ -3,8 +3,8 @@ import {
DatabaseOperations,
ExecuteSqlOptions,
} from '@supabase/mcp-server-supabase/platform'
import { executeQuery } from './query'
import { applyAndTrackMigrations, listMigrationVersions } from './migrations'
import { executeQuery } from './query'
export type GetDatabaseOperationsOptions = {
headers?: HeadersInit
@@ -16,35 +16,30 @@ export function getDatabaseOperations({
return {
async executeSql<T>(_projectRef: string, options: ExecuteSqlOptions) {
const { query } = options
const response = await executeQuery({ query, headers })
const { data, error } = await executeQuery<T>({ query, headers })
if (response.error) {
const { code, message } = response.error
throw new Error(`Error executing SQL: ${message} (code: ${code})`)
if (error) {
throw error
}
return response as T
return data
},
async listMigrations() {
const response = await listMigrationVersions({ headers })
const { data, error } = await listMigrationVersions({ headers })
if (response.error) {
const { code, message } = response.error
throw new Error(`Error listing migrations: ${message} (code: ${code})`)
if (error) {
throw error
}
return response as any
return data
},
async applyMigration<T>(_projectRef: string, options: ApplyMigrationOptions) {
async applyMigration(_projectRef: string, options: ApplyMigrationOptions) {
const { query, name } = options
const response = await applyAndTrackMigrations({ query, name, headers })
const { error } = await applyAndTrackMigrations({ query, name, headers })
if (response.error) {
const { code, message } = response.error
throw new Error(`Error applying migration: ${message} (code: ${code})`)
if (error) {
throw error
}
return response as T
},
}
}

View File

@@ -1,6 +1,13 @@
import { source } from 'common-tags'
import { makeRandomString } from 'lib/helpers'
import { executeQuery } from './query'
import { PgMetaDatabaseError, WrappedResult } from './types'
import { assertSelfHosted } from './util'
export type ListMigrationsResult = {
version: string
name?: string
}
const listMigrationVersionsQuery = () =>
'select version, name from supabase_migrations.schema_migrations order by version'
@@ -40,8 +47,31 @@ export type ListMigrationVersionsOptions = {
headers?: HeadersInit
}
export async function listMigrationVersions({ headers }: ListMigrationVersionsOptions) {
return await executeQuery({ query: listMigrationVersionsQuery(), headers })
/**
* Lists all migrations in the migrations history table.
*
* _Only call this from server-side self-hosted code._
*/
export async function listMigrationVersions({
headers,
}: ListMigrationVersionsOptions): Promise<WrappedResult<ListMigrationsResult[]>> {
assertSelfHosted()
const { data, error } = await executeQuery<ListMigrationsResult>({
query: listMigrationVersionsQuery(),
headers,
})
if (error) {
// Return empty list if the migrations table doesn't exist
if (error instanceof PgMetaDatabaseError && error.code === '42P01') {
return { data: [], error: undefined }
}
return { data: undefined, error }
}
return { data, error: undefined }
}
export type ApplyAndTrackMigrationsOptions = {
@@ -50,11 +80,18 @@ export type ApplyAndTrackMigrationsOptions = {
headers?: HeadersInit
}
export async function applyAndTrackMigrations({
/**
* Applies a SQL migration and tracks it in the migrations history table.
*
* _Only call this from server-side self-hosted code._
*/
export async function applyAndTrackMigrations<T = unknown>({
query,
name,
headers,
}: ApplyAndTrackMigrationsOptions) {
}: ApplyAndTrackMigrationsOptions): Promise<WrappedResult<T[]>> {
assertSelfHosted()
const initializeResponse = await executeQuery<void>({
query: initializeHistoryTableQuery(),
headers,
@@ -64,7 +101,7 @@ export async function applyAndTrackMigrations({
return initializeResponse
}
const applyAndTrackResponse = await executeQuery({
const applyAndTrackResponse = await executeQuery<T>({
query: applyAndTrackMigrationsQuery(query, name),
headers,
})

View File

@@ -1,23 +1,47 @@
import { fetchPost } from 'data/fetchers'
import { PG_META_URL } from 'lib/constants/index'
import { ResponseError } from 'types'
import { constructHeaders } from '../apiHelpers'
import { PgMetaDatabaseError, databaseErrorSchema, WrappedResult } from './types'
import { assertSelfHosted } from './util'
export type QueryOptions = {
query: string
headers?: HeadersInit
}
export async function executeQuery<T = unknown>({ query, headers }: QueryOptions) {
const response = await fetchPost<T[]>(
`${PG_META_URL}/query`,
{ query },
{ headers: constructHeaders(headers ?? {}) }
)
/**
* Executes a SQL query against the self-hosted Postgres instance via pg-meta service.
*
* _Only call this from server-side self-hosted code._
*/
export async function executeQuery<T = unknown>({
query,
headers,
}: QueryOptions): Promise<WrappedResult<T[]>> {
assertSelfHosted()
if (response instanceof ResponseError) {
return { error: response }
} else {
return { data: response }
const response = await fetch(`${PG_META_URL}/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...constructHeaders(headers ?? {}),
},
body: JSON.stringify({ query }),
})
try {
const result = await response.json()
if (!response.ok) {
const { message, code, formattedError } = databaseErrorSchema.parse(result)
const error = new PgMetaDatabaseError(message, code, response.status, formattedError)
return { data: undefined, error }
}
return { data: result, error: undefined }
} catch (error) {
if (error instanceof Error) {
return { data: undefined, error }
}
throw error
}
}

View File

@@ -0,0 +1,23 @@
import z from 'zod/v4'
export type WrappedSuccessResult<T> = { data: T; error: undefined }
export type WrappedErrorResult = { data: undefined; error: Error }
export type WrappedResult<R> = WrappedSuccessResult<R> | WrappedErrorResult
export const databaseErrorSchema = z.object({
message: z.string(),
code: z.string(),
formattedError: z.string(),
})
export class PgMetaDatabaseError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number,
public formattedError: string
) {
super(message)
this.name = 'PgMetaDatabaseError'
}
}

View File

@@ -0,0 +1,10 @@
import { IS_PLATFORM } from 'lib/constants'
/**
* Asserts that the current environment is self-hosted.
*/
export function assertSelfHosted() {
if (IS_PLATFORM) {
throw new Error('This function can only be called in self-hosted environments')
}
}

View File

@@ -1,6 +1,7 @@
import { constructHeaders } from 'lib/api/apiHelpers'
import apiWrapper from 'lib/api/apiWrapper'
import { executeQuery } from 'lib/api/self-hosted/query'
import { PgMetaDatabaseError } from 'lib/api/self-hosted/types'
import { NextApiRequest, NextApiResponse } from 'next'
export default (req: NextApiRequest, res: NextApiResponse) =>
@@ -24,8 +25,12 @@ const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
const { data, error } = await executeQuery({ query, headers })
if (error) {
const { code, message } = error
return res.status(code ?? 500).json({ message, formattedError: message })
if (error instanceof PgMetaDatabaseError) {
const { statusCode, message, formattedError } = error
return res.status(statusCode).json({ message, formattedError })
}
const { message } = error
return res.status(500).json({ message, formattedError: message })
} else {
return res.status(200).json(data)
}

View File

@@ -3,6 +3,7 @@ import { NextApiRequest, NextApiResponse } from 'next'
import { constructHeaders } from 'lib/api/apiHelpers'
import apiWrapper from 'lib/api/apiWrapper'
import { applyAndTrackMigrations, listMigrationVersions } from 'lib/api/self-hosted/migrations'
import { PgMetaDatabaseError } from 'lib/api/self-hosted/types'
export default (req: NextApiRequest, res: NextApiResponse) =>
apiWrapper(req, res, handler, { withAuth: true })
@@ -26,8 +27,12 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => {
const { data, error } = await listMigrationVersions(headers)
if (error) {
const { code, message } = error
return res.status(code ?? 500).json({ message })
if (error instanceof PgMetaDatabaseError) {
const { statusCode, message, formattedError } = error
return res.status(statusCode).json({ message, formattedError })
}
const { message } = error
return res.status(500).json({ message, formattedError: message })
} else {
return res.status(200).json(data)
}
@@ -40,8 +45,12 @@ const handlePost = async (req: NextApiRequest, res: NextApiResponse) => {
const { data, error } = await applyAndTrackMigrations({ query, name, headers })
if (error) {
const { code, message } = error
return res.status(code ?? 500).json({ message, formattedError: message })
if (error instanceof PgMetaDatabaseError) {
const { statusCode, message, formattedError } = error
return res.status(statusCode).json({ message, formattedError })
}
const { message } = error
return res.status(500).json({ message, formattedError: message })
} else {
return res.status(200).json(data)
}