关于 netlify-mcp-servers
netlify-mcp-servers 是一项指导开发者在 Netlify Functions 上构建自有 MCP 服务的技能,供希望向 AI Agent 开放应用工具和数据的团队使用。它介绍基于官方 SDK 的无状态 HTTP 服务、认证、权限、安全及客户端连接;并区分自建服务与 Netlify 托管的平台管理服务。
主要功能
- 01使用官方 MCP SDK 和 HTTP 传输
- 02在 Netlify Function 中定义工具
- 03配置共享密钥或用户 API 密钥
- 04验证输入并设置速率限制
- 05采用最小权限和工具调用日志
- 06通过客户端连接远程 MCP 服务
适用场景
- 01向 AI Agent 提供应用工具和数据
- 02将 MCP 端点部署为 Netlify Function
- 03为 MCP 调用配置认证和请求限流
MCP 相关
指导创建通过 HTTP 向 MCP 客户端暴露应用工具的远程 MCP 服务,并说明客户端连接方式。内容针对自建服务,而非 Netlify 托管的 Netlify 平台管理服务。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ netlify/ context-and-tools --skill netlify-mcp-servers安装后在 Claude Code 中输入/netlify-mcp-servers确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/authentication.mdconnecting-clients.mdfile-uploads.md详细介绍
Netlify MCP 服务
MCP 服务会公开工具(以及可选的资源/提示词),供 AI 客户端(Claude Desktop、Claude Code、Cursor)调用。在 Netlify 上,远程 MCP 服务就是一个通过 HTTP 使用 MCP 协议的 Netlify Function。本指南将帮助你搭建一个可用且安全的服务,并将客户端连接到它。
“Netlify MCP”指两种不同的东西——请确保你要构建的是正确的那一种。 Netlify 发布了自有的托管 MCP 服务,允许 AI 客户端代表你操作 Netlify 平台——通过你的 Netlify 帐户创建项目、触发部署、管理环境变量和基础设施。你无需编写这个服务;只需按照 Netlify 的 MCP 服务文档,将客户端指向 Netlify 托管的 MCP 服务(如需让 Agent 在你的网站上运行,请参阅 netlify-agent-runner 技能)。本指南讲的是另一种:构建你自己的 MCP 服务——在 Netlify Function 上托管一个端点,向 Agent 公开你自己的应用工具和数据。如果你的需求是“让我的 Agent 管理我的 Netlify 站点/部署/环境变量”,那就应该使用 Netlify 托管的 MCP 服务,而不是自己编写一个 Function。
同一套方案有两种用法:
- 独立服务——仓库唯一的用途就是提供 MCP 端点(例如封装第三方 API)。
- 添加到现有应用——在站点旁再添加一个 Function。让它的工具调用UI 和 REST 路由已经使用的同一服务/数据层,避免重复实现逻辑。
开始构建之前
请先确定一件事,因为它会影响身份验证代码:
- 谁会调用这个服务? 只有你(个人/单用户服务)→ 使用单个共享密钥。多人各自以自己的身份操作 → 使用由 Netlify Identity 支持的每用户 API 密钥。请参阅身份验证。
如果不确定,就从单个共享密钥开始——只需几行代码,之后也可以再添加每用户密钥。除非你另有说明,否则我会默认采用这种方式。
技术栈
使用官方 MCP SDK 及其符合 Web 标准的 Streamable HTTP 传输,在 Netlify Function 中以无状态方式运行。
npm install @modelcontextprotocol/sdk zod
Netlify Function 已经支持 Web 平台——它接收一个 Request 并返回一个 Response。SDK 提供的传输基于完全相同的基本对象构建,即 WebStandardStreamableHTTPServerTransport(这是 SDK 内部运行所用的同一核心,也是 Cloudflare Workers/Deno/Bun 使用的核心):将 Request 交给它,再返回它生成的 Response 即可——无需适配器,也无需固定版本。较早的指南会使用 Node 风格的 StreamableHTTPServerTransport,再搭配 fetch-to-node 桥接层来模拟它所需的 Node req/res 对象;在 Netlify 上,这两者都不需要,而且省去它们不仅更简单,也已验证可行。
有一点需要注意,与上述内容无关:对于任何 Accept 标头中缺少 application/json 和 text/event-stream 两者的 POST 请求,传输都会返回 HTTP 406。这是 MCP 规范的要求,必须由客户端满足——收到 406 表示需要修正客户端的 Accept 标头,而不是服务器。让 SDK 负责协议,也意味着你无需手动维护 JSON-RPC 分帧或协议版本握手。
服务 Function
使用符合 Web 标准的传输只需几行代码——较早指南中的大部分内容都是 Netlify 不需要的 Node 桥接层。将它放在 netlify/functions/mcp.ts 中:
import type { Config, Context } from "@netlify/functions";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { z } from "zod";
import { checkBearer } from "../lib/mcp/bearer"; // see Authentication
function buildServer() {
const server = new McpServer({ name: "my-mcp", version: "0.1.0" });
server.tool(
"get_item",
"Fetch a single item by id. Read-only.",
{ id: z.string().describe("The item's unique id") },
async ({ id }) => ({
content: [{ type: "text", text: JSON.stringify(await getItem(id)) }],
}),
);
return server;
}
export default async (req: Request, _context: Context) => {
if (!checkBearer(req)) return new Response("Unauthorized", { status: 401 });
// Stateless JSON server: it only does request/response over POST. Reject other
// methods — a GET makes the transport open an SSE stream that never closes, which
// a serverless function can't serve (you'll get a 502).
if (req.method !== "POST") return new Response("Method not allowed", { status: 405 });
// Fresh server + transport per request, no session to persist. enableJsonResponse
// returns one application/json body instead of an SSE stream — the right fit here.
const server = buildServer();
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});
// Hand over the Web Request, return the Web Response. The transport owns JSON-RPC
// framing, body parsing (a malformed body comes back as a clean 400), and the handshake.
await server.connect(transport);
return transport.handleRequest(req);
};
export const config: Config = { path: "/mcp" };
这就是一个完整且可部署的服务。其余工作是添加工具、身份验证和安全措施。
基于浏览器的客户端与 CORS
Netlify Functions 不会替你添加 CORS 标头,而且上面的服务会对所有非 POST 方法返回 405——包括浏览器发送的 OPTIONS 预检请求。通常情况下这没问题:原生 MCP 客户端(Claude Code、Cursor、Claude Desktop、mcp-remote 桥接程序)不是浏览器,不会执行相同的源策略,因此完全不需要 CORS——这就是这些客户端能够正常工作,而浏览器请求却不行的原因。
只有当 MCP 客户端运行在浏览器中时,CORS 才会产生影响——例如 Web 应用跨源调用服务。此时,除非响应携带 Access-Control-Allow-Origin,否则浏览器会阻止请求;并且浏览器会先发送 OPTIONS 预检请求,该请求必须返回 2xx,且包含 Access-Control-Allow-Methods(包括 POST)和 Access-Control-Allow-Headers(包括 Authorization 和 Content-Type)。浏览器控制台中出现“blocked by CORS policy: No Access-Control-Allow-Origin header”错误,原因就在这里——这并不表示服务损坏或平台存在问题。请在函数中、检查 405 之前处理预检请求,并在 POST 响应中也回显 CORS 标头:
const CORS = {
"Access-Control-Allow-Origin": Netlify.env.get("MCP_ALLOWED_ORIGIN") ?? "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Authorization, Content-Type, Mcp-Session-Id",
};
// In the handler, before the 405 check:
if (req.method === "OPTIONS") return new Response(null, { status: 204, headers: CORS });
// ...then reject other non-POST methods with 405, and add CORS to the transport's Response.
函数必须自行设置这些标头——不要把浏览器 CORS 错误当作需要上报给 Netlify 的问题,也不要通过放宽身份验证来绕开它。
定义工具
每个工具都包含一个 name、一行 description、一个 zod 输入架构,以及一个返回 { content: [...] } 的处理程序。模型能看到的只有描述和参数的 .describe() 文本——请将它们写成面向 Agent 的 API 文档:说明工具的功能、适用场景,并指出任何不可逆操作。
随着工具数量增加,请为每个工具创建独立模块,并在 buildServer() 中注册。包含许多工具的服务通常会维护一个注册表(由 { name, description, inputSchema, handler } 组成的数组),并统一接入 tools/list 和 tools/call——无论采用哪种方式,上面的传输设置都相同。
身份验证
MCP 客户端必须证明自己有权调用你的服务。每个请求都携带 Authorization: Bearer <token>;其他情况一律拒绝,并返回 401。
单个共享密钥(个人/单用户)。使用一个环境变量,并通过恒定时间比较进行校验。将其放入 netlify/lib/mcp/bearer.ts:
import { timingSafeEqual } from "node:crypto";
export function checkBearer(req: Request): boolean {
const expected = Netlify.env.get("MCP_BEARER_TOKEN");
if (!expected) return false;
const match = req.headers.get("authorization")?.match(/^Bearer\s+(.+)$/i);
if (!match) return false;
const a = Buffer.from(match[1]);
const b = Buffer.from(expected);
// Length check first because timingSafeEqual throws (RangeError) on unequal-length
// buffers. The token is fixed-length, so the early return leaks nothing useful.
return a.length === b.length && timingSafeEqual(a, b);
}
使用 openssl rand -hex 32 生成令牌,并将其作为机密环境变量存储。
每用户 API 密钥(多用户)。Netlify Identity 为 Web UI 提供访问控制,每位用户都可以在其中生成自己的密钥;你只存储每个密钥的哈希值(绝不存储明文),并将其与对应用户关联;每次请求时,根据密钥解析出用户,并将该用户传递给工具处理程序,以便工具以正确用户的身份执行操作。完整模式——包括架构、生成、哈希、撤销和用户解析——请参阅身份验证。
从简单的作用域控制开始。 最简单的模型是非此即彼:有效密钥可以代表其所属用户调用所有工具——这通常是合适的起点。出现具体需求时(例如只读密钥),再添加逐密钥作用域;如果应用确实需要,再扩展为逐工具作用域或角色层级。如果有人要求更完整的 RBAC 设计,请先介绍简单的基线方案,再在此基础上添加作用域,而不要一开始就把完整层级视为必需。
安全与权限
工具是提供给自主 Agent 的公共 API。请谨慎设计:
- 只开放完成任务所需的最少权限。 将读取与写入分开,并仔细考虑是否要开放破坏性工具。一个常见且稳妥的选择是完全不提供删除工具,并将破坏性操作保留在由人工操作的界面中。
- **对不可逆或公开的操作设置防护,**在工具描述中加入明确说明,例如:“发布前向用户展示确切文本并征得确认。”这是模型层面的软性防护,因此还应配备真正的紧急关闭机制:可随时立即撤销的令牌。
- 将客户端凭证与后端凭证分开。 客户端向你的服务进行身份验证(bearer/API 密钥);你的服务则使用自己的密钥向数据库或第三方 API 进行身份验证。切勿将后端的超级密钥交给客户端。
- 为后端凭证设置最小权限——使用应用密码或限定范围的令牌,而不是账户级凭证,这样即使泄露,影响也有限且可以撤销。
- 验证输入(你的
zod架构会执行此操作),并记录每次工具调用,以便查看 Agent 执行了什么操作——console.info的内容会显示在 Netlify 函数日志中。
速率限制
MCP 服务是自主 Agent 可以短时间内循环调用的公共端点——务必限制调用频率。Netlify Functions 内置了声明式速率限制,因此不要自行编写计数器(每个实例的内存计数器无法跨函数实例工作——请参阅下一节)。在函数的 config 导出中添加 rateLimit 区块:
export const config: Config = {
path: "/mcp",
rateLimit: {
windowSize: 60, // time window in seconds; capped at 180
windowLimit: 100, // max requests per window
aggregateBy: ["ip", "domain"], // group by ip, domain, or both
},
};
超过限制时,平台默认返回 HTTP 429(也可以设置 action: "rewrite",并指定 to 路径,将超出的流量发送到专用页面)。函数速率限制只能配置在函数的 config 导出中,不能定义在 netlify.toml 中。
文件上传
如果工具需要 Agent 提供文件(例如要发布的图片或要附加的文档),不要通过工具调用以 base64 格式传输字节数据——这会占用大量模型上下文,并可能超出载荷限制。应向 Agent 提供一个短期有效、仅可使用一次的预签名 URL,让它通过 PUT 上传原始字节,将文件存储在 Netlify Blobs 中,并在其他工具中通过稳定的键引用文件。使用密钥环境变量作为密钥,对上传 id、content-type、大小和过期时间计算 HMAC-SHA256 签名,并以恒定时间验证签名——签名本身就是授权,因此 PUT 请求无需携带 bearer 令牌。在上传端点上,强制校验声明的 content-type 和大小,并拒绝重放。完整的三步流程(prepare_upload → PUT → finalize_upload)及代码见:文件上传。
状态无法在请求之间保留
每个请求都会新建服务和传输,而且任何一次调用都可能被分配到不同的函数实例,或分配到刚刚冷启动的实例。模块级内存不会在实例之间共享,也无法在冷启动后持久保留。因此,需要在调用之间持久保存的状态不能放在模块作用域的 Set/Map/变量中,例如上述预签名上传的单次使用/重放跟踪、幂等键、“此 id 已处理”的防护,以及手动跟踪的每用户计数器。内存防护在本地和单个温实例上看起来正确,但一旦请求由另一个实例处理,就会悄无声息地放过重放的上传(或对调用进行重复处理)。请将此类状态保存在持久化存储中——Netlify Blobs 或你的数据库——并以上传/请求 id 为键,在其中进行检查并标记。(这也是服务本身以无状态方式运行并设置 sessionIdGenerator: undefined 的原因。)
连接客户端
原生远程 MCP 支持现已成为常态;只有在备用情况下才使用 mcp-remote 桥接工具。
- Claude Code —
claude mcp add --transport http my-mcp https://<site>.netlify.app/mcp --header "Authorization: Bearer <token>" - Cursor — 将服务添加到
mcp.json,并设置 URL 和Authorization标头。 - Claude Desktop / claude.ai — 添加自定义连接器(设置 → 连接器)。连接器面向 OAuth;对于静态 bearer 服务,
mcp-remote桥接工具是可靠的选择。 - 备用方案(较旧的 / 仅支持 stdio 的客户端) —
npx mcp-remote https://<site>.netlify.app/mcp --header "Authorization: Bearer <token>"
完整的客户端对照表以及 OAuth / 自定义连接器详解:连接客户端。
本地开发与部署
- 运行:
netlify dev会在http://localhost:8888/mcp提供该函数。 - **测试:**使用 MCP Inspector —
npx @modelcontextprotocol/inspector— 通过 Streamable HTTP 连接到你的 URL,设置Authorization: Bearer标头,然后列出/调用工具。或者将claude mcp add --transport http指向 localhost URL。 - 身份验证注意事项:Netlify Identity 在
netlify dev下无法使用,因此必须在部署预览中测试按用户密钥进行的身份验证。请参阅 netlify-identity 技能。 - **部署:**推送到 Git,或运行
netlify deploy --build --prod。 - **密钥:**将令牌/密钥设为环境变量(
netlify env:set MCP_BEARER_TOKEN <value> --secret)——绝不要写入代码。
通用规则
- 绝不要将机密硬编码。将令牌、API 密钥和签名密钥存储为 Netlify 环境变量(并标记为机密)。除了泄露风险之外,将 bearer 令牌或签名密钥写入源代码(或任何会由构建发布的文件),都会触发 Netlify 的机密扫描,并导致部署失败,即使构建其他部分已成功也不例外——解决方法是将其移入机密环境变量,并在运行时通过
Netlify.env.get(...)读取;如果令牌已提交,则应轮换令牌,而不是禁用扫描器。扫描控制选项请参阅 netlify-deploy。 - 在函数内部,使用
Netlify.env.get("VAR")读取环境变量,而不是process.env。 - 将
.netlify添加到.gitignore。