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
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: base-pr-5043
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -341,6 +341,8 @@ export class McpClient { | |
| /** Server-side name of each listed tool, which differs from `tool.name` when a prefix is set. */ | ||
| private _serverToolNames = new WeakMap<McpTool, string>() | ||
| private _serverToolDefinitions = new Map<string, McpServerToolDefinition>() | ||
| /** Whether tools/list ran for this client, independent of how many tools it returned. */ | ||
| private _serverToolsListed = false | ||
| private _registeredToolNames = new Set<string>() | ||
| private _onToolsChanged: ((oldTools: string[], newTools: McpTool[]) => void) | undefined | ||
| private _refreshingTools = false | ||
|
|
@@ -633,6 +635,7 @@ export class McpClient { | |
| } | ||
|
|
||
| this._serverToolDefinitions = toolDefinitions | ||
| this._serverToolsListed = true | ||
|
|
||
| // Per-call overrides are transient, so they must not become the baseline that a later | ||
| // tools-changed refresh reports as the previously registered names. | ||
|
|
@@ -843,11 +846,22 @@ export class McpClient { | |
| operation?: TaskOperation, | ||
| completeLegacyTask = false | ||
| ): Promise<McpToolCallOutcome> { | ||
| await this.connect(false, operation ? { signal: operation.signal } : undefined) | ||
| const connectSignal = operation?.signal ?? options.signal | ||
| await this.connect(false, connectSignal ? { signal: connectSignal } : undefined) | ||
| if (this._state === 'failed') throw new Error('MCP server failed to connect. Call connect(true) to retry.') | ||
|
|
||
| const params = this._prepareToolCall(tool, args) | ||
|
|
||
| // Task support is read from tools/list metadata, so a tool invoked before any listing | ||
| // fetches it once. | ||
| if ( | ||
| completeLegacyTask && | ||
| this._client.getNegotiatedProtocolVersion() === '2025-11-25' && | ||
| this._client.getServerCapabilities()?.tasks?.requests?.tools?.call !== undefined | ||
| ) { | ||
| await this._ensureServerToolDefinitions(operation!) | ||
| } | ||
|
|
||
| // The upstream codec rejects extension result types before custom result schemas run. | ||
| if (completeLegacyTask && this._supportsLegacyTask(params.name)) { | ||
| const outputSchema = compileToolOutputSchema( | ||
|
|
@@ -1107,6 +1121,22 @@ export class McpClient { | |
| } | ||
| } | ||
|
|
||
| private async _ensureServerToolDefinitions(operation: TaskOperation): Promise<void> { | ||
| if (this._serverToolsListed) return | ||
| this._serverToolsListed = true | ||
| try { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Important |
||
| const result = await this._client.listTools(undefined, { | ||
| timeout: remainingTime(operation.deadline, this._tasksConfig!.requestTimeoutMs), | ||
| signal: operation.signal, | ||
| }) | ||
| this._serverToolDefinitions = new Map(result.tools.map((tool) => [tool.name, toMcpServerToolDefinition(tool)])) | ||
| } catch (error) { | ||
| // A cancelled or expired operation must surface as such, not as a listing failure. | ||
| if (operation.signal.aborted) throw error | ||
| logger.warn(`error=<${error}> | failed to list tools for legacy task support detection`) | ||
| } | ||
| } | ||
|
|
||
| private async _callLegacyTask(params: CallToolRequest['params'], operation: TaskOperation): Promise<CallToolResult> { | ||
| const requestOptions = (): CallToolRequestOptions => ({ | ||
| signal: operation.signal, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion
Issue:
_ensureServerToolDefinitions(operation!)relies on the non-null assertion holding because the gating condition includescompleteLegacyTask, which today is onlytrueon the task path whereoperationis always supplied. This invariant is implicit and could silently break if a future caller setscompleteLegacyTaskwithout an operation.Suggestion: Consider making
operationa required parameter whencompleteLegacyTaskis set, or add a brief comment at this call site documenting whyoperationis guaranteed present here, so the!is self-explanatory.