mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
- Eval harness's only live tool, `search_docs`, no longer needs the in-process MCP client or its dummy token — it now calls the public docs GraphQL API (`https://supabase.com/docs/api/graphql`) directly. Low risk as this is an eval-harness change only. Production assistant path (`mcp-tools.ts`) untouched. **Update:** per [@mattrossman's review](https://github.com/supabase/supabase/pull/50092#discussion_r3980396341), the eval tool's description embeds the Content API's own GraphQL schema (fetched via a `{ schema }` query and minified with `gqlmin`), mirroring how `@supabase/mcp-server-supabase`'s `docs-tools.ts`/`loadSchema` populates production's `search_docs` description. Without it, the model had no schema to work from and issued malformed queries, which caused the 218 `search_docs` errors and the -25pp Docs Faithfulness regression in the first eval run on this PR. Schema loading is required: `createSearchDocsTool()` rejects if the schema fetch fails, so preflight and the gated eval job fail loudly instead of producing untrustworthy fallback results. `createSearchDocsTool` is async because the `ai` package's `tool()` only accepts a plain string `description`, unlike the MCP SDK's async description support; both callers (`getMockTools`, `evals/preflight.ts`) await it. `gqlmin` is a direct `apps/studio` dependency and was already transitive via `@supabase/mcp-server-supabase`. ### Verification - `pnpm -C apps/studio exec -- tsc --noEmit` reaches the compiler; it reports only the pre-existing unrelated `packages/ui-patterns/src/McpUrlBuilder/components/InstructionBlocks.tsx` `StaticImageData` error. - `pnpm -C apps/studio exec -- vitest run lib/ai/tools/mock-tools.test.ts lib/ai/tools/mcp-tools.test.ts` — 21/21 passed. - `pnpm exec tsx evals/preflight.ts` — live docs API schema fetch and search_docs call passed. - `NEXT_PUBLIC_CONTENT_API_URL=http://127.0.0.1:1/graphql pnpm -C apps/studio exec -- tsx evals/preflight.ts` — failed fast as expected, proving schema/API failures gate evals. - Fresh `run-evals` pass: Docs Faithfulness 55.7% (0pp), with no systemic `search_docs` regression. Risk: eval-harness-only; schema/API outage now fails the eval job before scoring rather than allowing fallback descriptions. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added documentation search powered by the public Supabase documentation GraphQL API. * Documentation search results now include live schema information and clearer error handling for failed or invalid requests. * **Bug Fixes** * Improved evaluation tooling reliability by removing unnecessary connection-abort behavior. * Updated validation to detect missing search tools and malformed documentation responses. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
118 lines
4.3 KiB
TypeScript
118 lines
4.3 KiB
TypeScript
import { tool, type Tool } from 'ai'
|
|
import gqlmin from 'gqlmin'
|
|
import { z } from 'zod'
|
|
|
|
const searchDocsInputSchema = z.object({
|
|
graphql_query: z.string().describe('A valid GraphQL query against the Supabase docs API.'),
|
|
})
|
|
|
|
const CONTENT_API_URL =
|
|
process.env.NEXT_PUBLIC_CONTENT_API_URL ?? 'https://supabase.com/docs/api/graphql'
|
|
|
|
/**
|
|
* Sends a GraphQL query to the public Supabase docs API.
|
|
*
|
|
* Mirrors the @supabase/mcp-server-supabase content API client: GET
|
|
* `<url>?query=<encoded>` with `Accept: application/json`, returning the
|
|
* GraphQL envelope's `data` field.
|
|
*/
|
|
async function queryContentApiGraphQL(graphqlQuery: string): Promise<unknown> {
|
|
const url = new URL(CONTENT_API_URL)
|
|
url.searchParams.set('query', graphqlQuery)
|
|
|
|
const response = await fetch(url, {
|
|
method: 'GET',
|
|
headers: {
|
|
Accept: 'application/json',
|
|
'User-Agent': 'supabase-studio-evals',
|
|
},
|
|
// A stalled connection or response body would otherwise hang getMockTools
|
|
// and preflight indefinitely.
|
|
signal: AbortSignal.timeout(10_000),
|
|
})
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch Supabase Content API: HTTP status ${response.status}`)
|
|
}
|
|
|
|
const body = (await response.json()) as {
|
|
data?: unknown
|
|
errors?: Array<{ message: string; locations?: Array<{ line: number; column: number }> }>
|
|
}
|
|
if (body.errors?.length) {
|
|
throw new Error(
|
|
`Supabase Content API GraphQL error: ${body.errors
|
|
.map((error) => {
|
|
const location = error.locations?.[0]
|
|
return `${error.message} (line ${location?.line ?? 'unknown'}, column ${location?.column ?? 'unknown'})`
|
|
})
|
|
.join(', ')}`
|
|
)
|
|
}
|
|
if (!body.data) {
|
|
throw new Error('Supabase Content API returned no data')
|
|
}
|
|
|
|
return body.data
|
|
}
|
|
|
|
const STATIC_DESCRIPTION =
|
|
'Search the Supabase documentation using GraphQL. Must be a valid GraphQL query. ' +
|
|
'You should default to calling this even if you think you already know the answer, ' +
|
|
'since the documentation is always being updated.'
|
|
|
|
/**
|
|
* Fetches and minifies the Content API's own GraphQL schema (via the `{
|
|
* schema }` query it exposes), mirroring
|
|
* `@supabase/mcp-server-supabase`'s `loadSchema` so the eval tool's
|
|
* description is as close as practical to what production Assistant sees.
|
|
*/
|
|
async function loadContentApiSchema(): Promise<string> {
|
|
const data = (await queryContentApiGraphQL('{ schema }')) as { schema?: unknown }
|
|
if (typeof data.schema !== 'string' || !data.schema) {
|
|
throw new Error('Supabase Content API `{ schema }` query returned no schema string')
|
|
}
|
|
return gqlmin(data.schema)
|
|
}
|
|
|
|
/**
|
|
* Builds the tool description with the live GraphQL schema embedded, so the
|
|
* model has the same schema context production's `search_docs` gives it (see
|
|
* `@supabase/mcp-server-supabase`'s `docs-tools.ts`).
|
|
*
|
|
* Schema loading is required: running an eval without the schema makes the
|
|
* model's GraphQL queries untrustworthy and can hide a real docs-search
|
|
* regression behind fallback results.
|
|
*/
|
|
async function buildDescription(): Promise<string> {
|
|
const schema = await loadContentApiSchema()
|
|
return `${STATIC_DESCRIPTION}\n\nBelow is the GraphQL schema for this tool:\n\n${schema}`
|
|
}
|
|
|
|
/**
|
|
* Self-contained `search_docs` tool for the eval harness: calls the public
|
|
* docs GraphQL API directly, so no MCP client or access token is needed.
|
|
* Emits the MCP text-content shape the scorers parse
|
|
* (mcpTextContentSpanOutputSchema / docsFaithfulnessScorer):
|
|
* `{ content: [{ type: 'text', text: JSON.stringify({ result }) }] }`.
|
|
*
|
|
* `description` is resolved before construction (the `ai` package's `tool()`
|
|
* only accepts a plain string, not an async function like the MCP SDK's
|
|
* `docs-tools.ts` uses), so this factory is async.
|
|
*/
|
|
export type SearchDocsTool = Tool<
|
|
z.infer<typeof searchDocsInputSchema>,
|
|
{ content: Array<{ type: 'text'; text: string }> }
|
|
>
|
|
|
|
export async function createSearchDocsTool(): Promise<SearchDocsTool> {
|
|
const description = await buildDescription()
|
|
return tool({
|
|
description,
|
|
inputSchema: searchDocsInputSchema,
|
|
execute: async ({ graphql_query }: { graphql_query: string }) => {
|
|
const result = await queryContentApiGraphQL(graphql_query)
|
|
return { content: [{ type: 'text' as const, text: JSON.stringify({ result }) }] }
|
|
},
|
|
})
|
|
}
|