Skip to content

feat(mcp-ts)!: swap to @modelcontextprotocol/client 2.0 - #4093

Open
poshinchen wants to merge 1 commit into
strands-agents:mainfrom
poshinchen:feat/mcp-v2-ts-swap
Open

poshinchen wants to merge 1 commit into
strands-agents:mainfrom
poshinchen:feat/mcp-v2-ts-swap

Conversation

@poshinchen

@poshinchen poshinchen commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Description

MCP revision 2026-07-28 replaces the initialize handshake, sessions, and server-initiated requests. The legacy @modelcontextprotocol/sdk package supports protocol versions only up to 2025-11-25, and support for the new revision will not be added to it. This PR swaps the MCP internals to @modelcontextprotocol/client 2.0, which negotiates the revision on each connect: probe server/discover, fall back to legacy initialize. Servers on both revisions work from one code path. No compat layer is needed on our side, unlike Python's _compat; the official client handles both protocol versions internally.

Part of #1659.

The two design decisions in this PR

Transports: no runtime adapter. The 2.0 Transport interface only adds optional members over the legacy one, so transport instances built from the legacy package stay structurally assignable. McpTransport remains a type-level widening, and user-supplied legacy transports keep working unwrapped. The MCP integration suite drives the new client through legacy stdio and streamable HTTP transport instances to prove it.

Tasks: temporarily disabled, pending the SEP-2663 rebuild. The 2.0 client ships the SEP-2663 wire types but no task runtime, and the legacy experimental tasks API is gone, so this PR has nothing to drive tasksConfig with. tasksConfig is not deprecated and keeps its name and shape. While it is set, the client warns at construction and callTool throws with a pointer to #1659. listTools still works, so loadServers configs keep connecting. Throwing a clear error was chosen over silently changing what the option means. PR #3661 restores task execution on both server generations by rebuilding it on the finalized SEP-2663 tasks extension, matching strands-py, which supports tasks on both mcp lines since #4125. The intent is to land #3661 in the same release as this PR so tasksConfig users never see the throw. Until it lands, tool calls run under the MCP client's default 60-second inactivity timeout, the same default that applied on main without tasksConfig. The task-only integration suites are skipped citing #1659.

Public API Changes

