Files
supabase/apps/studio/lib/api/self-hosted/mcp.ts
Pedro Rodrigues 47595f8ac7 feat(self-hosted): implement queryLogs for the MCP debugging tools (#48900)
> [!IMPORTANT]  
>
> Only merge this when (https://github.com/supabase/platform/pull/36804)
is merged, as the AI assistant will not have access to the `query_logs`
tool for the remote MCP server

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Feature (self-hosted / CLI Studio MCP server).

## What is the current behavior?

Self-hosted `getDebuggingOperations`
(`apps/studio/lib/api/self-hosted/mcp.ts`) implements only `getLogs`, so
the MCP `debugging` group exposes `get_logs` — a fixed per-service log
dump built by `getLogQuery`. Logs are served by Logflare, which speaks
BigQuery SQL.

## What is the new behavior?

Bumps `@supabase/mcp-server-supabase` to `^0.10.0` (adds `query_logs` +
`logsDialect`, and hides `get_logs` wherever a platform declares
`queryLogs`) and moves logs over to it.

- **Self-hosted `query_logs`:** declares `logsDialect: 'bigquery'` and
implements `queryLogs`, passing the model's SQL straight through to the
same Logflare `logs.all` endpoint (arbitrary `sql` param) — no new
endpoint, no dialect translation.
- **Drops `get_logs` from self-hosted:** `getLogs` throws (the server
hides it once `queryLogs` exists) and the per-service `getLogQuery`
builder is deleted; the model now writes its own BigQuery SQL, guided by
the dialect schema hint.
- **Honors no-logs mode:** `query_logs` throws when `logs:all` is
disabled — the self-hosted default, enabled via the
`docker-compose.logs.yml` override.
- **Assistant:** switches the dashboard assistant from `get_logs` to
`query_logs` (allowlist, drift guard, prompt, mocks, evals).

Refs AI-1046


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * AI debugging can query recent project logs using read-only SQL.
* Log queries support optional time-range filters, filtering,
aggregation, and joins.
* Self-hosted debugging checks whether logging is enabled before running
queries.

* **Bug Fixes**
* Updated debugging workflows and validation to consistently use the new
log-query capability.
* Removed reliance on legacy service-specific log filtering and query
behavior.

* **Documentation**
* Updated MCP debugging tool guidance to describe SQL-based log queries.

* **Tests**
* Expanded coverage for enabled, disabled, and unsupported logging
scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 13:11:23 +01:00

191 lines
5.4 KiB
TypeScript

import {
ApiKey,
ApiKeyType,
ApplyMigrationOptions,
DatabaseOperations,
DebuggingOperations,
DevelopmentOperations,
ExecuteSqlOptions,
QueryLogsOptions,
} from '@supabase/mcp-server-supabase/platform'
import { isFeatureEnabled, type Feature } from 'common/enabled-features'
import { getEnabledFeaturesOverrideDisabledList } from 'common/enabled-features/overrides'
import { DEFAULT_EXPOSED_SCHEMAS } from './constants'
import { generateTypescriptTypes } from './generate-types'
import { getLints } from './lints'
import { retrieveAnalyticsData } from './logs'
import { applyAndTrackMigrations, listMigrationVersions } from './migrations'
import { executeQuery } from './query'
import { getProjectSettings } from './settings'
import { ResponseError } from '@/types'
export type GetDatabaseOperationsOptions = {
headers?: HeadersInit
}
export type GetDevelopmentOperationsOptions = {
headers?: HeadersInit
}
export type GetDebuggingOperationsOptions = {
headers?: HeadersInit
}
export function getDatabaseOperations({
headers,
}: GetDatabaseOperationsOptions): DatabaseOperations {
return {
async executeSql<T>(_projectRef: string, options: ExecuteSqlOptions) {
const { query, parameters, read_only: readOnly } = options
const { data, error } = await executeQuery<T>({ query, parameters, headers, readOnly })
if (error) {
throw error
}
return data
},
async listMigrations() {
const { data, error } = await listMigrationVersions({ headers })
if (error) {
throw error
}
return data
},
async applyMigration(_projectRef: string, options: ApplyMigrationOptions) {
const { query, name } = options
const { error } = await applyAndTrackMigrations({ query, name, headers })
if (error) {
throw error
}
},
}
}
export function getDevelopmentOperations({
headers,
}: GetDevelopmentOperationsOptions): DevelopmentOperations {
return {
async getProjectUrl(_projectRef) {
const settings = getProjectSettings()
return `${settings.app_config.protocol}://${settings.app_config.endpoint}`
},
async getPublishableKeys(_projectRef) {
if (process.env.SUPABASE_PUBLISHABLE_KEY) {
const publishableKeysArray: ApiKey[] = [
{
api_key: process.env.SUPABASE_PUBLISHABLE_KEY,
name: 'publishable',
type: 'publishable' as ApiKeyType,
},
]
return publishableKeysArray
}
const settings = getProjectSettings()
const anonKey = settings.service_api_keys.find((key) => key.name === 'anon key')
if (!anonKey) {
throw new Error('Anon key not found in project settings')
}
const publishableKeysArray: ApiKey[] = [
{
api_key: anonKey.api_key,
name: anonKey.name,
type: 'legacy' as ApiKeyType,
},
]
return publishableKeysArray
},
async generateTypescriptTypes(_projectRef) {
const response = await generateTypescriptTypes({ headers })
if (response instanceof ResponseError) {
throw response
}
return response
},
}
}
// Logs are disabled by default for self-hosted; enabled via the
// `docker-compose.logs.yml` override, which sets ENABLED_FEATURES_LOGS_ALL=true.
function assertLogsEnabled() {
const disabledFeatures = getEnabledFeaturesOverrideDisabledList(process.env) as Feature[]
if (!isFeatureEnabled('logs:all', disabledFeatures)) {
throw new Error(
'Logs are disabled on this instance. Enable the `docker-compose.logs.yml` override to query logs.'
)
}
}
export function getDebuggingOperations({
headers,
}: GetDebuggingOperationsOptions): DebuggingOperations {
return {
// Self-hosted logs are served by Logflare, which speaks BigQuery SQL.
logsDialect: 'bigquery',
// `query_logs` replaces `get_logs` on self-hosted. Declaring `queryLogs`
// makes the MCP server hide `get_logs` from clients (see `getDebuggingTools`
// in @supabase/mcp-server-supabase), so this method is never reached over
// MCP. The `DebuggingOperations` interface still requires it, so it throws
// defensively rather than serving logs.
async getLogs() {
throw new Error('get_logs is not supported on self-hosted; use query_logs instead.')
},
async queryLogs(projectRef: string, options: QueryLogsOptions) {
assertLogsEnabled()
// Pass the model's SQL straight through to the Logflare `logs.all`
// endpoint, which accepts an arbitrary `sql` param.
const { data, error } = await retrieveAnalyticsData({
name: 'logs.all',
projectRef,
params: {
sql: options.sql,
iso_timestamp_start: options.iso_timestamp_start,
iso_timestamp_end: options.iso_timestamp_end,
},
})
if (error) {
throw error
}
return data
},
async getSecurityAdvisors(_projectRef) {
const { data, error } = await getLints({
headers,
exposedSchemas: DEFAULT_EXPOSED_SCHEMAS,
})
if (error) {
throw error
}
return data.filter((lint) => lint.categories.includes('SECURITY'))
},
async getPerformanceAdvisors(_projectRef) {
const { data, error } = await getLints({
headers,
exposedSchemas: DEFAULT_EXPOSED_SCHEMAS,
})
if (error) {
throw error
}
return data.filter((lint) => lint.categories.includes('PERFORMANCE'))
},
}
}