Source: integration_test_coverage_audit.md.
This document slices the audit into mid-size, independent PRs. Any order works; no PR depends on another. No upfront scaffolding PR is required (helpers each PR needs are small enough to land with the tests).
PR title format follows Conventional Commits (≤70 chars). Branch test/<slug>. Each PR adds tests only — no production code changes — so risk is low and review is fast.
Branch: test/base-protocol-and-error-paths
Scope: Block A + Block E from the audit. Lock down what the server returns at initialize time and what the unhappy paths of tools/call and prompts/get look like.
Test cases (all in tests/integration/suite.ts):
should expose serverInfo, instructions and declared capabilities on initialize— assertclient.getServerVersion().name === SERVER_NAME, version matchespackage.json,getInstructions()non-empty,getServerCapabilities()containstools.listChanged,tasks.{list,cancel,requests.tools.call},resources,prompts,logging.should respond to ping—await client.ping()does not throw.should return JSON-RPC error for tools/call with unknown tool name— expect rejection with code/message indicating not found.should return InvalidParams for prompts/get with unknown name— asserterror.code === ErrorCode.InvalidParams.should return InvalidParams for prompts/get with invalid args— pass args that fail the prompt's AJV validator; assertInvalidParams.
Files touched: tests/integration/suite.ts only.
Why mid-size: 5 tests, single file, no helpers, no transport-specific gating.
Acceptance: all 5 cases pass against stdio, sse, streamable-http; existing suite unchanged.
Estimate: ~45 min.
Branch: test/resources-end-to-end
Scope: Block B. Today only unit tests touch resource_service; nothing verifies the request handlers in server.ts:464,468,472 are wired.
Test cases (all in tests/integration/suite.ts):
should list resources via resources/list—await client.listResources(); assertArray.isArray(resources). Run a second client withuiMode: 'openai'; assert at least one resource URI starts withui://(widget).should list resource templates via resources/templates/list—await client.listResourceTemplates(); assert array shape.should read a known widget resource and return mime+content— pick aui://URI from (1),await client.readResource({ uri }), assertcontents[0].mimeType === RESOURCE_MIME_TYPEand non-empty body.
Files touched: tests/integration/suite.ts only.
Acceptance: all 3 pass across all 3 transports.
Estimate: ~45 min.
Branch: test/notifications-progress-logging
Scope: Block C + Block D. Both need a notification accumulator helper, so they share one PR.
New helper — tests/integration/utils/notifications.ts:
collectNotifications<T>(client, schema)returns a function pair:{ get(): T[], unsubscribe() }. Internally callsclient.setNotificationHandler(schema, ...)and pushes each notification to an array.- Trivial (~20 lines), follows the existing pattern of the
task_waits.tshelper.
Test cases (all in tests/integration/suite.ts):
should emit notifications/progress when progressToken is supplied({ retry: 1 }, gatedrunIf(streamable-http || sse)) — startapify/python-examplewith_meta.progressToken: 'pt-1'viaclient.request({ method: 'tools/call', ..., _meta: { progressToken: 'pt-1' } }, CallToolResultSchema). After completion, assert at least one collectedProgressNotificationSchemanotification hasparams.progressToken === 'pt-1'.should filter notifications/message by setLevel({ retry: 1 }, gatedrunIf(streamable-http)) — collectLoggingMessageNotificationSchema.await client.setLoggingLevel('error'). Trigger a tool call that emits info logs (e.g.search-apify-docs). Assert no collected notification haslevel !== 'error'after the call. ThensetLoggingLevel('debug'), repeat the call, assert at least one info-or-lower notification arrives.
Files touched: tests/integration/suite.ts, new tests/integration/utils/notifications.ts.
Acceptance: 2 tests pass on streamable-http; helper is exported and unused elsewhere is fine.
Estimate: ~1.5 h (notifications are slightly flaky; budget for retry: 1).
Branch: test/streamable-http-isolation
Scope: Block F + Block G. Both target the streamable HTTP transport. Highest-stakes PR — covers multi-tenant correctness.
Test cases:
In tests/integration/actor.server_streamable.test.ts (append a describe('streamable HTTP wire level', ...) block — these are raw fetch calls, not MCP client traffic, so they don't fit the shared suite):
should return 405 on GET /—fetch(httpServerHost + '/')→ status 405,Allow: POST.should return 404 on POST / without session and non-initialize body— POST{ jsonrpc:'2.0', method:'tools/list', id:1 }with noMcp-Session-Id; assert status 404 and JSON body haserror.code === -32000.
In tests/integration/suite.ts (gated runIf(streamable-http)):
3. should isolate tools and tasks across two concurrent sessions — open client a with actors: ['apify/python-example'] and client b with actors: ['apify/rag-web-browser']. Assert listTools(a) contains python-example and not rag-web-browser; vice versa for b. Start a long-running task on a, assert b.experimental.tasks.listTasks() returns zero tasks while a sees one. Close in opposite order.
Files touched: tests/integration/actor.server_streamable.test.ts, tests/integration/suite.ts.
Acceptance: 3 tests pass on streamable-http; SSE/stdio unaffected.
Estimate: ~1.5 h (concurrency teardown care).
Branch: test/meta-apify-token-propagation
Scope: Block H. The hosted server relies on _meta.apifyToken arriving in tools/call params (server.ts:638). Currently no test exercises this path; only the bearer header / env-var paths are covered.
Helper change — tests/helpers.ts:
- Add an
omitToken?: booleanflag toMcpClientOptions. When set:createMcpStdioClientdoes not putAPIFY_TOKENin the spawned env, and the streamable-http variant does not send theAuthorizationheader. ~10 lines.
Test cases (in tests/integration/suite.ts, gated runIf(stdio || streamable-http)):
should accept apifyToken via tools/call _meta and run successfully— create client withomitToken: true. Sendtools/callforapify/python-examplewith_meta.apifyToken = process.env.APIFY_TOKEN. Assert run completes successfully.
Files touched: tests/helpers.ts, tests/integration/suite.ts.
Acceptance: 1 test passes on each gated transport.
Estimate: ~1 h.
Why optional: arguably already exercised end-to-end by the hosted-server tests in apify-mcp-server-internal. Land if we want the public repo to own this guarantee.
Independent in code, but in business value:
- PR 4 first — multi-tenant safety net.
- PR 1 — base protocol lock-in is the cheapest broad coverage win.
- PR 2 — closes the resources gap.
- PR 3 — notifications/progress/logging.
- PR 5 — last; optional.
| PR | Tests | Files | Helpers | Est. effort |
|---|---|---|---|---|
| 1 | 5 | 1 | 0 | 45 min |
| 2 | 3 | 1 | 0 | 45 min |
| 3 | 2 | 2 | 1 new util | 1.5 h |
| 4 | 3 | 2 | 0 | 1.5 h |
| 5 | 1 | 2 | 1 flag | 1 h |
| Total | 14 | — | — | ~5.5 h |
The following are not in any PR:
- Resource subscribe/updated, completions/complete, elicitation, sampling, roots — not declared in
capabilities. - JSON-RPC framing, batch handling — SDK responsibility.
- Pure transport reconnect /
Last-Event-IDresumability — not implemented indev_server.ts. - Cursor pagination on
tools/list— we do not returnnextCursor.
Re-evaluate when the corresponding feature is turned on in the server, not before.