Temporarily disabled: tasksConfig (and the DEFAULT_TTL / DEFAULT_POLL_TIMEOUT statics). The type and option remain exported, and tasksConfig is not deprecated. Setting it makes callTool throw until the tasks rebuild lands (#1659, restored by #3661).

Changed (compatible): ElicitationContext is now the 2.0 ClientContext plus the existing top-level signal field. The 2.0 client surfaces the abort signal at context.mcpReq.signal, and the SDK mirrors it to context.signal so existing callbacks keep working. context.signal is deprecated in favor of context.mcpReq.signal:

// Keeps working (deprecated)
const callback: ElicitationCallback = async (context, params) => {
  context.signal.throwIfAborted()
  return { action: 'accept', content: await promptUser(params) }
}

// Preferred
const callback: ElicitationCallback = async (context, params) => {
  context.mcpReq.signal.throwIfAborted()
  return { action: 'accept', content: await promptUser(params) }
}

Breaking Changes

  1. Peer dependency: @modelcontextprotocol/sdk@modelcontextprotocol/client. Consumers who install the peer manually must switch packages.
  2. tasksConfig temporarily performs no task-augmented execution. callTool throws while it is set. Task execution returns with the SEP-2663 tasks rebuild in feat(mcp/ts): add SEP-2663 task support #3661 (tracked in [FEATURE] MCP Specification 2026-07-28 adoption (parent tracker) #1659), intended to land in the same release. While tasksConfig is disabled, its ttl/pollTimeout values also stop shaping request timeouts, so affected tool calls fall back to the MCP client's default 60-second inactivity timeout.
  3. Error text: rejected tool calls lose the legacy MCP error <code>: message prefix. ProtocolError.message carries the plain message.

Related Issues

Part of #1659

Documentation PR

The MCP docs under site/ still show legacy-sdk transport imports. Those imports keep working against this client; a docs refresh follows as a separate PR.

Type of Change

Breaking change

Testing

  • MCP integration suites against real in-process servers, including legacy stdio and streamable HTTP client transports driving the 2.0 client end to end (agent conversations, filtering, prefixing, error handling, elicitation)

  • Manual end-to-end scripts against @modelcontextprotocol/server 2.0 over InMemoryTransport: listTools/callTool round trips, and tasksConfig throws with the tracker link

  • Unit tests pin the elicitation compatibility contract: the callback receives the abort signal at both context.signal and context.mcpReq.signal

  • I ran hatch run prepare

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@poshinchen
poshinchen requested a review from a team as a code owner August 31, 2026 23:16
@poshinchen

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent review this PR carefully.

@poshinchen
poshinchen requested review from Unshure and pgrayy August 31, 2026 23:24
@poshinchen poshinchen changed the title feat(mcp): swap to @modelcontextprotocol/client 2.0 feat(mcp-ts): swap to @modelcontextprotocol/client 2.0 Aug 31, 2026
@github-actions github-actions Bot added chore Maintenance tasks, dependency updates, CI changes, refactoring with no user-facing impact area-mcp MCP related typescript Pull requests that update typescript code complexity/low Touched functions have low cognitive complexity (<=10) size/m strands-running labels Aug 31, 2026
@@ -221,11 +223,17 @@ export class McpClient {
}
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The notification/request handlers now key on string literals ('notifications/message' here, 'elicitation/create' at L322) instead of the old schema constants. If the 2.0 client types these method names as a plain string, a typo would silently register a handler that never fires — the compiler wouldn't catch it, and the log/elicitation path would go quietly dead.

Suggestion: Confirm @modelcontextprotocol/client provides typed overloads keyed on these literal method names (so a typo is a compile error). If it doesn't, consider referencing an exported constant/schema to keep this checked. Non-blocking if the overloads are typed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed — a typo is a compile error. The 2.0 client types both handler registrations as overloads keyed on literal method maps, so a misspelling fails to compile:

src/mcp/client.ts(226,41): error TS2345: Argument of type '"notifications/mesage"'
  is not assignable to parameter of type 'NotificationMethod'.

A valid but wrong method is caught too, because the handler's parameter type is keyed to the method — swapping in 'notifications/cancelled' errors on the missing level/data fields. Since type-check is a pre-commit and CI gate, the string-literal form is no weaker than the schema form here.

@github-actions

Copy link
Copy Markdown
Contributor

API review label: This PR is labeled chore ("no user-facing impact"), but the description itself lists Public API Changes that are observable and at least one is breaking:

  • ElicitationContext now aliases ClientContext, moving the abort signal from context.signal to context.mcpReq.signal. Any existing elicitation callback reading context.signal breaks at runtime with no compile error surfaced to the SDK's own consumers.
  • Rejected tool-call error text drops the legacy MCP error <code>: prefix — anything matching on that string changes behavior.
  • The peer dependency changes from @modelcontextprotocol/sdk to @modelcontextprotocol/client.

Suggestion: These change an existing public API contract, so per team/API_BAR_RAISING.md this should carry api/needs-review (not just chore) and get a designated API reviewer. The breaking ElicitationContext signal move in particular warrants a call-out in release notes / a migration snippet for consumers, beyond the PR body.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Comment

Clean, well-scoped swap to @modelcontextprotocol/client 2.0 — the migration is complete (no stray legacy imports outside an intentional backward-compat comment), tests were updated to the new call shapes with whole-object assertions, and the two design calls (transport structural assignability, tasks→timeout mapping) are clearly reasoned in both the description and code comments. Main gaps are documentation drift and the API-review classification.

Review Categories
  • API governance: Labeled chore despite observable/breaking public API changes (ElicitationContext signal relocation, error-prefix removal, peer-dep swap) — should route through api/needs-review and get a consumer migration note.
  • Documentation accuracy: Several customer-facing TSDoc blocks (TasksConfig fields, default constants) still describe the removed polling model and contradict the updated interface-level docs.
  • Type safety (minor): Handlers now key on string-literal method names; worth confirming the client types these as literals rather than plain string.

Nice work keeping the public McpClient surface stable through a substantial dependency change.

@strandly-the-agent strandly-the-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: requesting changes — the swap itself is solid and the legacy-transport compat claim held up under a live repro, but tasksConfig breaks in two independently reproduced ways for users who upgrade without opting into anything, and four doc comments promise the opposite behavior.

Endorsing the earlier API-label comment: this needs api/needs-review and a Breaking Changes section — and note the changelog tooling marks a release breaking only via ! in the title or a breaking change label, so as titled/labelled today this publishes as non-breaking.

Inline: 2 🔴 (task-required tools become uncallable · ttl silently becomes a hard cap) and 4 🟡. Everything non-blocking is collapsed below.

Evidence — what was verified
✅ reviewed branch feat/mcp-v2-ts-swap @ 4d3c211 (base main @ 1adf5f1)
✅ build · type-check · lint · format:check — all clean
✅ npm run test → 4505/4507; the 2 failures are src/telemetry/__tests__/config.test.ts picking up this
   sandbox's OTEL_RESOURCE_ATTRIBUTES env (6/6 pass with it unset) — unrelated to this diff
✅ focused MCP + tools suites → 277/277
⚪ integ suites not run here (need AWS credentials)
✅ legacy-transport compat survived a live repro: legacy 1.30 server + legacy in-memory transport →
   probe server/discover → -32601 → legacy initialize fallback → connect + listTools + callTool OK
   (one extra round trip per connect)
✅ elicitation round-trip on a 2.0 stack (callback receives request.params; context.mcpReq.signal is a
   real AbortSignal; 'signal' in context === false) and abort-mid-call (~200 ms, with and without
   tasksConfig) both behave as documented
🔴 repro: task-required tool — main returns the result; this branch throws ProtocolError -32602
🔴 repro: ttl 1200 ms + progress every 300 ms → timed out at 1203 ms; identical call with a progress
   handler registered → resolved at 3011 ms
🔴 measured: stdio server spawns per connect — legacy mode 1, auto mode 2
✅ proposed fixes verified: onprogress → tsc clean (3 assertions to update); elicitation shim → tsc
   clean (5 fixtures to update); warn-test addition passes and catches the mutation

Process note: seven independent review passes ran (correctness, scope/alignment, API design, adversarial repro, test quality, model-facing text, docs accuracy); the API-design and adversarial passes were re-run on a smaller model tier after infrastructure timeouts on the first attempt.

Questions (3 blocking · 4 non-blocking)

Blocking

  • Is the wholesale peer swap the ratified direction? #3708 (maintainer-approved) took the opposite posture ("the dependency pin stays <2 and nothing changes for current installs"), Python still pins mcp<2.0.0 behind a compat shim, and #4038 asked exactly this question — dual support vs a breaking swap, offering to write a design proposal — with no maintainer answer yet. I found no decision record in team/DECISIONS.md, team/designs/, #1659 or its sub-issues.
  • Is dropping TS task execution outright the agreed shape of "unwinding both tasks implementations"? Python keeps real task execution behind an identically-named TasksConfig, so after this PR the same name means different things in the two SDKs. Should tasksConfig be deprecated/no-op for the gap rather than repurposed?
  • May vendor types appear directly in the Strands public surface (see the ElicitationContext comment)? Worth a decision record either way — this is the second break of that exact field.

Non-blocking

  • Would client-level requestTimeouts and/or the existing per-call options be a cleaner home for these knobs than repurposing tasksConfig?
  • Is McpTransport still needed now that 2.0's Transport already declares sessionId?: string | undefined — and are the as McpTransport / as Transport casts in this diff intentional?
  • Is tools-changed auto-refresh still functional against a 2026-07-28 stateless server (the client is 2.0 but the listChanged options are legacy-shaped), and is any gap tracked?
  • What drives the new @modelcontextprotocol/server devDependency versus reusing the legacy fixture server the integ fixtures deliberately keep?
Reading order

Start with strands-ts/package.json — the peer moving from sdk to client is the whole user-visible contract, and most findings trace back to it. Then src/mcp/client.ts, the bulk of the change: the import collapse at the top, the constructor (negotiation mode and the new warning), the elicitation handler registration, and last the callTool tasks branch with its doc comments — that branch is where both blockers live. src/types/elicitation.ts is three lines but is the headline breaking change; read it next, then src/tools/mcp-tool.ts for the error-class rename and its model-facing text, then src/mcp/config.node.ts (import paths only). Tests after that: client.test.ts (the new mock strategy, then the tasksConfig and elicitation blocks), config.test.node.ts, and client-annotations.test.ts — the one place a real 2.0 server is driven. Finish with the two describe.skips in test/integ/mcp/mcp-tasks.test.node.ts, which read best alongside the first blocker.

Appendix — non-blocking (7)
  • TasksConfig field docs still describe polling ("time-to-live … for task polling", "wait for task completion during polling") when nothing polls — folds into the doc pass of the second blocker; the earlier bot comment noted this too.
  • ⚪ The McpTransport doc rationale is stale (it describes the legacy base type; 2.0 already declares sessionId?: string | undefined), and "without requiring explicit casts" is contradicted by this diff's own as McpTransport / as Transport casts.
  • ⚪ The new tasksConfig warning isn't actionable: no tracking reference, no "what to do instead", and no @experimental/@deprecated JSDoc tag despite in-repo precedent for both — IDEs and coding agents see nothing.
  • ⚪ The auto-negotiation probe sets no probe timeout (inherits the 60 s default): a non-conforming server that silently swallows unknown methods stalls connect() for 60 s and then hard-fails. Contrived — spec-conforming servers reply -32601.
  • ⚪ A -32042 with an empty/missing elicitations payload now loses the "URL elicitation required" semantic in the model-facing text (bare Error: Auth required). Degenerate servers only. (Otherwise the error-text change is a net improvement for the model — the numeric code never reached it as a field anyway.)
  • client.test.ts:1091,1119 build the expected text from the same String(error) expression the implementation uses, so they can't detect rendering drift — the exact thing this PR changes. The sibling tests pin literals; these two should too.
  • Pre-existing, filed #4095: connect(true) cannot reconnect a StreamableHTTP client on main or this branch (already started!), with a latent skip-handshake hazard noted for whoever fixes it.

Suggested follow-ups once direction is settled: a user-like install CI job (peer-only — would have caught the README/examples imports), a "restore task-augmented invocation + re-enable the mcp-tasks integ suites" tracker cited from the skips, and an ./mcp subpath export as the path to an optional peer.

Comment thread strands-ts/src/mcp/client.ts Outdated
Comment thread strands-ts/src/mcp/client.ts Outdated
Comment thread strands-ts/src/types/elicitation.ts Outdated
...(this._elicitationCallback ? { capabilities: { elicitation: { form: {}, url: {} } } } : undefined),
// Probe for protocol revision 2026-07-28 and fall back to the legacy initialize
// handshake, mirroring the Python SDK's negotiate_auto posture.
versionNegotiation: { mode: 'auto' },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Auto negotiation is unconditional, and it starts every stdio MCP server process twice per connect.

Measured with the repo's own fixture server, only this option differing:

legacy mode: 1 spawn
auto mode:   2 spawns

The probe runs on a throwaway sibling process, so any stdio server with startup side effects — locks, state writes, registration, billed cold starts — does them twice. Every stdio user gets this with no opt-out (the dependency's own default is 'legacy'), and on HTTP the probe adds hard connect() failure modes where the previous code connected.

Suggestion: expose the mode as an McpClientOptions field defaulting to 'auto' so probe-hostile setups have an escape hatch, and mention the double start in the PR body. Cost: one new public option, which is itself API surface.

Comment thread strands-ts/package.json
Comment thread strands-ts/src/mcp/__tests__/client.test.ts Outdated
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Re-review (updated commit) — Assessment: Comment (was Request Changes)

The two blockers from the prior review are resolved, and cleanly:

  • Task-required tools: callTool now throws up front when tasksConfig is set, TasksConfig is @deprecatedrequestTimeouts, and the integ skips cite [FEATURE] MCP Specification 2026-07-28 adoption (parent tracker) #1659. Explicit removal, not a silent remap. ✅
  • resetTimeoutOnProgress no-op: timeout knobs moved to a dedicated requestTimeouts option and _buildCallOptions registers the onprogress shim so the reset actually fires; a test pins it. ✅
  • Warn test is now bidirectional. ✅

Two smaller items remain open (details on the respective threads):

Remaining
  • Docs/examples install break (higher impact): README.md:219 and examples/mcp/src/index.ts:2-3 still import the now-removed @modelcontextprotocol/sdk peer — a fresh install following our own README fails with ERR_MODULE_NOT_FOUND. config.node.ts already has the correct import targets.
  • Elicitation test coverage: fixtures still pass the pre-2.0 { signal } shape, so the headline context.mcpReq.signal contract change is verified by nothing.

Nice turnaround on the two hard problems — the deprecate-and-throw plus requestTimeouts split is a cleaner shape than the original remap.

@poshinchen

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent review the updated changes

@strandly-the-agent strandly-the-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of 79d37c1 — both blockers are fixed and I re-verified them with live repros; what's left is small and known.

Prior finding Status
🔴 task-required tools uncallable Fixed — deprecate-and-throw; repro re-run (listTools works, callTool throws with the #1659 link)
🔴 resetTimeoutOnProgress inert FixedrequestTimeouts + onprogress shim; full timeout matrix repro'd against a live server
🟡 warn test one-sided / stale elicitation fixtures ½ — warn test now bidirectional ✅; fixtures still pass the pre-2.0 { signal } shape (mcpReq appears nowhere in tests)
🟡 README + examples/mcp install break Still open (thread stands) — 3 import lines + 1 package.json line
🟡 ElicitationContext vendor alias · 🟡 auto-negotiation double stdio spawn Open, acknowledged in the body as pending maintainer input

The rewritten PR body (Breaking Changes section, "Type of Change: Breaking change") resolves my process finding — but the labels haven't caught up: still chore, no api/needs-review, and the changelog tooling only marks a release breaking via ! in the title or a breaking change label, so as labelled this still publishes as non-breaking. With the new requestTimeouts surface added, the api/needs-review label is now doubly warranted.

I'd flip to approve once the README/examples fix and the labels land; the remaining design threads are maintainer calls, not author defects.

Evidence — what was verified on 79d37c1
✅ delta reviewed: 4d3c211 → 79d37c1 (452 diff lines; force-push, same single commit)
✅ build · type-check · lint · format:check — all clean
✅ unit tests → 4508/4508 (run with the sandbox's OTEL_RESOURCE_ATTRIBUTES unset, per yesterday's finding)
✅ repro (tasksConfig): listTools OK with tasksConfig set; callTool threw as documented at ~122 ms with the
   #1659 link; construction warn fired, and only when tasksConfig is set
✅ repro matrix (requestTimeouts, live 2.0 server, tool answering at 3 s, progress every 300 ms):
   · {timeout:1200, resetTimeoutOnProgress:true}          → RESOLVED at 3005 ms (reset works via the shim)
   · {timeout:1200}                                       → timed out at 1201 ms (reset off, as documented)
   · {timeout:1200, reset:true, maxTotalTimeout:2000}     → capped at 2106 ms (total cap reachable now)
   · reset:true + per-call abort at 500 ms                → rejected at 501 ms (signal survives the merge)
✅ the corrected doc comments now match measured behavior (the four false progress-reset claims are gone)

Repro caveat worth recording: my first run of the reset case timed out and briefly looked like the fix was broken — the fault was my harness (wrong notify signature corrupted the progress notifications on the wire). With well-formed notifications the fix works exactly as documented.

New in this delta — minor (2)
  • ⚪ The new loadServers per-server requestTimeouts key (config.node.ts:123) has no test in config.test.node.ts — the sibling tasksConfig plumb line does.
  • ⚪ "Per-call options take precedence on overlap" (client.ts:127): McpCallToolOptions is still { signal? }, so no overlap is actually expressible today — fine as future-proofing, just noting the doc slightly leads the type.
Process note

Per the follow-up-review procedure this delta was verified directly by me (repros + gates) rather than a full pass fan-out, since it responds to my own findings — with one exception: the new public requestTimeouts/McpRequestTimeouts surface warranted a fresh API-design pass, which is still running as I post. If it surfaces anything material I'll add it to this thread; absence of a follow-up comment means it came back clean.

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Re-review (61312dd) — Assessment: Comment (all code-level findings resolved; remaining items are maintainer-direction calls)

Every defect raised across the prior rounds is now closed and verified against the branch:

Resolved
  • 🔴 Task-required toolscallTool throws up front under tasksConfig; @deprecatedrequestTimeouts; listTools still connects; integ skips cite [FEATURE] MCP Specification 2026-07-28 adoption (parent tracker) #1659.
  • 🔴 resetTimeoutOnProgress no-op → dedicated requestTimeouts option + internal onprogress shim so the reset actually fires; test pins it.
  • 🟡 Warn test one-directional → now asserts the negative case.
  • 🟡 README/examples install break → both import @modelcontextprotocol/client and the example's package.json adds the peer; only the intentional comment in client.ts:25 references the old package.
  • 🟡 Elicitation contract untested → fixtures use { mcpReq: { signal } } and assert receivedContext.mcpReq.signal.
  • Docs: a proper Breaking Changes section with migration snippets is now in the PR body.

The only things still open are the three maintainer-direction questions the author explicitly lists (swap-vs-dual-support per #4038, whether vendor types may sit on the public surface, exposing the negotiation mode) — those are governance decisions, not code issues, so I'm leaving the verdict at Comment for a maintainer to make the directional/api/needs-review call rather than auto-approving a breaking change.

Solid, responsive iteration — the deprecate-and-throw + requestTimeouts split turned the riskiest part of this PR into its cleanest.

@strandly-the-agent

Copy link
Copy Markdown
Contributor

The API-design pass on the new requestTimeouts surface finished — shape is sound, two things worth attention (one factual, one design):

🟡 The PR body's Python-parity rationale doesn't match the merged Python code. The body says the throw "mirrors the recorded Python decision … raise a clear error rather than maintain a second implementation" — but strands-py today keeps task-augmented execution working on mcp 2.x: _should_use_task_call_tool_as_task_and_poll_async (mcp_client.py:1464,1468) with _compat.task_support reading execution.task_support on 2.x (_compat.py:103), and no raise for tasks_config anywhere. The throw may still be the right call for TS (the TS 2.0 client genuinely has no tasks runtime, Python's 1.x line still does) — but then the two SDKs deliberately diverge on whether tasksConfig works at all, and the body should say that rather than cite a Python precedent. Worth correcting before merge since reviewers will lean on that rationale.

Design questions (non-blocking) + one nit
  • "Per-call options take precedence on overlap" can't actually fire todayMcpCallToolOptions is still { signal? }, so no timeout key can overlap. Would it be simpler to put timeout/maxTotalTimeout/resetTimeoutOnProgress on McpCallToolOptions and treat requestTimeouts as the client-level default? That makes the documented precedence real, gives Python's per-call read_timeout_seconds a TS sibling, and leaves one options shape instead of two.
  • Name breadth: requestTimeouts applies only to callToollistTools keeps vendor defaults (which is the right behavior; discovery inheriting a 5-minute tool timeout would be wrong). Is toolCallTimeouts the more honest name, or is the broad name intentional headroom?
  • ⚪ The resetTimeoutOnProgress docstring could mention that the SDK registers an internal no-op progress handler to make it work — a user with their own progress expectations needs that from the docstring, not from a source comment.

@poshinchen poshinchen changed the title feat(mcp-ts): swap to @modelcontextprotocol/client 2.0 feat(mcp-ts)!: swap to @modelcontextprotocol/client 2.0 Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Re-review (39c394b) — Assessment: Comment / approve-leaning (last blocking item resolved; only a belt-and-suspenders nit + the open API-direction questions remain)

The breaking-marking gap from the previous round is closed, and the tasksConfig reframe was applied cleanly end to end:

Verified on this commit
  • Title now feat(mcp-ts)!: ... — the ! satisfies the changelog tooling's breaking condition.
  • tasksConfig reframe is consistent: all @deprecated tags are gone from src/mcp/ (grep clean); JSDoc, the construction warning (L257), and the callTool throw (L490–492) all now say "temporarily unavailable while task support is rebuilt on the MCP tasks extension ([FEATURE] MCP Specification 2026-07-28 adoption (parent tracker) #1659)", and the throw message reads clearly ("Unset tasksConfig to call tools now. Use requestTimeouts…"). Doc and code agree.
  • api/needs-review present; the cross-SDK context (strands-py feat(mcp/py): support SEP-2663 tasks #4125 shipped the rebuild; candidate feat(mcp/ts): add SEP-2663 task support #3661 to rebase) is now documented.
  • ✅ Prior fixes still hold: requestTimeouts progress-token shim, client-package imports, mcpReq.signal elicitation fixtures.

Suggestion (non-blocking): the underlying commit subject is still feat(mcp): swap … without !. A squash merge inherits the PR title (which has !), so this is fine on the default path — but adding the breaking change label is a cheap safety net in case the merge isn't squashed or the squash title gets edited.

Handing the three open direction questions (swap-vs-dual-support #4038, vendor types on the public surface, exposing negotiation mode) to the api/needs-review owner — those are the only things left, and they're design calls rather than defects. Nice, disciplined iteration on what was a tricky breaking change.

@github-actions

Copy link
Copy Markdown
Contributor

Rebase check (c664101) — no change to my prior approve-leaning assessment; the rebase is clean.

Verified the rebase-risk areas rather than taking "no functional changes" on faith:

What I checked on the rebased tip
  • prefixWithServerName (main) × requestTimeouts (this PR) coexist — they sit at different levels (prefixWithServerName on McpLoadServersOptions, requestTimeouts per-server on McpServerConfig), both are wired in config.node.ts (L128–132), and the prefix precedence (server-name vs. defaults prefix vs. explicit server.prefix) is covered by dedicated tests (L400–432). This is exactly where a rebase could silently drop one option; both survived.
  • Peer swap is like-for-like@modelcontextprotocol/sdk was a required peer on main, and @modelcontextprotocol/client ^2.0.0 is required too, so no undocumented "now-required peer" beyond Breaking Change ci: update sphinx requirement from <6.0.0,>=5.0.0 to >=5.0.0,<9.0.0 #1; ^2.0.0 keeps a <3.0.0 upper bound.
  • Node ≥22 engines adopted from main (feat!: require Node.js 22+, drop Node 20 support #4145); legacy @modelcontextprotocol/sdk ^1.25.2 retained as devDep for the legacy-transport compat tests.

Only remaining items are still the three API-direction questions for the api/needs-review owner — not defects. Clean rebase; nothing new to fix from my side.

@poshinchen

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent review the changes and descriptions

@strandly-the-agent strandly-the-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at c664101 — every code-level finding from my two earlier rounds is fixed and re-verified on the rebased tip, and the description now matches the code. What remains is the api/needs-review sign-off on two design threads I left open (vendor type as ElicitationContext; unconditional auto-negotiation), which are maintainer calls, not defects.

Prior finding Status on c664101
🔴 task-required tools uncallable · 🔴 resetTimeoutOnProgress inert ✅ still fixed — live repros re-run on the rebased tip (throw with the new wording; reset-on survives to 3 s, reset-off dies at 1.2 s, maxTotalTimeout caps at 2 s)
🟡 README + examples/mcp install break ✅ fixed — both import @modelcontextprotocol/client, example declares the peer
🟡 elicitation fixtures on pre-2.0 shape ✅ fixed — { mcpReq: { signal } } and an assertion on receivedContext.mcpReq.signal
🟡 Python-parity claim in the body ✅ fixed — now cites #4125 (verified merged 2026-09-04) instead of a non-existent "raise" decision
🟡 ElicitationContext vendor alias · 🟡 stdio double-spawn under auto negotiation Open by design → api/needs-review owner

Description nits, none blocking: the body should mention the negotiation probe's side effect (stdio servers start twice per connect) since it now has a place for behavior changes; "per-call options taking precedence" still can't fire because McpCallToolOptions is only { signal? }; the Testing section lost the npm run check line (I ran the TS gates — all green); and chore is still on a feat! PR.

Evidence — verified on c664101 (rebased onto 7462908)
✅ interdiff of the PR's own change, 79d37c1 → c664101: 87 lines, all accounted for — README/example imports,
   example package.json peer, elicitation fixtures + mcpReq.signal assertion, new loadServers requestTimeouts
   test, @deprecated tags removed (0 left in src/mcp) → "temporarily unavailable" wording in JSDoc / warn /
   throw, resetTimeoutOnProgress docstring now documents the internal handler. No behavior change; the rest
   of the diff-vs-main delta is the rebase (prefixWithServerName + requestTimeouts coexist in config.node.ts).
✅ build · type-check · lint · format:check — clean
✅ unit tests → 4611/4611 (165 files)
✅ repros (live 2.0 server, InMemoryTransport): tasksConfig → listTools OK, callTool throws with #1659;
   requestTimeouts {1200, reset:true} → resolved 3006 ms; {1200} → timed out 1202 ms;
   {1200, reset:true, maxTotal:2000} → capped 2106 ms
✅ description claims: #4125 merged (Python tasks on both mcp lines) — true; #3661 exists as an open draft
   with an unfilled template ("candidate implementation" is fair, "draft" would be more precise);
   tasksConfig "not deprecated" — true; Breaking Changes 1–4 match the code; title carries `!`.
⚪ integ suites not run here (need AWS credentials)

Process note: the delta only repairs earlier findings plus wording, so I verified it directly rather than fanning out passes. Threads I opened that the author resolved were re-checked against the tip before I counted them fixed.

@github-actions

Copy link
Copy Markdown
Contributor

Re-verify (fd2e762) — assessment unchanged (Comment / approve-leaning). This tip is functionally identical to the last reviewed one (re-rebase + body housekeeping: rebase note dropped, hatch run prepare now ticked). Spot-checked that nothing regressed: no @deprecated in src/mcp/, tasksConfig warn/throw + requestTimeouts wiring intact (client.ts), loadServers prefixWithServerName×requestTimeouts coexistence intact (config.node.ts L128–132), and the peer dep is @modelcontextprotocol/client ^2.0.0 with sdk removed. No new defects — only the three API-direction questions remain for the api/needs-review owner.

Comment thread strands-ts/src/types/elicitation.ts Outdated
Comment thread strands-ts/src/mcp/config.ts Outdated
Comment thread strands-ts/src/mcp/client.ts
@github-actions

Copy link
Copy Markdown
Contributor

Re-review (e4c101f) — ⚠️ Assessment changed to Request Changes. This force-push (committer date Sep 18) reverted the requestTimeouts work that was present on the tips I reviewed on Sep 10–11 (c664101/fd2e762). The code and the PR description are now materially out of sync.

🔴 Critical — requestTimeouts is gone from the code but is still the headline of the description

  • grep -rn requestTimeouts src/ test/0 matches. It's not on McpClientOptions, not on config.ts's McpServerConfig (the per-server loadServers key is gone), not wired in config.node.ts, and has no tests.
  • McpCallToolOptions (client.ts:58) now exposes only { signal?: AbortSignal } — there is no client-level or per-call timeout anywhere. callTool just forwards options.
  • Consequence: Breaking Change ci: update mypy requirement from <1.0.0,>=0.981 to >=0.981,<2.0.0 #2's migration path points to an API that doesn't exist. Consumers losing the tasksConfig: { ttl, pollTimeout } timeout behavior have nothing to migrate to except wiring their own AbortSignal. The Testing section also claims requestTimeouts.timeout: 300 was verified end-to-end, which can't be true on this tip.
  • Action: either restore requestTimeouts (if it was dropped unintentionally in the rebase), or, if it was intentionally deferred to feat(mcp/ts): add SEP-2663 task support #3661, rewrite the "Added: requestTimeouts" section, Breaking Change ci: update mypy requirement from <1.0.0,>=0.981 to >=0.981,<2.0.0 #2, and the Testing bullet so the description matches the code and gives consumers a real migration story.

🟡 Important — DEFAULT_TTL / DEFAULT_POLL_TIMEOUT status mismatch
The description says these statics are "temporarily disabled," but both are still exported public statics (client.ts:154, client.ts:161), only doc-annotated "Unused." Align the description with the code (they're retained, not removed).

🟢 Suggestion (a good change the description undersells) — ElicitationContext is now soft-deprecated, not hard-breaking
client.ts:327–329 mirrors signal onto the callback context, and elicitation.ts:11 marks .signal @deprecated instead of removing it. So Breaking Change #3 is effectively non-breaking — existing context.signal callbacks keep working. That's a strict improvement worth advertising rather than listing as a hard break requiring migration.

Net: the swap/transport/tasks core still looks solid, but the description no longer describes this commit. Worth confirming whether the requestTimeouts drop was intended before this goes further.

@poshinchen

Copy link
Copy Markdown
Contributor Author

@strandly-the-agent review the updated changes.

@strandly-the-agent strandly-the-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of e4c101f — code is clean and the description now matches it (the bot's "out of sync" flag predates the body edit). The delta since my approval does two things, both verified: requestTimeouts is removed end to end, and ElicitationContext gains a compat signal field so the breaking change becomes a deprecation. One description sentence still misleads; the rest is the maintainer's release-plan call.

Delta item Verified
requestTimeouts removed (type, option, loadServers key, wiring, tests) ✅ zero references left in src/ or test/; callTool forwards per-call options as on main; body no longer advertises it
ElicitationContext = ClientContext & { signal } shim (client.ts:329) ✅ lossless at runtime — the vendor context is a plain object with no prototype members or getters, mcpReq is copied by reference with notify/send intact, signal === mcpReq.signal, elicitation round-trip completes; test pins both paths (client.test.ts:587-603)
Breaking Changes list ElicitationContext correctly moved out of it; items 1–3 match the code
My open thread: unconditional auto-negotiation (stdio double spawn) still open → api/needs-review owner

🟡 Description, Breaking Change #2 — "while tasksConfig is disabled … affected tool calls fall back to the MCP client's default 60-second inactivity timeout" reads as if calls degrade gracefully. They don't: while tasksConfig is set, every callTool throws (client.ts:471); the 60-second default only applies after the user removes the option. Suggest: "callTool throws while tasksConfig is set. After removing it, tool calls run under the MCP client's default 60-second inactivity timeout with no ttl/pollTimeout equivalent until #3661 lands."

For pgrayy's open question, the concrete exposure if #3661 does not make the same release: a tasksConfig user upgrades, connects fine, and hits a thrown error on the first tool call; with requestTimeouts gone there is no timeout knob to migrate to — only the 60 s default plus a per-call AbortSignal. If that's acceptable as a same-release dependency, nothing else blocks on the code side.

Evidence — verified on e4c101f (base 381ab48)
✅ interdiff of the PR's own change, c664101 → e4c101f: 196 lines — requestTimeouts removal (McpRequestTimeouts,
   McpClientOptions/McpServerConfig fields, config.node.ts plumb, _buildCallOptions, 4 tests, both barrel exports),
   ElicitationContext intersection + @deprecated signal, spread shim in the elicitation handler, tests asserting
   receivedContext.signal and receivedContext.mcpReq.signal, warn/throw text drops the requestTimeouts pointer
✅ build · type-check · lint · format:check — clean
✅ unit tests → 4658/4658 (167 files)
✅ shim probe (live 2.0 server + InMemoryTransport, elicitation triggered from a tool): raw vendor context own
   props = sessionId,mcpReq,http, prototype = Object.prototype, 0 getters/symbols → spread loses nothing;
   shimmed context adds signal, identity preserved, callback result round-trips to the server
✅ tasksConfig path unchanged from c664101: warn at construction, listTools works, callTool throws with #1659
⚪ integ suites not run here (need AWS credentials)

Process note: delta is a removal plus a shim I had proposed, so I verified it directly (interdiff, gates, runtime probe) rather than fanning out passes. One forward-looking caveat on the shim, not a finding: it's lossless because the vendor context is a plain object today; if a future @modelcontextprotocol/client makes it a class instance, the spread would drop prototype members silently — an owned interface would be immune, but that's a later refactor, not a blocker.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api/needs-review Makes changes to the public API surface area-mcp MCP related chore Maintenance tasks, dependency updates, CI changes, refactoring with no user-facing impact complexity/low Touched functions have low cognitive complexity (<=10) size/m typescript Pull requests that update typescript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants