Token导航 LogoToken导航TokenDH.com
Supabase MCP Template logo
数据服务stdio官方级别未说明来源级核验

Supabase MCP Template

MCP Server

@modelcontextprotocol/inspector

一个基于Supabase Edge Functions的MCP(Model Context Protocol)服务器模板,用于构建AI模型与外部工具和数据源交互的标准化接口。

工具数

2

提示词数

0

GitHub Stars

0

资源数

0
边缘计算服务器模板TypeScriptClaudeClaude DesktopClaude

安装说明

本站只整理中文说明和来源信息,不托管安装包,也不代用户安装。

作者 / 组织

matt-fournier

提供方

matt-fournier

最后核验

2026/5/17 20:20

运行时

Node.js

快速接入

先看主来源和安装命令,再打开仓库或文档;下面只保留这个条目的关键接入事实。

命令预览

npx @modelcontextprotocol/inspector

详细介绍

Supabase Edge函数的MCP——最佳实践指南

用于构建部署为Supabase边缘功能的模型上下文协议(MCP)服务器的脚手架参考。作为一个活的文档——分叉它,扩展它,并使其适应你的堆栈。

______________________________________________________________________

目录

  1. 本指南涵盖的内容
  2. 核心概念
  3. 项目结构
  4. MCP服务器模板
  5. 定义工具
  6. 身份验证和授权
  7. 环境变量和秘密
  8. 错误处理
  9. 连接到Supabase服务
  10. 传输层(SSE与流式HTTP)
  11. CORS配置
  12. 本地测试
  13. 部署
  14. 连接到Claude.ai
  15. 安全检查列表
  16. 性能和限制
  17. 常见陷阱
  18. 完整工作示例

______________________________________________________________________

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_recordsgetData
create_invoicedoInvoice
send_notificationnotify
list_projectsprojects

规则:

  • 使用 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...

关键点旋转 (零停机时间):

  1. 生成新密钥
  2. 将新密钥添加到 MCP_API_KEYS (暂时保留旧的)
  3. 更新客户端以使用新密钥
  4. 从中取出旧钥匙 MCP_API_KEYS

方法3——Supabase JWT(网络用户)

对于用户通过Supabase Auth(电子邮件/密码、OAuth、Magic Link等)登录的web应用程序。

Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

所需机密:

秘密描述自动注射?
SUPABASE_URLSupabase项目URL
SB_PUBLISHABLE_KEY新的可发布密钥(2025年5月后的项目)否--必须手动设置
SUPABASE_ANON_KEY旧密钥

验证策略(级联):

  1. getClaims(token) --新方法(Supabase JS v2+)。使用非对称密钥在本地验证JWT。速度快,很少需要网络。
  2. 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 = falseconfig.toml:

[functions.mcp-server]
verify_jwt = false

4.部署 --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 = false

Supabase网关与新的非对称签名密钥(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_KEYSUPABASE_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中:

  1. 打开 Claude.ai→ 设置→ 集成
  2. 添加新的集成:

- 网址: https://YOUR_PROJECT.supabase.co/functions/v1/mcp-server - 身份验证: 承载令牌(您的Subabase JWT或API密钥)

  1. 克劳德会打电话的 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)

______________________________________________________________________

目录标签

目录标签

边缘计算服务器模板TypeScriptClaudeAI集成本地部署Supabase工具协议

支持客户端

Claude DesktopClaude

接入字段

传输方式(transport,传输协议)

stdio

鉴权方式(authType,认证方式)

oauth

运行时(runtime,运行环境)

Node.js

来源包(packageName,安装包名)

@modelcontextprotocol/inspector

工具数量(toolCount,工具数)

2

资源数量(resourceCount,资源数)

0

提示词数量(promptCount,提示词数)

0

权限和风险

stdiooauth部署方式未说明

接入前请确认传输方式、认证方式和部署位置,并根据实际工具能力限制访问范围。

安装前确认

不要直接授予不必要的文件、网络或账号权限;先核对安装命令和配置内容。

来源信息

继续浏览同类 MCP