Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 93 additions & 35 deletions apps/docs/content/guides/getting-started/byo-mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,18 @@ This guide covers MCP servers that do not require authentication. Auth support f

</Admonition>

## Deploy your MCP server

### Prerequisites
## Prerequisites

Before you begin, make sure you have:

- [Docker](https://docs.docker.com/get-docker/) installed (required for local Supabase development)
- [Docker](https://docs.docker.com/get-docker/) or a compatible runtime installed and running (required for local development)
- [Deno](https://deno.land/) installed (Supabase Edge Functions runtime)
- [Supabase CLI](/docs/guides/cli/getting-started) installed
- [Supabase CLI](/docs/guides/local-development) installed and authenticated
- [Node.js 20 or later](https://nodejs.org/) (required by Supabase CLI)

## Deploy your MCP server

### Create a new project
### Step 1: Create a new project

Start by creating a new Supabase project:

Expand All @@ -32,43 +33,38 @@ cd my-mcp-server
supabase init
```

### Create the MCP server function
<Admonition type="note">

After this step, you should have a project directory with a `supabase` folder containing `config.toml` and an empty `functions` directory.

</Admonition>

---

### Step 2: Create the MCP server function

Create a new Edge Function for your MCP server:

```bash
supabase functions new mcp
```

Create a `deno.json` file in `supabase/functions/mcp/` with the required dependencies:

```json
{
"imports": {
"@hono/mcp": "npm:@hono/mcp@^0.1.1",
"@modelcontextprotocol/sdk": "npm:@modelcontextprotocol/sdk@^1.24.3",
"hono": "npm:hono@^4.9.2",
"zod": "npm:zod@^4.1.13"
}
}
```

<Admonition type="tip">

This tutorial uses the [official MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk), but you can use any MCP framework that's compatible with the [Edge Runtime](/docs/guides/functions), such as [mcp-lite](https://github.com/fiberplane/mcp-lite), [mcp-use](https://github.com/mcp-use/mcp-use), or [mcp-handler](https://github.com/vercel/mcp-handler).
This tutorial uses the [official MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) with the `WebStandardStreamableHTTPServerTransport`, but you can use any MCP framework that's compatible with the [Edge Runtime](/docs/guides/functions), such as [mcp-lite](https://github.com/fiberplane/mcp-lite) or [mcp-handler](https://github.com/vercel/mcp-handler).

</Admonition>

Replace the contents of `supabase/functions/mcp/index.ts` with:

```ts
```ts name=supabase/functions/mcp/index.ts
// Setup type definitions for built-in Supabase Runtime APIs
import 'jsr:@supabase/functions-js/edge-runtime.d.ts'

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StreamableHTTPTransport } from '@hono/mcp'
import { Hono } from 'hono'
import { z } from 'zod'
import { McpServer } from 'npm:@modelcontextprotocol/sdk@1.25.3/server/mcp.js'
import { WebStandardStreamableHTTPServerTransport } from 'npm:@modelcontextprotocol/sdk@1.25.3/server/webStandardStreamableHttp.js'
import { Hono } from 'npm:hono@^4.9.7'
import { z } from 'npm:zod@^4.1.13'

// Create Hono app
const app = new Hono()
Expand All @@ -92,17 +88,31 @@ server.registerTool(
})
)

// Handle MCP requests at the root path
app.all('/', async (c) => {
const transport = new StreamableHTTPTransport()
// Handle MCP requests
app.all('*', async (c) => {
const transport = new WebStandardStreamableHTTPServerTransport()
await server.connect(transport)
return transport.handleRequest(c)
return transport.handleRequest(c.req.raw)
})

Deno.serve(app.fetch)
```

### Local development
<Admonition type="note">

After this step, you should have a new file at `supabase/functions/mcp/index.ts`.

</Admonition>

<Admonition type="caution">

Within Edge Functions, paths are prefixed with the function name. If your function is named something other than `mcp`, configure Hono with a base path: `new Hono().basePath('/your-function-name')`.

</Admonition>

---

### Step 3: Test locally

Start the Supabase local development stack:

Expand All @@ -128,7 +138,44 @@ The `--no-verify-jwt` flag disables JWT verification at the Edge Function layer

</Admonition>

### Test your MCP server
#### Test with curl

You can also test your MCP server directly with curl. Call the `add` tool:

```bash
curl -X POST 'http://localhost:54321/functions/v1/mcp' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "add",
"arguments": {
"a": 5,
"b": 3
}
}
}'
```

<Admonition type="note">

The MCP Streamable HTTP transport requires the `Accept: application/json, text/event-stream` header to indicate the client supports both JSON and Server-Sent Events responses.

</Admonition>

**Expected response:**

The response uses Server-Sent Events (SSE) format:

```
event: message
data: {"result":{"content":[{"type":"text","text":"8"}]},"jsonrpc":"2.0","id":1}
```

#### Test with MCP Inspector

Test your server with the official [MCP Inspector](https://github.com/modelcontextprotocol/inspector):

Expand All @@ -138,7 +185,13 @@ npx -y @modelcontextprotocol/inspector

Use the local endpoint `http://localhost:54321/functions/v1/mcp` in the inspector UI to explore available tools and test them interactively.

### Deploy to production
<Admonition type="note">

After this step, you should have your MCP server running locally and be able to test the `add` tool in the MCP Inspector.

</Admonition>

### Step 4: Deploy to production

When you're ready to deploy, link your project and deploy the function:

Expand All @@ -155,11 +208,17 @@ https://<your-project-ref>.supabase.co/functions/v1/mcp

Update your MCP client configuration to use the production URL.

<Admonition type="note">

After this step, you have a fully deployed MCP server accessible from anywhere. You can test it using the MCP Inspector with your production URL.

</Admonition>

## Examples

You can find ready-to-use MCP server implementations here:

- [MCP server examples on GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/mcp/)
- [Simple MCP server](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/mcp/simple-mcp-server) - Basic unauthenticated example

## Resources

Expand All @@ -168,5 +227,4 @@ You can find ready-to-use MCP server implementations here:
- [Supabase Edge Functions](/docs/guides/functions)
- [OAuth 2.1 Server](/docs/guides/auth/oauth-server)
- [MCP Authentication](/docs/guides/auth/oauth-server/mcp-authentication)
- [MCP server examples on GitHub](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/mcp)
- [Building MCP servers with mcp-lite](/docs/guides/functions/examples/mcp-server-mcp-lite) - Alternative lightweight framework
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe(`DeleteBucketModal`, () => {
status: 'ACTIVE_HEALTHY',
},
})
// useBucketsQuery
// usePaginatedBucketsQuery
addAPIMock({
method: `get`,
path: `/platform/storage/:ref/buckets`,
Expand Down
39 changes: 25 additions & 14 deletions apps/studio/components/interfaces/Storage/useBucketPolicyCount.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import { formatPoliciesForStorage } from 'components/interfaces/Storage/Storage.utils'
import { useCallback, useMemo } from 'react'

import { extractBucketNameFromDefinition } from 'components/interfaces/Storage/Storage.utils'
import { useDatabasePoliciesQuery } from 'data/database-policies/database-policies-query'
import { useBucketsQuery } from 'data/storage/buckets-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'

export const useBucketPolicyCount = () => {
const { data: project, isPending: isLoadingProject } = useSelectedProjectQuery()
const { data: buckets = [], isPending: isLoadingBuckets } = useBucketsQuery({
projectRef: project?.ref,
})
const { data: policiesData = [], isPending: isLoadingPolicies } = useDatabasePoliciesQuery({
export function useBucketPolicyCount() {
const { data: project, isPending: isProjectPending } = useSelectedProjectQuery()
const { data: policiesData = [], isPending: isPoliciesPending } = useDatabasePoliciesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: 'storage',
})
const storageObjectsPolicies = policiesData.filter((policy: any) => policy.table === 'objects')
const formattedPolicies = formatPoliciesForStorage(buckets, storageObjectsPolicies)

const getPolicyCount = (bucketName: string) => {
return formattedPolicies.find((x) => x.name === bucketName)?.policies.length ?? 0
}
const policyCountByBucket = useMemo(() => {
const countMap = new Map<string, number>()
for (const policy of policiesData) {
if (policy.table !== 'objects') continue
const bucketName =
extractBucketNameFromDefinition(policy.definition) ??
extractBucketNameFromDefinition(policy.check)
if (bucketName) {
countMap.set(bucketName, (countMap.get(bucketName) ?? 0) + 1)
}
}
return countMap
}, [policiesData])

const getPolicyCount = useCallback(
(bucketName: string) => policyCountByBucket.get(bucketName) ?? 0,
[policyCountByBucket]
)

return { getPolicyCount, isLoading: isLoadingProject || isLoadingBuckets || isLoadingPolicies }
return { getPolicyCount, isLoading: isProjectPending || isPoliciesPending }
}
19 changes: 0 additions & 19 deletions apps/studio/data/storage/buckets-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,25 +133,6 @@ export const useBucketQuery = <TData = BucketData>(
})
}

/**
* @deprecated - use usePaginatedBucketsQuery instead for better performance
*/
export const useBucketsQuery = <TData = BucketsData>(
{ projectRef }: BucketsVariables,
{ enabled = true, ...options }: UseCustomQueryOptions<BucketsData, BucketsError, TData> = {}
) => {
const { data: project } = useSelectedProjectQuery()
const isActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY

return useQuery<BucketsData, BucketsError, TData>({
queryKey: storageKeys.buckets(projectRef),
queryFn: ({ signal }) => getBuckets({ projectRef }, signal),
enabled: enabled && typeof projectRef !== 'undefined' && isActive,
...options,
retry: shouldRetryBucketsQuery,
})
}

export const useBucketNumberEstimateQuery = (
{ projectRef }: BucketsVariables,
{ enabled = true, ...options }: UseCustomQueryOptions<number | undefined, ResponseError> = {}
Expand Down
6 changes: 3 additions & 3 deletions apps/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"@heroicons/react": "^2.1.3",
"@hookform/resolvers": "^3.1.1",
"@mjackson/multipart-parser": "^0.10.1",
"@modelcontextprotocol/sdk": "^1.24.0",
"@modelcontextprotocol/sdk": "^1.25.2",
"@monaco-editor/react": "^4.6.0",
"@next/bundle-analyzer": "^16.0.3",
"@number-flow/react": "^0.3.2",
Expand All @@ -59,8 +59,8 @@
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.5.0",
"@supabase/auth-js": "catalog:",
"@supabase/mcp-server-supabase": "^0.6.1",
"@supabase/mcp-utils": "^0.3.1",
"@supabase/mcp-server-supabase": "^0.6.2",
"@supabase/mcp-utils": "^0.3.2",
"@supabase/pg-meta": "workspace:*",
"@supabase/realtime-js": "catalog:",
"@supabase/shared-types": "0.1.83",
Expand Down
25 changes: 24 additions & 1 deletion apps/studio/tests/pages/api/mcp/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
import { describe, it, expect, beforeEach } from 'vitest'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { createMocks } from 'node-mocks-http'
import { mswServer } from 'tests/lib/msw'
import handler from '../../../../pages/api/mcp/index'

// Mock the MCP SDK and Supabase MCP server to avoid Hono/node-mocks-http compatibility issues.
//
// Starting with MCP SDK v1.25.x (required by @supabase/mcp-server-supabase@0.6.2), the SDK
// uses @hono/node-server for converting between Node.js HTTP and Web Standard APIs. This is
// incompatible with the node-mocks-http library used in these tests.
//
// Since these tests focus on query parameter validation in the API handler rather than
// testing the MCP transport implementation, we mock both packages to avoid hitting the
// incompatible Hono code paths.
vi.mock('@modelcontextprotocol/sdk/server/streamableHttp.js', () => ({
StreamableHTTPServerTransport: vi.fn().mockImplementation(() => ({
handleRequest: vi.fn().mockResolvedValue(undefined),
})),
}))

vi.mock('@supabase/mcp-server-supabase', () => ({
createSupabaseMcpServer: vi.fn().mockReturnValue({
connect: vi.fn().mockResolvedValue(undefined),
}),
}))

describe('/api/mcp', () => {
beforeEach(() => {
// Disable MSW for these tests
mswServer.close()
// Clear mock state between tests
vi.clearAllMocks()
})

describe('Method handling', async () => {
Expand Down
8 changes: 4 additions & 4 deletions apps/www/components/Contribute/UnansweredThreadsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ function ThreadRow({
return (
<TableRow className="relative group [&.hovering-badge>td]:hover:!bg-transparent">
{/* Thread title and product areas */}
<TableCell className="min-w-[400px]">
<TableCell className="w-auto max-w-[600px]">
<div className="flex items-center gap-3 overflow-hidden">
{/* Channel icon */}
<div className="flex items-center justify-center bg-surface-200 h-10 w-10 rounded-md">
Expand Down Expand Up @@ -483,7 +483,7 @@ function ThreadRow({
</div>
</TableCell>
{/* Stack */}
<TableCell>
<TableCell className="w-[300px]">
<div
onMouseEnter={(e) => {
const row = e.currentTarget.closest('tr')
Expand Down Expand Up @@ -595,8 +595,8 @@ function ThreadRow({
</TableCell>

{/* Replies */}
<TableCell className="text-right">
<div className="flex flex-row items-center gap-2">
<TableCell className="text-right w-[100px]">
<div className="flex flex-row items-center justify-end gap-2">
{thread.message_count !== null && thread.message_count !== undefined && (
<MessageSquareReply
size={18}
Expand Down
Loading
Loading