Supabase Edge函数的MCP——最佳实践指南
用于构建部署为Supabase边缘功能的模型上下文协议(MCP)服务器的脚手架参考。作为一个活的文档——分叉它,扩展它,并使其适应你的堆栈。
______________________________________________________________________
目录
- 本指南涵盖的内容
- 核心概念
- 项目结构
- MCP服务器模板
- 定义工具
- 身份验证和授权
- 环境变量和秘密
- 错误处理
- 连接到Supabase服务
- 传输层(SSE与流式HTTP)
- CORS配置
- 本地测试
- 部署
- 连接到Claude.ai
- 安全检查列表
- 性能和限制
- 常见陷阱
- 完整工作示例
______________________________________________________________________
1.本指南涵盖的内容
本指南将引导您了解构建MCP(模型上下文协议)服务器的架构和最佳实践,该服务器作为 Supabase边缘功能。当您想要:
- 通过MCP将您的Supabase数据库、存储或第三方API暴露给LLM
- 保留您的服务器 无服务器 没有要管理的基础设施
- 杠杆作用 Supabase认证 对MCP客户端进行身份验证
- 以最小的延迟在边缘进行全球部署
主控程序 是一个开放协议(由Anthropic提供),规范了人工智能模型与外部工具和数据源的交互方式。将其视为AI集成的USB-C标准。
Supabase边缘函数 运行在Deno上,通过Cloudflare的网络在全球部署。它们原生支持流式响应,这对MCP的SSE传输至关重要。
______________________________________________________________________
2.核心概念
MCP图元
| 原始 | 描述 |
|---|---|
| 工具 | LLM可以调用的功能(例如查询数据库、发送电子邮件) |
| 资源 | LLM可以读取的数据(例如,文档、模式定义) |
| 提示 | LLM可以调用的可重用提示模板 |
对于大多数边缘函数用例,您将主要实现 工具.
请求/响应流
LLM (Claude) → MCP Client → HTTPS POST → Supabase Edge Function → Your Logic → Response边缘函数充当 无状态MCP服务器每个请求都是完全独立的。
______________________________________________________________________
3.项目结构
supabase/
├── functions/
│ ├── deno.json # Import map: @shared/ → ./_shared/
│ ├── _shared/
│ │ └── mcp-auth/
│ │ ├── mod.ts # Entry point — authenticate(req)
│ │ ├── api-key.ts # Strategy: API key (mcp_sk_...)
│ │ ├── supabase-jwt.ts # Strategy: Supabase JWT (getClaims/getUser)
│ │ └── types.ts # AuthIdentity, AuthResult
│ └── mcp-server/
│ ├── index.ts # Entry point — handles HTTP and routes to MCP
│ ├── server.ts # MCP server definition and tool registration
│ ├── tools/
│ │ ├── index.ts # Re-exports all tools
│ │ ├── query.ts # Example: database query tool
│ │ └── storage.ts # Example: storage tool
│ ├── auth.ts # Re-export from @shared/mcp-auth
│ ├── cors.ts # CORS headers
│ └── types.ts # Shared types
├── .env.local # Local secrets (never commit)
└── config.toml # Supabase project config公约: 将每个工具保存在自己的文件中。随着MCP的增长,它使测试、文档和代码审查变得更加容易。
______________________________________________________________________
4.MCP服务器模板
index.ts --入口点
import { corsHeaders, handleCors } from "./cors.ts";
import { authenticate } from "./auth.ts";
import { createMcpServer } from "./server.ts";
Deno.serve(async (req: Request) => {
// Handle CORS preflight
const corsResponse = handleCors(req);
if (corsResponse) return corsResponse;
try {
// Authenticate every request (see Section 6)
const result = await authenticate(req);
if (!result.success) {
return new Response(JSON.stringify({ error: result.error }), {
status: result.status,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Route to MCP handler
const server = createMcpServer(result.identity);
return await server.handle(req);
} catch (error) {
console.error("[MCP] Unhandled error:", error);
return new Response(JSON.stringify({ error: "Internal server error" }), {
status: 500,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
});server.ts --MCP服务器定义
import { McpServer } from "npm:@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "npm:@modelcontextprotocol/sdk/server/streamableHttp.js";
import { allTools } from "./tools/index.ts";
export function createMcpServer(user: AuthUser) {
const server = new McpServer({
name: "my-mcp-server",
version: "1.0.0",
});
// Register all tools, passing user context
allTools.forEach((tool) => tool.register(server, user));
return {
async handle(req: Request): Promise {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless — no session needed
});
const response = await transport.handleRequest(req, async () => {
await server.connect(transport);
});
return response;
},
};
}______________________________________________________________________
5.定义工具
工具界面模式
为所有工具定义一致的界面:
// types.ts
export interface AuthUser {
id: string;
email: string;
role: string;
}
export interface McpTool {
register(server: McpServer, user: AuthUser): void;
}示例工具——数据库查询
// tools/query.ts
import { z } from "npm:zod";
import { createClient } from "npm:@supabase/supabase-js";
import type { McpTool, AuthUser } from "../types.ts";
export const queryTool: McpTool = {
register(server, user) {
server.tool(
// Tool name — use snake_case, descriptive, action-oriented
"query_records",
// Human-readable description — critical for LLM to know when to use it
"Query records from the database. Returns matching rows as JSON. " +
"Use this when you need to look up data, filter records, or retrieve information.",
// Input schema using Zod
{
table: z.string().describe("The table name to query"),
filters: z.record(z.string()).optional().describe(
"Optional key-value pairs to filter results. Example: { status: 'active' }"
),
limit: z.number().min(1).max(100).default(20).describe(
"Maximum number of records to return (default: 20, max: 100)"
),
},
// Handler — receives validated inputs
async ({ table, filters, limit }) => {
try {
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);
// Always scope queries to the authenticated user
let query = supabase
.from(table)
.select("*")
.eq("user_id", user.id) // Row-level scoping
.limit(limit);
if (filters) {
Object.entries(filters).forEach(([key, value]) => {
query = query.eq(key, value);
});
}
const { data, error } = await query;
if (error) throw error;
return {
content: [{
type: "text",
text: JSON.stringify(data, null, 2),
}],
};
} catch (error) {
return {
content: [{
type: "text",
text: `Error querying ${table}: ${error.message}`,
}],
isError: true,
};
}
}
);
},
};工具注册索引
// tools/index.ts
import { queryTool } from "./query.ts";
import { storageTool } from "./storage.ts";
export const allTools = [queryTool, storageTool];工具命名最佳实践
| ✅ 好 | ❌ 避免 |
|---|---|
query_records | getData |
create_invoice | doInvoice |
send_notification | notify |
list_projects | projects |
规则:
- 使用
verb_noun格式 - 具体来说——LLM使用名称和描述来决定调用哪个工具
- 在服务器上保持名称唯一
- 永远不要使用像这样的保留名称
list_tools,call_tool
编写好的工具描述
描述是工具最重要的部分。把它写下来,就像你在向初级开发人员解释这个工具一样,他们需要确切地知道何时以及如何使用它。
// ❌ Weak description
"Get project data"
// ✅ Strong description
"Retrieve a list of projects for the current user. Returns project name, status, " +
"start date, and team members. Use this when the user asks about their projects, " +
"wants to see what's in progress, or needs project details. " +
"Does NOT return archived projects — use list_archived_projects for those."______________________________________________________________________
6.身份验证和授权
架构概述
身份验证集中在共享模块中(_shared/mcp-auth/)由所有MCP功能导入。Supabase网关可以 不 验证JWT(verify_jwt = false)--验证完全在函数代码内部处理。这是必需的,因为Supabase的网关JWT验证与新的非对称签名密钥(2025年后)不兼容。
参考文献
文件结构
supabase/functions/
├── deno.json # Import map: @shared/ → ./_shared/
├── _shared/
│ └── mcp-auth/
│ ├── mod.ts # Entry point — authenticate(req)
│ ├── api-key.ts # Strategy: API key (mcp_sk_...)
│ ├── supabase-jwt.ts # Strategy: Supabase JWT (getClaims/getUser)
│ └── types.ts # AuthIdentity, AuthResult
├── mcp-server/
│ ├── auth.ts # Re-export from @shared/mcp-auth
│ └── ...导入地图(deno.json)
这 _shared/ Supabase bundler在部署时不会自动解析文件夹。别名在 supabase/functions/deno.json 解决这个问题:
{
"imports": {
"@shared/": "./_shared/"
}
}身份验证流程
Incoming HTTP request
│
▼
┌─ SKIP_AUTH=true ? ──────────────────────── Yes ─── Return DEV_IDENTITY (local dev)
│ │
│ No
│ │
│ ▼
│ Authorization header present?
│ │
│ No ───────────────────────────────── 401 "Missing Authorization header"
│ │
│ Yes
│ │
│ ▼
│ Extract Bearer token
│ │
│ ▼
│ Token starts with mcp_sk_ ?
│ │
│ Yes ──── validateApiKey() ─────────── Check against MCP_API_KEYS
│ │ │
│ No Found? ── Yes ── AuthIdentity (method: api_key)
│ │ │
│ │ No ── 401 "Invalid API key"
│ ▼
│ validateSupabaseJwt()
│ │
│ ▼
│ getClaims(token) available?
│ │
│ Yes ──── Try getClaims() ──── Success? ── AuthIdentity (method: supabase_jwt)
│ │ │
│ │ Fail ── Fallback to getUser()
│ No
│ │
│ ▼
│ getUser(token) ────────────────────── Success? ── AuthIdentity (method: supabase_jwt)
│ │
│ Fail ── 401 "Invalid or expired JWT"类型
/** Authenticated identity returned by the middleware. */
export interface AuthIdentity {
id: string;
email: string;
role: string;
method: "api_key" | "supabase_jwt" | "skip_auth";
}
/** Result of an authentication attempt. */
export type AuthResult =
| { success: true; identity: AuthIdentity }
| { success: false; error: string; status: number };方法1-SKIP_AUTH(仅限本地开发)
对于无需任何身份验证的快速本地开发:
# .env.local
SKIP_AUTH=true返回一个固定的开发标识。代码发出 console.warn 标记身份验证已禁用。 切勿在生产中启用。
方法2-API密钥(机器对机器)
对于Claude Desktop、Cowork、服务器脚本、后端集成——任何无法交互式刷新JWT的客户端。
令牌格式: mcp_sk_ 后面是随机字符串(建议:64个十六进制字符)。
Authorization: Bearer mcp_sk_a1b2c3d4e5f6...秘密配置: 这 MCP_API_KEYS 机密包含逗号分隔 name:key 对:
MCP_API_KEYS="claude-desktop:mcp_sk_abc123,backend-app:mcp_sk_xyz789"该名称标识了哪个客户端发出了请求(对日志和每个密钥的撤销很有用)。
生成密钥:
openssl rand -hex 32
# Result: a1b2c3d4e5f6...
# Full key: mcp_sk_a1b2c3d4e5f6...关键点旋转 (零停机时间):
- 生成新密钥
- 将新密钥添加到
MCP_API_KEYS(暂时保留旧的) - 更新客户端以使用新密钥
- 从中取出旧钥匙
MCP_API_KEYS
方法3——Supabase JWT(网络用户)
对于用户通过Supabase Auth(电子邮件/密码、OAuth、Magic Link等)登录的web应用程序。
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...所需机密:
| 秘密 | 描述 | 自动注射? |
|---|---|---|
SUPABASE_URL | Supabase项目URL | 是 |
SB_PUBLISHABLE_KEY | 新的可发布密钥(2025年5月后的项目) | 否--必须手动设置 |
SUPABASE_ANON_KEY | 旧密钥 | 是 |
验证策略(级联):
getClaims(token)--新方法(Supabase JS v2+)。使用非对称密钥在本地验证JWT。速度快,很少需要网络。getUser(token)--遗产回退。对Auth服务器进行网络调用。速度较慢,但与所有项目兼容。
集成新的MCP功能
1.创建 auth.ts 在您的功能文件夹中(重新导出):
export { authenticate } from "@shared/mcp-auth/mod.ts";
export type { AuthIdentity, AuthResult } from "@shared/mcp-auth/mod.ts";2.打电话 authenticate(req) 在 index.ts:
import { authenticate } from "./auth.ts";
Deno.serve(async (req: Request) => {
const result = await authenticate(req);
if (!result.success) {
return new Response(JSON.stringify({ error: result.error }), {
status: result.status,
headers: { "Content-Type": "application/json" },
});
}
const identity = result.identity;
// ... MCP logic with identity
});3.设置 verify_jwt = false 在 config.toml:
[functions.mcp-server]
verify_jwt = false4.部署 --no-verify-jwt:
supabase functions deploy mcp-server --no-verify-jwt生产部署——循序渐进
在新的MCP功能上将身份验证部署到生产环境的具体配方:
步骤1-生成API密钥:
openssl rand -hex 32
# Result: a1b2c3d4e5f6...
# Full key: mcp_sk_a1b2c3d4e5f6...这 mcp_sk_ 前缀允许auth模块将API密钥与SubabaseJWT区分开来,并路由到正确的验证策略。
步骤2——在Supabase secrets中注册密钥:
supabase secrets set MCP_API_KEYS="claude-desktop:mcp_sk_a1b2c3d4e5f6..."这 name:key 该格式标识日志中的每个客户端,并允许按密钥撤销。多个键以逗号分隔。
步骤3——禁用 verify_jwt 在网关处:
在 supabase/config.toml:
[functions.mcp-server]
verify_jwt = falseSupabase网关与新的非对称签名密钥(2025年后)不兼容。身份验证完全在函数代码中处理,遵循 Supabase推荐的模式.
步骤4——使用相对进口 _shared:
Supabase bundler没有可靠地解决 @shared/ 在部署时导入映射。使用 相对路径 在您的函数代码中:
// ✅ Relative import — works with Supabase deploy bundler
import { authenticate } from "../_shared/mcp-auth/mod.ts";
// ❌ Import map alias — may fail during supabase functions deploy
import { authenticate } from "@shared/mcp-auth/mod.ts";注: A.deno.json随着@shared/映射仍然可以用于本地开发和IDE支持,但生产部署需要相对路径。
步骤5——部署:
supabase functions deploy mcp-server --no-verify-jwt步骤6——验证:
curl -X POST https://
.supabase.co/functions/v1/mcp-server \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Authorization: Bearer mcp_sk_YOUR_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}'成功的响应包含 serverInfo 使用您的服务器名称和版本,确认API密钥身份验证在生产中有效。
步骤7——配置客户端:
对于克劳德桌面/协作,请使用 npx supergateway 作为stdio↔ 流式HTTP代理(请参阅下面的客户端配置示例)。 不要使用 mcp-remote --它执行与Supabase边缘函数不兼容的强制OAuth发现。对于web应用程序,请通过Supabase session.access_token 作为不记名代币。
授权模式
1.始终将数据库查询范围限定为经过身份验证的用户:
supabase.from("projects").select("*").eq("user_id", identity.id)2.使用Supabase行级安全(RLS)作为安全网: 即使你的工具代码是正确的,RLS也能在出现问题时防止数据泄漏。在每个表上启用RLS并定义策略。
-- Example RLS policy
CREATE POLICY "Users can only access their own records"
ON projects FOR ALL
USING (auth.uid() = user_id);3.基于角色的工具访问:
server.tool("admin_export_all", "...", {}, async () => {
if (identity.role !== "admin") {
return {
content: [{ type: "text", text: "Access denied: admin role required." }],
isError: true,
};
}
// ... admin logic
});客户端配置示例
克劳德桌面/协作:
使用 supergateway 桥接stdio↔ 流式HTTP。Claude Desktop不支持远程URL claude_desktop_config.json.
{
"mcpServers": {
"my-mcp": {
"command": "npx",
"args": [
"-y",
"supergateway",
"--streamableHttp",
"https://
.supabase.co/functions/v1/mcp-server",
"--header",
"Authorization: Bearer mcp_sk_YOUR_KEY"
]
}
}
}警告: 不要使用mcp-remote--它执行强制OAuth发现(registerClient)在连接之前,这会对Supabase Edge Functions产生影响ServerError.使用supergateway相反,它通过Streamable HTTP直接与提供的标头连接。
Web应用程序(Supabase Auth):
const { data: { session } } = await supabase.auth.getSession();
const response = await fetch(
"https://
.supabase.co/functions/v1/mcp-server",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${session.access_token}`,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "list_my_projects", arguments: {} },
}),
}
);服务器脚本(curl):
curl -X POST https://
.supabase.co/functions/v1/mcp-server \
-H "Content-Type: application/json" \
-H "Authorization: Bearer mcp_sk_YOUR_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'故障排除
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
401 "Missing or malformed Authorization header" | 没有 Authorization: Bearer ... header | 添加带有正确标记的标头 |
401 "Invalid API key" | mcp_sk_... 在中找不到密钥 MCP_API_KEYS | 用以下方式检查秘密 supabase secrets list |
401 "Invalid or expired JWT" | Supabase JWT已过期或无效 | 刷新客户端令牌 |
500 "API key authentication is not configured" | MCP_API_KEYS 秘密失踪 | supabase secrets set MCP_API_KEYS=... |
500 "Supabase JWT authentication is not configured" | 失踪 SB_PUBLISHABLE_KEY 和 SUPABASE_ANON_KEY | 将可发布密钥作为秘密公开 |
{"msg":"Missing authorization header"} (功能前) | verify_jwt 仍在网关处启用 | 使用重新部署 --no-verify-jwt |
安全规则
- 从不启用
SKIP_AUTH在生产中。 - 永远不要暴露
mcp_sk_...客户端密钥 (浏览器、公共源代码)。 - 始终使用部署
--no-verify-jwt--Supabase网关与新的密钥模型和MCP模式不兼容。 - 使用HTTPS 对于所有生产通信(默认情况下由Supabase提供)。
______________________________________________________________________
7.环境变量和秘密
必需变量
# Always available automatically in Supabase Edge Functions
SUPABASE_URL
SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY
SUPABASE_DB_URL
# Auth — API keys for machine-to-machine clients (see Section 6)
MCP_API_KEYS=claude-desktop:mcp_sk_XXXX,cowork:mcp_sk_YYYY
# Auth — Supabase publishable key for JWT validation (post-May 2025 projects)
SB_PUBLISHABLE_KEY=sb_publishable_XXXX
# Your custom secrets
EXTERNAL_SERVICE_API_KEY=...设置秘密
# API keys for machine-to-machine clients
supabase secrets set MCP_API_KEYS="claude-desktop:mcp_sk_XXXX,cowork:mcp_sk_YYYY"
# Supabase publishable key (for JWT validation with new asymmetric keys)
supabase secrets set SB_PUBLISHABLE_KEY=sb_publishable_XXXX
# Your custom secrets
supabase secrets set EXTERNAL_API_KEY=abc123
# List all secrets (values hidden)
supabase secrets list本地开发
创建 supabase/.env.local (切勿提交此文件):
# Skip auth entirely for local dev (never use in production)
SKIP_AUTH=true
# Or test with API key auth:
# SKIP_AUTH=false
# MCP_API_KEYS=dev-test:mcp_sk_test123
EXTERNAL_API_KEY=test-key添加 .gitignore:
supabase/.env.local访问代码中的秘密
// Always use Deno.env.get — never hardcode secrets
const apiKey = Deno.env.get("EXTERNAL_API_KEY");
if (!apiKey) throw new Error("EXTERNAL_API_KEY is not configured");______________________________________________________________________
8.错误处理
三个错误级别
1级——工具错误 (预期失败,返回LLM):
return {
content: [{ type: "text", text: "Record not found: id 123 does not exist." }],
isError: true,
};级别2——服务器错误 (意外故障,记录并返回HTTP 500):
try {
// ... tool logic
} catch (error) {
console.error("[tool:query_records] Unexpected error:", error);
return {
content: [{ type: "text", text: "An unexpected error occurred. Please try again." }],
isError: true,
};
}级别3——身份验证/验证错误 (在MCP层之前返回HTTP 401/400):
if (!user) {
return new Response("Unauthorized", { status: 401 });
}错误消息指南
- 足够具体 LLM可以自我纠正或有意义地通知用户
- 不要泄漏 生产中的内部详细信息(堆栈跟踪、SQL错误、内部ID)
- 做日志 服务器端使用的完整错误详细信息
console.error
// ❌ Too vague
return { content: [{ type: "text", text: "Error" }], isError: true };
// ❌ Too much information (leaks internals)
return { content: [{ type: "text", text: error.stack }], isError: true };
// ✅ Actionable, safe
return {
content: [{
type: "text",
text: `Failed to create invoice: the project "${projectName}" does not exist or you don't have access to it.`,
}],
isError: true,
};______________________________________________________________________
9.连接到Supabase服务
数据库(具有服务角色--绕过RLS)
仅用于受信任的服务器端操作。始终更喜欢范围查询。
import { createClient } from "npm:@supabase/supabase-js";
const adminClient = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!
);数据库(带有用户令牌——强制RLS)
首选图案。RLS策略会自动应用。
const userClient = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: `Bearer ${userToken}` } } }
);存储
const { data, error } = await supabase.storage
.from("documents")
.download(`${user.id}/report.pdf`);边缘函数调用其他边缘函数
const response = await fetch(
`${Deno.env.get("SUPABASE_URL")}/functions/v1/other-function`,
{
method: "POST",
headers: {
Authorization: `Bearer ${Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ payload }),
}
);______________________________________________________________________
10.传输层(SSE与流式HTTP)
MCP支持两种传输模式。根据您的客户选择:
流式HTTP(推荐用于边缘功能)
- 单端点,无状态,适用于任何HTTP客户端
- 最适合Supabase边缘功能(无持久连接)
- 由Claude.ai远程MCP连接支持
import { StreamableHTTPServerTransport } from "npm:@modelcontextprotocol/sdk/server/streamableHttp.js";
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // Stateless mode
});SSE(遗留问题——避免新项目)
- 需要持久连接——边缘功能超时有问题
- 仍然支持与旧MCP客户端的向后兼容性
- 如果必须支持它,请实现会话存储(例如,Supabase Realtime或KV)
______________________________________________________________________
11.CORS配置
cors.ts
// Adjust allowed origins for your environment
const ALLOWED_ORIGINS = [
"https://claude.ai",
"http://localhost:3000",
"http://localhost:5173",
];
export const corsHeaders = {
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type, x-api-key",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
};
export function handleCors(req: Request): Response | null {
const origin = req.headers.get("Origin") ?? "";
const allowedOrigin = ALLOWED_ORIGINS.includes(origin)
? origin
: ALLOWED_ORIGINS[0]; // Default fallback
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
...corsHeaders,
"Access-Control-Allow-Origin": allowedOrigin,
},
});
}
return null; // Not a preflight — let the request proceed
}安全说明: 避免 Access-Control-Allow-Origin: * 在生产中。准确列举您信任的来源。______________________________________________________________________
12.本地测试
启动Supabase并提供功能
# Start local Supabase
supabase start
# Serve your edge function locally with environment variables
supabase functions serve mcp-server --env-file supabase/.env.local您的功能现在可以在以下网址使用:\ http://localhost:54321/functions/v1/mcp-server
使用MCP检查员进行测试
官方MCP Inspector是交互式测试工具的最快方法:
npx @modelcontextprotocol/inspector指向 http://localhost:54321/functions/v1/mcp-server 并添加您的授权标头。
卷曲测试
# List available tools
curl -X POST http://localhost:54321/functions/v1/mcp-server \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}'
# Call a specific tool
curl -X POST http://localhost:54321/functions/v1/mcp-server \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "query_records",
"arguments": {
"table": "projects",
"limit": 5
}
}
}'自动化测试
// tests/query_tool_test.ts
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
Deno.test("query_records returns data for valid user", async () => {
const response = await fetch("http://localhost:54321/functions/v1/mcp-server", {
method: "POST",
headers: {
"Authorization": "Bearer TEST_TOKEN",
"Content-Type": "application/json",
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "tools/call",
params: { name: "query_records", arguments: { table: "projects" } },
}),
});
const data = await response.json();
assertEquals(response.status, 200);
assertEquals(data.result?.content?.[0]?.type, "text");
});______________________________________________________________________
13.部署
部署功能
# Deploy a single function (--no-verify-jwt is required for MCP auth — see Section 6)
supabase functions deploy mcp-server --no-verify-jwt
# Deploy all functions
supabase functions deploy设定制作秘密
# Auth secrets (see Section 6)
supabase secrets set MCP_API_KEYS="claude-desktop:mcp_sk_XXXX,cowork:mcp_sk_YYYY"
supabase secrets set SB_PUBLISHABLE_KEY=sb_publishable_XXXX
# Your custom secrets
supabase secrets set EXTERNAL_API_KEY=prod-api-key验证部署
# Check function logs
supabase functions logs mcp-server --tail
# Test production endpoint
curl -X POST https://YOUR_PROJECT.supabase.co/functions/v1/mcp-server \
-H "Authorization: Bearer YOUR_JWT" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'CI/CD(GitHub操作)
# .github/workflows/deploy.yml
name: Deploy MCP Server
on:
push:
branches: [main]
paths:
- "supabase/functions/mcp-server/**"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Supabase CLI
uses: supabase/setup-cli@v1
with:
version: latest
- name: Deploy Edge Function
run: supabase functions deploy mcp-server --project-ref ${{ secrets.SUPABASE_PROJECT_REF }}
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}______________________________________________________________________
14.连接到第.ai条
部署后,将MCP服务器作为远程集成连接到Claude.ai中:
- 打开 Claude.ai→ 设置→ 集成
- 添加新的集成:
- 网址: https://YOUR_PROJECT.supabase.co/functions/v1/mcp-server - 身份验证: 承载令牌(您的Subabase JWT或API密钥)
- 克劳德会打电话的
tools/list自动发现您的工具
生成用于测试的Supabase JWT
// Generate a test JWT using the Supabase client
const { data } = await supabase.auth.signInWithPassword({
email: "test@example.com",
password: "your-password",
});
console.log(data.session?.access_token);______________________________________________________________________
15.安全检查表
在投入生产之前,请验证:
- \[ \] 对每个请求都强制执行身份验证 --没有有效令牌,无法访问任何工具
- \[ \]
SKIP_AUTH已禁用 生产中(从未设置SKIP_AUTH=true外部本地开发) - \[ \]
verify_jwt = false已设置config.toml并部署了--no-verify-jwt - \[ \] API密钥(
mcp_sk_...)从不暴露在客户端 --仅由服务器/桌面客户端使用 - \[ \] RLS已启用 在您的工具接触过的每张Supabase桌子上
- \[ \] 服务角色密钥从未公开 到客户端--仅用于服务器端
- \[ \] 输入验证 通过Zod模式在所有工具参数上完成
- \[ \] 允许的来源 在CORS配置中明确列出(无通配符
*) - \[ \] 秘密被储存 在Supabase Vault/secrets中,而不是在代码或环境文件中
- \[ \] 敏感错误不会返回 到LLM--仅通用消息
- \[ \] 日志不包含 PII、API密钥或令牌
- \[ \] 速率限制 被认为是昂贵的工具(使用Supabase的内置速率限制或Redis/KV中的自定义计数器)
- \[ \] 工具范围很小 --每个工具只做一件事,使用尽可能窄的数据库权限
______________________________________________________________________
16.性能和限制
Supabase边缘函数限制
| 限制 | 值 |
|---|---|
| 最大执行时间 | 150秒(付费计划为400秒) |
| 最大请求正文 | 6 MB |
| 最大响应正文 | 6 MB |
| 冷启动 | ~300ms(首次调用) |
| 并发 | 无限制(自动缩放) |
性能最佳实践
保持工具快速:
- 每次工具调用的目标时间\ tool.register(server, user)); // Once
return { handle: async (req) => ... }; }
### ❌ 忘记处理CORS飞行前
Claude.ai和浏览器发送 `OPTIONS` 在实际开机自检之前进行飞行前检查。没有它,请求就会悄无声息地失败。
### ❌ 在用户范围的操作中使用服务角色键
服务角色绕过RLS。如果将其用于面向用户的查询,一个用户可能会通过过滤器逻辑中的错误访问另一个用户的数据。使用RLS强制查询作为默认值。
### ❌ 返回原始数据库错误
数据库错误通常包含表名、列名或查询片段。在回到法学硕士之前,一定要抓住并重新措辞。
### ❌ 不验证输入类型
在没有Zod模式的情况下, `limit = "hello"` 可能会访问您的数据库查询并导致令人困惑的失败。
### ❌ 阻塞事件循环
Supabase边缘函数是单线程的(Deno)。长同步操作会阻止所有请求。始终使用异步I/O。
______________________________________________________________________
## 18.完整工作示例
一个最小但完整的MCP服务器,只需一个工具:
// supabase/functions/mcp-server/index.ts import { McpServer } from "npm:@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "npm:@modelcontextprotocol/sdk/server/streamableHttp.js"; import { createClient } from "npm:@supabase/supabase-js"; import { z } from "npm:zod"; import { authenticate } from "./auth.ts";
const corsHeaders = { "Access-Control-Allow-Origin": "https://claude.ai", "Access-Control-Allow-Headers": "authorization, content-type", "Access-Control-Allow-Methods": "POST, OPTIONS", };
Deno.serve(async (req: Request) => { // CORS preflight if (req.method === "OPTIONS") { return new Response(null, { status: 204, headers: corsHeaders }); }
// Auth (supports API keys, Supabase JWT, and SKIP_AUTH for local dev) const result = await authenticate(req); if (!result.success) { return new Response(JSON.stringify({ error: result.error }), { status: result.status, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); }
const identity = result.identity;
// MCP Server const server = new McpServer({ name: "minimal-mcp", version: "1.0.0" });
server.tool( "list_my_projects", "List all projects belonging to the current user. Returns id, name, and status.", { limit: z.number().min(1).max(50).default(10) }, async ({ limit }) => { const adminClient = createClient( Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")! );
const { data, error } = await adminClient .from("projects") .select("id, name, status") .eq("user_id", identity.id) .limit(limit);
if (error) { return { content: [{ type: "text", text: Failed to load projects: ${error.message} }], isError: true, }; }
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], }; } );
// Transport const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, });
const response = await transport.handleRequest(req, async () => { await server.connect(transport); });
// Inject CORS headers into MCP response const newHeaders = new Headers(response.headers); Object.entries(corsHeaders).forEach(([k, v]) => newHeaders.set(k, v));
return new Response(response.body, { status: response.status, headers: newHeaders, }); });
______________________________________________________________________
## 参考文献
- [MCP协议规范](https://modelcontextprotocol.io)
- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
- [Supabase Edge函数文档](https://supabase.com/docs/guides/functions)
- [Supabase行级安全](https://supabase.com/docs/guides/database/postgres/row-level-security)
- [MCP检查员](https://github.com/modelcontextprotocol/inspector)
______________________________________________________________________