☰
Apify MCP Server 端到端测试套件深度解析:用 mcpc + jq 为 v1 协议面建立行为基线
2026/9/26 2:38:16 网站建设 项目流程

【免费下载链接】apify-mcp-server

The Apify MCP server enables your AI agents to extract data from social media, search engines, maps, e-commerce sites, or any other website using thousands of ready-made scrapers, crawlers, and automation tools available on the Apify Store.

项目地址:https://gitcode.com/gh_mirrors/ac/apify-mcp-server
点击查看免费下载

本篇文章围绕 tests/e2e/README.md 展开,深入剖析 Apify MCP Server 仓库中这套"一次性"端到端(e2e)测试套件的设计动机、运行方式、数据模型与已知陷阱。你将学会:如何用pnpm run test:e2e驱动mcpc客户端对构建产物dist/stdio.js发起真实协议交互,如何用jq过滤器断言响应、捕获中间值实现跨用例数据流转,如何在无本地构建的情况下用E2E_HTTP_ONLY=1把同一张用例表打到远端部署,以及如何规避 mcpc 桥接器挂死、退出码语义漂移、网络受限环境等"假绿色"风险。

一、背景:为什么需要一份"临时"的 v1 协议锁定套件

套件本身定位十分特殊——它不是常驻的测试资产,而是无状态(stateless)迁移期间的一块脚手架。仓库正在推进 stateless 迁移(issue #1128),期间必须证明v1(传统 sessionful)协议表面没有因迁移而回退,这套 e2e 套件就是为此存在的"行为基线(behavior pin)"。

文档在开头明确标注了三条边界,理解它们才不会误用:

  • 临时性:目录内每个文件均由 AI Agent 生成、未经过人工逐行审查,只能当作"待核实的线索"而非事实,不要在其上构建任何东西;
  • 删除契约:当 #1128 关闭时,应删除整个 tests/e2e 目录、vitest.config.ts 中的e2eproject 以及package.json中的test:e2e脚本;
  • 职责边界:tests/integration/suite.ts 才是常驻、人工维护的套件(通过createIntegrationTestsSuite参数化注册registration/tools/actors/apps/tasks/storage/payments各能力用例),不得把覆盖范围迁移到这里,也不得接入 CI。

套件通过mcpc驱动构建产物dist/stdio.js(src/stdio.ts 编译后的 Stdio 入口),并用jq断言响应。全部 85 个用例都有真实断言:对行为确定性的测试 Actor 用精确值校验(固定输入恒等于相同结果、恒写入相同的 5 个 store key),对不确定部分做结构校验(是否有这些 key、形状是否如此),错误路径则做错误消息校验。

二、运行方式与前置条件

pnpm run test:e2e

这条脚本在 package.json 中展开为pnpm run build && vitest run --project e2e,即先构建再跑。原因在于套件驱动的是dist/stdio.js而非源码:protocol_v1.test.ts 中SERVER_ENTRY = resolve('dist/stdio.js'),beforeAll里如果找不到它会直接抛错提示先pnpm run build。

运行前置条件:

  • jq必须在 PATH 上:beforeAll会执行jq --version做硬性校验;
  • mcpc从node_modules/.bin解析:MCPC_BIN = resolve('node_modules/.bin/mcpc'),因此不经过pnpm run时裸跑vitest也能工作(因为 mcpc 是 devDependency,只会在pnpm run下出现在 PATH);
  • APIFY_TOKEN:套件会真实计费运行测试 Actor,必须提供有效的 Apify API Token。

环境变量总览

变量作用
E2E_HTTP_BASEHTTP 配置的基准 URL,例如http://localhost:3001(需同时运行pnpm run dev)。未设置时这些配置被跳过。
E2E_HTTP_ONLY设为1时,所有配置都改走 HTTP 打到E2E_HTTP_BASE——见下文"远程服务器"一节。
E2E_PROBE_TIMEOUT_MS单个探针(probe)的挂钟超时,防止 mcpc 桥接器挂死时整个套件卡住;默认 45000,超时按探针名失败——见"已知不稳定"。
E2E_WORKERS并发的服务器配置数,默认 6——见"并行模型"。

E2E_PROBE_TIMEOUT_MS在 protocol_v1.test.ts 中读取为Number(process.env.E2E_PROBE_TIMEOUT_MS ?? 45_000),每个子进程都挂上该超时并SIGKILL;E2E_WORKERS则映射到 vitest.config.ts 中 e2e project 的maxConcurrency(同时testTimeout: 300_000、hookTimeout: 60_000)。

三、远程服务器模式:同一张用例表打到部署环境

pnpm run test:e2e:remote # https://mcp.apify.com E2E_HTTP_BASE=https://other-deployment/ pnpm run test:e2e:remote # 其他任意部署

test:e2e:remote的展开式为E2E_HTTP_ONLY=1 E2E_HTTP_BASE=${E2E_HTTP_BASE:-https://mcp.apify.com/} vitest run --project e2e。

E2E_HTTP_ONLY=1时,每个 stdio 配置被翻译成等价 URL 而不是拉起dist/stdio.js——例如--tools=actors变成?tools=actors。翻译逻辑在 protocol_v1.test.ts 的httpUrlFor中:只有当配置的 flag 全部落在白名单HTTP_QUERY_FLAGS = new Set(['tools', 'actors', 'ui', 'payment', 'telemetry-enabled'])内时才能转成 URL(--tools=actors→?tools=actors),任何其他 flag 都会变成被静默忽略的查询参数,因此会被拒绝。配置所变化的每个 flag 都同时是查询参数,所以整张用例表都能对部署的服务器生效。本机不会启动任何进程,因此也没有构建步骤。

有三类配置没有 URL 形式,会被跳过并打印名称:no-token、env-tools、env-ui-mode——它们改变的是服务器进程自身的环境变量(如TOOLS=docs、UI_MODE=apps),无法用 URL 表达。

远端部署的已知失败(非本仓库回归)

  • reads dataset items as a resource、rejects a missing dataset resource——issue #1176;
  • starts a detached task——其 pin 的^[0-9a-f]{32}$是 SDK 内存存储的 ID 格式,该存储忽略了 legacy_server.ts 中提议的 ID;而托管存储会遵从它,因此那边的 ID 形如call-tool-<tool>-<uuid>。

特别要注意:它测试的是"已部署的东西",不是你的工作树,并且会把真实 Actor 运行费用记到APIFY_TOKEN对应的账户上。

四、并行模型:约 280 个探针如何在 8 分钟内跑完

完整用例表约 280 个探针,串行约需 8 分钟。protocol_v1.test.ts 通过it.concurrent.each(activeConfigs)让最多 6 个服务器配置并发运行(E2E_WORKERS可调)。每个配置内部,探针保持数组顺序——因为capture 值会在同一配置内向前流动(abort/cancel 与任务生命周期探针依赖前面用例先创建出目标)。

category-all的实况探针链被拆分到errors、actor-details、tasks、abort四个配置中,让这些链可以并发执行;这些重复配置设置excludeFromAll: true,避免重复跑静态探针。

五、套件结构:cases.json 的数据模型与运行器

cases.json 承载全部数据:configs(服务器配置)与cases(探针)。

配置(configs)

一个用例列出它要跑到的配置;用"configs": ["__all__"]表示"适用于所有配置"——静态表面探针都这么做,这样新增配置时不必逐个修改用例。配置按"它们改变了什么"命名:category-actors、retired-preview、telemetry-off等。配置可以设置"excludeFromAll": true退出__all__用例——用于"仅为了并发跑独立探针链而复制另一配置"的场景。

仓库中共有 30 个配置,按变化维度可分为几组:

维度配置示例说明
工具类别category-actors、category-docs、category-runs、category-storage、category-dev、category-all--tools=<类别列表>启用不同工具集;category-all为--tools=actors,docs,runs,storage
空列表tools-empty、actors-empty、both-empty--tools=、--actors=空值
指定 Actoractor-single、actor-multi、actor-via-tools、merge-compat--actors=/--tools=直接挂 Actor(如apify/rag-web-browser、apify/normal-mode-test-actor)
指定工具名tool-by-name--tools=search-actors
退役类别retired-add-actor、retired-experimental、retired-preview、retired-mixed验证已退役工具选择器的兼容行为
UI 模式ui-apps、ui-default、ui-auto、full-apps--ui=三种取值;full-apps组合--ui=apps
代理mcp-proxy--tools=apify/example-mcp-server,验证 MCP 代理
环境no-token、env-tools、env-ui-mode通过env覆盖服务器进程环境(如APIFY_TOKEN: null)
遥测telemetry-off、telemetry-on--telemetry-enabled=false
并行链errors、actor-details、tasks、abort均excludeFromAll: true
HTTPhttp-docs、http-storage、http-ui-apps、http-payment-skyfireurl: "${E2E_HTTP_BASE}?tools=docs"等

用例字段

字段含义
assert用jq -e执行的过滤器。可引用同一配置中更早用例的{{name}}捕获值。
expectError期望错误响应——协议级(非零退出)或工具级(isError: true)。
capture存储为{{name}}供后续用例(args或assert)插值使用的 jq 过滤器。
pollWhileWorkingmcpc 报告任务仍在运行时重试(tasks-result在任务终态前持续报错)。

运行器实现要点

protocol_v1.test.ts 的运行器并不复杂,但有几个值得注意的实现细节:

  • 会话生命周期:每个配置是一个 Vitest test,在tmpdir下创建临时目录写入mcp.json(含服务器入口:stdio 配置写{command: 'node', args: [dist/stdio.js, ...], env},HTTP 配置写{type: 'http', url, headers: {Authorization: 'Bearer ${APIFY_TOKEN}'}}),然后mcpc connect <config>:server <session>,结束时mcpc close <session>并删除临时目录;
  • 进程管理:runProcess用spawn启动 mcpc,因为"mcpc 桥接器可能保持继承的管道打开导致close永不触发",所以依赖exit事件而非close,并在结束后显式销毁 stdout/stderr;
  • 捕获插值:interpolate用{{name}}正则替换,捕获缺失时直接报错提示"是否有更早用例捕获过它";
  • 超时命名失败:runMcpc把超时错误包装为"...produced no output within Nms. A hung bridge, not a server failure — see tests/e2e/README.md",让挂死按探针名失败而非被运行器匿名杀掉;
  • 限流重试:isRateLimited检测Rate limit exceeded(部署环境按 token 限流,多配置并发时普通方法也会触发),对任何用例无差别重试,最多 20 次、间隔 2 秒,不把它当作服务器故障。

六、两类错误与 mcpc 退出码的版本漂移

套件把错误分为两类——工具级错误与协议级错误。关键在于 mcpc 对第一类的退出码跨版本不稳定,所以运行器按 payload 形状分类,而不是按退出码:

类别示例退出码(0.2.x)退出码(0.5.x+)Payload
工具级错误的 dataset id、被禁止的 URL02stdout 上的{content, isError: true}
协议级未知工具、缺少必填参数22stderr 上的{"error": …},stdout 为空

对应实现是 protocol_v1.test.ts 中的三个判定函数:

  • isToolLevelErrorResponse(stdout):stdout 能否解析为携带isError: true且content为数组的合法CallToolResult——即服务器选择返回的合法响应,而非桥接或协议故障;
  • isGenuineFailure(result):非零退出且不是合法工具级错误响应(协议错误、桥接崩溃、或 mcpc 自身 CLI 参数解析拒绝);
  • respondsWithSomeError(result):非零退出或工具级错误响应——expectError用例统一用它判定,而非裸退出码。

这样同一个用例定义就能跨越 mcpc 从 0.2.x 到 0.5.x 的退出码变更。同理,实况依赖可能合理不可达的用例(外部 Actor、外部搜索后端)会把assert写成"接受工具级错误"而非无条件断言成功。

典型错误路径用例(均挂expectError):

  • rejects unknown tool:tools-call does-not-exist,断言.error | test("was not found");
  • rejects missing required argument:get-key-value-store-record缺参,断言.error | test("must have required property");
  • rejects waitSecs above the maximum:get-actor-run runId:="x" waitSecs:=99,断言.error | test("must be <= 45")(对应waitSecs上限 45 的输入校验);
  • rejects resource subscribe:resources-subscribe ui://widget/actor-run.html <file>,断言.error | test("does not support resource subscriptions");
  • rejects a non-Apify resource URL:resources-read https://example.com,断言.error | test("only Apify API URLs")。

七、安全:_mcpc信封中的明文令牌

mcpc 的_mcpc信封以明文携带解析后的APIFY_TOKEN。运行器在任何断言或失败消息看到它之前都会将其剥离——stripMcpcEnvelope先执行jq 'if type == "object" then del(._mcpc) else . end',非 JSON 输出(如 mcpc 崩溃)则原样处理。同时,断言和失败消息只面向剥离后的 payload:协议错误写 stderr、工具级错误写 stdout,所以取stdout.trim() || stderr作为待断言 payload。

八、已知不稳定:mcpc 桥接器可能无限挂死

这是文档特别强调、在信任任何绿色结果前必读的部分:

mcpc 在tools/call或prompts/get的协议错误上可能无限挂起,两条流都没有输出、永不退出。已在 0.2.6 和 0.5.0 上可复现(在清空~/.mcpc、杀掉所有桥接后的全新会话中)。0.6.0 未重测(它把桥接切到了 MCP TypeScript SDK v2)——假设仍然复现。

同一会话里ping、tools-list、tasks-get、resources-read以及返回isError: true的工具调用都在 1 秒内应答,说明服务器本身正常——是桥接器没有转发错误。已排除的原因包括:磁盘空间、负载、内存、服务器 MCP SDK 版本、mcpc 版本、错误消息中的工具数量、陈旧会话、陈旧状态目录、孤儿进程、依赖漂移(对全新 lockfile 和全新构建重测过)。

受影响的探针是协议错误类:

  • rejects unknown tool
  • rejects missing required argument
  • rejects unknown prompt
  • rejects waitSecs above the maximum

开发早期全套 281/281 四次通过、这些探针全绿,说明该行为是环境或状态依赖的,而非永久性缺陷。E2E_PROBE_TIMEOUT_MS(默认 45000)为每个探针设上界,挂死时按名失败——"produced no output within Nms... a hung bridge, not a server failure"——而不是拖垮整轮运行或被误读为服务器回归。若这些探针挂起,先在全新容器里重跑,再对服务器下结论。

九、mcpc 版本演进记录:从拒绝到采纳

@apify/mcpc当前为^0.6.0(见 package.json devDependencies)。它曾被评估后拒绝(退出码语义变更),又在运行器不再依赖退出码分类后被采纳。0.2.x 到 0.5.x 的实际变化及对策:

  1. 工具级isError: true的退出码从 0 变成 2——与真实协议错误的退出码完全相同。对策是结构性修复(isToolLevelErrorResponse检查 payload 形状而非退出码),而不是给每个碰巧在某些环境下返回isError: true的用例加expectError: true。这种"打地鼠"方案一开始试过,漏掉了两个用例(searches the docs、searches the docs with paging——两者都在网络受限沙箱中因search-apify-docs的 Algolia 后端不可达而失败),之后才被结构性修复取代,它们的assert现在显式接受两种结果。
  2. resources-subscribe新增必填<file>参数——rejects resource subscribe现在传入该参数,让调用真正到达服务器而非死在 mcpc 自己的 CLI 解析上;服务器正确答复 "Server does not support resource subscriptions (no resources.subscribe capability)",用例直接对此断言。
  3. 没有修复上面记录的 mcpc 挂死问题——在 0.5.0 上可复现。

0.6.0 引入了 MCP2026-07-28支持,而这正是本套件不想要的:mcpc 现在会用server/discover探测每个服务器,只在失败时回退到2025-11-25。对本地 stdio 服务器,回退仍然落在2025-11-25(用mcpc @stdio验证过——MCP: version 2025-11-25 / stdio (stateful)),所以 pin 仍然测量的是 v1。它只对test:e2e:remote构成风险:如果部署服务器协商到2026-07-28,--detach与tasks-*探针会直接失败(mcpc 尚不支持新 tasks 扩展)、logging-set-level返回错误。真发生时可加--protocol-version 2025-11-25显式把时代钉在旧协议。另两个 0.6.0 变更在此套件路径上但无害:对不支持任务的服务器--detach现在直接失败而非静默同步运行工具;logging-set-level已弃用(在2025-11-25上仍可用)。

由于升级后尚未重跑(套件计费真实 Actor 运行、需要APIFY_TOKEN),文档明确要求:信任它之前请手动跑pnpm run test:e2e。

十、套件覆盖盲区:这些 v1 表面没有被 pin

mcpc 是 shell 驱动的请求/响应客户端,因此以下 v1 表面超出其能力范围、不由本套件锁定,发布前需用其他方式验证:

未覆盖项原因归属
notifications/progressmcpc 不暴露服务器通知tests/integration/suite.ts
notifications/message过滤logging-set-level能往返,但投递的日志不可见tests/integration/suite.ts
tools/call上的_meta.apifyTokenmcpc 不发送自定义_metatests/integration/suite.ts
并发会话隔离每个配置一个会话,顺序开关手工验证;需要两个活跃会话
HTTP 线缆层(GET /SSE 流、无会话POST /400、DELETE /)不是 MCP 流量tests/integration/actor.server_streamable.test.ts
notifications/tools/list_changed服务器从不主动发起,只能转发代理的 Actor-MCP 服务器的不适用

HTTP 配置 pin 的是每会话的查询参数解析(?tools=、?ui=、?payment=),不是并发隔离。

十一、环境敏感性:网络受限时"绿"的含义

部分探针依赖出站网络可达性,受限网络上会返回isError。它们的assert接受快乐路径或该特定失败,所以用例仍然通过——但在受限网络上它只证明"失败模式符合预期",并没有锻炼真实行为:

  • searches Actors、searches the docs、searches Actors with an offset、searches the docs with paging——Algolia(Actor Store 搜索)或文档搜索后端可能不可达;对应断言写成if .isError then (.content[0].text | test("allowlist"; "i")) else ... end;
  • calls a tool through the MCP proxy——依赖apify/example-mcp-server可达。注意--tools=apify/example-mcp-server(mcp-proxy配置)在 Actor 不可达时合法地加载零个工具,因为加载 MCP-server Actor 意味着连接它并枚举其工具;
  • calls the search-actors widget——商店索引无匹配时返回count: 0,该退出不携带 widget 元数据(issue #1177),所以只有搜索有结果时才要求_meta.ui。

fetches Actor details, rating and metadata断言的是响应形状而非数值:rating和modifiedAt是账户数据,非生产环境不携带,所以只断言output: {rating, metadata}返回actorInfo段且无其他内容。

no-token配置依赖~/.apify/auth.json不存在:src/stdio.ts 的getTokenFromAuthFile会回退读取该文件,所以已登录apifyCLI 的开发者会被静默注入 token,配置就不再测试它声称的内容。它还必须选择--tools=docs:带鉴权要求的工具会让服务器process.exit(1),就没有可探测的会话了。

十二、从用例表看 v1 协议面的完整轮廓

最后,通过 cases.json 的 85 个用例,可以勾勒出这套套件实际 pin 住的 v1 协议能力面:

静态表面(__all__覆盖每个配置):server info(serverInfo.name == "apify-mcp-server"且 capabilities 含tools/tasks/resources/prompts/logging)、ping、tools list --full(每个工具含name、description、inputSchema)、prompts list(当前为空数组)、resources list、resources templates list(长度为 5,每个含uriTemplate与name)、logging set level debug。

Actor 运行与存储全链路(category-all的 capture 链):call-actor(同步等待成功、waitSecs:=0返回READY、maxItems上限、callOptions内存/超时)→ 捕获runId/datasetId/kvStoreId→get-actor-run、get-actor-run-log(含lines:=5截断)、get-actor-run-list(含状态过滤)→ dataset 系列(items 精确断言sum == 3 && isSumPrime == true、flatten、fields投影、omit、分页降序、clean、schema、list)→ key-value store 系列(keys 断言count == 5且排序后为["COVER","INPUT","LOG","RESULT","STATS"]、INPUT 记录、LOG 的text/plain、COVER 的image/png)→ 资源读取(dataset items、KV record、run、run log、store keys 的resources-read)。

Actor 详情(actor-details链):output各组合只返回请求的段(inputSchemaonly、descriptiononly、statsonly、pricingonly、readmeonly、mcpToolsonly——对非 MCP 服务器返回固定提示文本、组合段排除未请求字段)。

任务生命周期(tasks链):--detach启动(pin^[0-9a-f]{32}$)→tasks-list找到它 →tasks-get→ 第二个--detach→tasks-cancel断言cancelled→tasks-result带pollWhileWorking轮询到终态;errors配置另有rejects an unknown task。

中止(abort链):waitSecs:=0启动后捕获runId,abort-actor-run断言ABORTING或ABORTED。

UI 模式(full-apps链):widget 资源(ui://widget/search-actors.html、ui://widget/actor-run.html的mimeType == "text/html;profile=mcp-app"且内容超 1000 字符)、widget 工具(search-actors-widget、fetch-actor-details-widget带_meta.ui)。

其他:category-dev的report-problem精确断言响应文案;no-token验证无 token 时search-apify-docs存在而call-actor不存在。

结语

这套 e2e 套件虽然被刻意标注为"临时、AI 生成、不接 CI",但它演示了一种成本低而覆盖面广的协议回归锁定方法:以mcpc为黑盒客户端、jq为断言语言、JSON 用例表为数据驱动核心,在 8 分钟内用约 280 个真实探针把 v1 协议面钉死。对任何正在做大规模迁移的 MCP 服务器项目,其"按 payload 形状而非退出码分类错误""环境敏感断言二选一""挂死按名失败"等设计,都是可以直接借鉴的工程实践。迁移结束、#1128 关闭后,记得按文档契约删除 tests/e2e 目录与相关配置,把守护职责交还给常驻的 tests/integration/suite.ts。

【免费下载链接】apify-mcp-server

The Apify MCP server enables your AI agents to extract data from social media, search engines, maps, e-commerce sites, or any other website using thousands of ready-made scrapers, crawlers, and automation tools available on the Apify Store.

项目地址:https://gitcode.com/gh_mirrors/ac/apify-mcp-server
点击查看免费下载
上一篇:如何标准化navi多人开发环境:团队协作的终极指南
下一篇:如何实现多视频同步播放:GridPlayer让你的观影体验全面升级

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询