Files
supabase/apps/studio/routes/api/mcp/index.ts
2f90228f04 feat(studio): port API handlers to TanStack server routes (stack 4/6, from #46424) (#47113)
**Stack 4/6** of the TanStack Start migration (#46424). Stacked on
**#47112** (S3).

> [!NOTE]
> Mechanical and homogeneous — every file is the same shape: a
`createFileRoute(...)` whose `server.handlers` delegate to the existing
`pages/api` handler via `toWebHandler` (the compat shim from S2). The
pages-router handlers are unchanged; Next still serves them directly and
ignores `routes/`.

## What's in this PR
- `routes/api/**` (~104 files): platform (`pg-meta`, auth, storage,
integrations, profile, telemetry, organizations, projects…), `ai/*`,
`v1/*`, `connect`, `content`/`mcp`, and standalone endpoints
(`deployment-mode`, `get-ip-address`, etc.).
- `routeTree.gen.ts` — **regenerated** for the routes present so far
(root + auth/app + api).

## Review tip
The route files are near-identical wrappers, so this is fast to skim.
The generated `routeTree.gen.ts` isn't meaningful review surface.

## Verification
On top of S1–S3: `studio` typecheck ✓, lint (0 errors) ✓.


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

## Summary by CodeRabbit

* **New Features**
* Added Model Context Protocol (MCP) API endpoint with configurable
feature support and read-only mode
* Added function artifact streaming capability for self-hosted functions

* **Chores**
  * Migrated API route infrastructure for improved system architecture

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alaister Young <10985857+alaister@users.noreply.github.com>
Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
2026-06-30 17:18:35 +08:00

111 lines
3.8 KiB
TypeScript

import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'
import { createSupabaseMcpServer, SupabasePlatform } from '@supabase/mcp-server-supabase'
import { createFileRoute } from '@tanstack/react-router'
import { stripIndent } from 'common-tags'
import { z } from 'zod'
import { commaSeparatedStringIntoArray, zBooleanString } from '@/lib/api/apiHelpers'
import {
getDatabaseOperations,
getDebuggingOperations,
getDevelopmentOperations,
} from '@/lib/api/self-hosted/mcp'
import { DEFAULT_PROJECT } from '@/lib/constants/api'
// Twin of `pages/api/mcp/index.ts`. Web-fetch rewrite — the
// pages-router version used the SDK's Node-shaped
// `StreamableHTTPServerTransport`, which writes directly to the Node
// `res` and can't run through the buffering `toWebHandler` shim. MCP's
// `WebStandardStreamableHTTPServerTransport` accepts a Web `Request`
// and returns a Web `Response` directly, so the handler can pass the
// request through unmodified.
const supportedFeatureGroupSchema = z.enum(['docs', 'database', 'development', 'debugging'])
const mcpQuerySchema = z.object({
features: z
.string()
.transform(commaSeparatedStringIntoArray)
.optional()
.describe(
stripIndent`
A comma-separated list of feature groups to filter tools by. If not provided, all tools are available.
The following feature groups are supported: ${supportedFeatureGroupSchema.options.map((group) => `\`${group}\``).join(', ')}.
`
)
.pipe(z.array(supportedFeatureGroupSchema).optional()),
read_only: zBooleanString()
.default('false')
.describe(
'Indicates whether or not the MCP server should operate in read-only mode. This prevents write operations on any of your databases by executing SQL as a read-only Postgres user.'
),
})
const POST = async ({ request }: { request: Request }) => {
const url = new URL(request.url)
const query = Object.fromEntries(url.searchParams.entries())
const { error, data } = mcpQuerySchema.safeParse(query)
if (error) {
return new Response(JSON.stringify({ error: error.flatten().fieldErrors }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
const { features, read_only } = data
const headers = request.headers
const platform: SupabasePlatform = {
database: getDatabaseOperations({ headers }),
development: getDevelopmentOperations({ headers }),
debugging: getDebuggingOperations({ headers }),
}
try {
const server = createSupabaseMcpServer({
platform,
projectId: DEFAULT_PROJECT.ref,
features,
readOnly: read_only,
})
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless, don't use session management
enableJsonResponse: true, // Stateless, discourage SSE streams
})
await server.connect(transport)
return await transport.handleRequest(request)
} catch (err) {
// Errors at this point will be due to MCP setup issues. Subsequent
// errors are handled at the JSON-RPC level inside the protocol.
if (err instanceof Error) {
return new Response(JSON.stringify({ error: err.message }), {
status: 400,
headers: { 'Content-Type': 'application/json' },
})
}
// `err` here is a non-Error throw, so stringify a safe representation
// rather than embedding the raw value — a circular/non-serializable
// object would make JSON.stringify throw inside the catch and turn this
// into an unhandled 500.
return new Response(
JSON.stringify({ error: 'Unable to process MCP request', cause: String(err) }),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
}
)
}
}
export const Route = createFileRoute('/api/mcp/')({
server: {
handlers: {
POST,
},
},
})