关于 Openagenda
Openagenda 是面向 Agent 的 MCP 服务,通过 code-mode 将 OpenAgenda v3 API 提供给 LLM 客户端,适合探索和原型开发;托管端使用 OAuth,并为执行代码配置隔离边界。
主要功能
- 01搜索 API 文档
- 02通过代码执行 API 操作
- 03支持读取与写入管理操作
- 04托管端采用 OAuth 身份范围
- 05支持本地与托管运行模式
适用场景
- 01查找 OpenAgenda API 操作
- 02探索或原型化活动管理操作
- 03使用 OAuth 连接托管服务
MCP 相关
提供 OpenAgenda v3 API 的 MCP 接口,使用 search_docs 查找操作,并通过 execute 运行代码。托管服务通过 Streamable HTTP 提供并要求 OAuth;本地模式可通过 stdio 运行。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
https://mcp.openagenda.com/mcpclaude mcp add --transport http openagenda https://mcp.openagenda.com/mcp{"mcpServers":{"openagenda":{"type":"http","url":"https://mcp.openagenda.com/mcp"}}}在 Cursor 中打开{"servers":{"openagenda":{"type":"http","url":"https://mcp.openagenda.com/mcp"}}}在 VS Code 中打开{"mcpServers":{"openagenda":{"type":"http","url":"https://mcp.openagenda.com/mcp"}}}{"mcpServers":{"openagenda":{"serverUrl":"https://mcp.openagenda.com/mcp"}}}命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。
工具
详细介绍
@openagenda/mcp
一个 MCP 服务,通过 code-mode 模式向 LLM 客户端开放 OpenAgenda v3 API:使用两个工具——search_docs(查找合适的操作)和 execute(运行针对 API 的代码),而不是为每个端点设置一个工具。execute 会针对 v3 接口运行调用方提供的任意代码(读取以及写入/管理/审核操作),因此,限制其能力范围的是沙箱的出站网络边界、按调用方划分的 OAuth 权限范围,以及需要人工确认的破坏性工具。
⚠️ 部署为托管式多租户服务 (
https://mcp.openagenda.com,强制使用 OAuth)。托管策略层(µVM 边界、OAuth、速率限制、审计、维护停用开关)已就绪。在依赖写入功能之前,请参阅 托管/多租户,了解仍然适用的注意事项。
连接到托管服务
推荐使用托管服务来使用此 MCP:无需安装,可通过 OpenAgenda 账户进行 OAuth 身份验证(首次连接时会打开浏览器确认授权),并按调用方划分权限范围和速率限制。端点为
https://mcp.openagenda.com/mcp(Streamable HTTP)。
Claude Code
claude mcp add --transport http openagenda https://mcp.openagenda.com/mcp
然后输入 /mcp 进行身份验证。
Claude Desktop / claude.ai — 依次选择 Settings → Connectors → Add custom
connector,然后粘贴 https://mcp.openagenda.com/mcp。
Cursor — 添加到 ~/.cursor/mcp.json(或项目的 .cursor/mcp.json):
{
"mcpServers": {
"openagenda": { "url": "https://mcp.openagenda.com/mcp" }
}
}
VS Code
code --add-mcp '{"name":"openagenda","type":"http","url":"https://mcp.openagenda.com/mcp"}'
其他客户端 — 此服务是受标准 OAuth 保护的 MCP 资源
(通过 https://mcp.openagenda.com/.well-known/oauth-protected-resource/mcp 进行 RFC 9728 发现);任何支持远程 MCP + OAuth 的客户端均可使用。
想自行运行?npx -y @openagenda/mcp 可通过 API 密钥使用 stdio 进行 MCP 通信——请参阅快速入门(本地)。
使用 API 构建
此服务适用于探索 OpenAgenda 并基于它进行原型开发的 Agent。若要正式发布网站或工具,请使用 npm 上同一套操作的类型化 SDK @openagenda/api-client——execute 沙箱打包的正是这个 SDK,因此,Agent 在此处开发的原型代码可直接在你的应用中运行。与 execute 请求体的唯一区别是只需进行一次客户端初始化:
npm install @openagenda/api-client
import { OpenAgenda, client } from '@openagenda/api-client';
client.setConfig({ baseUrl: 'https://api.openagenda.com/v3', auth: 'oa_pk_…' });
const oa = new OpenAgenda();
const { data, error } = await oa.agendas.events.list({
path: { agendaUid },
query: { relative: ['upcoming'] },
});
公开读取请使用只读公钥(oa_pk_…,不关联用户身份);写入请使用密钥(oa_sk_…,仅限服务端使用)。该包也导出了 schemas zod 验证器。search_docs(以及 API 文档中的 Scalar)是操作参考;其中展示的调用就是 SDK 的调用。
架构
openapi.yaml ─▶ (docs index) ─▶ search_docs ┐
├─▶ MCP server (stdio) ─▶ SandboxExecutor ─▶ runtime
client writes JS ───────────────▶ execute ──┘ (pluggable)
通过配置选择一个精简接口 SandboxExecutor。约 95% 的服务代码共用;只有运行代码的适配器不同——因此,同一代码库无需重复代码,即可同时提供兼容性广泛的本地模式和严格的多租户边界。
引擎(OA_EXECUTOR) | 运行时隔离 | 是否控制出站网络? | 用途 |
|---|---|---|---|
node | 权限沙箱(禁止 fs/子进程/workers)+ 功能限制 | ❌ 无主机级网络权限 | 本地默认模式(零安装) |
deno | 默认拒绝权限 + 墙钟计时终止 + 堆内存上限 | ✅ 受限的 --allow-net | 本地加固(受限出站网络) |
microsandbox | 硬件隔离的微型 VM(独立内核) | ✅(µVM) | 托管/多租户 |
引擎是一个维度;由谁控制网络出站流量(OA_CODE_EGRESS_AUTHORITY)是另一个维度。srt 不是引擎——它是参考用的包装器。两个维度、有效性矩阵和失败时封闭的规则均见下文的执行模型:引擎与隔离包装器。
执行模型——引擎与隔离封装层
两个相互独立的维度:
- 执行引擎——实际运行所提交 JS 的程序:
node·deno·microsandbox。 - 代码出站权限——由_谁_决定代码可以访问网络上的哪些位置。权限主体恰好只有一个,且必须明确无歧义:
executor——由引擎强制执行(deno--allow-net=<host>;µVM)。wrapper——由外层隔离层管理。srt 是_一种实现方式_——Docker--network、gVisor 或自定义代理也是。none——无人强制限制出站流量。仅适用于可信场景。
srt 是封装层,不是引擎。它会隔离进程_及其整个后代进程树_(命名空间 + seccomp + 出站代理),因此在其下启动的基础引擎会继承该边界。这就是它归属出站权限维度,而非引擎维度的原因——srt -- node server.js,由部署者选择,与引擎无关。
MCP 服务本身始终运行在 Node.js 上(通过
node启动)。OA_EXECUTOR仅控制execute工具用于运行_所提交_代码的运行时——因此,OA_EXECUTOR=deno并不意味着要用 deno 启动服务。
两种出站范围——不要混为一谈
OA_CODE_EGRESS_AUTHORITY 仅管控执行代码。编排器自身的出站流量属于_另一个_范围:你可以在 srt 下运行整个 MCP 进程以实现纵深防御,同时让代码在 microsandbox 中运行。这不属于 microsandbox + wrapper——在该模式下,封装层限制的是_我们的_进程(仅允许访问 microsandbox 控制端点),而非代码的 µVM。下方的代码出站矩阵针对范围 1;范围 2 完全由部署方式决定。
唯一权威来源——有效性矩阵(代码出口)
| 引擎 + 权限方 | 判定 |
|---|---|
deno + executor | ✅ deno 限定 --allow-net=<apiHost> — 自包含 |
node + wrapper | ✅ 由 wrapper 控制出口;node 遵循其代理设置(NODE_USE_ENV_PROXY=1) |
deno + wrapper | ✅ deno 以宽松模式运行——不得同时限定 --allow-net,否则会阻止 wrapper 的代理(macOS) |
microsandbox + executor | ✅ 由 µVM 控制其出口 |
node + executor | ❌ 策略拒绝 — Node 的权限模型无法限定出口(Node 24 没有网络权限;Node 25 的 --allow-net 只能全开或全关,不能限定主机)。如需真正的边界,请使用 wrapper 或 microsandbox。 |
node + none | ⚠️ 本地默认模式(零安装)。引擎仍会自行应用 Node 的权限沙箱(--permission,Node ≥ 24:禁止 fs/subprocess/workers),除非设置 OA_LOCAL_NO_SANDBOX=1——但出口不受限制;启动时会显示提示横幅。 |
other + none | ⚠️ 仅当设置了 OA_MCP_MODE=local 并明确确认 OA_LOCAL_NO_SANDBOX=1 后才接受——绝不会作为静默默认值 |
microsandbox + wrapper | ❌ 不支持 — srt 的 seccomp 会拒绝 µVM 所需的 KVM/device 原语,且此配置多余(µVM 已经是硬边界) |
hosted + anything else | ❌ 失败时关闭 — hosted 要求使用 microsandbox + executor |
wrapper 是应用无法验证的部署约定
使用 wrapper 时,边界是部署者作出的承诺——应用无法证明 srt 配置正确。因此,应用不会盲目信任它,而是让错误配置明显暴露且不易偏离:
- 对上方 ❌ 行采取失败关闭策略(拒绝启动,而非发出警告)。
- 输出策略,让部署者使用应用自行推导的允许列表进行封装,而不是手动复制列表:
openagenda-mcp print-egress-policy(--format=srt|json,以及policySha256指纹)。 - 当
authority=wrapper时打印启动横幅:“此进程本身不强制执行出口策略;wrapper 必须只允许<apiHost>。”
推荐的部署方式
- 本地默认 →
node+none(无需安装,Node ≥ 24)。引擎在 Node 的权限沙箱(--permission)中运行代码:无法访问文件系统、子进程、工作线程或插件。它无法限制的是出站流量——经提示词注入的脚本仍可将你的 API 密钥发送到任何地方(但已无法读取本地文件)。启动时会显示提示横幅。 - 本地强化(推荐) →
OA_EXECUTOR=deno(加上默认的executor)。安装一次即可添加限定范围的出站流量边界(--allow-net=<apiHost>):密钥实际上无法发送到其他主机。 - 本地裸运行(选择退出) →
OA_LOCAL_NO_SANDBOX=1。同时禁用权限沙箱——完全没有文件系统/网络边界。仅用于可信且已明确确认的场景。 - 本地双重保护 →
node+wrapper,在srt下运行(见下文)。增加操作系统级文件系统/进程隔离,并由 wrapper 管理出站流量。 - 托管/公开部署 → 使用
microsandbox+executor执行代码;同时,还要让编排器在srt下运行(仅允许 microsandbox 控制端点)。两层、两个范围——绝不要将 microsandbox 放在 srt 下运行。
此模型仅控制网络边界。资源限制(超时、堆内存)仍由引擎和编排器负责;速率限制、并发控制和审计仍由编排器负责——这些都独立于上述维度。
在 srt(wrapper 权限控制方)下运行
生成准确的策略——不要手动编写,以免策略偏离 OA_BASE_URL(也能避免触发下文所述的读取陷阱):
openagenda-mcp print-egress-policy --format=srt > srt-settings.json
该文件是完整的 srt 设置对象:
{
"network": { "allowedDomains": ["api.openagenda.com"], "deniedDomains": [] },
"filesystem": {
"denyRead": ["~"],
"allowRead": [".", "/home/you/.nvm/versions/node/vXX"],
"allowWrite": [],
"denyWrite": []
}
}
三个边界,其中读取边界不可省略:
- 网络 — 仅允许访问;只有 API 主机可访问。
- 写入 — 仅允许访问;不可写入任何内容。
- 读取 — srt 默认允许读取所有位置(先拒绝,再允许)。如果
denyRead为空,~/.ssh、.env、云凭据等仍可读取——而代码模式会将执行结果返回给调用方,因此return readFileSync('~/.ssh/id_rsa')会通过结果通道泄露数据,完全绕过网络允许列表。因此,生成的策略会禁止访问主目录,并仅重新允许工作区(.)和运行时安装根目录 (/usr等系统路径仍可读取)。
allowRead中的运行时路径因机器而异(取决于你的 node/deno 安装位置)——请在每台主机上重新生成策略。如果在 srt 下运行 deno 引擎,还要重新允许 deno 的缓存目录(DENO_DIR,默认为~/.cache/deno),该目录位于被禁止访问的主目录下。
然后使用该策略,在 srt 下启动 MCP:
OA_EXECUTOR=node OA_CODE_EGRESS_AUTHORITY=wrapper \
srt --settings srt-settings.json -- node public/mcp/src/index.js
srt 会将进程及其启动的引擎限制在沙箱中,因此它是出站流量和读取权限的统一控制方;node 通过 NODE_USE_ENV_PROXY=1 使用 srt 的代理(由引擎设置)。在托管部署中,同样可以通过这种封装强化编排器——此时,允许列表中应包含 microsandbox 控制端点。
快速开始(本地)
前提条件: Node ≥ 24,仅此而已。默认引擎是在自身权限沙箱中运行的 node(无 fs/子进程权限;网络访问未受限制——参见推荐部署)。建议进行加固:安装 deno,并设置 OA_EXECUTOR=deno,以限定出口网络边界。有关 srt 包装器的设置,请参见在 srt 下运行。
# From npm — no checkout needed:
# The API needs a credential (no anonymous read): any OpenAgenda API key for stdio (oa_pk_… shown; a least-privilege/read key is advised — it's baked into the sandboxed code). HTTP uses OAuth.
OA_API_KEY=oa_pk_xxx npx -y @openagenda/mcp # speaks MCP over stdio (node engine)
# Or from a checkout:
yarn install
export OA_API_KEY=oa_pk_xxx
export OA_BASE_URL=https://dapi.openagenda.com/v3 # dev; defaults to production
node public/mcp/src/index.js
在 MCP 客户端(例如 Claude Desktop / Claude Code)中注册:
{
"mcpServers": {
"openagenda": {
"command": "npx",
"args": ["-y", "@openagenda/mcp"],
"env": { "OA_API_KEY": "oa_pk_xxx" }
}
}
}
(从代码检出目录运行时,改用 "command": "node"、
"args": ["/abs/path/to/public/mcp/src/index.js"]。)
Claude Desktop,一键安装——从
最新版本获取 openagenda.mcpb,
并双击它(或将其拖入“设置”)。Desktop 会提示你输入 API 密钥,并为你配置上述 stdio 服务。该软件包只是一个轻量启动器——它会运行
npx @openagenda/mcp,因此始终会拉取当前发布的版本;只有其元数据发生变化时,.mcpb 文件本身才会更新。(此方式使用 API 密钥;
若要使用无需密钥的 OAuth,请改为通过 URL 添加托管远程服务——参见
连接到托管服务。)你也可以使用 yarn pack:mcpb 自行构建。
然后尝试提问,例如 “agenda X 中每个城市有多少场即将举行的活动?”——模型会调用 search_docs,使用 oa.agendas.events.facets(...) 编写脚本,再由 execute
在沙箱中运行。
CLI 标志
非敏感的调用参数也可通过标志设置——每个标志都对应一个环境变量(标志优先;完整列表请查看 --help):
npx -y @openagenda/mcp --executor=deno --base-url=https://dapi.openagenda.com/v3
密钥(OA_API_KEY 等)按设计仅支持通过环境变量设置:argv 在
ps//proc 中可见,因此不会提供任何用于传递凭据的标志。
配置(环境变量)
| 变量 | 默认值 | 含义 |
|---|---|---|
OA_MCP_MODE | local | local | hosted(决定默认值和失败关闭防护) |
OA_EXECUTOR | node(local)/ microsandbox(hosted) | 引擎:node | deno | microsandbox(参见 Execution model;deno = 受限出站访问升级方案) |
OA_CODE_EGRESS_AUTHORITY | none(node)/ executor(其他) | 由谁负责代码出站访问:executor | wrapper | none |
OA_LOCAL_NO_SANDBOX | 关闭 | 裸 node:同时禁用权限沙箱(完全没有边界);对于非 node 引擎,这也是 egress=none 的确认选项 |
OA_BASE_URL | https://api.openagenda.com/v3 | v3 基础 URL |
OA_API_KEY | 无 | 任意 OpenAgenda API 密钥(Bearer)——stdio 凭据;建议使用权限最小化的只读密钥(HTTP 使用 OAuth) |
OA_SANDBOX_TIMEOUT_MS | 5000 | 硬性墙钟超时后终止 |
OA_SANDBOX_MEMORY_MB | 256 | V8 堆内存上限(node/deno)/ µVM 硬性 RAM 上限(microsandbox——需要更多余量) |
OA_MAX_CONCURRENCY | 4 | execute 同时运行的最大数量——主机 RAM 防护上限 |
OA_EXEC_MAX_QUEUE | OA_MAX_CONCURRENCY × 10 | 在返回可重试的“容量已满”错误前,等待空闲槽位的 execute 调用最大数量(默认值会随上限自动调整) |
OA_EXEC_QUEUE_TIMEOUT_MS | 30000 | 超过容量上限的 execute 等待空闲槽位的最长时间,超时后返回可重试的“容量已满”错误 |
OA_RATE_LIMIT_PER_MIN | 60 | 每位调用者(OAuth sub)持续每分钟可发起的 execute 调用数;仅适用于 transport=http——每个令牌的速率限制 |
OA_RATE_LIMIT_BURST | 20 | 每位调用者在返回可重试的“已达到速率限制”错误前可突发发起的 execute 调用数(令牌桶大小) |
OA_MAX_CONCURRENCY_PER_CALLER | 2 | 每位调用者(OAuth sub)最多可占用的 execute 同时运行数量;仅适用于 transport=http——公平性上限,防止单一调用者占用所有全局槽位。调高此值(≥ OA_MAX_CONCURRENCY + OA_EXEC_MAX_QUEUE)即可有效禁用此限制 |
OA_INSIGHT_OPS_TOKEN | 无 | InsightOps 日志令牌——将日志和审计信息发送到 InsightOps(生产环境)。如需输出到 stderr,请设置 DEBUG=openagenda-mcp* |
OA_EXECUTE_DISABLED | 关闭 | 1 → 拒绝 execute(维护/事件期间);search_docs 仍可用。针对单个调用者的封禁由 AS 管理,不在此处设置 |
OA_MICROSANDBOX_IMAGE | node:24-alpine | µVM 使用的 OCI 镜像(microsandbox;官方 Node Alpine,node 位于 PATH 中;生产环境请固定摘要——参见 config.js) |
OA_SANDBOX_RUNTIME | node | µVM 内部的 JS 运行时:node | llrt(仅适用于 microsandbox;llrt ≈ RAM 减少 52% / 预热速度约快 3 倍——参见 docs/microsandbox.md) |
OA_LLRT_BIN | (未设置) | 主机上的静态 llrt 二进制文件路径,将以绑定挂载方式挂载(可选;若镜像已将 llrt 烘焙到 PATH 中,例如使用 llrt.Dockerfile,则不设置) |
OA_MICROSANDBOX_POOL_SIZE | 0(关闭) | 预热的单次使用备用 µVM(microsandbox;优化吞吐量,但会占用 RAM——参见 docs/microsandbox.md) |
OA_USE_SYSTEM_CA | 关闭 | 仅限开发:信任操作系统证书存储(Node 自带证书) |
OA_EXTRA_CA_CERTS | 无 | 仅限开发:额外 PEM CA 证书包的路径 |
开发 TLS。
dapi.openagenda.com使用 私有 CA(O=OADEV),该 CA 不在 Node 内置的根证书中 →UNABLE_TO_VERIFY_LEAF_SIGNATURE。设置OA_USE_SYSTEM_CA=1(开发 CA 已加入系统证书库),或设置OA_EXTRA_CA_CERTS=docker/devinstaller/ssl/certs/ca.crt。生产环境(api.openagenda.com)两者都不需要——保持关闭。
日志去向。 两个相互独立、由环境变量控制的输出目标(与 NODE_ENV 无关)。stderr:设置
DEBUG=openagenda-mcp*(标准的debug开关)——用于在终端或 dev/docker 中查看日志。InsightOps:设置OA_INSIGHT_OPS_TOKEN(生产环境)。如果两者都未设置,服务启动时会警告一次(“no log sink configured”),随后保持静默——这样日志和审计记录就不会被悄无声息地丢弃;致命错误和启动安全提示始终会直接输出到 stderr。两者可以同时启用;都不会触及 stdout(兼容 stdio)。
托管 / 多租户
在将其作为公共服务运行之前需要采取的预防措施——也就是说,互联网上的任何人都可以提交代码,并让代码在你的基础设施上执行。本地设置(deno,或在 srt 下运行的 node)是针对有限信任环境的防护措施;它们不是抵御不可信代码的硬性边界。config.js 默认拒绝:OA_MCP_MODE=hosted 要求设置 OA_EXECUTOR=microsandbox 和 OA_CODE_EGRESS_AUTHORITY=executor,并拒绝其他所有配置。
microsandbox 参考文档(测得的延迟/占用、SNI 出站模型、镜像选择理由、池大小和生命周期不变量)位于 docs/microsandbox.md。引擎本身已实现(启动、由主机强制执行的出站控制、硬性上限),并已在真实 KVM 主机上通过验证;每位调用者的 OAuth 令牌、速率限制、每个工具的审计日志以及维护用停用开关(
OA_EXECUTE_DISABLED)也已就绪。该服务现已部署到生产环境(https://mcp.openagenda.com,通过 OAuth 对生产环境 AS 强制授权)。请在虚拟化主机上运行 µVM 集成测试:OA_MSB_IT=1 yarn workspace @openagenda/mcp test。
出站允许列表才是防止数据外泄的边界,而非
oa客户端。 执行的代码不可信,且与oa客户端处于同一作用域:它可以读取内置密钥并直接调用fetch——oa客户端根本不是边界。真正防止密钥外泄的是沙箱的网络出站允许列表(deno--allow-net、srtallowedDomains),其范围限定为 API 主机。请强化这项控制;客户端不是安全边界。
每项措施都对应一种具体威胁:
- 任意代码/沙箱逃逸 → 每次执行都在临时的硬件隔离微型虚拟机中运行(microsandbox;gVisor
--runtime=runsc可作为久经考验的备用方案)。每次调用使用一个虚拟机,执行后销毁——绝不在不同调用者之间重复使用。共享内核容器或srt/deno不足以应对这种情况。 - 无限循环/挂起 → 在虚拟机级别进行严格的墙钟时间终止(而不是在代码中使用
setTimeout——不可信代码会忽略它)。 - 处理负载过重 → 设置 RAM、CPU 和 PID 上限(在虚拟机内使用 cgroups)。注意,
srt没有资源限制——这也是为什么像 srt 这样的包装器无法作为托管边界的另一个原因。 - 可疑的获取请求/SSRF/数据外泄 → 默认拒绝出站流量,只允许 API 主机加入白名单,并且屏蔽云元数据端点
169.254.169.254、RFC1918 网段和 localhost。(典型的 SSRF → 云凭据盗窃。) - 滥用/自我 DoS → 三种互补的防护措施,均已完成:
- 对同时运行的沙箱设置全局并发上限,并使用有界队列(
OA_MAX_CONCURRENCY,默认值为 4 → 突发请求达到上限时会排队,随后返回可重试的繁忙状态,而不是无限启动约 116 MiB 的 µVM,导致主机 OOM)。concurrencyLimit.js为每种引擎和传输方式包装执行器——从全局限制瞬时负载(不区分身份)。 - 对每个调用者设置并发上限(
callerConcurrency.js,以 OAuthsub为键;OA_MAX_CONCURRENCY_PER_CALLER,默认值为 2,仅限transport=http)——限制单个调用者同时占用流水线的运行次数(正在运行或在全局队列中等待),这样单个调用者就无法占满所有全局槽位并让其他调用者挨饿(全局上限本身不区分身份,而远超上限的突发请求会让一个调用者抢占所有槽位)。达到上限的调用者会收到可重试的繁忙状态。按实例保存在内存中。 - 对
execute设置每个调用者的速率限制(rateLimiter.js,基于令牌桶并以 OAuthsub为键;OA_RATE_LIMIT_PER_MIN/OA_RATE_LIMIT_BURST,仅限transport=http)——限制每个调用者的持续负载,防止单个调用者长时间独占共享配额。超出速率限制的调用会收到可重试的“已达到速率限制”结果。按实例保存在内存中(实际限制会随副本数量增加而扩大,与并发上限一样)。
- 对同时运行的沙箱设置全局并发上限,并使用有界队列(
- 通过 API 本身提升权限 → 不提供环境权限:将调用者的受限 OAuth 令牌注入执行代码;API 在服务端强制执行作用域,因此成功的运行绝不会超出调用者自身的权限。服务器不持有代码可访问的共享密钥。只有在这种情况下 OAuth 才是强制要求(见下文“身份验证”)——在这里使用共享密钥会让任何调用者都能使用或外泄对所有人均有效的权限。
- 审计与取证 → 已完成:按工具记录审计日志(
log.js),为每次search_docs/execute调用向 InsightOps(生产环境,OA_INSIGHT_OPS_TOKEN)/stderr(DEBUG)输出一条结构化记录,其中包含调用者sub和客户端应用、传输方式、结果、耗时、限制为 8 KiB 的代码正文及其完整 sha256、bytes_out、不可逆的credential_fp——绝不记录密钥,绝不记录结果载荷。终止开关为OA_EXECUTE_DISABLED(关闭execute,保留search_docs);封禁特定调用者是授权服务器(AS)的职责(撤销授权),而不是使用本地拒绝名单。 - 供应链/成熟度 → microsandbox 尚新(测试版)——固定版本、审核升级,并保留 gVisor 作为已知的边界备用方案。
沙箱提供的是隔离;它并非完整的策略。出站流量白名单、资源上限和受限令牌模型都需要由你自行配置——任何引擎都不会免费提供这些保障。
身份验证:密钥或 OAuth,与传输方式相互独立
身份验证方式、传输方式和租户模式是相互独立的维度——不要把“托管”与“OAuth”混为一谈:
- API 密钥(stdio 传输):摩擦最小——由运营方使用自己的 OpenAgenda API 密钥运行服务(任何密钥都可用;
oa_pk_…只是示例。由于密钥会嵌入沙盒化代码,因此建议使用权限最小化的只读密钥)。适用于本地使用,也适用于某个组织在自己的边界内为其可信调用方托管服务(单租户、私有)。在这种情况下,共享密钥是合理的运营方选择。 - OAuth 2.1(HTTP 传输):独立的 OAuth 资源服务器——根据 AS JWKS(颁发者 + 受众)在本地验证 Bearer JWT,并在每次
execute调用中,将调用方的令牌换成短时有效的aud=api令牌(RFC 8693),使执行的代码以已同意的调用方身份运行,绝不使用共享的环境密钥。基于slice-auth/better-auth。这是公开 + 多租户场景所需的方案。
传输方式的选择会确定身份验证模型,而不是留下一个容易误用的选项:
没有 OAuth 配置(颁发者、资源、交换密钥)时,transport=http 会默认拒绝请求,因此共享密钥绝不会通过网络暴露——没有可选择启用的“通过 HTTP 使用共享密钥”路径。对于不受限的出口流量,也会默认拒绝请求:由于 http 面向网络,默认优先使用 node 的设置(egress=none)会被拒绝——可自托管的 http 服务必须通过 OA_EXECUTOR=deno(或 microsandbox)限制出口流量,在 wrapper 下运行,或通过 OA_LOCAL_NO_SANDBOX=1 明确接受使用可信的单租户机器。
OA_API_KEY是 stdio 凭据——其本质就是共享且自动可用。服务会从其返回的任何错误文本中删去该密钥;此凭据适用于本地 / 单租户使用。公开的多租户接口使用上述按调用方隔离的 OAuth。
写入操作与审核
v3 接口包含写入、管理和审核功能,而 execute 可以运行任意代码——因此它可以执行修改操作,且代码模式会把危险调用隐藏在脚本中:审计日志会记录脚本(见上文“审计与取证”),但不会对单项操作进行审批。因此,execute 被标注为非只读,以便客户端继续对此进行限制。计划采用的缓解措施是混合方案:保留 execute 用于读取和组合操作,并新增少量针对敏感操作的显式工具(标注 destructiveHint,且须经人工批准),例如删除议程、审核/拒绝内容、移除成员——这些尚未实现,因此目前 execute 是唯一的写入路径,并依赖 OAuth 作用域授权 + 沙盒边界。
设计决策
- 两个工具,一次往返。 Code-mode 提供
search_docs+execute,而不是第三个describe_operation(后者会让每次调用都增加一次 LLM↔MCP 往返)——search_docs一次性返回签名、类型化参数、响应结构和可运行示例,并根据排名调整详情(完整呈现排名靠前的结果,简洁呈现长尾结果),使载荷在目录扩展到数十个操作时仍保持有限。若查询内容恰好是某个操作 ID,则只返回该操作卡片,这也是查看长尾条目的方式;若查询为空或没有匹配项,则返回目录,并以简洁形式呈现所有操作。这与 Stainless 的“SDK Code Mode”形式一致。 search_docs使用 MiniSearch 排名(词法 BM25),而非嵌入。 对于规模很小(数十个操作)、关键词密集且使用自然语言查询的目录,词法搜索是合适的工具,也是业界默认选择(Anthropic 开箱即用地提供 BM25 + regex 工具搜索;嵌入是其处理数百到数千个工具时的备用方案)。MiniSearch 是唯一满足所有约束、且没有多余负担的候选方案:纯 ESM、零依赖、无原生二进制、同步、确定性的 BM25+,并支持模糊匹配、前缀匹配和字段加权。对权衡过的替代方案的说明:- Orama(唯一值得考虑的升级方案)增加了向量/混合搜索,但其离线进程内嵌入器需要 TensorFlow.js 原生后端,其核心 API 类型为
T | Promise<T>(异步会渗入每个调用点),而且变动更多。只有在获得资助的多语言语义搜索成为需求时才重新评估——即便如此,也应通过自带预计算向量的方式实现,而不是使用tfjs-node。 - 语义搜索/嵌入在此处表现较差:区分性最高的词元是标识符、参数名和枚举值,而密集向量恰恰会在这些方面削弱相对于 BM25 的特异性;多语言词干提取也无关紧要,因为索引语料是英文 API 契约,所以任何语言的自然语言查询都是同义词/扩展问题(由小型人工整理的
SYNONYMS映射表 + 模糊匹配处理),而不是词干提取问题。
- Orama(唯一值得考虑的升级方案)增加了向量/混合搜索,但其离线进程内嵌入器需要 TensorFlow.js 原生后端,其核心 API 类型为
- 仅对较长词项启用模糊匹配(
> 6个字符)。不设条件的模糊匹配距离会让短词产生误匹配(show→how、events→event),从而显示错误的操作;前缀匹配负责覆盖不完整的词,模糊匹配则仅用于处理较长词项中的拼写错误。 - 示例以
x-codeSamples的形式存放在 OpenAPI 契约中(与各操作放在一起,也可供 Scalar 使用),并以自动派生的骨架作为后备方案——这样,MCP 和未来的参考文档都能使用同一来源。
此服务有意不做的事情
-
microsandbox引擎(托管式 µVM 边界)已实现,并已在真实 KVM 主机上完成验证;托管式策略层也已就位——包括全局并发上限 + 有界队列(concurrencyLimit.js)、每位调用者的并发上限(callerConcurrency.js)、每位调用者的速率限制(rateLimiter.js)、每个工具的审计日志(log.js→ InsightOps),以及维护停用开关(OA_EXECUTE_DISABLED)。参见 docs/microsandbox.md。 -
HTTP 传输 + OAuth 2.1 资源服务器已实现并上线(本地 JWKS 验证、受众绑定、每位调用者的 RFC 8693 令牌交换,以及未配置 OAuth 时
transport=http会默认拒绝),并接入生产环境 AS(https://openagenda.com/api/auth),部署于https://mcp.openagenda.com。 stdio 传输仍使用共享的OA_API_KEY(接受任意密钥;建议遵循最小权限原则;错误文本会隐藏密钥)——适用于本地或单租户场景,不适用于使用共享密钥的公开部署。参见上文的 Auth。 -
暂无
email声明用于补充审计调用者信息(OA 的uid声明已与sub一同提供)。AS 侧已实现按调用者封禁:封禁或移除用户会撤销其 OAuth 授权,且令牌交换时会重新检查用户是否仍处于活跃状态,因此被封禁的调用者将无法再签发aud=api令牌。 -
变更操作没有逐项审批机制(审计日志记录的是完整的
execute脚本,而非每个 API 调用)——参见 Mutations & moderation。 -
尚未决定审计输入的隐私保护与保留策略:审计日志会将用户的
search_docs查询和(有长度上限的)execute代码存储到 InsightOps——公开部署必须披露此情况并设置保留期限。这不是代码层面的阻碍。 -
应用代码不会采集主机资源遥测:CPU/RAM/KVM 数据由 node_exporter 抓取(例如 Alloy),刻意不放在应用代码中。服务器确实会自行生成 OpenTelemetry 数据——通过一个
NodeSDK(telemetry.js)发送全部三类信号,并通过 OTLP 发往同一主机 Agent(Alloy);在托管模式下,只有设置了 OTLP 端点时才会启用(否则关闭):- 指标 → Mimir:每个工具的结果、延迟和响应大小、预热池命中/未命中、实时并发数,以及每个 µVM 的主机 RAM 峰值 + CPU 时间。后两者从 libkrun VMM 的
/proc读取——VmHWM(单调递增)和utime+stime(累计值),即使运行时间不足 150 ms 也能捕获;SDK 的sb.metrics()每秒采样一次,无法做到这一点。 - 追踪 → Tempo:每次工具调用创建一个 span(
mcp.tool/execute、…/search_docs),并为耗时步骤创建子 span(mcp.credential.exchange、mcp.sandbox.run),因此可以定位execute耗时的具体阶段,而不只是看到总耗时。待上下文传递到 µVM 的 API 调用中后,executespan 将作为 v3 API 追踪的父 span(路线图)。 - 日志 → Loki:
@openagenda/logs记录也会通过 OTLP 发送(otel: true),同时发送到 InsightOps;工具 span 内生成的记录会自动关联到相应追踪。
因此:通过 OTel→Alloy(Mimir/Tempo/Loki)发送指标/追踪/日志,同时也会将日志+审计数据发送到 InsightOps(
@openagenda/logs);主机数据则通过 node_exporter→Mimir 发送。按职责分开;日志会刻意同时发送到 InsightOps 和 OTel。 - 指标 → Mimir:每个工具的结果、延迟和响应大小、预热池命中/未命中、实时并发数,以及每个 µVM 的主机 RAM 峰值 + CPU 时间。后两者从 libkrun VMM 的
-
存活探测:无需身份验证的
GET /health会返回{ "status": "ok" },供运行时间检查使用(未来也可用于负载均衡器 / 水平扩展探测)。该检查刻意保持轻量——不会访问 AS/执行令牌交换,也不会启动沙箱,因此探测不会增加负载,也不会让存活状态依赖 AS 是否可用。 -
本地引擎(node/deno)不会为每次调用提供进程隔离——失控进程会通过进程组 SIGKILL 终止,并将输出限制在 1 MiB,而不是使用 VM 边界(microsandbox 引擎会对每次运行进行隔离)。
已部署于 https://mcp.openagenda.com(已强制执行 OAuth,托管式策略层已就位)。在公开开放写入功能之前,上述注意事项仍然适用:变更操作逐项审批、审计隐私与保留策略,以及单主机单点故障问题。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。