v1 → v2 PHP API Audit

July 7, 2026 · View on GitHub

This is the WS6 audit of logiscape/mcp-sdk-php API-surface changes between the stable 1.x line and v2 — the changes visible at the Composer/PHP API level, as distinct from wire-level protocol changes (which version negotiation handles automatically; see docs/v2-development-plan.md). It is the source material for WS10's user-facing migration guide (docs/migration-v2.md), which will restate the Breaking and Behavioral sections with runnable before/after snippets.

Every entry names the workstream that introduced it; the authoritative narrative for each lives in the corresponding Status section of the development plan.

1. Breaking changes (v1 code may need edits)

#Changev1 behaviorv2 behaviorMigrationWS
B1HTTP client errors are typedJSON-RPC error responses on the HTTP transport threw RuntimeException("Critical MCP error: …")They throw Mcp\Shared\McpError with the JSON-RPC code/data intactCatch McpError (it does not extend RuntimeException); inspect ->getCode()/error data instead of parsing message stringsWS2
B2McpError from tool handlers is a protocol errorAn Mcp\Shared\McpError thrown inside an McpServer tool handler was converted into an isError: true tool resultIt propagates as a JSON-RPC protocol error (matching the long-standing McpServerException behavior)Handlers that want a tool-execution error (isError result) should throw any other exception typeWS2
B3callTool() return type widenedClientSession::callTool(): CallToolResultCallToolResult|CreateTaskResult — a server augmenting the call as a SEP-2663 task returns the task handleCode with strict return-type expectations must branch on instanceof CreateTaskResult (then poll via getTask())WS4
B4Experimental v1 Tasks surface removedtasks/list / tasks/result RPCs, TaskCapability, TaskStatusNotification, and their client conveniencesRemoved without shims (pre-release surface; SEP-2663 redesign). tasks/list and tasks/result answer -32601; the v1 tasks capability slot is gone (Tasks is declared via the SEP-2133 extensions map)Use tools/callCreateTaskResult + getTask()/updateTask()/cancelTask(); results are inlined in the completed tasks/get responseWS4
B5Pre-registered OAuth credentials require issuer bindingClientCredentials carried no issuer; credentials were presented to whatever authorization server discovery producedThe 2026-07-28 Authorization Server Binding rule is the default: unbound pre-registered credentials are rejected before any token request (REASON_UNBOUND_CLIENT_CREDENTIALS)Set ClientCredentials::$issuer to the AS that issued the credentials, or opt into the published-spec legacy behavior with OAuthConfiguration::$allowUnboundClientCredentials = trueWS3
B6Modern HTTP tools/list filters invalid x-mcp-header toolsResults were returned as-servedOn modern (2026-07-28) HTTP sessions, tools whose x-mcp-header annotations violate SEP-2243 are excluded from listTools(), and callTool() on such a tool throws InvalidArgumentException before any wire traffic (spec MUST). Legacy and stdio results are unfilteredFix the tool's annotations server-side; clients can inspect the exclusion reason in the thrown messageWS3
B7ElicitationContext in legacy HTTP prompts/get fails loudlyThe context parameter was injected but silently non-functionalA legacy-era HTTP prompts/get whose callback declares an ElicitationContext fails with BadMethodCallException (-32603) — prompt-side input gathering is modern-only by designGather prompt input via the modern MRTR path, or drop the context parameter from legacy-serving prompt callbacksWS3
B8Stubbed task parameter removed from ElicitationContextform()/url() accepted a stubbed pre-release task parameterRemoved (SEP-2663 settled on inputRequests/inputResponses via tasks/get/tasks/update instead)Delete the argument; in-task input needs no per-call opt-inWS4

Dispositions decided during this audit:

  • HttpServerTransport::start() idempotency (WS2 post-commit): v1 threw RuntimeException('Transport already started') on a second call; v2 silently returns (required by the per-request ephemeral sessions of the stateless lifecycle). Disposition: the old throw is not treated as supported v1 surface — no v1 code plausibly relied on catching it — so this is recorded as a behavioral change (M5), not a break.
  • The v1 experimental Tasks surface and the stubbed elicitation task parameter are removed without deprecation shims per the project's pre-release policy (recorded in the roadmap); they appear as B4/B8 because v1.x did ship the experimental surface.

2. Behavioral changes (same API, different observable behavior)

