Files
supabase/apps/studio/lib/ai/tools/mcp-tools.ts
Matt RossmanandJoshen Lim 4bb36b944f feat(studio): let High Compliance projects opt-in to Assistant data access (#50548)
Orgs with the HIPAA add-on had the Assistant's opt-in level forced to
`disabled` on any project marked High Compliance, regardless of what the
org picked in its AI settings. The restriction predated our AI provider
BAAs. The consequence is those users see the Assistant failing to answer
questions about their data w/ no clear path how to fix it, even though
the LLM provider supports this use case.

This PR removes these Assistant restrictions on the server and client so
those projects honor the org's chosen level. Braintrust conversation
tracing is unchanged and still blocked for these projects, see [this
test
case](https://github.com/supabase/supabase/blob/b9800ccf16/apps/studio/lib/ai/braintrust-logger.test.ts#L16-L20).
See
[comments](https://linear.app/supabase/issue/AI-1153/allow-hipaa-orgs-to-opt-in-to-assistant-data-access-for-high#comment-485a0d46)
for legal approval and conditions.

The client-side changes enable features like "Debug with AI" on SQL
query failures, “Generate/Rename with AI” for snippet titles, and
generated Assistant chat titles for these customers.

The AI opt-in copy now adds a reminder to obtain consent from data
subjects, linking the [shared responsibility
model](https://supabase.com/docs/guides/deployment/shared-responsibility-model)
based also on [this
comment](https://linear.app/supabase/issue/AI-1153/allow-hipaa-orgs-to-opt-in-to-assistant-data-access-for-high#comment-f81ee610).

<img width="400" alt="CleanShot 2026-09-17 at 5 01 02 PM@2x"
src="https://github.com/user-attachments/assets/d02123f2-3e32-4d83-9f98-7d15e59222ef"
/>

To test with a HIPAA-enabled project in staging, you can use this [Plan
Change
[Staging]](https://app.hex.tech/supabase/app/Plan-Change-Staging-032BD32jo1EaisCS85qunf/latest)
Hex to add the HIPAA add-on. Once the add-on is present, you can turn on
High Compliance from a project's settings. Also in org settings, crank
up the Assistant data opt-in level and verify the Assistant is able to
answer questions about the project's data.

My results testing with opt-in level "Schema, Logs & Database Data":

| High compliance setting | Data opt-in working |
|--------|--------|
| <img width="1302" height="422" alt="CleanShot 2026-09-17 at 5 03 36
PM@2x"
src="https://github.com/user-attachments/assets/c416371b-2eb8-49df-9c07-6d8eababb443"
/> | <img width="1566" height="1516" alt="CleanShot 2026-09-17 at 5 05
14 PM@2x"
src="https://github.com/user-attachments/assets/39624355-7f8f-46ce-9f08-a8acfb9da830"
/> |

Closes AI-1153


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

## New Features

- AI-assisted query renaming, snippet title generation, debugging, and
tools now follow organization AI opt-in settings rather than project
HIPAA status.
- Debugging assistance and AI actions remain available for eligible
users without additional HIPAA-based blocking.
- AI metadata warnings consistently show standard opt-in messaging and
permission settings.
- AI settings remind users to obtain consent before entering personal
data and link to shared responsibility guidance.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-09-18 08:21:50 -04:00

124 lines
5.0 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 { 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 remote MCP server over HTTP and fetch its tools, replacing
// the local tools. A remote failure (outage, timeout, auth) degrades to the
// remaining tools in `getTools` rather than breaking the assistant.
const mcpClient = await createSupabaseMCPClient({
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
}
}