关于 Mandala Computer
Mandala Computer 是用于操作云端 Linux 桌面的 MCP 服务,面向 AI Agent 和 MCP 客户端。它提供屏幕图像以及桌面交互、来宾系统命令、文件和计算机生命周期管理工具;项目目前处于 alpha 阶段。
主要功能
- 01创建和管理云计算机
- 02通过截图查看桌面
- 03点击、输入、滚动和导航
- 04运行命令并管理文件
- 05管理快照与操作
适用场景
- 01打开网站并查看屏幕
- 02为 Agent 自动执行桌面交互
- 03在 Linux 来宾系统中运行命令
- 04保存或恢复计算机快照
MCP 相关
该服务提供创建、选择、操控和管理 Mandala 云计算机的工具。MCP 客户端以子进程方式运行服务,并通过已保存的配置或 API 密钥进行身份验证。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
https://app.mandala.computer/mcpclaude mcp add --transport http mandalacomputer https://app.mandala.computer/mcp{"mcpServers":{"mandalacomputer":{"type":"http","url":"https://app.mandala.computer/mcp"}}}在 Cursor 中打开{"servers":{"mandalacomputer":{"type":"http","url":"https://app.mandala.computer/mcp"}}}在 VS Code 中打开{"mcpServers":{"mandalacomputer":{"type":"http","url":"https://app.mandala.computer/mcp"}}}{"mcpServers":{"mandalacomputer":{"serverUrl":"https://app.mandala.computer/mcp"}}}命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。
工具
详细介绍
原文在翻译后有更新,译文可能不是最新。
mandala-computer-mcp
适用于 Mandala Computer 的 MCP 服务——为 AI Agent 提供的云桌面。
让 Claude Code、Claude Desktop 或任何其他支持 MCP 的客户端连接到真正的 Linux 桌面,并查看和操控它。屏幕截图会以图像形式返回,因此模型可以查看屏幕并点击所见内容。
状态:alpha。 工具接口仍在调整;在 1.0 之前可能会有不兼容变更。 与平台的
/api/v1保持同步,而该接口本身也仍在变化。
安装
MCP 要求 Node 20.3 或更高版本。TypeScript CLI 登录设置要求 Node 22 或更高版本(npm 上的 mandala-computer)。登录一次后,再添加 MCP 服务:
npm install -g mandala-computer
mandala login
claude mcp add mandala -- npx -y mandala-computer-mcp
在 Node 20 上,MCP 可以使用已有的已保存配置或环境密钥。TypeScript CLI 会引导你通过浏览器完成授权,并将 API 密钥保存到 ~/.mandala/credentials.json。MCP 只读取该文件;它不会签发或批准设备、写入凭据,也不会自动启动登录。Python 发行版也提供 mandala 命令;登录请使用 TypeScript CLI。
若要选择单独的配置和工作区:
mandala login --profile Work --workspace Research
claude mcp add mandala -- npx -y mandala-computer-mcp --profile Work
--profile 的优先级高于 MANDALA_PROFILE,其次才是已保存的默认配置。名称区分大小写。显式密钥或非空的 MANDALA_API_KEY 优先于所有配置,并且不会访问凭据存储。显式密钥为空时会报错;为空或仅包含空白字符的环境密钥则视为不存在。你仍可通过 MANDALA_API_KEY 使用 Settings → Credentials → API keys 中的密钥。
已保存的凭据要求 POSIX 保护:目录必须是真实目录、归你所有且权限为 0700;文件必须是归你所有的普通文件、权限为 0600 且链接数为 1。在发出任何 API 请求前,符号链接、硬链接、不安全的权限、格式错误的文件以及不受支持的保护方式都会被拒绝。不支持在 Windows 上加载文件;仍可使用显式密钥或环境密钥。只会读取你主目录下的存储。
已保存的配置还会绑定其 API 基础 URL。--base-url 或 MANDALA_BASE_URL 必须与存储的基础 URL 经规范化后匹配,包括完整路径前缀和端口。使用已保存凭据时,显式设置为空的基础 URL 标志无效。每个 stdio 会话只解析一次:若要使用另一个已保存的密钥,请重启。撤销 Settings → Credentials → API keys 中的密钥后,后续调用会失败;MCP 不会切换配置、重新读取文件,也不会重试被拒绝的操作。需要新凭据时,请显式运行登录。
已保存的配置文件也可以携带默认工作区,使用
mandala workspaces use <workspace> 设置(使用 --clear 清除);CLI 会将其
保存在 ~/.mandala/defaults.json 中,与 credentials.json 放在一起,并且绝不会
修改后者。基于该配置文件启动后,如果调用中未提供 workspace_id,秘密工具(list_secrets、
get_secret、create_secret、set_secret、replace_secret、
delete_secret)、create_computer 和 list_computers 会使用此默认值,
mandala computers create 和 computers list 也是如此;显式指定的 workspace_id 始终优先。它们的说明会注明这一点并写明
工作区;list_secrets、list_computers 和 create_computer 还会说明
工作区是由默认值选定的;启动时则会在 stderr 上打印该工作区。没有
“所有工作区”这一选项:要列出其他工作区的内容,请传入另一个 workspace_id(或
'unassigned'),或者从没有默认工作区的配置文件启动服务。MCP 只会在启动时读取该文件一次,
并执行与 credentials.json 相同的检查。若配置文件中的密钥仅限于某个工作区(适用该密钥自身的范围)、
默认工作区是为与当前登录账户不同的账户保存的,或者使用的是显式密钥
或环境变量密钥,则不会应用该默认值。无法读取的文件(不是有效 JSON、版本不符,
或其他人也可读取)会在 stderr 上提示:此时 list_secrets、get_secret 和 list_computers 会
使用账户级范围,但 create_secret、set_secret、
replace_secret、delete_secret 和 create_computer 会拒绝不带
workspace_id 的调用,不会发送任何内容,也不会改为使用账户级范围;请传入
workspace_id,或修复或删除该文件后重启服务。托管的
HTTP 服务绝不会读取该文件,其工具也不会改变。
Claude Code — 作为插件使用,可同时安装服务和技能:
# Run mandala login first, or export MANDALA_API_KEY in this shell.
/plugin marketplace add mandalacomputer/mcp
/plugin install mandala-computer@mandala
技能 — plugin/skills/mandala-computer/SKILL.md
— 用于说明工具本身无法表达的内容:云桌面何时才是合适的选择、在挂起或停止之前会持续产生费用、
run_agent 通常是合适的层级,不应每次点击都截图,以及哪些拒绝值得再试一次。它描述的是何时以及如何使用,
而不是第二个客户端;服务安装后,它不会妨碍使用。
如果同时导出了 MANDALA_MODEL_KEY,该变量会透传,并在已配置的筛选条件允许时启用
run_agent。若未设置该变量,只要账户的 AI 额度足以支付,run_agent 就会使用这些额度(请参阅 使用 AI
额度运行 Agent)。
若要使用环境密钥而非已保存的配置文件:
claude mcp add mandala -e MANDALA_API_KEY=com_… -- npx -y mandala-computer-mcp
Claude Desktop — 在 claude_desktop_config.json 中:
{
"mcpServers": {
"mandala": {
"command": "npx",
"args": ["-y", "mandala-computer-mcp"]
}
}
}
Cursor、Windsurf 及其他客户端会在各自保存 MCP 服务配置的文件中使用相同的三个字段 — command、
args、env。
MCP 客户端会将其作为子进程启动。它使用已保存配置文件中的 API
基址;若使用环境变量密钥,则默认使用 https://app.mandala.computer/api/v1。
本地编程宿主可以调用 runStdio({ profile: 'Work' });导出的
StdioConfig 支持本地密钥或配置文件。createServer 仍然需要显式的 API 密钥,
而 HttpConfig 不提供本地凭据选项。
使用
说出你想做什么。通常,首次会话会是这样:
使用基础模板创建一台计算机,打开 example.com,并告诉我屏幕上显示了什么。
在这之下,模型大致会执行以下操作:
create_computer(template="base") → builds it and selects it for the session
wait_for_computer(until="guest") → the desktop inside is answering, not just the VM
open_url(url="https://example.com") → puts the page on the screen
screenshot() → an image the model can point at
click(x=640, y=400) → clicks what it saw
screenshot() → looks again
create_computer(size="large") 是最快的方式:平台会让 list_sizes 中列出的命名规格保持预启动状态,因此选择其中一种通常只需约一秒即可得到响应;自定义 cpu/ram_mb/disk_gb 规格则需要冷启动。size 会同时设置模板和各项数值,因此只传它,或只传明确指定的字段。
use_computer 会将一台机器绑定到会话,因此之后的每次调用都可以省略 computer_id。在任何调用中明确传入 computer_id,即可覆盖绑定而不改变绑定状态,这样就能同时操作两台机器。
设置 MANDALA_COMPUTER_ID 可在启动时绑定一台机器,并完全跳过 use_computer。
工具
以下是完整的工具清单。下面的筛选器可以在工具列表和调用中隐藏工具;本 README 中的工作流仅适用于所需工具可用的情况。离线测试样例验证了操作覆盖范围;针对在线发布版本的验证是另一项必需的 CI 门槛。参数和响应模式支持情况仍属于单独的契约。
选择机器 — list_templates、list_sizes、list_computers、get_computer、
use_computer、wait_for_computer、get_desktop_url
生命周期 — create_computer、start_computer、stop_computer、
suspend_computer、restart_computer、update_computer、clone_computer、
delete_computer、move_computer_to_workspace、move_computer、list_moves、get_operation、
list_operations、wait_for_operation — 参见生命周期操作
move_computer_to_workspace 会更改工作区成员关系以及有权访问某台计算机的用户。
它需要账户级别的成员或所有者凭据,并且必须明确提供 computer_id 和
workspace_id 参数(null 表示未分配)。所选计算机和默认工作区均不会被使用。
计算机必须处于停止状态,且没有已保存的会话、快照或快照历史副本、密钥绑定或代理凭据。
如果操作被拒绝,这些资源会保持不变;该工具绝不会为了强行移动而删除它们。这与
move_computer 不同,后者会将计算机迁移到不同硬件上。
操作桌面 — screenshot、click、type_text、paste_text、
press_key、scroll、drag、move_mouse、mouse_button、cursor_position、
wait
所有会执行操作的工具——除 screenshot 和 cursor_position 外——都接受
context: true,并会在操作完成后立即返回桌面当前状态:list_windows
会列出的窗口,以及当前获得焦点的窗口,无需再次调用。这些信息只会读取一次,
且读取时间紧接操作完成之后;因此,仍在打开中的窗口可能尚未列出。如果无法读取,
操作仍会执行,响应中也会说明原因。若当前获得焦点的窗口是 Chromium,响应还会包含
page:页面的 URL 和标题,以及页面中每个可见交互元素的一行信息——元素是什么、
显示什么内容(如果其可访问名称不同,也会提供该名称),以及可供点击的位置。默认
浏览器 Firefox 不提供 page;此时,page_not_read 会在窗口信息旁说明平台给出的原因。
访客系统内部 — exec、exec_poll、exec_kill、get_execution、
read_execution_output、open_url、
list_windows、window_action、read_clipboard、write_clipboard、
read_file、write_file、write_file_chunk、get_file_upload、cancel_file_upload、list_directory
write_file 会替换路径上已有的文件。设置 overwrite: false 时,只有路径上没有文件才会创建;如果路径已被占用,则会拒绝操作,且不会尝试写入任何内容(仅限 Linux 计算机)。不完整的内容绝不会发布到该路径,但发布或返回结果时发生故障,可能会导致完整文件留在该路径上,因此发生任何错误后,重试或覆盖前都应先读取该路径。read_file 和 write_file 接受 no_wake: true,如果计算机未运行,则会拒绝(409),而不是恢复计算机。对于嵌入式使用,Api 会将此类拒绝作为 FileExistsError 抛出;如果无法读取原因,则会将仅创建操作的 409 作为 CreateOnlyConflictError 抛出,该错误不会说明路径的情况;这两种情况的 isTransient 均为 false。
保留版本 — retain_execution_output、get_result、read_result_output、delete_result、publish_artifact、get_artifact、read_artifact、delete_artifact
被动元数据 — list_activities、get_activity、get_activity_results、read_signals
被通知,而非主动询问 — wait_for_event、poll_events、wait_for_file_change
快照 — list_snapshots、snapshot_holdings、create_snapshot、restore_snapshot、clone_snapshot、snapshot_schedule、get_retention、delete_snapshot
自有模板 — get_template_schema、check_template、publish_template、get_template、retire_template
构建模板 — build_template、list_builds、get_build、watch_build
账户配额 — get_account
了解自己的身份 — whoami、list_api_keys、list_workspaces、get_workspace、list_workspace_members、create_workspace、rename_workspace、delete_workspace。API 密钥的签发和撤销有意不设为工具 — 参见了解自己的身份和 API 密钥。
有意不设为工具 — 5 个 API 操作,每个操作都可能将凭据放入模型的上下文:API 密钥的签发和撤销(见上文),以及 3 个浏览器连接路由(创建、续期和撤销)。浏览器连接是一种可直接驱动访客浏览器的能力;TypeScript 和 Python SDK 的浏览器驱动程序会创建、续期和撤销该连接,并将其保留在自身内部。其他所有 API 操作都有对应的工具。
每个工具都会拒绝未声明的参数。 拼写错误或未知的键会导致输入错误,并指出该键;不会发送任何内容 — 因此,标志拼写错误绝不会被丢弃后继续执行调用。
支出 — get_usage、get_credits
在其他地方接收通知 — list_webhooks、create_webhook、get_webhook、update_webhook、rotate_webhook_secret、test_webhook、list_webhook_deliveries、delete_webhook
SSH 访问 — list_ssh_keys、add_ssh_key、remove_ssh_key、get_computer_ssh、set_computer_ssh。密钥属于 API 密钥的签发对象,而非账户。通过 add_ssh_key 添加的密钥(使用 API 密钥或已连接的应用)仅适用于其凭据所作用账户中启用了 SSH 的计算机;凭据撤销时,该密钥也会被移除。通过控制面板添加的密钥,则可用于该用户作为所有者或成员加入的每个账户。list_ssh_keys 会标明每个密钥在 API 密钥所作用的账户中的适用范围:所有账户、仅此账户,或仅其他账户(此处会拒绝;不会说明是哪个账户)。每个密钥只注册一次,因此,绑定到其他账户的密钥无法在此处再次添加。remove_ssh_key 只会移除标记为仅此账户的密钥:对于通过控制面板添加的密钥(所有账户)或绑定到其他账户的密钥,系统会像该密钥不存在一样响应;用户需通过控制面板将其移除。作用域限定为工作区的密钥可以读取密钥,但不能添加或移除密钥。
在互联网上展示应用 — list_public_urls、create_public_url、
update_public_url、delete_public_url。计算机内部的端口会通过
https://<label>.<base_domain> 提供服务(域名会出现在 list_public_urls
的回答中,例如 mandala.dev),使用 443 端口,地址中不带端口。默认公开:任何人都能通过链接打开;access: "private" 需要提供此账户的 API
密钥作为 Authorization: Bearer,该密钥绝不会传到应用。应用必须监听
0.0.0.0,而不是 127.0.0.1。看起来像平台或其他公司的地址的标签会被拒绝。已删除 URL 的标签仍归该账户所有:其他账户永远无法占用,且该账户可以再次使用,除非平台已将其封锁。超出套餐限额的计算机只会提供最早创建的 URL,列表会标记其余 URL。
浏览器打开新账户的 URL 时,可能会先看到一次性提示页面。任何看起来像浏览器在打开页面的请求都会显示该页面——根据其 fetch 元数据判断是否为页面导航;如果没有 fetch 元数据,则根据 Mozilla/ 用户代理发送的 Accept: text/html 判断。因此,发送浏览器标头的程序也会看到该页面。设置了标头的客户端可以通过 mandala-skip-warning: 1 跳过;在计算机自己的浏览器中,由于 open_url 无法发送标头,请点击 Continue。
密钥 — list_secrets、get_secret、create_secret、set_secret、
replace_secret、delete_secret 用于账户的密钥存储;get_computer_secrets、set_computer_secrets 以及 create_computer 的 secrets
用于指定计算机接收哪些密钥。密钥值通过 create_secret、
set_secret(创建名称;如果该作用域中已有此名称,则替换其值——名称比较时忽略 ASCII 大小写)或 replace_secret 写入,且永远无法读取。任何路由都不会返回密钥值,密钥存储工具的结果只包含已解码的文档所述字段(成功时),或者状态、reason
单词及其自身的一句话(拒绝时)。结果绝不包含平台的响应文本,Api 也不会为这些路由保留任何内容。绑定信息只携带密钥 ID、版本和名称。计算机会通过环境变量(env)或 /run/mandala-secrets/user/files 下的文件(file)接收密钥。设置操作会替换整个绑定列表([] 会移除所有绑定),并在计算机下次启动或重启时传到虚拟机;发送读取操作返回的 version,如果列表自读取后发生变化,请求会以 409 被拒绝。替换密钥值也会传到正在运行的计算机:文件绑定会就地重写文件;在支持此功能的映像上,环境变量绑定会传给新 shell;每次 exec(无论是否设置 desktop: true)都会使用当时绑定的变量运行(已在运行的程序会继续使用旧值,直到重启)。
replace_secret
和 delete_secret 需要当前的 revision_id,过期的值会导致 409。
delete_secret 还需要 confirm: true:仍绑定已删除密钥的计算机无法再次启动,直到移除该绑定。绑定了密钥的计算机会先运行,其虚拟机也会先响应,几秒后密钥才会送达,因此对此类计算机调用 wait_for_computer(until="guest") 也会等待密钥送达;
get_computer 会显示这段等待状态:“其密钥仍在传入”。
浏览器代理 — 在 create_computer 和 update_computer 上设置
browser_proxy: {server, bypass},可让 Linux 计算机上的浏览器(Chromium、Chrome、Firefox)通过代理连接;计算机上的其他任何内容都不会使用该代理。在 update_computer 上,该设置需单独更新,会整体替换原设置,而 null 会将其移除。哪些代理会被接受由平台规则决定,拒绝信息会原样返回。正在运行的计算机将在几秒内应用更改:
wait_for_computer(until="guest") 会等待浏览器应用该设置,而
get_computer 会显示这段等待状态:“浏览器代理仍在应用中”。
所有出站流量的代理 — 在 create_computer 和 update_computer 中设置 egress_proxy: {server, credentials_secret_id},即可让计算机的所有出站 TCP 流量(包括 exec、终端、包管理器和浏览器)都通过代理。代理在计算机所在的主机上接管流量,因此计算机内部的任何程序都无法绕过代理。服务器地址为带显式端口的 http://、https:// 或 socks5://,且不支持绕过列表。credentials_secret_id 指定一个存放 user:password 的密钥,该密钥不会绑定到计算机,也永远不会传到计算机:计算机所在的主机使用它向代理进行身份验证。代理不可用或拒绝连接,或者主机尚未取得凭据时,连接都会失败,不会直连;同时会阻止发往互联网的 UDP 流量和 ICMP 流量,且不会代理 DNS 查询。在 update_computer 中,该设置必须单独提交——如果同时包含任何其他字段,请求发送前就会被拒绝;此设置会整体替换原有配置,设为 null 则会移除该设置。get_computer 会显示代理地址;如果主机正在等待凭据,还会说明“其出站代理正在等待凭据;凭据到达前,连接将被关闭”。
委派 — 有模型密钥时注册 run_agent、run_agent_chat:stdio 模式下使用 MANDALA_MODEL_KEY,HTTP 模式下使用调用方自己的 X-Model-Key 标头。没有密钥时,只有在账户的 AI 额度足以支付一次运行的情况下才会注册 run_agent(参见使用 AI 额度运行 Agent)。这两个工具还必须通过已配置的筛选器。
工具筛选器
设置 MANDALA_READ_ONLY=1,只注册 annotations.readOnlyHint 确切为 true 的工具。启用时接受 1、true、yes、on;禁用时接受 0、false、no、off、空值或未设置。空白字符会被忽略,字母大小写不敏感。任何其他值都会导致启动失败。安全性不会根据工具名称或 HTTP 方法推断:read_file 和 cursor_position 可能会唤醒计算机并产生费用,因此不会提供。这同样适用于读写混合工具,例如 wait_for_computer 和 snapshot_schedule。screenshot、read_clipboard 和 list_windows 会保留其只读提示。
设置 MANDALA_TAGS=input,guest,即可选择多个已命名工具组的并集。名称只能使用小写字母,以逗号分隔,并会去除首尾空白和重复项。留空或未设置时不启用标签筛选;逗号分隔列表中的空项会被忽略。未知的非空标签会导致 stdio 连接或 HTTP 开始监听前启动失败,并显示列出所有有效标签的错误。
| 标签 | 工具 |
|---|---|
account | get_account、whoami、list_api_keys、list_workspaces、get_workspace、list_workspace_members、create_workspace、rename_workspace、delete_workspace |
computers | list_computers、get_computer、use_computer、wait_for_computer、get_desktop_url、list_sizes |
lifecycle | create_computer、start_computer、stop_computer、suspend_computer、restart_computer、update_computer、clone_computer、delete_computer、move_computer_to_workspace、move_computer、list_moves、get_operation、list_operations、wait_for_operation |
input | screenshot、click、type_text、paste_text、press_key、scroll、drag、move_mouse、mouse_button、cursor_position、wait |
guest | exec、exec_poll、exec_kill、open_url、list_windows、window_action、read_clipboard、write_clipboard |
files | list_directory、read_file、write_file、write_file_chunk、get_file_upload、cancel_file_upload、wait_for_file_change |
executions | get_execution、read_execution_output |
results | get_activity_results、retain_execution_output、get_result、read_result_output、delete_result |
artifacts | publish_artifact、get_artifact、read_artifact、delete_artifact |
snapshots | 上文列出的所有快照工具,包括 get_retention |
templates | list_templates、所有自有模板工具,以及上文列出的所有构建工具 |
events | wait_for_event、poll_events、wait_for_file_change |
usage | get_usage、get_credits |
webhooks | 上文列出的所有 webhook 工具 |
ssh | 上文列出的所有 SSH 工具 |
public_urls | list_public_urls、create_public_url、update_public_url、delete_public_url |
secrets | list_secrets、get_secret、create_secret、set_secret、replace_secret、delete_secret、get_computer_secrets、set_computer_secrets |
agent | run_agent、run_agent_chat |
activities | list_activities、get_activity、get_activity_results |
signals | read_signals |
所选标签会合并为并集,然后与只读限制以及现有的生命周期和模型密钥限制取交集。即使选中了相应标签,MANDALA_NO_LIFECYCLE=1 仍会屏蔽其中的五个工具;仅选择 agent,如果没有每个会话的模型密钥,就无法启用 run_agent_chat;如果没有可支付费用的密钥或额度,也无法启用 run_agent。只读模式会屏蔽这两个 Agent 工具,但仅禁用生命周期时仍可使用它们。MANDALA_TAGS=files MANDALA_READ_ONLY=1 只会开放 list_directory。只读模式下选择 agent 是有效配置,但工具列表为空。activities,results 只会包含一次 get_activity_results;signals 与活动的 events 套接字相互独立。筛选条件不会授予 API 权限:API 会在每次请求时检查当前成员或所有者身份以及工作区授权。
use_computer 不是只读工具。如果该工具被屏蔽,请显式传入 computer_id,或在 stdio 启动时通过 MANDALA_COMPUTER_ID 绑定一台计算机。HTTP 调用方必须自行指定计算机。这些变量适用于两种传输方式,插件也会转发它们。嵌入方可以在 ServerConfig 中传入 readOnly: true 和 tags: ['input', 'guest'];服务端本身不会读取环境变量。
生命周期操作
每个已受理的创建、克隆、启动、停止、挂起、重启、快照恢复、调整大小、主机迁移和删除请求都会记录一条生命周期操作;发起调用的工具会在其说明句中以 (operation op_…) 的形式,并在 JSON 中,给出其 operation_id。move_computer 会将其附加到所报告的结果中。如果平台无法记录操作,则不会包含该字段;但无论如何,调用都已发生。
get_operation 用于读取单个操作,list_operations 按时间从新到旧分页读取操作(computer_id、limit、cursor、idempotency_key;computer_id 不会默认使用当前选定的计算机;idempotency_key 可查找使用该密钥发送的调用所记录的操作,即使调用的答复已丢失)。wait_for_operation 会持续轮询单个操作,直到操作结束:操作为 succeeded 时返回;操作为 failed 时会报错,并附带平台的 error.code(可能包括 start_failed、build_failed、computer_gone、move_failed、resize_not_applied、lost 等,后续还可能增加)和错误说明。
succeeded 表示平台已完成其步骤,并不意味着桌面已启动:等待桌面作出响应仍需使用 wait_for_computer。大多数操作在工具返回时已是 succeeded;克隆操作在磁盘复制完成前会保持 running,移动操作在到达目的地前也会保持 running。这三种工具都是读取工具;即使生命周期工具被禁用,它们仍会注册,因为启动、停止、挂起和重启也会记录操作。
十一个生命周期工具(create_computer、clone_computer、start_computer、stop_computer、suspend_computer、restart_computer、update_computer、move_computer、delete_computer、restore_snapshot、clone_snapshot)都接受可选的 idempotency_key。如果省略,系统会为你生成一个新密钥;如果失败且结果未知,错误中会注明所用的密钥。接下来该怎么做取决于返回结果的是谁:
- 响应丢失(请求发出后连接中断,或平台前面的代理放弃了请求),或者平台返回
idempotency_in_progress:使用相同的idempotency_key再次调用同一工具。平台会返回首次调用的结果,或告知调用仍在运行;该步骤不会执行两次。 - 平台返回了未命名
operation_id的5xx:可能什么都没做。在调用发送到任何地方之前就拒绝请求——例如已有另一个启动操作正在进行、没有可用的主机,或模板目录不完整——都不会命名operation_id,平台也会释放该键。使用相同的idempotency_key再次调用同一工具:如果该键已释放,平台就会执行操作;否则会返回idempotency_outcome_unknown。只有此时才适用下面的情况。 - 平台返回了命名
operation_id的5xx,或返回idempotency_outcome_unknown:该键已被占用,再次使用它发送请求只会返回idempotency_outcome_unknown。 无论发生了什么,平台都会将该调用对应的操作保持为pending,最长一小时;期间主机可能正在执行该操作。要读取什么取决于工具:start_computer、stop_computer、suspend_computer、delete_computer、move_computer和update_computer:读取get_computer(并使用错误中命名的operation_id调用get_operation:succeeded表示步骤已执行,running表示等待该操作,单独的pending并不构成等待的理由)。如果步骤未生效,则使用新键或不带键重新发送调用。restart_computer无法通过get_computer确认:重置前后,计算机的状态都显示为running。应改为读取操作(使用错误中命名的operation_id调用get_operation,或使用该键调用list_operations):succeeded表示已执行重启,running表示等待该操作。其他任何状态都意味着重启可能已经执行,因此使用新键或不带键重新发送之前,应先询问用户;第二次重启会再次重置来宾系统。create_computer、clone_computer、clone_snapshot和restore_snapshot:如果立即读取计算机,可能会看不到正在构建或恢复、但之后会完成的操作效果;再次调用则会创建第二台计算机或进行第二次覆盖。应先读取操作——使用错误中命名的operation_id调用get_operation,或使用该键调用list_operations——如果状态为pending或running,则等待(调用wait_for_operation),不要使用任何键重新发送。只有当操作最终状态为failed(例如error.code为lost),或已无法找到该操作,并且get_computer(创建或克隆后调用list_computers)显示步骤未执行时,才使用新键或不带键重新发送调用。如果完全找不到操作,请在几分钟后再次读取计算机,再重新发送。
idempotency_key_reused:已有另一个调用使用了该键,且什么都没做。使用新键或不带键发送此次调用。
键的有效期为 24 小时。
你是谁,以及 API 密钥
whoami 不接受任何参数,并读取 GET /api/v1/whoami:此服务的 API 密钥发给了谁、该密钥代表哪个账户和角色(当前角色)、密钥受限于哪个工作区,以及密钥本身的信息——包括 manage_keys,也就是该密钥是否可以管理 API 密钥。此工具不需要任何权限,任何角色都可以使用,已暂停的账户也可以调用;如果账户已暂停,回答会明确说明。在托管服务的 OAuth 登录流程中,密钥属于已连接的应用(prefix 为 oauth)。
list_api_keys 按创建时间从新到旧列出该密钥持有者在账户中的 API 密钥——绝不返回原始密钥。每个密钥都带有 minted_by_key_id:通过 API 创建该密钥的密钥 ID;如果密钥是在控制面板中创建的,则为 null。撤销某个密钥不会撤销它创建的密钥。此工具要求当前服务的密钥具有管理密钥的权限;只有用户可以在控制面板中开启该权限(Settings → Credentials → API keys → 密钥菜单 → 允许管理密钥);如果没有此权限,工具会返回平台的 403 错误,其错误句子会明确说明原因。已连接的应用密钥永远不会拥有此权限。
此处有意不提供创建或撤销密钥的功能。 创建密钥时会完整返回新密钥,且只返回一次。模型的上下文——包括对话、客户端日志以及任何共享对话记录的地方——是存放长期凭据的最糟糕之处,而且一旦泄露,没人能将它收回。撤销操作无法撤销;模型读取密钥列表后进行的撤销操作,可能会让用户的 CI、另一个 Agent,甚至当前会话中断。用户可以在控制面板中执行这两种操作,也可以使用 mandala api-keys CLI。
list_workspaces、get_workspace 和 list_workspace_members 用于读取账户的工作区,工作区负责划分账户中的计算机。受限于某个工作区的密钥只能列出该工作区,而且无法读取成员列表(403),因为成员列表涵盖整个账户:工作区不会划分人员。这三个工具都只读取数据。
create_workspace、rename_workspace 和 delete_workspace 会更改工作区,因此需要未受限于工作区的所有者密钥:任何受限于某个工作区的密钥都会被拒绝(403),所以这些工具都无法操作此服务自己的密钥所属的工作区。delete_workspace 需要 confirm: true,并会撤销受限于该工作区的所有 API 密钥,无论密钥由谁持有——用户的 CI 或另一个 Agent 可能正在使用其中的密钥;工具会返回撤销的密钥数量。只能删除空工作区:如果工作区中仍有计算机或有效密钥,则操作会被拒绝(409),并说明数量;不会删除任何内容,也不会撤销任何密钥。使用 move_computer_to_workspace 将其中的计算机移出(或删除计算机),并使用 delete_secret 删除其中的密钥,然后再次发起删除操作。
只读会话(MANDALA_READ_ONLY)不提供这三个工具。
账户级密钥无需受限于某个工作区,也可以在该工作区中操作:create_computer(workspace_id: ...) 会在指定工作区中创建计算机,并从该工作区的作用域中指定其 secrets 和出口代理的 credentials_secret_id(包括该工作区自己的密钥和账户级密钥);list_computers(workspace_id: ...) 会列出该工作区中的计算机,使用 'unassigned' 则会列出未分配到任何工作区的计算机。密钥无权访问的工作区不会被找到;受限密钥也无法找到 'unassigned'。如果省略工作区参数,这两个工具会像密钥工具一样使用已保存配置文件的默认工作区(见上文);如果没有默认工作区,则会将计算机放入密钥所属的工作区,或不分配到任何工作区,而列表会包含该密钥可以查看的所有计算机。
当前账户配额
get_account 不接受参数,并使用调用方的账户凭据读取一次 GET /api/v1/account。需要 Viewer 或更高级别的访问权限。它会报告账户级汇总数据,包括使用工作区范围密钥时的数据,但不包含资源身份信息。它不需要选定的计算机或模型密钥,不会打开事件流,并且在只读和无生命周期筛选条件下仍可使用。选择 account 标签即可单独启用它。
报告包含生效的套餐、资源池上限、每台计算机的最大值、Windows 功能、当前用量和剩余配额。已配置的 CPU 和磁盘用量包含保留的计算机,不论其电源状态如何;运行中或已预留的 RAM 用量包含当前预留量。CPU、MB、GB 和快照字节数均保留 API 使用的单位。get_usage 则单独报告随时间变化的历史计量用量。
配额仅供参考:observed_at 是观测时间,不是预留凭证或一致性令牌。之后的创建、调整大小、启动或快照请求仍可能被拒绝,现有的 402 消息保持不变。快照余量以已索引的存储字节数为准;不包括正在进行的捕获预留,也不能据此确定新的捕获是否能成功。
complete.computers 和 complete.snapshots 彼此独立。不完整的分组中,其所有用量和剩余配额数据均为 null,表示未知;另一个分组仍可保留数值。已确认的零用量和零剩余配额仍以数值零表示。即使报告不完整,已确认的套餐上限仍会提供,包括仍有保留用量但没有套餐的账户。格式错误的报告和 HTTP 失败仍会作为错误处理,绝不会变成空账户。该工具会在投影公开字段前输出未知状态和参考性说明。
值得了解的事项
目前所有计算机都是 Linux 桌面。 目前没有任何套餐提供 Windows 客户机;本 README 中提到 Windows 的地方,是在描述客户端已支持、以备将来提供 Windows 时使用的行为。
运行中的计算机会产生费用,而忘记关闭的计算机会持续产生费用。
create_computer 会在自身说明中指出这一点;所有通过其他途径启动计算机的工具也是如此——restore_snapshot 会启动已停止的计算机,而 write_clipboard、read_file 和 cursor_position 都会恢复已挂起的计算机,因为它们都需要连接客户机 Agent 才能完成工作。这些操作的收费方式与其他启动操作相同;如果套餐已达上限,它们可能会返回 402,而不是结果。列表中最容易让人意外的是两个读取操作,因此它们都没有标注 readOnlyHint:否则会自动批准只读工具的主机可能会在无人要求时唤醒计算机并产生费用。screenshot、read_clipboard 和 list_windows 确实不会启动任何东西,因此保留了该提示。一段工作结束后,可以使用 suspend_computer(暂停:start_computer 会在大约一秒内恢复同一会话)或 stop_computer(关机:磁盘会保留,会话不会)。空闲挂起功能会处理模型忘记关闭的计算机,但要在连续 30 分钟未操作后才会触发。get_usage 可以查看这些操作产生的费用。
这里的部分默认值有意与 SDK 不同。 screenshot 的 fresh 默认值为 true(SDK 默认值为 false),因为点击操作后,缓存的画面可能早于该操作,导致模型读取画面后再次点击。超出 64-3840 范围的 width 会被拒绝,而不是像 API 那样自动限制到范围内。wait_for_computer 每 2 秒轮询一次(SDK 启动流程每 3 秒轮询一次),并会拒绝已挂起的计算机,同时提示使用 start_computer;而 SDK 的 waitForGuest 会恢复它。
截图不一定要显示整个屏幕。 screenshot 支持 region(裁剪区域,以缩放前全尺寸截图的像素为准)、scale(0 到 1)、format(png 或 jpeg)和 quality(JPEG,1-100)。裁剪或缩放后的图像有自己的像素坐标系,因此该工具会同时说明如何将图像中的位置转换回屏幕坐标。
实时截图还会返回 capture 名称,以及裁剪或缩放前的原始尺寸。请使用该尺寸,而不是计算机记录中的 resolution,二者可能不同。要裁剪已测量的画面帧,请将其 capture 与 region 一起发送;省略 fresh 或将其设为 false。如果没有 capture,截图默认仍为最新画面。fresh: true 不能与 capture 一起使用。如果捕获已不再保留(stale_capture),请在不带 capture 或 region 的情况下重新截图,然后重新测量区域并使用新的捕获名称。以这种方式无法固定已暂停计算机的已保存画面帧。
点击十次,不一定要截十次图。 每次只调用一个工具来操作桌面——screenshot、click、screenshot——都会在调用模型的上下文中放入一张图像。run_agent 则会用纯语言向平台自身的循环下达任务;它会在平台内部截图、判断并点击,最后用一句话和已执行操作列表作答。存在模型密钥时会注册此工具(参见配置),运行费用计入该密钥;否则,账户的 AI 额度足以支付时也会注册(参见使用 AI 额度运行 Agent)。max_steps 限制的是操作次数,而不是费用:一步是在桌面上执行一个操作;一次模型回复可以要求执行多个操作,每个操作各占一步;暂停的回合会消耗令牌但不占用步数;而且并非每一步都会截图。此处默认值为 20,上限为 100。
要了解屏幕上是什么样子,得看截图。 点击成功和点击无效会产生相同的工具结果,因此,模型不看屏幕就采取行动,是这类会话出错最常见的原因。工具的说明中对此有提示,服务端的指令也会再次说明。
但要了解是否发生了什么,已不必再靠截图。 平台的计算机会报告其状态变化——窗口打开、关闭或获得焦点,剪贴板所有权变更,后台命令退出,桌面启动,机器进入空闲状态,以及每一次电源状态转换——而 wait_for_event 会阻塞,直到其中一项发生,不再需要通过循环截图来确认什么都没发生。
值得理解的是,套接字位于何处。模型轮流发言;它不会在循环中读取数据流,而且在它两次发言之间,没有其他对象会在这里读取数据流。因此,此服务端会为每台计算机持有一个连接;工具首次查询某台计算机时会打开连接,并在后续回合中保持连接。模型处理其他事情期间到达的内容会被缓冲,下一次调用 wait_for_event 或 poll_events 时会按顺序交给它。模型持有游标,但永远不会知道套接字的存在。
设计的全部要点,归结为以下三条:
- 等待超时不会错过任何内容。 工具调用未运行期间,数据流仍保持打开。这就是超时上限设为 55 秒,而不是
wait_for_computer允许的 15 分钟的原因——短暂等待没有代价,因为再次调用会恰好从上次停止的位置继续。这里的超时是正常结果,不是错误。 - 已经发生的事件仍会结束等待。
computer.ready每个桌面会话只会触发一次,因此,已经运行一小时的机器不会再次发送该事件;连接到这样的机器时,会收到标记为synthesized的computer.ready,而不是永远等待一个不可能到来的事件。 - 历史记录中的缺口会得到补充,而不会直接转交。 如果平台无法从此服务端上次处理到的位置重放事件,返回的幸存事件会附带未收到事件的数量,以及缺失事件原本会报告的状态——窗口列表和计算机自身的记录。
gap帧本身永远不会传给模型,因为模型如果收到它,会自行编造恢复流程。 一旦你发出请求,文件写入就会成为一个事件。file.changed是这个事件流中唯一不会在无人请求时发送的事件:必须在连接上指定一个目录,否则平台不会发送任何文件事件。wait_for_file_change在一次调用中完成指定目录和等待两件事——向它提供客体中的绝对目录,它就会阻塞,直到该目录下有内容被创建、修改或删除。可用它等待构建写入输出、下载文件到达或脚本生成文件;另一种做法是在循环中运行ls,这就是文件版的截图循环。
使用前,有三件事值得了解,因为每一件都可能让你误读返回结果:
- 指定目录不等于开始监视。 必须请求客体开始监视;如果某台计算机上还没有人打开过终端,就得先在客体中安装监视器——需要几秒,而不是几毫秒。inotify 报告的是变化,而不是状态,因此在目录树启动监视之前发生的任何事都不会被报告。这个工具绝不会在这段时间内返回“没有变化”:在目录树真正开始被监视之前,它会明确说明这一点,并提示你再次调用。
lost不代表错误。 如果目录树变化速度快于事件流的报告速度,它会以一个标记代替成千上万个事件——这正是让构建期间的监视变得可用的关键。监视仍在进行,目录树也仍在被监视;丢失的是你对其当前状态的了解,因此请列出目录内容并继续操作。唯一的例外是unwatchable,这表示目录树并未被监视:路径尚不存在、不是目录、无法读取,或是符号链接。符号链接会被拒绝,而不会跟随。- 指定你关心的最小目录树。 在构建期间监视主目录,每秒可能产生数千次变化,而你得到的只会是洪流标记,而不是具体变化。每台计算机上的会话最多可同时监视四个目录树——第五个会挤掉最早指定的那个,并告知你被移除的是哪个;而计算机本身在所有已连接客户端中最多监视 32 个目录树。超过此上限的指定请求会被拒绝,但 websocket 客户端不会收到任何提示,因此这个服务会通过排除法找出原因:它会移除目录树,恢复其余事件流,并告知你被移除的是哪一个。添加监视绝不会影响你已有的窗口和进程事件。
一台计算机最多可同时保持 8 个事件流,一个账号在一台服务上最多可保持 128 个。 超出任一限制的请求都会被拒绝,并返回 409,且不说明原因;即使等待,限制也不会解除,直到另一个事件流关闭。websocket 客户端不会收到任何拒绝通知,而且计算机的状态仍显示为 running,因此这个服务会在连续五次连接失败后报告此事:事件流停止,poll_events 会提示你关闭该计算机上的另一个事件流。每次 SDK waitFor/events 调用会占用一个事件流,每个已请求该计算机事件的 MCP 会话也会占用一个。
不过,并非所有其他事情都会成为事件。点击落下和页面完成绘制都不是事件,等待多久也不会产生事件——这时仍应使用 screenshot、list_windows 和 exec_poll。如果请求的是该客体永远不会产生的事件,wait_for_event 会立即拒绝,并指出这台计算机能够发出哪些事件。
来宾机侧并非只有一种能力。 Windows 来宾机完全没有事件流。Linux 来宾机若硬件不带终端通道,也不会产生任何来宾机自我报告的信息。但 file.changed 使用的是 libc 自带的 inotify 调用,只需要该通道;而 window.*、剪贴板和就绪状态还需要桌面监听器所依赖的 X 绑定。因此,较旧的 Linux 镜像会报告每次文件更改,却完全没有窗口事件;而早到不支持文件监视的主机则恰好相反。拒绝信息会说明具体是哪种情况,因为这三种情况需要不同的处理方式:停止并启动可获得通道,什么操作都无法让镜像获得其缺少的绑定,而对于早于该功能的主机,调用方根本无能为力。
running 不代表已就绪。 管理程序启动 VM 后,计算机就会报告为运行中;桌面环境还要再过几秒才会启动。wait_for_computer(until="guest") 会等待软件作出响应,这才是 exec、文件操作和屏幕画面实际需要的状态。在 Linux 计算机上,它还会等待桌面会话登录,这样紧接着发送带有 desktop: true 的 exec 就不会被拒绝。这比 SDK 中的 waitForGuest / wait_for_guest 多等了一步;后两者只要来宾机 Agent 作出响应就会返回。该值仍名为 "guest",因为改名会导致调用方无法使用。
扩容可能会收到附带迁移方案的拒绝,而非单纯的否决。 如果计算机扩容后超过当前主机的承载能力,系统会拒绝请求,并指出该区域内还有另一台主机可以承载它。这种情况不会因等待而消失——只要计算机仍在原主机上,重试相同的扩容操作就会得到相同答复。move_computer 用于接受这个方案:它会将计算机迁移到不同的硬件上,复制磁盘以完成迁移,并在到达后应用新规格。调用前,请先告知与你共事的人这会产生哪些费用;如果等待超时,请查看 list_moves。如果迁移最终状态是 moved 而非 done,就要仔细查看——计算机确实已迁移到另一台主机,但规格仍是原来的;再调用普通的 update_computer 即可完成操作。
Webhook 会将同一批事件发送给不在当前对话中的接收方。 wait_for_event 面向模型,模型会轮流执行操作,也可以等着询问。CI 作业或队列工作器无法这样做;create_webhook 可以唤醒它们:平台会通过 HTTPS 将每个事件 POST 到指定端点,内容与套接字帧中的对象逐字节一致,并使用三个 Standard Webhooks 请求头进行签名。用于签名的密钥只会在创建响应中显示一次,之后无法再读取,因此响应的第一行会特别说明这一点;只有 rotate_webhook_secret 才能获取另一个密钥。此服务只负责设置 Webhook 和查询其状态——它不会接收 Webhook,也没有 verify,因为没有端点的服务无事可验证。投递重试耗尽后,可在 list_webhook_deliveries 中查看;没有事件会被悄无声息地丢弃。
捕获任务的生命周期会超过发起它的请求。 POST /computers/:id/snapshots 会返回 202 和一条占位记录,之后再复制磁盘;复制需要几分钟,并且耗时会随写入的数据量增长,超过任何 HTTP 请求的存续时间。因此,create_snapshot 会轮询:它会保留平台在复制前分配的 ID,并通过 list_snapshots 监视该记录,直到状态不再是 capturing,然后返回已完成的快照。它等待的是不再处于捕获中,而非 pending,因为复制可能会在两次轮询之间直接将小型快照推进到 durable;它会按 ID 匹配,而不是查找该计算机的最新记录,因为同一时间段内完成的定时捕获会导致这种查找方式出错,尤其是在这种方式最重要的长时间捕获中。对于更愿意按自己的计划轮询的调用方,wait: false 会直接返回 ID。
它最终可能得到的三种结果被刻意区分开来。成功完成的捕获就是快照。捕获在复制过程中失败,不会留下快照或记录——capturing 记录会直接消失,而这就是唯一的信号,因此绝不能让不完整的列表来判定结果。等待超时则表示捕获仍在进行,并给出要跟踪的 ID,因为这种情况需要的是查看进度,而不是再次尝试。
删除也会在请求结束后继续进行,而且读取方式正好相反。
DELETE /snapshots/:id 会返回 202,随后解除依赖快照的关联并删除已存储的对象。没有表示已删除的状态,因此 delete_snapshot 会轮询,直到记录消失——这与捕获正好相反:捕获会轮询,等待记录保留并发生变化。记录仍然存在,说明操作已停滞;它会处于 deleting 状态,而普通列表会隐藏这种状态:因此轮询会使用 include=unfinished,否则半删除的快照就无法与已删除的快照区分开来。平台会每隔十五分钟自动重试停滞的删除操作,因此放弃时的提示会建议继续观察,而不是重复操作。
由于这里以记录缺失作为成功信号,只有在完整读取列表后才会显示“已删除”。
如果记录缺失是因为虚拟机管理程序没有响应,那就不代表快照已消失;这种误判无法挽回,不像其他情况:没人会去查找一个已被告知已经销毁的快照。若收到 409,表示快照正在删除,这是进展,不是故障——应等待删除完成,绝不要转而删除其他内容。
计划安排的是时间窗口,而不是持续时长。 snapshot_schedule 设置计算机自动创建快照的时间窗口;get_retention 则说明这些快照会保留多少个。它不需要指定计算机,因为时间窗口属于账户。只有自动快照会随时间过期,因此模型要想保留某个快照,就应使用 create_snapshot 创建它。
用量总额偏低时,表面上看不出来。 get_usage 返回账户已花费的金额——批量操作多台计算机前后都应读取它;有人询问费用时也应读取它。返回的每个数值都是账户所用计算机所在虚拟机管理程序的汇总,因此无法连接的主机不会留下缺口:只会让总额悄悄偏低。如果发生这种情况,回答会在第一行说明,置于数字之前,因为等看到数字后才注意到的警告,可能已经为时过晚。有两种情况,只有其中一种可以通过重试消除。
每次最多查询一个时间窗口,跨度最多 62 天,最远回溯 399 天——每个虚拟机管理程序都会逐日重放账本来作答,因此要读取更早的时段,应同时指定 from 和 to,而不是扩大其中一个范围。单独指定 to 时,会以当前计费周期为基准计算,因此会被拒绝。
exec 以 root 身份运行,且没有显示屏。 未设置 desktop: true 就启动 GUI 应用,应用将无法绘制界面。open_url 是在屏幕上打开网页的可靠方式——但它会在浏览器绘制页面前返回,有时会提前十秒。
变量应放在 env 中,而不是命令前面。 exec 接受 env 对象,而 FOO=bar cmd 是另一回事:它属于 Shell 语法,因此值中若包含空格或引号,就需要自行正确加引号;否则值会被悄悄截断。这样还会把值放进来宾系统的 ps 输出,供任何登录到机器上的人查看,而且后台命令的命令行会出现在每个 exec_poll 的回答中。
任何耗时操作都应使用 background: true。 在前台运行构建或安装操作会导致超时,而任务仍会在来宾系统中继续运行,输出也无法读取。使用句柄即可获取退出码和输出,exec_kill 则可以停止任务。
稳定的后台读取。 当被接受的 exec 返回 execution_id 时,使用 get_execution 获取最近观测到的 running、exited 或 lost 状态。只有 exited 会提供 ended_at 和带符号的 exit_code;running 不能证明计算机处于唤醒状态,而 lost 既不能确认成功,也不能确认失败。较早的回复可能不包含 ID。若提供的 ID 格式错误,已接受的命令及其有效 PID 仍可使用,但不会提供稳定标识:不要为了获取标识而重放命令。PID 轮询或终止操作都不会重建这种关联。
read_execution_output 接受该 ID,以及 stdout_offset 和 stderr_offset 两个字节位置。每个读取器都从零开始,然后传入各自返回的位置。例如:
{"execution_id":"exec_0123456789abcdef0123456789abcdef","stdout_offset":0,"stderr_offset":0,"limit":4096}
默认情况下,每个数据流的读取上限为 4,096 字节,最高为 16,384 字节。完整且无损的 UTF-8 内容会以 stdout/stderr 返回,并保留 BOM。NUL、二进制内容和被拆分的 UTF-8 区块会原样保留在规范的 stdout_b64/stderr_b64 中;不会进行截断或替换。stdout_more 和 stderr_more 相互独立,值为 false 表示此时已到达 EOF,并不表示命令已完成。
单独的 diagnostic/diagnostic_b64 会在每次读取时重复返回。最多显示可用的 65,536 字节中的 4,096 字节:请检查 diagnostic_available_bytes、diagnostic_displayed_bytes 和 diagnostic_display_truncated。独立的 diagnostic_truncated 标志表示平台的捕获限制。诊断信息不会推进任一游标。两个新工具都不会消耗其他读取器或共享的 exec_poll 游标中的输出,并且都带有只读、非破坏性、幂等注解。
这些都是可取消的单次请求,不会自动恢复、重试、监视、捕获或重放命令。元数据读取不会访问来宾文件。输出读取会执行来宾 I/O,但不会刷新活动状态,因此不适用于被动活动记录/历史记录。文件是可变的来宾数据,不是保留的工件;句柄可能在重启、替换或清理时消失,已观测到的退出状态会在十分钟后过期。读取不可用时会报错,而不会返回空输出。每个新工具的结果序列化后最多为 256 KiB。
明确保留的不可变版本。 retain_execution_output 接受一个 execution_id,并通过一次 POST 捕获其易失性输出的一个版本。它会执行来宾 I/O,但不会恢复或重放命令。可选的 max_bytes_per_stream 默认为 1 MiB(最高为 4 MiB);retention_seconds 默认为 86400(最高为 604800)。诊断信息单独处理,最多为 64 KiB。每次捕获都会创建一个版本;不会自动捕获或重试。
get_result 通过 result_id 读取有限的元数据。read_result_output 需要 result_id、stream(stdout、stderr 或 diagnostic)和 offset;limit 默认为 4096,且上限为 16384 字节。它会读取一个独立页面,并返回精确的 offset、next_offset、eof 和 bytes 字节数。内容将以无损的 text(保留 BOM)或用于二进制内容、控制字符和被拆分 UTF-8 的规范 base64 形式返回。EOF 表示到达此保留前缀的末尾,并不表示任务已完成。单个页面无法验证完整的清单哈希。
delete_result 会删除该版本一次;重复请求返回的 404 仍表示不可用。
现有的同步 exec 接受 retain_output: true,或接受包含相同两个选项的严格对象。值为 false 或未提供时,默认行为不变。此选项不能与 background: true 同时使用。返回规范的 result_id 表示可选保留已确认;不会伪造执行 ID。可选元数据缺失、格式错误或不受支持时,不会改变命令结果,也不允许重放命令。保留前缀截断与上游响应截断是两回事。同步结果没有诊断数据流;显式读取诊断信息可能返回 409。
指定的文件版本。 publish_artifact 要求调用者提供绝对 path、expected_size 和 expected_sha256。例如:
{"path":"/tmp/empty.txt","expected_size":0,"expected_sha256":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}
路径支持合法的 Unicode 字符、空格和 Linux 反斜杠;Windows 驱动器路径和 UNC 路径也可接受,但须符合平台当前的操作系统凭据。
发布时只会指定一次来捕获来宾文件,不会在客户端执行 stat、list、read、hash 或 exec 预检。max_bytes 默认为 8 MiB,上限为 64 MiB;预期大小必须在此范围内。保留期限遵循上述界限。可选的 execution_id 记录的是已验证的调用方选择,并不能证明该执行创建了此文件。每次发布都会创建一个独立的不可变版本。
get_artifact 会读取元数据。read_artifact 会先读取元数据,且仅当完整大小不超过 max_bytes(默认为 4096,最大为 16384)时,才会读取整个保留对象并验证其精确长度和 SHA-256。超过上限的对象会在任何内容请求前返回元数据并拒绝读取;请使用设置了足够上限的 SDK 完整下载。不支持部分制品、Range 请求、本地目标路径、文件名、图像/HTML 预览或来宾回退。已验证的内容使用相同的无损 text/base64 表示。
delete_artifact 只会删除存储的该版本,不会删除来宾文件;重复出现的 404 仍会如实报告失败。
所有八个保留工具都会解析一次计算机选择。读取操作是被动的,但需要当前授权和可用的作用域;当主机无法验证访问权限、数据过期或已删除,或存储不可用时,保留的字节可能无法访问。它们绝不会唤醒来宾或替换为实时输出。新操作共享一个 90 秒预算,涵盖标头、正文和两次制品请求。MCP 客户端可以施加更早的截止时间。取消、重定向和格式错误的响应都不会触发重试;如果发布响应丢失,则表示提交状态未确认,而不是已撤销。每个完整序列化的保留工具结果上限为 256 KiB。读取操作标记为只读;捕获/发布会创建新版本;删除操作具有破坏性,并带有幂等的删除效果。活动结果详情仍不属于此 MCP 运行时。
超过约两分钟后,甚至不会收到超时响应。平台前端的代理会放弃一段时间内未产生响应的请求(大约两分钟),并返回 524;这会显示为 GatewayTimeoutError——无论 timeout_s 设置了什么,因为放弃请求的那一跳根本没有看到它。针对 app.mandala.computer 的测量结果:sleep 130 在 timeout_s: 300 时于 125.2 秒失败,在 timeout_s: 3600 时于 125.3 秒失败。提高 timeout_s 毫无作用;background: true 是唯一有效的方法。被放弃的命令仍会继续运行,因此在这种情况之后调用的工具往往会报告来宾 Agent 正忙——这是第一次失败仍在继续,并非第二次失败。
大约两分钟的上限来自托管代理。对于未经过该代理访问的 MANDALA_BASE_URL,服务端接受 1 到 600 秒的整数前台 timeout_s 值(默认为 30),工具也开放了这一范围。HTTP 客户端为响应标头预留了 630 秒,以便服务端能够报告 600 秒超时。较长的任务请使用 background: true 并轮询句柄。
list_windows 能看到截图看不到的内容。 它可以帮助你区分应用启动失败和界面尚未绘制的情况。请匹配 class(应用程序),而不是 title(应用当前显示的任意页面)。
剪贴板是两个工具,不是一段 shell 操作配方。 read_clipboard 和
write_clipboard 可以访问桌面上的 CLIPBOARD 选区——也就是 Ctrl-C 写入、
Ctrl-V 粘贴的内容——适用于桌面镜像中包含相应剪贴板工具的 Linux 计算机:
X11 上的 xclip,或 Wayland 镜像(例如 Omarchy)上的 wl-clipboard
(wl-paste/wl-copy)。不包含该工具的镜像会让这两个工具都永久返回 400
错误,并指出缺少的工具;更改计算机的运行时状态或重试都无法解决镜像依赖问题。
将 write_clipboard 与 press_key 搭配使用,并设置 keys: ["ctrl","v"]——两个按键名称,而不是字符串 "ctrl+v"——即可将文本输入当前获得焦点的位置。不要改用 exec 调用 xclip:
exec 会运行登录 shell,因此来宾用户的配置文件会与命令输出写入同一处,破坏你尝试解析的读取结果;通过这种方式写入则需要常驻持有进程、重定向、base64 和轮询循环。
这里的写入会由平台在回复前重新读取选区来确认。输入上限为 64 KiB,输出上限为 128 KiB;读取超过上限时会被拒绝,而不是截断。write_clipboard 会恢复已暂停的计算机;read_clipboard 不会。
计算机会自行暂停。 默认情况下,30 分钟无人操作后就会暂停。输入、exec 和文件传输都算作使用,并会自动恢复计算机;
截图则刻意不算,因此只进行监看的循环可能会眼看着自己的计算机停机。
停止操作会先请求关机,再强制关机。 stop_computer 会请求来宾系统关机,并等待最多 20 秒;如果来宾系统在此之后仍未关机——例如 X 会话卡死、出现模态“未保存的更改”对话框,或某个服务忽略了自身的关机请求——平台就会强制关机。因此,普通停止操作最终会使计算机处于停止状态,且回复可能需要约一分钟。force: true 会跳过请求和等待,立即切断电源——相当于一直按住电源按钮——来宾系统尚未写入磁盘的内容也会随之丢失。它只能节省 20 秒,无法挽救失败的停止操作,因此除非计算机必须立即关机,否则不要使用它。
清除快照必须以你看到的快照集合为准。 删除计算机时,默认会保留其快照。如要一并销毁快照,请先读取 snapshot_holdings——其中包含数量、字节总数和指纹——并将该指纹作为 expect 传给 delete_computer。如果你查看后集合发生了变化,清除操作就会被拒绝,以免误删你作出决定时尚未考虑到、期间刚完成的捕获。没有 expect 时,delete_computer 不会清除快照;平台只会在调用方无法读取快照持有情况时,才允许省略 expect。
列表太短时会拒绝返回,而不是悄悄提供不完整结果。 list_computers、
list_snapshots 和 list_builds 会向多个虚拟机监控程序发起请求;如果其中一个无法访问,平台就会返回 503,而不会悄悄漏掉部分条目后仍提供列表。设置 allow_partial: true 即表示接受不完整的响应——此时结果会以一行 INCOMPLETE: 开头,说明列表不完整,因为列表过短看起来就像缺失的条目已被删除。
list_builds 是唯一一种你始终只能从这行提示中获知情况的列表。平台不会记录哪个虚拟机监控程序运行了哪个构建,因此,构建列表不完整时只会少几行,缺失数量未知,也不会有任何标记说明存在缺漏。
另外两个工具会为每个无法访问的对象追加一行,并标记为 unreachable,但会根据密钥类型有所不同。工作区范围的密钥不会获取带标记的 快照 行,因为要列出缺失的 ID,就意味着要读取一个没有工作区列的主机缓存,并将其受限工作区之外的工作区 ID 交给受限凭据。其 计算机 列表仍会将平台自身记录中标记为无法访问或正在删除的行列出,因为该记录有缓存所缺少的工作区列。(此类密钥根本无法调用 list_builds:构建需要账户范围的密钥。)无论密钥类型如何,INCOMPLETE: 行都是每个简短列表都会携带的唯一信号,因此会以说明文字的形式放在最前面。
计算机有自己的生命周期,与其访客系统正在执行的操作无关。 state 是平台对计算机是否存在的记录——live、deleting、deleted、lost;如果列表无法通过其主机确认该行,则为 unreachable。而 status 则是主机对访客系统状态的回答。通过记录提供的行有前者而没有后者,因此如果两者都有,list_computers 会同时打印出来:计算机可以同时处于运行和删除状态。
未筛选的列表包含 live、unreachable 和 deleting。两个终止状态不会出现在其中,因此,已消失的计算机只有通过 list_computers(state: 'deleted') 才会显示——而筛选列表返回空结果,说明的是筛选条件没有匹配项,而不是账户中没有计算机;这正是该结果的含义,而不是在邀请你创建一台计算机。
删除过程中的快照仍会计费,但会隐藏。 已开始但尚未完成的删除仍会占用对象并计入存储,而默认列表会将其排除——普通调用方问的都是“我可以恢复什么”。当问题涉及存储时,应使用 list_snapshots(include_unfinished: true) 标志。
读取 reason,而不是状态码。 拒绝是否会自行解除,由平台的 reason 字段说明;每个工具的回答都会解释其含义:contention(有操作正在进行)和 starting(访客 Agent 仍处于启动窗口内)会解除,因此稍后重试相同调用即可;unavailable、running、unsupported、exists 和 revoked 则不会。409 并不表示“等待”:对已停止的计算机执行 exec、输入操作和剪贴板操作会返回 409 unavailable,需要调用 start_computer 才能解决;而 running 则需要先停止计算机。没有 reason 的 409 属于未分类错误,会按不会自行解除处理——例如标签已被占用、计算机达到其公开 URL 限制、webhook 数量达到上限、SSH 密钥已注册——除非其 code 是平台说明会解除的情况(idempotency_in_progress,或迁移稳定期间的 WORKSPACE_MOVING)。平台自身的错误消息会原样传递,因为它们就是为了便于采取行动而编写的。
HTTP 失败会保留其实际响应状态。已知路径上的不支持方法会返回 MethodNotAllowedError(405),并在可用时附上收到的 Allow 值。找不到计算机、快照、路由或访客文件时,仍会返回 NotFoundError(404);其他访客失败则保留其自身的状态和消息。上述两种响应都不会触发自动重试或方法切换。
isTransient(err) 回答的是“是否值得原样再次发送”。只有 GET 或 HEAD 请求收到的 503 才是暂态错误。任何更改请求收到 503 时,更改可能已经发生,也可能没有发生,因为平台在请求已发送后发生故障时,响应方式也是如此。因此,无论 reason 是什么,isTransient 对这类请求都会返回 false;工具的回答会提示先读取当前状态,并且 error.method 会记录请求方法。此版本不认识的 reason(例如新增的词)会被视为未分类。reasonKind 对它会返回 undefined,此时由状态码决定——对于 409,答案是否定的:只有当其 reason 为 clears,或其 code 为 idempotency_in_progress 或 WORKSPACE_MOVING(且没有 reason 表示其他情况)时,409 才是暂态错误。APIError.code 保存平台的代码。
密钥存储在 api.secrets 上提供了类型化调用:list、create、get、replace 和 delete。每个响应都会严格解码为文档规定的字段,且都不包含任何值:
const { secrets } = await api.secrets.list();
const s = await api.secrets.create({ name: 'OPENAI_API_KEY', value: key });
await api.secrets.replace(s.id, { value: next, revisionId: s.revision_id });
嵌入方可以检查每个 APIError 上的可选诊断信息:
import { Api, APIError, MethodNotAllowedError } from 'mandala-computer-mcp';
const api = new Api(process.env.MANDALA_API_KEY!);
try {
await api.json('GET', 'account');
} catch (error) {
if (error instanceof APIError) {
console.error({ status: error.status, requestId: error.requestId,
reason: error.reason, allow: error.allow,
wwwAuthenticate: error.wwwAuthenticate });
if (error instanceof MethodNotAllowedError) {
// Inspect error.allow before correcting the request method.
}
}
}
requestId 会优先使用非空白的 X-Request-ID 请求头,其次使用顶层 request_id 响应体字段。它是一个不透明的诊断信息,绝不是幂等键。原始响应体仍可通过 error.body 获取,其中包括任何不同的响应体 ID 和嵌套的聊天用量信息。allow 和 wwwAuthenticate 仅来自收到的响应头。HEAD 请求失败时,即使没有响应体,也可能包含这些字段。旧版服务、网络中介和连接故障可能不会提供这些信息。现有构造函数参数的含义保持不变;可选的末尾 APIErrorMetadata 对象会添加这三个字段和 method。
MCP 错误结果会将提供的 code、reason、request_id、allow、www_authenticate 和 retry_after_ms 作为带标签的 JSON 元数据包含在内。code 是平台提供的机器可读代码(LABEL_TAKEN、WORKSPACE_MOVING 等),应根据它进行判断,而不是根据句子。诊断字符串的长度上限依次为 128、128、256、512 和 512 个字符;超出上限的字段会被省略并附带提示,因此经过截短的 Allow 绝不会被当作完整内容呈现。有关部分完成的工作的工具专属警告、保留的发布内容、执行读取以及显式模板续接的警告仍然适用。序列化后的 JSON 对象或数组前缀不会作为错误文本显示。有效的标量错误消息会保留原有措辞;嵌入方仍可通过 APIError.message 和 APIError.body 获取原始信息以供诊断。即使没有 reason 或请求 ID,原生 Agent 错误帧仍会保留其提供的数值状态码;这一点与承载它们的成功 HTTP 流彼此独立。
对于 401,missing 表示未提供平台凭据;invalid 表示平台不接受所提供的平台凭据;revoked 表示该凭据的权限已失效。未知的原因仍会显示。未分类的 401 本身无法判断被拒绝的是账户密钥还是模型密钥,包括嵌套的聊天失败。403 仍表示权限或授权被拒绝。再次运行前,请检查已记录的工作;不会进行密钥回退、登录或自动重放。嵌套错误原因和带内流错误都不会授权重放已部分完成的运行。
对于桌面事件,请在 WebSocket 客户端中使用返回的原始 events_url,包括其中的桌面功能凭据。仅有 REST Bearer 密钥并不足够;HTTP 事件响应提供的是 JSON 指引,而不是另一套登录流程。
桌面链接就是凭证。 get_desktop_url 默认返回仅观看 URL——平台会丢弃发往该套接字的输入,因此可以安全地交给他人。control: true 返回完全控制权限的 URL,在该机器上相当于 root 权限。其他任何工具的输出都不会包含这两种 URL,这是刻意为之:工具结果会进入模型的上下文,进而进入任何捕获了该上下文的地方。这也是 get_desktop_url 没有 readOnlyHint 的原因,尽管其路由既不写入也不消费资源:主机将该提示视为可以免询问直接调用的许可,因此保留它会让模型在无人确认的情况下交出桌面控制权。
模板和构建归账户所有,需要账户级密钥。 仅限于工作区的密钥会被 publish_template、retire_template、针对账户自身命名空间的 get_template,以及所有构建工具——build_template、list_builds、get_build 和 watch_build——拒绝(403)。这种密钥仍可用于 list_templates、读取 system 模板,以及通过已发布模板的 ref 启动计算机。构建的 spec.secrets 只会从账户级密钥中解析,绝不会从某个工作区的密钥中解析。
退役模板不可撤销,而且影响比表面看起来更大。 不带 version 的 retire_template 会退役该名称的所有版本——这就是退役模板的含义,而且与 get_template 所指的“最新版本”刻意不同。之后,已退役的 ref 会被永久拒绝,即使内容完全相同也不例外,因此已退役版本永远无法再次发布。它不会影响任何计算机:计算机构建自 ref 所解析到的镜像,并且不保留对文档的引用,因此任何已运行、已停止或已暂停的计算机都会继续工作。该工具会在自身描述中说明所有这些内容,带有 destructiveHint,并要求 confirm: true——与 delete_computer、restore_snapshot 和 delete_snapshot 一样。它的可恢复性严格低于这些工具:已删除快照的名称可以再次使用,而已退役的 ref 永远不能再次使用。
这里会直接拒绝空的 version,而不会将其发送出去。这种写法——相比程序,模型更可能为可选参数生成这种写法——在平台上会被解读为“未指定版本”,从而退役整个模板。平台现在会对此返回 400;此服务不会发送空的 version。
构建需要几分钟,watch_build 可用于跟踪进度。 build_template 会立即返回一个任务;监视该任务会流式传输平台自身的进度,每个步骤都会发出进度通知和日志行,因此漫长的构建过程能清楚地显示仍在进行,而不会像是卡住了。
如果要监视实际构建,请设置 resetTimeoutOnProgress。 MCP 的默认请求超时为 60 秒,只有进度通知才能重置超时——但 SDK 只会为传入该选项的调用方重置超时,因此仅仅接受进度通知的客户端仍会在十五分钟构建进行到一分钟时被取消。对于无法让请求保持打开状态的客户端,应使用 get_build:它会读取一次并返回。
这里所有需要等待的工具也都适用。 wait_for_computer、move_computer、create_snapshot 和 delete_snapshot 都会轮询,并且每次轮询都会报告进度——内容变化时立即输出一行,内容未变时每 10 秒发送一次心跳,这样请求可以保持打开状态,同时不会让客户端被消息淹没。此前一次实时捕获耗时 107 秒,并在 60 秒时被取消;在此功能出现之前,这会让每个经过仔细措辞、旨在说明实际发生情况的答复都变成传输错误。对于无法选择启用此功能的客户端,每个工具都有应对方式:两个快照工具支持 wait: false,移动后可调用 list_moves,等待时可使用较短的 timeout_s 并再次调用。构建失败是 watch_build 的正常答复,而非错误——它会指出导致构建停止的步骤,也就是需要修复的内容。error 事件表示流发生故障,并不说明构建情况;工具会明确说明这一点,而不会任由模型改写一份本来没有问题的文档。
如果有符合条件的可用容量和镜像,已完成的模板构建即可通过 create_computer 启动。发布模板和完成其构建是两个独立步骤;仅发布模板并不能保证有可用的启动容量。
当 API 支持继续进行镜像准备时,create_computer 返回代码为 template_image_preparing 的拒绝响应,会在附加到工具错误文本的 JSON 中保留 template_transfer 和 preparation(包括 state 和 error)。如果存在 retry_after_ms,它就是解析后的 Retry-After 延迟时间,单位为毫秒;嵌入程序也可以通过 APIError.retryAfterMs 获取该值。支持整数秒数和 HTTP 日期;缺失或无效的延迟值会被省略。为适配计时器,延迟最长为 2,147,483,647 毫秒。
对于状态为 preparing、copying 或 ready 且具有可用令牌和延迟值的情况,请等待相应延迟后,使用完全相同的原始创建参数(包括 template)再次调用,并原样添加该令牌。令牌必须是非空白字符串,必须提供原始 template,且不能与 size 同时使用。若准备状态为 failed,工具会报告错误,供你检查后再决定下一步操作。状态缺失或未知,以及延迟值缺失或无效时,都不会提供自动重试建议。服务端绝不会自动重放创建请求;对于此拒绝代码,isTransient 会返回 false,因为原样重放并非继续准备的协议。
该令牌用于选择镜像构建;它不是创建请求的幂等键。成功后请停止操作。若响应丢失或结果不明确,请先检查 list_computers,再决定下一步操作;切勿自动重放创建请求。
将其作为服务运行
同一服务器也支持可流式传输的 HTTP,供无法启动子进程的客户端使用 — claude.ai、移动端、共享团队端点:
MANDALA_ALLOWED_HOSTS=mcp.example.com npx mandala-computer-mcp --http --port 3000
claude mcp add --transport http mandala https://mcp.example.com/mcp \
--header "Authorization: Bearer com_…"
MCP 端点是 /mcp;/healthz 会返回 JSON { ok, name, version },供任何检查进程是否已启动的对象使用。
它本身不持有任何凭据。 每个调用方的密钥都会作为其自己的 bearer token 传入,且仅用于其会话;没有存储,除了密钥的摘要外,不会有任何信息留存到会话结束之后 — 保留摘要是为了让后续请求能够证明来自同一持有者。这意味着仅泄露会话 ID,还不足以操控他人的桌面。HTTP 启动和请求绝不会读取操作者的凭据存储、MANDALA_API_KEY 或 MANDALA_PROFILE。--profile 仅供本地使用,与 --http 一起使用时会被忽略。
这也是任何人都可以运行自己的实例的原因:将同一容器指向同一个 API 即可正常运行,无需配置密钥。
绑定到默认的回环地址时,它只响应 127.0.0.1、localhost 和 [::1],因此用户恰好正在访问的网页无法通过解析自己的名称来连接它。如果通过某个名称提供服务,或绑定到 0.0.0.0,就无法推断出这个默认值;此时 MANDALA_ALLOWED_HOSTS 会重新启用检查。
这在一种看似两者都不是的部署方式中尤其重要:绑定到 127.0.0.1:3000 并置于代理之后 — nginx、Caddy、cloudflared、ngrok。代理会转发原始的 Host: mcp.example.com,而回环地址默认允许的列表中没有该名称,于是每个请求都会被拒绝并返回 403。请将 MANDALA_ALLOWED_HOSTS 设置为它对外提供服务时使用的名称。启动时会说明当前生效的列表,因此之前能正常工作的部署若出现 403,其上方会有一行提示,指出如何修复。
托管服务,使用 OAuth
https://app.mandala.computer/mcp 上的端点是此服务器以 --http 运行在平台自有代理之后的实例,平台充当其 OAuth 2.1 授权服务器。用户无需粘贴密钥:
claude mcp add --transport http mandala https://app.mandala.computer/mcp
需要两个变量,HTTP 服务才会以这种方式运行;不设置它们,一切保持不变 — 自托管用户仍需提供 API 密钥:
MANDALA_MCP_RESOURCE_METADATA_URL=https://app.mandala.computer/.well-known/oauth-protected-resource/mcp \
MANDALA_MCP_SERVICE_SECRET=… \
MANDALA_ALLOWED_HOSTS=app.mandala.computer \
npx mandala-computer-mcp --http --host 127.0.0.1 --port 3000
在此模式下,只有来自回环对端的 X-Forwarded-For 才会被信任,因此前置代理必须运行在同一台机器上,并且必须将其设为真实客户端地址。
- 每个
/mcp请求都需要 bearer,包括 initialize 和tools/list。 未携带 bearer 的请求会收到401,响应头严格为WWW-Authenticate: Bearer resource_metadata="<that URL>", scope="mcp:tools", 客户端据此找到授权位置。/healthz仍然开放。 - bearer 会原样传递——可以是
mcpat_…访问令牌,也可以是平台仍接受的 API 密钥。访问令牌仅对这个端点有效。它不是 API 密钥:若直接发送到 API,平台会以401拒绝(reason: "invalid")。用户可在 Settings → Credentials → Connected apps 中撤销该令牌。 - bearer 在获得任何内容之前,都会先由平台验证。 只有平台接受令牌后,initialize 才会创建会话——也就是
GET whoami返回2xx;所有有效凭据都会获得此响应,包括已暂停账户的凭据——这样,编造的令牌就无法占满会话池。401表示上述质询;任何其他响应均为503,且不会记住任何内容。每个来源地址每分钟最多允许 20 次被拒绝的 initialize。超过后,已接受的令牌仍然通过,新令牌仍然会接受检查,但每个地址每 5 秒只检查一个;其余请求会收到带有Retry-After的429。每分钟结束后,限额会恢复。携带请求的 POST 在分发前会再次检查,检查结果会按令牌摘要缓存 60 秒,因此过期或已撤销的令牌会在任何数据流打开前得到干净的401。 - 一个 bearer 最多持有 16 个会话;已暂停账户总共最多持有一个(见下文),因此单个令牌无法占满会话池。达到上限时,initialize 会先创建自己的会话,然后关闭该 bearer 最近最少使用的空闲会话以腾出空间(被拒绝的 initialize 不会关闭任何会话);旧会话已失效的客户端需要重新 initialize。如果没有空闲会话,响应为带有
Retry-After的429。有一种不同的竞态:如果计划关闭的空闲会话在新会话创建前开始工作,则会改为丢弃新会话,initialize 会收到404(Session not found,不带Retry-After),SDK 客户端会将其报告为连接失败。绝不会影响其他 bearer 的会话,已暂停账户的会话除外(见下文)。429会说明 bearer 当前持有的会话数,因此仍然超出上限的 bearer 会被告知等待忙碌会话,而不是关闭一个会话。runHttp将 16 设为maxSessionsPerBearer,CLI 从MANDALA_MCP_MAX_SESSIONS_PER_TOKEN读取该值(1 到 256 的整数,即整个会话池的大小;其他值会在启动时被拒绝;令牌也受其账户上限约束,见下文)。可自托管的--http服务也会应用此限制,但不会向平台验证 bearer,因此任何字符串都会被视为独立的桶:每个 bearer 的上限可分散单个密钥的客户端,而不受信任调用方的限制是进程级上限(256),以及尚未对外开放端口时默认绑定的 loopback 地址。 - 一个账户最多持有 64 个会话,即整个会话池的四分之一,所有令牌共享此限制。 会话按平台
whoami为每个令牌指定的账户 ID 计数,因此拥有多个密钥或授权的账户只计一次;whoami未指定账户的令牌会被视为独立账户。正在进行的 initialize 也计入,因此同一账户使用多个密钥同时发起突发请求也无法突破上限。达到上限时,initialize 会通过关闭两类空闲会话腾出空间:首先是账户下那些令牌已在请求时被平台拒绝的会话,然后是发起请求的令牌最近最少使用的会话,即使该令牌尚未达到自身上限也一样。如果仍然不够,响应为带有Retry-After的429,说明这是账户上限,并只列出该数字。不会为了满足此上限而关闭其他令牌的活动会话,也不会关闭其他账户的任何会话;上述竞态同样适用(404)。被拒绝的会话在关闭前仍会计数,因此撤销密钥不会让账户突破上限;若令牌已被撤销但之后从未再次发送请求,则服务器不会知道它已被拒绝,该会话会一直计数到空闲超时。客户端刷新令牌后留下的会话也是如此(见下文):它既不属于新令牌,也未被拒绝,因此如果客户端刷新时账户持有的会话超过其上限的一半,账户可能会在最长一个会话空闲超时时间(30 分钟)内收到429,比Retry-After所暗示的等待时间更长,除非这些客户端关闭旧会话。即使会话持有者不断发送通知、保持数据流打开,或发送服务器会拒绝的HEAD /mcp或DELETE /mcp,已撤销令牌的会话仍会在大约一个授权缓存时长(60 秒)加上空闲超时时间内进入空闲超时:不携带请求的流量不会向平台验证,因此只有令牌的授权结果仍在缓存时,流量才会维持会话。会话池已满(256)时,无论由谁持有,响应均为带有Retry-After的503,且不会关闭任何会话来腾出空间。256 个会话中的最后 64 个只分配给低于其份额的账户,其中份额为其余 192 个除以持有会话的账户数(包括发起请求的账户;正在进行的 initialize 也计为已持有):如果只剩这 64 个空位时某账户已达到其份额,该账户也会收到带有Retry-After的503,说明这一限制,且不会为其关闭任何会话。因此,即使提高上限,单个账户最多也只能持有 192 个会话,而每个新账户仍可接入。runHttp将账户上限设为maxSessionsPerAccount,CLI 从MANDALA_MCP_MAX_SESSIONS_PER_ACCOUNT读取该值(1 到 256 的整数;其他值会在启动时被拒绝)。可自托管服务不会验证账户,因此每个令牌都会被视为独立账户,同时受账户上限和自身上限约束;若要让单个令牌持有超过 64 个会话,必须同时提高这两个上限。 - 一个工作区最多持有 16 个会话,即其账户上限的四分之一,所有令牌共享此限制。 工作区范围的密钥既按平台
whoami为其指定的工作区计数,也按账户计数;账户整体范围的密钥(workspace: null)不受工作区上限约束。工作区范围的密钥与其运营方共享上述账户上限:运营方自身的会话与所有终端客户的会话合计仍须低于 64,因此即使工作区有空位,账户满额时也会收到账户的429。两个账户下具有相同 ID 的工作区仍视为两个不同工作区。腾出空间以及统计正在进行的 initialize 的方式均与账户上限相同;超出上限时,响应为带有Retry-After的429,说明这是工作区上限,并只列出该数字。runHttp将其设为maxSessionsPerWorkspace(默认为账户上限的四分之一,无论该上限设为何值;若超过,则取账户上限),CLI 从MANDALA_MCP_MAX_SESSIONS_PER_WORKSPACE读取该值(1 到 256 的整数;其他值会在启动时被拒绝)。仅托管服务适用:可自托管服务不会验证工作区。 - 一个已暂停账户的所有令牌合计最多持有一个会话(足以调用
whoami,这是平台仍会对其响应的唯一调用)。账户是否暂停,以平台最近一次针对该账户任一令牌给出的响应为准:每次检查开始时都会编号,较早开始但较晚结束的检查绝不会覆盖较新的检查,无论检查结果如何。因此,较新的检查若发现账户已恢复活动,会立即解除所有令牌的限制;若发现账户已暂停,则会限制所有令牌,包括其自身授权仍在缓存中的令牌。受到限制时,来自该账户任何令牌的 initialize(例如使用已刷新的令牌)都会关闭账户的空闲会话以腾出唯一名额:首先关闭令牌已被平台拒绝的会话,然后关闭最近最少使用的会话。正在处理请求的会话绝不会被关闭;若账户持有的唯一会话正忙,initialize 会收到说明账户已暂停的带有Retry-After的429。账户在创建会话后被暂停时,只能保留一个会话:该账户任一会话上的下一个 initialize、请求、通知或事件流都会关闭其他空闲会话,同时保留正在服务的会话;忙碌的会话则会等到空闲后,在之后发生上述任一操作时关闭。通知和数据流不会向平台查询,只有令牌授权仍在缓存中(60 秒)时才会触发此操作。可自托管服务不会向平台查询,因此不会有账户被暂停的情况。 - 如果平台在调用期间拒绝某个令牌,响应会原样返回相同的
401,而不是工具错误,以便客户端刷新令牌或重新授权——此时响应会一直保留到首字节发出。唯一无法奏效的情况是:令牌在数据流已提交之后失效,例如 SDK 的 15 秒 keep-alive、进度通知或部分结果已经发出。该调用会以工具错误结束,会话会记住此次拒绝,而该令牌的下一个请求会在不访问平台的情况下收到401。 - 刷新后的令牌会启动一个新会话。 会话绑定到创建它的 bearer 摘要;在该会话上使用不同 bearer 会收到
404 Unknown session,这是 MCP 规范要求客户端重新 initialize 的信号。访问令牌不会提供此服务可用来检查其所属授权的信息,因此,若允许重新绑定,任何获知会话 ID 的有效凭据都能接管其绑定的计算机、缓冲事件和保留结果。刷新不会关闭旧会话:它仍会计入令牌所属账户的上限(见上文),直到客户端使用旧令牌执行DELETE /mcp将其关闭(只会比较令牌摘要,因此即使令牌已过期也可使用),或会话在 30 分钟后进入空闲超时。若账户已暂停,新令牌的 initialize 会在旧会话空闲时将其关闭(见上文)。 X-Mandala-MCP-Service会在每次平台请求中携带MANDALA_MCP_SERVICE_SECRET,且只会发送至MANDALA_BASE_URL,这样令牌就无法被签发它的应用直接重放到 API。客户端自行设置的同名标头绝不会被转发。
Configuration
| 变量 | 含义 |
|---|---|
MANDALA_API_KEY | 可选的本地 API 密钥,优先于已保存的凭据。HTTP 模式下忽略;每个调用方都应发送自己的 bearer token。 |
MANDALA_PROFILE | 本地已保存的配置;--profile 会覆盖它。否则使用文件默认值。HTTP 模式下忽略。 |
MANDALA_BASE_URL | 使用已保存的配置时,必须与其中存储的 base 一致。否则默认为 https://app.mandala.computer/api/v1。 |
MANDALA_COMPUTER_ID | 在启动时绑定一台计算机,这样就不需要调用 use_computer。仅限 stdio——使用 --http 时会忽略,而不会绑定到每个调用方的会话中,因为它指定的是操作员账户中的一台计算机。 |
MANDALA_MODEL_KEY | Anthropic 或 OpenAI 密钥。在过滤条件允许时启用 run_agent 和 run_agent_chat,让平台使用此密钥运行自己的循环。若没有此密钥,且账户的 AI 额度足以支付,run_agent 将使用这些额度运行。仅限 stdio——使用 --http 时,每个调用方都应通过 X-Model-Key 发送自己的密钥,并且此变量会被忽略。 |
MANDALA_NO_LIFECYCLE | 设为 1、true、yes 或 on 时,隐藏 create_computer、clone_computer、clone_snapshot、delete_computer 和 delete_snapshot——所有创建或销毁计算机的工具。设为 0、false、no、off 或未设置时,则保留这些工具。任何其他值都会导致启动时拒绝运行,而不是按关闭处理:否则,此处的拼写错误会导致工具仍然可用,而服务器操作员却以为它们已经移除。--no-lifecycle 标志使用相同的取值规则,并以相同方式拒绝无效值,但它没有表示未设置的写法:--no-lifecycle= 会被拒绝,而不是忽略,因此,如果启动器模板中的变量未展开,程序会停止,而不会悄悄保留这些工具。 |
PORT、HOST | 用于 --http。默认值分别为 3000、127.0.0.1。 |
MANDALA_READ_ONLY | 仅保留标注为 readOnlyHint: true 的工具;严格布尔值解析规则见“工具过滤器”一节。 |
MANDALA_TAGS | 以逗号分隔的小写工具标签;工具清单和交集规则见“工具过滤器”一节。 |
MANDALA_ALLOWED_HOSTS、MANDALA_ALLOWED_ORIGINS | 以逗号分隔。指定此服务会响应哪些 Host 和 Origin 值。绑定到回环地址时,主机列表默认采用指定的地址,因此无需配置即可启用 DNS 重绑定保护;在某个名称下提供服务时,请设置此项。 |
MANDALA_MCP_RESOURCE_METADATA_URL | 仅限 --http。此服务发布时使用的 OAuth 受保护资源元数据 URL。设置后,它会按“托管模式与 OAuth”一节所述响应 OAuth 客户端;未设置时,调用方需提供 API 密钥。 |
MANDALA_MCP_SERVICE_SECRET | 仅限 --http。每次平台请求都会携带 X-Mandala-MCP-Service,且仅发送到 MANDALA_BASE_URL。绝不会记录到日志中。 |
MANDALA_MCP_MAX_SESSIONS_PER_TOKEN | 仅限 --http。单个 bearer token 可持有的活动会话数。默认值为 16;必须是 1 到 256 之间的整数(即服务器的整个会话池),否则启动时会拒绝运行。无论此值如何,暂停的账户总共只能持有一个会话,且适用于其所有 token。token 也受其所属账户的下方限额约束。 |
MANDALA_MCP_MAX_SESSIONS_PER_ACCOUNT | 仅限 --http。单个账户通过其所有 token 可持有的活动会话数。默认值为 64,即会话池的四分之一;必须是 1 到 256 之间的整数,否则启动时会拒绝运行。托管模式(已设置元数据 URL)下,账户是平台 whoami 为该 token 指定的账户;自托管时,或 whoami 未指定账户时,每个 token 都单独视为一个账户。超过限额后,initialize 会被拒绝并返回 429,随后只会关闭该 token 自己的空闲会话,或该账户被拒绝的会话。无论此值如何,暂停的账户都只能持有一个会话。无论此值为何,会话池最后四分之一的容量只分配给未超过自身份额的账户。 |
MANDALA_MCP_MAX_SESSIONS_PER_WORKSPACE | 仅限 --http,且仅适用于托管模式。单个工作区通过其所有 token 可持有的活动会话数。默认值为每账户限额的四分之一(64 个中为 16 个);必须是 1 到 256 之间的整数,否则启动时会拒绝运行;如果超过每账户限额,则按每账户限额计算。对于工作区级 token,按平台 whoami 指定的工作区计数;此类 token 还会与该账户自己的 token 及其其他工作区共享上方所述的账户限额。 |
除了模型密钥、两个工具筛选器、两个托管设置(MANDALA_MCP_RESOURCE_METADATA_URL 和 MANDALA_MCP_SERVICE_SECRET)以及三个会话限制外,每一项都有对应的标志;标志会覆盖环境变量:--http、--port、--host、--base-url、--computer、--allowed-hosts、--allowed-origins、--no-lifecycle、本地 --profile,以及 --help 和 --version。--key 供调用方在不同密钥下启动多个服务时使用,使用时会发出警告,因为参数向量可被 ps 读取、会写入 shell 历史记录,也会被任何 exec 审计原样记录——而 MANDALA_API_KEY 不会出现这些情况。 |
有必要说明一下 run_agent。它会把任务交给平台自己的 Agent 循环,由该循环在平台内操作计算机并用一句话作答。当一段像素操作原本会让调用模型每一步都要获取一次屏幕截图时,这个工具就很值得用——十次点击不再需要十张图。费用计入你所选的提供商密钥,平台绝不会存储该密钥。
run_agent 和 run_agent_chat 都接受 provider: "openai",默认使用 gpt-6.1-sol,也可根据已识别的 GPT 模型名称推断为 OpenAI。将匹配的提供商密钥设置为 MANDALA_MODEL_KEY(stdio)或 X-Model-Key(HTTP)。自定义模型名称必须显式指定 provider;省略模型和提供商时则保留 Anthropic 默认值。OpenAI 仅支持 BYOK,Responses 延续内容会存储在 OpenAI;任务、屏幕截图和工具结果会发送给该提供商。原生 OpenAI 操作批次必须在开始前符合剩余步数和 API 预算。发生部分失败时会停止运行,不会重放输入。
使用 AI 额度的 Agent
没有模型密钥时,run_agent 可以使用账户的 Mandala AI 额度运行。服务会使用会话自己的 API 密钥读取 GET /credits,并且仅当账户未被冻结且至少有 $0.25 可用时才会注册 run_agent;运行的首次模型调用会预留这笔金额。stdio 模式下,此读取在启动时执行一次。HTTP 模式下,则会在每个会话初始化时执行一次,并使用该调用方的 bearer token;发送 X-Model-Key 的调用方不会被询问,也不会使用任何操作员密钥或设置。如果读取失败——被拒绝(工作区范围的密钥无法读取账户级账本)、发生错误、超时或记录无法解析——则不会注册 run_agent。无额度时启动的会话不会在之后获得该工具;请重新连接,或重启 stdio 服务。
使用额度运行 run_agent 时,可传入 max_cost_usd(0.25 至 20;平台默认值为 2),指定一次运行最多可以开始支出的金额。最后一次模型调用可能会超出此金额;达到该金额时,运行会以 stop: "max_cost" 结束。它不接受 system 或 provider:使用额度运行时,会使用平台自己的提示词和 Anthropic,默认模型为 claude-sonnet-5-5,也可按需使用 claude-opus-5-5。结果会报告 cost_usd(运行目前为止的费用)和 credits_remaining_usd(最后一步报告的余额)。运行过程中额度耗尽时,会以 stop: "credits_exhausted" 结束;无法为首次调用预留额度的运行会返回 402 并附上 top_up_url。工具会将此情况报告为账户额度耗尽,并提供账单页面,而不是报告计划限制或模型提供商的计费错误。平台会通过提供商状态报告因 Mandala AI 服务故障导致的额度运行失败,状态为 Mandala AI is unavailable。此类运行中,无论状态如何,该句以及任何其他 402 都会表述为服务故障,而不是调用方凭据、计算机、请求或模型密钥的问题。没有模型密钥时,绝不会注册 run_agent_chat:平台仅会使用调用方自己的密钥运行该路由。
get_credits 会读取账本:可用额度、余额、冻结额度、债务、账户是否被冻结、本期计划发放的额度、尚未过期的额度批次,以及一页日志(按时间从新到旧排列;可通过 before 和 limit 分页)。金额以整数美元微单位表示。它需要账户级密钥。
被动目录、活动历史记录和信号
list_directory 接受精确的访客端绝对 path。Unicode、空格和标点符号在查询编码后仍会保留。它要求计算机已在运行,不会恢复计算机或延长其空闲计时器,并且仍需通过常规速率/容量准入机制联系访客端。结果会保留 path、包含 name、type 和可选 size_bytes 的 entries、truncated 以及 skipped。对于不可用的条目,不会推断其类型或将大小设为零。不会跟随符号链接,并且会拒绝最终路径组件为目录符号链接的情况。不会读取文件内容。部分目录列表是无序子集:最多检查 512 个条目/128 KiB,且没有续传令牌。若省略了某些名称,请缩小路径范围;不要自动重新扫描。
list_activities 返回一个按时间从新到旧排列、具有固定水位线的历史记录页面。传入不透明的 cursor 以继续获取,或传入 changes:true 和 cursor,以获取较早条目的延迟最终更新。值为 false/缺省的 changes 会从线上请求中省略;没有 limit 参数。保留 next_cursor、changes_cursor、gap 和健康状态字段。出现间隙时必须刷新历史记录。这些工具不会缓存游标,也绝不会自动拉取完所有页面。get_activity 接受格式为 act_ 加 32 位小写十六进制字符的 activity_id;它是请求身份标识,而非幂等键。记录的状态、范围、修订版本、时间戳、执行身份和状态各不相同:已接受的后台工作并不代表执行已完成,而已派发的错误可能已经产生影响。历史记录只是经过筛选、保留且尽力提供的元数据,并不涵盖所有访客端工作,也不代表 Agent 身份。
get_activity_results 最多返回 8 个最新的元数据链接,并保留修订版本、可用性、关联关系、字节数/截断元数据和观察结果。more:true 不提供续传游标,也不表示已返回所有版本。不可用的条目不等同于内容为空但可用的条目。调用方选择的制品关联关系并不能证明制品已创建或命令已成功。此工具绝不会跟随链接读取内容、文件、捕获内容、下载内容或轮询执行状态。
read_signals 从被动守护进程读取单个页面,与活动事件套接字相互独立。省略 since 或传入空字符串,可获取仅含当前基准且不重放事件的结果。可选的 limit 范围为 1–100;省略时使用 API 默认值 50。即使 events 为空,也要保留返回的 cursor:被过滤掉的行可能使其前移。保留期是临时的;过期、重启或迁移的游标可能返回显式重置间隙。基准和间隙不能证明此前没有工作,也不能证明任务已成功。不支持(501)和不可用(503)的响应仍然是错误,绝不是空历史记录或新的检查点。此工具没有监视器、访客端操作、重试循环或自动检查点缓存。
使用自己的模型密钥进行 JSON 聊天
run_agent_chat 使用与你调用 run_agent 相同的 BYOK Anthropic 或 OpenAI 循环来控制所选计算机。它接受非空的 OpenAI 格式文本 messages 数组,内容可以是字符串,也可以是 {type:"text",text:"..."} 部分组成的数组。最后一条用户消息提供任务;系统消息提供常驻指令。不会重放之前的用户/助手对话。可选的 model 用于选择具备计算机控制能力的 Anthropic 或 OpenAI 模型。max_steps 范围为 1–100,默认值为 20。这是计算机控制功能,不是托管的通用推理服务或聊天 UI;它不会添加密钥存储或模型计费服务。
此工具会刻意发送 stream:false 并使用 JSON 响应,以便结果保留底层的 agent.stop、步骤数和 token 用量:此路由的流式形式不包含用量信息和 agent 块。代价是耗时。在托管 API 上,不使用流式传输的请求会在边缘节点运行约 120 秒后被截断(返回没有响应正文的 524),运行会就此停止,并丢失结果、用量和步骤;默认的 20 步可能会超过这个时限。任何可能超时的情况都应使用流式传输的 run_agent。工具遇到 524 时会明确说明,而不会将其描述为通用网关故障。
只有与完成状态的结束原因一致的明确 end_turn 才能报告成功。达到限制、拒绝、终止字段缺失或冲突,仍会被视为错误,但会保留有效的部分结果。嵌套失败会保留 Agent 的计算机、已记录的步骤和原生用量(包括分别统计的缓存读取和缓存写入 token 数),并附带 OpenAI 格式的汇总用量。原生详情格式错误时会明确标记为不完整。失败的调用可能包含已完成且已计费的工作:请先检查再决定后续操作,不要自动重放。任一 Agent 工具都不会在失败后启动计算机或切换端点。
调用方通过 stdio 的 MANDALA_MODEL_KEY 或 HTTP 自行提供模型密钥;HTTP 使用其自带的 X-Model-Key 标头。账户的 Authorization 仍然独立;HTTP 永远不会回退使用运营方的模型密钥。等待期间的心跳计入通知,不计为已完成操作。progressToken 加上 resetTimeoutOnProgress 可以让 MCP 客户端继续等待,但无法延长上述托管边缘节点的时限;max_steps 既不是时间上限,也不是花费上限。
这两个 Agent 工具会按照这些路由上的实际含义读取失败状态,而不是套用其他场景中的含义。平台重新核验调用方时会返回带有 reason: "revoked" 的 401 或 403,只有这种情况才会提供账户建议。402、504 或 529 是模型提供商针对所配置模型密钥返回的状态(分别表示其计费、超时或过载错误),因此建议检查模型提供商账户,而不是 Mandala 套餐;不带 revoked 的 403 可能是提供商针对该密钥返回的权限错误。Mandala 自身的 402 会附带 top_up_url:这表示运行会使用 AI 积分,但账户余额不足,并会明确说明这一点。例外情况是,run_agent_chat 在任何运行开始之前收到拒绝——返回来自角色门禁、错误请求正文、账户停用或计量器的简单 {error: string} 正文:没有任何内容运行,而且会像其他工具一样保留常规的角色和套餐建议。
开发
npm ci
npx vitest run # deterministic offline tests, including the synthetic contract
npm test # offline tests plus the separate private mirror check
npm run build
npm run lint
CI 会在 Node 20、22、24 和 26 上运行离线测试套件。另有一个 Node 22
published-openapi 作业,会在拉取请求和推送到 main 时运行:
npx vitest run --config vitest.openapi.config.ts
它会匿名获取一次固定发布版本
https://app.mandala.computer/api/docs/openapi.json,总时限为 20 秒,
响应体上限为 8 MiB,随后调用真实 MCP 工具进行测试,并要求每个已发布的 /api/v1 操作都有请求证据。每个测试变体都必须成功并发送请求,包括某个工具已成功发送请求后的后续变体。
非 v1 操作会被明确排除并报告。协议形式会被报告,但不作要求,因为没有工具会发送这些请求:OPTIONS(CORS 预检),以及与同一 URL 上的 GET 一同发布的 HEAD(不带响应体的 GET)。如果某个 HEAD 没有对应的 GET,则与其他操作一样必须测试。
检查会使用 OpenAPI 服务继承规则和字面路径优先级,而不是本地路由允许列表。
每条路径都会追加到其生效的服务基础路径;不会移除重复前缀。服务器缺失或为根路径时,带完整前缀的路径也能正常工作。
如果获取请求被拦截、发生重定向、文档无效或缺少操作,检查就会失败;没有夹具回退、凭据要求或跳过分支。
已提交的 OpenAPI 夹具是合成的,根据公开 MCP 路由清单组装而成,绝不会被当作下载的发布版本。离线测试通过只能证明实现覆盖情况具有确定性。每次 published-openapi 运行都会记录发布版本的摘要,以及匹配的操作数量、请求数量和工具数量;结果仅适用于该提交和该发布版本,每个分支头都必须独立通过检查。这些数量是观测结果,绝不是固定阈值或豁免条件。公开检查被拦截或未通过时,不得合并。参数和响应模式仍是独立的契约,并包含已记录的参数例外,以及仅支持 JSON 的聊天功能。
平台规则所在之处
此服务不具备任何特权访问权限。它执行的所有操作都通过 Python SDK 使用的同一精选 /api/v1 接口,并限定为密钥所属账户的资源,且会对该账户进行审计。它需要的任何 /api/v1 未公开功能,都应通过修改平台的路由表来实现,而不是在此处扩大透传范围。
维护者:接口检查
平台会为 /api/v1 允许响应的每条路由设置允许列表,其余路由均返回 404。
test/allowlist.ts 与该表保持一致,测试会断言两件事:每个成功调用的目标路由都在允许列表中,且镜像表中没有未测试的操作(UNIMPLEMENTED 为空)。工具名称和测试条目必须双向匹配,并且每个工具都必须在其回调期间产生可观测的 HTTP 请求。独立的公开检查还会检测尚未更新镜像表的新发布操作。
只要平台仓库恰好被检出在旁边,或者 MANDALA_PLATFORM_REPO 指向其位置,npm run check:surface 就会进一步将镜像表与平台发布的接口清单进行差异比较。该清单由平台根据自身表格生成,并像锁文件一样提交。没有该清单时,脚本会提示跳过并以 0 退出;平台团队以外的用户都会遇到这种情况。无法读取清单时会判定为失败,绝不会拿空内容进行比较。该差异检查由平台自己的 CI 强制执行:它会将此仓库检出到自身仓库旁边,并运行同一个脚本。
check:surface — the mirror matches the platform (N routes, N parameters, from …).
另请参阅
- typescript-sdk — TypeScript SDK 和
mandalaCLI,可用于针对同一 API 编写代码,而不是通过模型驱动它。 - python-sdk — Python SDK 和
mandala-pyCLI,可通过 Python 实现相同功能。
同一操作在各客户端中的名称
各客户端遵循各自语言的命名惯例,因此同一操作可能有多个名称。如果客户端无法执行该操作,单元格中会标明 —。
| 操作 | TypeScript SDK | Python SDK | MCP 工具 | mandala CLI | mandala-py CLI |
|---|---|---|---|---|---|
| 在后台运行命令 | execBackground(),然后调用 execPoll(pid) 和 execKill(pid) | start_exec(),然后调用 background_command(pid).poll() 和 .kill() | 使用 background: true 调用 exec,然后调用 exec_poll 和 exec_kill | computers exec --background,然后调用 computers exec-poll 和 computers exec-kill | — |
| 删除计算机及其快照 | delete({ deleteSnapshots: true, expect }) | delete(delete_snapshots=True, expect=…);purge_snapshots 是已弃用的名称 | 使用 delete_snapshots 和 expect 调用 delete_computer | computers delete --delete-snapshots --expect | — |
| 快照持有量(数量、字节数、指纹) | holdings(),或其别名 snapshotHoldings() | snapshot_holdings() | snapshot_holdings | snapshots holdings | — |
| 用量时间窗口 | usage.read({ from, to }) | usage.read(since=…, until=…),或使用 from_= 和 to= | 使用 from 和 to 调用 get_usage | usage --from … --to … | — |
更改计算机(PATCH) | update({ … }) | rename()、resize()、set_idle_suspend()、set_browser_proxy()、set_egress_proxy() | update_computer | computers rename、resize、idle-suspend、browser-proxy set/clear、egress-proxy set/clear | browser-proxy set/clear、egress-proxy set/clear |
| 等待计算机就绪 | waitForGuest()、waitForDesktop() | wait_for_guest()、wait_for_desktop() | 使用 until: "guest" 调用 wait_for_computer;在 Linux 上也会等待桌面会话就绪 | computers wait --until guest 或 --until desktop | — |
| 获取新截图或缓存截图 | screenshot(width, { fresh, capture }):除非设置 fresh: true,否则返回缓存截图 | screenshot(fresh=…, capture=…):除非设置 fresh=True,否则返回缓存截图 | screenshot:默认获取新截图,除非指定 capture;设置 fresh: false 可使用缓存截图 | computers screenshot --fresh | — |
| 输入操作后的窗口 | 在输入方法中设置 { context: true },例如 click(x, y, [], { context: true }) | 在输入方法中设置 context=True,例如 click(x, y, context=True) | 在输入工具中设置 context: true | — | — |
安全
请私下报告任何与安全相关的问题——请参阅 SECURITY.md,不要创建问题。
可续传文件上传
write_file_chunk、get_file_upload 和 cancel_file_upload 支持 Linux 上最大 2 GiB 的可续传上传。每次工具写入最多包含 256 KiB 解码后的 base64 数据,以限制模型上下文传输量;对于大型本地数据集,请优先使用 SDK 或 CLI。保留一个新生成的、包含 32 个字符的小写十六进制字符的随机 upload_id、目标 path、总 size 和未更改的源文件。按已确认的 offset 发送分块。发生任何错误后,重试前先读取状态。最后一个分块会以原子方式发布文件;如果目标已被占用,overwrite: false 会拒绝写入。
取消操作只会删除暂存内容和完成回执,不会删除已发布的目标文件。暂存内容会占用客户机磁盘空间,直到取消上传为止,并且不会自动过期。除非设置 no_wake,否则这些工具可能会唤醒计算机并产生运行费用;它们包含在 files 工具组中,且不属于只读模式。使用这些工具需要运行 Python 3 的 Linux 系统和已更新的主机。现有 write_file 行为保持不变。
许可
MIT。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。