Repository navigation
fix(mcp/ts): forward the call signal to connect and fetch task metadata for unlisted tools [replica of harness-sdk PR 5043] - #38
poshinchen wants to merge 2 commits into
Conversation
…ta for unlisted tools Two follow-ups from the strands-agents#4711 review: - Without tasksConfig, callTool now forwards the caller's signal to connect(), so aborting works during a stalled transport handshake instead of only after the connection completes. - Legacy task support is read from tools/list metadata. A tool invoked through a client that has not listed tools yet now fetches that metadata once, so taskSupport-required tools execute as tasks instead of failing with a plain tools/call. A failed listing logs a warning and keeps the plain-call behavior.
…ity-gated Review follow-ups on the unlisted-tool metadata fetch: - Track listing with a flag instead of a non-empty cache, so an empty tools/list result or a failed listing no longer refetches and rewarns on every call; listTools() sets the same flag. - Gate the fetch on the server's legacy tasks capability in addition to the protocol version, so ordinary 2025-11-25 servers pay no extra round-trip. - Bound the fetch by the remaining operation deadline like every other legacy task request, and rethrow when the operation was aborted so a user cancel surfaces as cancellation instead of a listing warning followed by a plain call.
poshinchen
left a comment
There was a problem hiding this comment.
Strandly v4-reason (optimized) review — run af1746, Opus 4.8
Assessment: comment
Assessment: Comment
Both fixes are well-targeted and thoroughly tested, with the plain-path connect signal forwarding being a clean, correct change. My main concern is the latching semantics of the new metadata-fetch guard.
Review Categories
- Correctness: The
_serverToolsListedflag is latched beforelistToolsresolves, so an aborted/timed-out fetch permanently poisons the once-per-client guard — a later retry with a fresh signal silently falls back to a plaintools/calland the exacttaskSupport: "required"failure this PR targets can recur. Genuine failures intentionally latching is fine; abort/timeout is the edge worth revisiting. - Maintainability:
_ensureServerToolDefinitions(operation!)depends on an implicitcompleteLegacyTask ⇒ operation presentinvariant; worth documenting or tightening. - Testing: Regression coverage is strong across all four new scenarios and matches the described behavior.
Nice work isolating the once-per-client fetch so servers without legacy tasks pay no round-trip.
| private async _ensureServerToolDefinitions(operation: TaskOperation): Promise<void> { | ||
| if (this._serverToolsListed) return | ||
| this._serverToolsListed = true | ||
| try { |
There was a problem hiding this comment.
Important
Issue: _serverToolsListed is set to true before listTools resolves. On an aborted/timed-out fetch, the catch re-throws (good), but the flag stays true and _serverToolDefinitions was never populated. A subsequent callTool with a fresh signal then skips the fetch (if (this._serverToolsListed) return), sends a plain tools/call with no task: {}, and a taskSupport: "required" tool fails with the confusing server error this PR set out to prevent. A genuine listing failure logging a warning and falling back is intentional per the description, but an abort/timeout permanently poisons the once-per-client flag so retries can never recover.
Suggestion: Only latch the flag on outcomes you intend to be permanent — e.g. set _serverToolsListed = true after a successful fetch and inside the non-abort branch of catch, leaving it false when the fetch is aborted/times out so a retry can re-attempt.
| this._client.getNegotiatedProtocolVersion() === '2025-11-25' && | ||
| this._client.getServerCapabilities()?.tasks?.requests?.tools?.call !== undefined | ||
| ) { | ||
| await this._ensureServerToolDefinitions(operation!) |
There was a problem hiding this comment.
Suggestion
Issue: _ensureServerToolDefinitions(operation!) relies on the non-null assertion holding because the gating condition includes completeLegacyTask, which today is only true on the task path where operation is always supplied. This invariant is implicit and could silently break if a future caller sets completeLegacyTask without an operation.
Suggestion: Consider making operation a required parameter when completeLegacyTask is set, or add a brief comment at this call site documenting why operation is guaranteed present here, so the ! is self-explanatory.
|
Live Strandly's actual comments on the original PR 5043 (sanitized copy: mentions escaped, links backticked) Review (COMMENTED):
Review (CHANGES_REQUESTED):
Review (CHANGES_REQUESTED):
Inline at
Inline at
Inline at
Inline at
|
Replica of
https://github.com/strands-agents/harness-sdk/pull/5043(backticked: no cross-reference). Below: the v4-reason optimized review and a sanitized copy of live Strandly's actual comments.