集成与上线
MCP 服务
Model Context Protocol 是一套标准做法,让一个系统把自己的工具暴露给任意智能体。接入一个服务,它的工具就能被你的智能体使用。
打开 配置 → MCP 服务。服务接入并在某个智能体上加入允许清单之后,该智能体就能发现并调用它的工具。
为什么这样设计
接入一个服务
- 1
填入端点
一个使用 Streamable HTTP MCP 的 HTTP(S) URL。它必须能从应用运行的位置访问到——内网里的服务,托管实例是访问不到的。 - 2
设置认证
Bearer 令牌或不认证。令牌加密存储;建议放进 密钥 并以引用方式使用,这样轮换只需改一处。 - 3
测试连接
页面会列出该服务声明的工具。列表为空说明连上了但没有暴露任何东西——通常是认证范围的问题。 - 4
在智能体上加入允许清单
在智能体构建器里启用mcp_call_tool,并选择该智能体可以访问哪些服务。允许清单为空的智能体调不了任何服务。
服务的每个字段
| 字段 | 取值 | 说明 |
|---|---|---|
| 名称 | text | 智能体如何指代它——调用时 server_name 里填的就是这个字符串,智能体的允许清单里也是它。 |
endpoint | https URL | Streamable HTTP MCP 端点。必须能从应用运行的位置访问到。 |
description | text | 给浏览列表的人看的。 |
auth_type | none | token | Bearer 令牌或匿名。 |
auth_token | text | 加密存储,保存后绝不回传给浏览器。建议改用密钥引用。 |
status | 已连接 / 已断开 | 只读,由最近一次测试写入。 |
tools_count | number | 只读——该服务声明了多少个工具。连接成功后为零,就是下面说的那个信号。 |
last_ping | timestamp | 只读。 |
服务的名称是标识符,不是标签
mcp_call_tool 接收的也是 server_name。在智能体已经引用之后给服务改名,会静默破坏这些引用——智能体只是找不到服务,然后报告说它做不了那件事。智能体如何使用它
三个工具,依次使用:
- list_mcp_servers
- 该智能体可以访问哪些服务。
- mcp_list_tools
- 某个服务提供什么,以及参数的 schema。
- mcp_call_tool
- 带参数调用指定服务上的指定工具。
发现类调用不算作来源——只有 mcp_call_tool 会计入答案下方展示的 来源 ,在那里它显示为远端工具名以及它来自哪个服务。
安全
- 允许清单是按智能体维度的。 在工作区级别接入某个服务,并不等于把它暴露给每个智能体;每个智能体都必须被显式授权。
- 出网请求受防护。 解析到私有地址或链路本地地址的端点——包括云元数据服务——都会被拒绝,因此恶意或手误写错的端点没法被用来探入你的内网。
- 令牌加密存储 ,保存后绝不回传给浏览器。
- 调用会被追踪。 每次调用都会出现在运行追踪里,附带参数和结果。
远端工具可能会产生实际动作
自己构建 MCP 服务
上面讲的都是接入别人运行的服务。 构建 → MCP 服务构建器 是另一个方向:你用 Python 配合 FastMCP写服务,它运行在与 开发者工作区 相同的沙箱内核上,并通过 Streamable HTTP 在此可访问: /api/mcp/s/<slug>.
为什么这样设计
运行器期望什么
一个模块级的 FastMCP 实例,命名为 mcp (server 和 app 也可以),并带上 @mcp.tool() 函数。 不会 次调用 mcp.run() ——平台会替你把这个对象跑起来,你自己调它会让启动死锁。下面就是一个完整的服务,照这样写就能部署:
from fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool()
def greet(name: str) -> str:
"""Return a friendly greeting for the given name."""
return f"Hello, {name}!"
@mcp.tool()
def word_count(text: str) -> dict:
"""Count words and characters in a block of text."""
words = text.split()
return {"words": len(words), "characters": len(text)}这就是 Hello World 入门 模板;另外两个——包装 HTTP API、检索知识库——同样是完整的。从模板出发,别从空文件开始。
- 类型标注会变成输入 schema ,文档字符串会变成调用方模型读到的描述。两者都值得认真写——模型正是靠它们判断你的工具是不是它要的那个。
greet上面这个声明了一个必填的字符串参数name,纯粹是由它的函数签名决定的。 - 装饰器要带括号写。
@mcp.tool()在所有 FastMCP 版本上都可用;不带括号的@mcp.tool需要 2.11 或更新版本,在旧镜像上会加载失败。 - 两个 SDK 都可以。 镜像同时自带
fastmcp(多数示例用的独立 2.x 包)和mcp,即官方 SDK——后者的mcp.server.fastmcp.FastMCP是一个同名但不同的类。运行器对两者做鸭子类型判断,所以from mcp.server.fastmcp import FastMCP同样有效。 - 额外软件包 写在「部署」页签里,每行一个,容器启动时安装。
httpx,pydantic,pandas,numpy,langchain,langgraph,llama_index以及zenstudio辅助模块已经内置。
使用密钥,以及访问真实 API
在「部署」页签里以 ENV_NAME={{secret:NAME}} 的形式绑定密钥,用 os.environ读取。它们只在容器启动时解析,且通过一次带认证的调用送达,因此绝不会出现在保存的配置、数据库或日志里。 封装 HTTP API 模板就是这个形状:
import os
import httpx
from fastmcp import FastMCP
mcp = FastMCP("http-api")
BASE_URL = os.environ.get("API_BASE_URL", "https://api.example.com")
API_TOKEN = os.environ.get("API_TOKEN", "")
def _client() -> httpx.Client:
headers = {"Authorization": f"Bearer {API_TOKEN}"} if API_TOKEN else {}
return httpx.Client(base_url=BASE_URL, headers=headers, timeout=20, trust_env=True)
@mcp.tool()
def get_customer(customer_id: str) -> dict:
"""Fetch one customer record by id."""
with _client() as c:
r = c.get(f"/customers/{customer_id}")
r.raise_for_status()
return r.json()在「部署」页签上,那个服务需要两个绑定—— API_BASE_URL=https://api.internal.example.com 和 API_TOKEN={{secret:INTERNAL_API_TOKEN}} ——其中 INTERNAL_API_TOKEN 只在此存一份: 密钥。之后轮换只需在那里改一处,再在这里重新部署。
先把该主机加进出网允许清单
trust_env=True 负责把请求经沙箱代理转出去。如果 api.internal.example.com 不在该实例的允许清单里,拒绝这次调用的是代理,不是远端服务——所以你看到的报错根本不会提到远端主机。管理员在此添加它: 管理 → 开发者运行时.冷启动与保活
服务默认可缩容到零:首次调用之前不存在容器,所以要付几秒的启动开销,空闲超时后又会停掉。对延迟敏感的服务请打开 保持运行 ——它会长期占住一个容器,这也正是它默认关闭的原因。
谁可以调用它
- 1
你的智能体(内部)
在「访问」下打开开关,该服务就会登记进你的已连接服务列表。这里的智能体和集群可以通过与其他服务相同的mcp_call_tool路径调用它。不会有任何东西暴露到互联网。如果你的实例设置了BLOCK_PRIVATE_NETWORK_FETCH,那么在PUBLIC_APP_URL指向一个应用能自己调用到的地址之前,登记会被拒绝——否则就会出现「登记看着没问题、每次智能体调用都失败」的情况。 - 2
任何持有密钥的人(公开)
先公开暴露,再生成密钥。外部 MCP 客户端调用 /api/mcp/s/<slug>,带上 Authorization: Bearer mcps_… 。密钥以哈希存储,可设过期时间,可限定到特定工具和来源地址,且是被撤销而不是删除——这样使用痕迹会保留下来。
在服务未公开暴露期间,它的端点对除你自己的智能体之外的一切都返回 404 ——和一个不存在的 slug 得到的响应一样,因此探测无法区分两者。
把外部客户端指向它
服务一旦暴露、你手上又有密钥,它就是一个普通的 Streamable HTTP MCP 端点。多数客户端只需要一个 URL 和一个请求头:
{
"mcpServers": {
"my-server": {
"type": "http",
"url": "https://your-instance.example.com/api/mcp/s/my-server-a1b2c3",
"headers": {
"Authorization": "Bearer mcps_xxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}slug 带有随机后缀,所以请从服务页面上复制,不要以为它和名称一致。想在完全没有客户端的情况下验证密钥:
curl -sS https://your-instance.example.com/api/mcp/s/my-server-a1b2c3 \
-H 'Authorization: Bearer mcps_xxxxxxxxxxxxxxxxxxxxxxxx' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'| 详情 | 取值 |
|---|---|
| 传输方式 | Streamable HTTP,仅 POST |
| GET | 405 是刻意如此——不存在服务端到客户端的流,客户端会回退到 POST |
| 协议版本 | 2025-06-18 |
| 登录会话 | Mcp-Session-Id 在响应中返回,后续调用需回带 |
| 结束会话 | DELETE 到同一个 URL |
| 密钥格式 | mcps_ 加 32 个字符,仅在创建时显示一次 |
404 是好几个不同问题的共同答案
404。这是刻意的——探测不应该能区分「私有服务」和「不存在的服务」——所以当一个原本能用的客户端开始收到 404时,先查密钥是否被撤销、公开访问是否被关掉,再去怀疑 URL。自建服务是如何被隔离的
- 与笔记本同一个沙箱。 非 root 运行、根文件系统只读、剥离全部能力、禁止提权、限制 CPU / 内存 / 进程数,除了做过滤的出网代理之外没有任何出口。
- 你的任何凭据都不会到达你的代码里。 调用方的 Authorization 请求头在边缘就被消费掉,绝不转发;沙箱里只持有一个短期的能力令牌——绝不会有提供商密钥或你的 Supabase 会话。
- 会话是被转译的。 该
Mcp-Session-Id调用方收到的是我们这边的,且绑定到开启它的那个密钥;服务自身的会话 id 从不离开平台。 - 只有工具类方法会被转发。
initialize,tools/list,tools/call,ping,以及initialized和cancelled通知。其他任何东西都在边缘被拒绝——这是允许清单,不是拒绝清单,因为 MCP 一直在扩展(resources、prompts、sampling、roots、elicitation),其中有几项能让服务反过来要求 客户端 去做某件事。将来新增的协议方法,不会因为某个依赖升级就变得可达。 - 工具变更需要重新批准。 任何改动了工具名、描述或 schema 的重新部署,都会阻断调用直到你审阅——见下文。
为什么改动工具描述会阻断调用
出网过滤是实例级别的
当构建起不来时
- 部署立刻失败
- 看「日志」页签——那是容器的 stdout 与 stderr,绑定的密钥值已被抹掉。缺包和语法错误都会在那里显示。
- 找不到 MCP 服务器
- 运行器找不到模块级的 FastMCP 实例。把它命名为 `mcp`,并确保它是在导入时创建的,而不是在某个函数内部。
- 部署卡住然后超时
- 通常是你自己的代码里调了 mcp.run()。平台负责把这个对象跑起来;你的文件只该定义它。
- 网络调用被拒绝
- 该主机不在实例的出网允许清单里。在「管理 → 开发者运行时」中添加——拒绝来自代理,不是来自远端服务。
排查已接入的服务
- 能连上但列不出工具
- 认证用的主体没有工具范围,或者该服务只在完成某个初始化步骤后才暴露工具,而那一步没走完。
- 智能体从不调用它
- 没有在那个智能体上加入允许清单,或者工具描述太含糊,模型没法把它和问题对上。描述来自服务方——去那边改进它。
- 端点被拒绝
- 该 URL 解析到了私有地址。把它暴露在一个可达的主机上,或者把应用部署到能访问它的位置。
- 超时
- 长时间运行的远端工具超出了调用时间预算。让远端工具快速返回、改用轮询,而不是阻塞等待。