Files
supabase/apps/studio/lib/ai/tools/mcp-tools.ts
T
Pedro RodriguesandClaude Opus 4.8 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

136 lines
5.8 KiB
TypeScript

// Type-only import (erased at build time — pulls no runtime code into this route).
import type * as SupabaseMcp from '@supabase/mcp-server-supabase'
import type { ToolSet } from 'ai'
import { createInProcessSupabaseMCPClient, createSupabaseMCPClient } from '../supabase-mcp'
import { filterToolsByOptInLevel, toolSetValidationSchema } from '../tool-filter'
import type { AiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
/**
* Union of the tool names exposed by the pinned `@supabase/mcp-server-supabase`
* version. Studio's dependency is bumped in lockstep with the remote MCP server
* (via automated bump PRs), so typing our tool-name lists against this makes an
* upstream rename/removal a **compile-time** failure (`pnpm typecheck`) in that
* PR, instead of a silent capability loss at runtime.
*/
type SupabaseMcpToolName = keyof typeof SupabaseMcp.supabaseMcpToolSchemas
// UI-executed tools handled locally by Studio (see getStudioTools); the remote
// MCP server's versions are removed so the UI-controlled Studio versions win.
const UI_EXECUTED_TOOLS = [
'execute_sql',
'deploy_edge_function',
] as const satisfies readonly SupabaseMcpToolName[]
// Read-only tools the assistant relies on from the remote MCP server — the
// MCP-sourced subset of the allowlist in tool-filter.ts (TOOL_CATEGORY_MAP).
// `satisfies` gives the compile-time drift guard; the runtime check below also
// catches a deployed server that returns fewer tools (feature flags / version
// skew). The allowlist remains the source of truth for what is allowed.
const EXPECTED_MCP_TOOLS = [
'search_docs',
'list_tables',
'list_extensions',
'list_edge_functions',
'list_branches',
'get_advisors',
'query_logs',
] as const satisfies readonly SupabaseMcpToolName[]
export const getMcpTools = async ({
accessToken,
projectRef,
aiOptInLevel,
signal,
}: {
accessToken: string
projectRef: string
aiOptInLevel: AiOptInLevel
// Required: the remote client holds an HTTP connection that must be torn down
// when the request ends. The caller owns that lifecycle via this signal.
signal: AbortSignal
}) => {
// Connect to the MCP server and fetch its tools, which replace the old local
// tools. `USE_REMOTE_MCP` gates the transport: the remote HTTP server (target
// state) or the legacy in-process server (fallback during the migration),
// defaulting to in-process until an environment opts in. Flip it per
// environment (staging → prod → Nimbus) once each one's prerequisites are met
// (dashboard-token support on the MCP API, remote MCP enabled for Nimbus);
// unset to roll back on the next deploy. Both transports expose the same tool
// surface, so the filtering, drift detection, and lifecycle handling below are
// transport-agnostic.
//
// TODO(AI-897): remove in process mcp — once every environment has been
// flipped and is stable, delete `createInProcessSupabaseMCPClient` and this
// fallback branch.
const useRemoteMcp = process.env.USE_REMOTE_MCP === 'true'
const createClient = useRemoteMcp ? createSupabaseMCPClient : createInProcessSupabaseMCPClient
const mcpClient = await createClient({
accessToken,
projectRef,
})
// The remote client keeps an HTTP connection open. The tools' `execute`
// functions are invoked later, while the response is streaming, so the
// connection must stay open until the request ends. Close it exactly once when
// the request is done (normal completion or abort) to avoid leaking a
// connection per request.
let closed = false
const closeClient = () => {
if (closed) return
closed = true
void mcpClient.close().catch(() => {})
}
// The request already ended before we could fetch tools; don't bother.
if (signal.aborted) {
closeClient()
return {} as ToolSet
}
signal.addEventListener('abort', closeClient, { once: true })
try {
const availableMcpTools = (await mcpClient.tools()) as ToolSet
// Runtime drift detection: `EXPECTED_MCP_TOOLS` is compile-time-checked
// against the pinned package (see its declaration), but the *deployed* remote
// server can still return fewer tools than the pinned types — feature flags,
// killswitches, or version skew during a bump. `filterToolsByOptInLevel`
// drops missing tools silently, so warn to make that observable.
const missingExpectedTools = EXPECTED_MCP_TOOLS.filter((name) => !(name in availableMcpTools))
if (missingExpectedTools.length > 0) {
console.error(
`Remote MCP server is missing expected tools: ${missingExpectedTools.join(', ')}. ` +
'The tool contract may have drifted; the assistant will operate without them.'
)
}
// Safety gate: `filterToolsByOptInLevel` keeps only tools in the allowlist
// (tool-filter.ts TOOL_CATEGORY_MAP) and drops everything else. This — not
// the `read_only` query param — is what prevents the remote server's
// write/destructive tools (apply_migration, create_branch, ...) from reaching
// the assistant. `read_only` is defense-in-depth (those tools throw at
// runtime). Do not remove this filter on the assumption `read_only` suffices.
const allowedMcpTools = filterToolsByOptInLevel(availableMcpTools, aiOptInLevel)
// Remove UI-executed tools handled locally
const filteredMcpTools: ToolSet = { ...allowedMcpTools }
UI_EXECUTED_TOOLS.forEach((toolName) => {
delete filteredMcpTools[toolName]
})
// Validate that only known tools are provided
const validation = toolSetValidationSchema.safeParse(filteredMcpTools)
if (!validation.success) {
console.error('MCP tools validation error:', validation.error)
throw new Error('Internal error: MCP tools validation failed')
}
return validation.data
} catch (error) {
// Don't leak the connection if fetching or validating tools fails
closeClient()
throw error
}
}