#ChangeDetailsWS
M1Client::connect() probes modern firstDefault protocolMode: 'auto' sends a server/discover probe before falling back to the legacy initialize. Operators of fragile legacy servers that mishandle unknown pre-initialize requests can pin protocolMode: 'legacy'WS2
M2readTimeout fires against silent peersA configured client readTimeout now also fires when the peer sends nothing at all (previously it only fired between messages). Very slow legacy servers may need a larger explicit readTimeoutWS2
M3SEP-2106 string returns with an outputSchemaAn McpServer tool with a declared outputSchema returning a string now produces JSON-encoded TextContent ("hello" with quotes) plus structuredContent; v1 emitted the raw string and no structuredContent. Wire-visible to legacy clients of such toolsWS1
M4HttpServerSession::toArray() deep-normalizes clientParamsFixes in-memory session stores silently dropping declared client capabilities (e.g. elicitation: {}) between requestsWS3
M5HttpServerTransport::start() is idempotentSecond and later calls return silently instead of throwing (see disposition above)WS2
M6Typed exceptions replace message sniffingTypes/ClientRequest throws Mcp\Shared\UnknownMethodException (subclass of InvalidArgumentException); client read timeouts throw Mcp\Client\Transport\ReadTimeoutException (subclass of RuntimeException). Messages are unchanged, so string-matching v1 code keeps working — but catch the typed forms going forwardWS3
M7Response adaptation clones before mutatingServerSession::adaptResponseForClient() adapts a shallow copy — a handler-cached Result reused across requests (and eras) keeps its own resultType/ttlMs/cacheScopeWS6
M8SEP-2596/2577 runtime deprecation warningsExercising Roots, Sampling, Logging (on a 2026-07-28 session), the deprecated includeContext values (from 2025-11-25), or Dynamic Client Registration emits one PSR-3 warning per feature per session. Wire behavior is unchangedWS6
M9OAuth hardening side effectsSEP-2468 iss validation, SEP-2352 credential/PRM re-checks on 401/403, SEP-837 application_type on registration, SEP-2207 offline_access gating — standards-driven changes inside existing flows; conformant servers are unaffectedWS3

3. Additive surface (new in v2, no migration required)

  • Negotiation: ClientSession::negotiate(mode, probeTimeout, preferredVersion), Client::connect() options protocolMode ('auto' | 'modern' | 'legacy'), probeTimeout, and the protocolVersion HTTP option; ClientSession::discover().
  • Client handlers: onSampling() (also services SEP-2322 MRTR sampling entries), onListRoots() servicing MRTR roots entries.
  • Tasks (SEP-2663): McpServer::enableTasks(), tool(..., taskSupport:), client getTask()/updateTask()/cancelTask(), CreateTaskResult.
  • Apps (SEP-1865): McpServer::ui(...), ExtensionIds::UI, Server::declareExtension().
  • Tool metadata: tool(..., annotations:) — spec ToolAnnotations behavioral hints as an array or typed instance, normalized via the new ToolAnnotations::parse(); auto-stripped for 2024-11-05 clients.
  • Subscriptions (SEP-2575): server-side subscriptions/listen (HTTP streaming + stdio in-session), SubscriptionBusInterface / FileSubscriptionBus, McpServer::subscriptionBus() and publish helpers, SubscriptionsListenResult (graceful end-of-subscription).
  • MRTR (SEP-2322): InputContext (batch input gathering), exchange-backed ElicitationContext/SamplingContext modern paths, RequestStateCodec.
  • Transport/headers: Mcp\Shared\McpHeaders (SEP-2243), JsonRpcMessage::$httpHeaderHints, x-mcp-header mirroring.
  • Session resume: modern-era (2026-07-28) HTTP detach/resume — a new optional $modernWireVersion parameter on Client::resumeHttpSession() and ClientSession::createRestored() (auto-detected from the persisted negotiated version when omitted); legacy resumes are unchanged.
  • Lifecycle: Mcp\Shared\FeatureLifecycle, session warnDeprecatedFeature().
  • Auth: client_credentials grant (private_key_jwt ES256/RS256, basic), SEP-990 cross-app access, ClientCredentials::$issuer, OAuthConfiguration::$allowUnboundClientCredentials, AuthorizationCallbackResult (string-returning handlers keep working).

4. Wire-level changes handled automatically

Version negotiation and per-request era detection make the 2026-07-28 protocol changes transparent to PHP code: the _meta envelope, removed handshake, session-header absence, SEP-2549 stamping/stripping, SEP-2164 error-code selection, and the SEP-2243 request-metadata headers are applied per negotiated revision. A v1-style server or client built on the v2 API keeps interoperating with peers on every revision back to 2024-11-05. These need no migration action and are listed in the development plan's workstream statuses rather than here.