集成与上线

MCP 服务

Model Context Protocol 是一套标准做法,让一个系统把自己的工具暴露给任意智能体。接入一个服务,它的工具就能被你的智能体使用。

打开 配置 → MCP 服务。服务接入并在某个智能体上加入允许清单之后,该智能体就能发现并调用它的工具。

为什么这样设计

在 MCP 之前,每个集成都是定制的:有人要为每个系统、每个平台各写一个包装层。MCP 把这件事反转了——拥有某项能力的系统只描述一次自己的工具,任何会说 MCP 的智能体都能用。实际好处是:工单系统的工具定义由拥有工单系统的团队来维护,而不是你去猜他们的 API。

接入一个服务

  1. 1

    填入端点

    一个使用 Streamable HTTP MCP 的 HTTP(S) URL。它必须能从应用运行的位置访问到——内网里的服务,托管实例是访问不到的。
  2. 2

    设置认证

    Bearer 令牌或不认证。令牌加密存储;建议放进 密钥 并以引用方式使用,这样轮换只需改一处。
  3. 3

    测试连接

    页面会列出该服务声明的工具。列表为空说明连上了但没有暴露任何东西——通常是认证范围的问题。
  4. 4

    在智能体上加入允许清单

    在智能体构建器里启用 mcp_call_tool ,并选择该智能体可以访问哪些服务。允许清单为空的智能体调不了任何服务。

服务的每个字段

字段取值说明
名称text智能体如何指代它——调用时 server_name 里填的就是这个字符串,智能体的允许清单里也是它。
endpointhttps URLStreamable HTTP MCP 端点。必须能从应用运行的位置访问到。
descriptiontext给浏览列表的人看的。
auth_typenone | tokenBearer 令牌或匿名。
auth_tokentext加密存储,保存后绝不回传给浏览器。建议改用密钥引用。
status已连接 / 已断开只读,由最近一次测试写入。
tools_countnumber只读——该服务声明了多少个工具。连接成功后为零,就是下面说的那个信号。
last_pingtimestamp只读。

服务的名称是标识符,不是标签

智能体是按名称把服务加入允许清单的,而 mcp_call_tool 接收的也是 server_name。在智能体已经引用之后给服务改名,会静默破坏这些引用——智能体只是找不到服务,然后报告说它做不了那件事。

智能体如何使用它

三个工具,依次使用:

list_mcp_servers
该智能体可以访问哪些服务。
mcp_list_tools
某个服务提供什么,以及参数的 schema。
mcp_call_tool
带参数调用指定服务上的指定工具。

发现类调用不算作来源——只有 mcp_call_tool 会计入答案下方展示的 来源 ,在那里它显示为远端工具名以及它来自哪个服务。

安全

  • 允许清单是按智能体维度的。 在工作区级别接入某个服务,并不等于把它暴露给每个智能体;每个智能体都必须被显式授权。
  • 出网请求受防护。 解析到私有地址或链路本地地址的端点——包括云元数据服务——都会被拒绝,因此恶意或手误写错的端点没法被用来探入你的内网。
  • 令牌加密存储 ,保存后绝不回传给浏览器。
  • 调用会被追踪。 每次调用都会出现在运行追踪里,附带参数和结果。

远端工具可能会产生实际动作

与检索不同,MCP 工具可能会改变某些东西——建工单、发消息、更新记录。而是否调用它,是模型做的决定。对任何有实际后果的操作,请在 集群 里给它前面加一个人工审批节点,而不是指望提示词能兜住。

自己构建 MCP 服务

上面讲的都是接入别人运行的服务。 构建 → MCP 服务构建器 是另一个方向:你用 Python 配合 FastMCP写服务,它运行在与 开发者工作区 相同的沙箱内核上,并通过 Streamable HTTP 在此可访问: /api/mcp/s/<slug>.

为什么这样设计

另一条路是在别处起一个服务,再用 URL 接回来——那意味着第二套部署、第二套凭据,以及第二个需要推敲「谁能调用什么」的地方。而在这里,源码、沙箱、密钥和审计痕迹是同一件事。

运行器期望什么

一个模块级的 FastMCP 实例,命名为 mcp (server 和 app 也可以),并带上 @mcp.tool() 函数。 不会 次调用 mcp.run() ——平台会替你把这个对象跑起来,你自己调它会让启动死锁。下面就是一个完整的服务,照这样写就能部署:

python
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 模板就是这个形状:

python
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. 1

    你的智能体(内部)

    在「访问」下打开开关,该服务就会登记进你的已连接服务列表。这里的智能体和集群可以通过与其他服务相同的 mcp_call_tool 路径调用它。不会有任何东西暴露到互联网。如果你的实例设置了 BLOCK_PRIVATE_NETWORK_FETCH,那么在 PUBLIC_APP_URL 指向一个应用能自己调用到的地址之前,登记会被拒绝——否则就会出现「登记看着没问题、每次智能体调用都失败」的情况。
  2. 2

    任何持有密钥的人(公开)

    先公开暴露,再生成密钥。外部 MCP 客户端调用 /api/mcp/s/<slug>,带上 Authorization: Bearer mcps_… 。密钥以哈希存储,可设过期时间,可限定到特定工具和来源地址,且是被撤销而不是删除——这样使用痕迹会保留下来。

在服务未公开暴露期间,它的端点对除你自己的智能体之外的一切都返回 404 ——和一个不存在的 slug 得到的响应一样,因此探测无法区分两者。

把外部客户端指向它

服务一旦暴露、你手上又有密钥,它就是一个普通的 Streamable HTTP MCP 端点。多数客户端只需要一个 URL 和一个请求头:

json
{
  "mcpServers": {
    "my-server": {
      "type": "http",
      "url": "https://your-instance.example.com/api/mcp/s/my-server-a1b2c3",
      "headers": {
        "Authorization": "Bearer mcps_xxxxxxxxxxxxxxxxxxxxxxxx"
      }
    }
  }
}

slug 带有随机后缀,所以请从服务页面上复制,不要以为它和名称一致。想在完全没有客户端的情况下验证密钥:

bash
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
GET405 是刻意如此——不存在服务端到客户端的流,客户端会回退到 POST
协议版本2025-06-18
登录会话Mcp-Session-Id 在响应中返回,后续调用需回带
结束会话DELETE 到同一个 URL
密钥格式mcps_ 加 32 个字符,仅在创建时显示一次

404 是好几个不同问题的共同答案

不存在的 slug、未公开暴露的服务、已过期的会话 id,三者都返回 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 解析到了私有地址。把它暴露在一个可达的主机上,或者把应用部署到能访问它的位置。
超时
长时间运行的远端工具超出了调用时间预算。让远端工具快速返回、改用轮询,而不是阻塞等待。