Skip to content

Commit bd8fcbf

Browse files
refactor: decouple callTool() from experimental callToolStream()
Restore callTool() to its original implementation instead of delegating to experimental.tasks.callToolStream(). This aligns with Python SDK's approach where call_tool() is task-unaware and call_tool_as_task() is the explicit experimental method. Changes: - Add guard for taskSupport: 'required' tools with clear error message - Restore original output schema validation logic - Add _cachedRequiredTaskTools to track required-only task tools - Remove unused takeResult import Tools with taskSupport: 'optional' work normally with callTool() since the server returns CallToolResult. Only 'required' tools need the experimental API.
1 parent 00a522d commit bd8fcbf

File tree

1 file changed

+62
-6
lines changed

1 file changed

+62
-6
lines changed

src/client/index.ts

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { mergeCapabilities, Protocol, type ProtocolOptions, type RequestOptions } from '../shared/protocol.js';
22
import type { Transport } from '../shared/transport.js';
3-
import { takeResult } from '../shared/responseMessage.js';
43

54
import {
65
type CallToolRequest,
@@ -203,6 +202,7 @@ export class Client<
203202
private _jsonSchemaValidator: jsonSchemaValidator;
204203
private _cachedToolOutputValidators: Map<string, JsonSchemaValidator<unknown>> = new Map();
205204
private _cachedKnownTaskTools: Set<string> = new Set();
205+
private _cachedRequiredTaskTools: Set<string> = new Set();
206206
private _experimental?: { tasks: ExperimentalClientTasks<RequestT, NotificationT, ResultT> };
207207

208208
/**
@@ -645,13 +645,57 @@ export class Client<
645645
*
646646
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
647647
*/
648-
async callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(
648+
async callTool(
649649
params: CallToolRequest['params'],
650-
resultSchema: T = CallToolResultSchema as T,
650+
resultSchema: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema = CallToolResultSchema,
651651
options?: RequestOptions
652-
): Promise<SchemaOutput<T>> {
653-
// Use experimental.tasks.callToolStream for implementation (temporary dependency)
654-
return await takeResult(this.experimental.tasks.callToolStream<T>(params, resultSchema, options));
652+
) {
653+
// Guard: required-task tools need experimental API
654+
if (this.isToolTaskRequired(params.name)) {
655+
throw new McpError(
656+
ErrorCode.InvalidRequest,
657+
`Tool "${params.name}" requires task-based execution. Use client.experimental.tasks.callToolStream() instead.`
658+
);
659+
}
660+
661+
const result = await this.request({ method: 'tools/call', params }, resultSchema, options);
662+
663+
// Check if the tool has an outputSchema
664+
const validator = this.getToolOutputValidator(params.name);
665+
if (validator) {
666+
// If tool has outputSchema, it MUST return structuredContent (unless it's an error)
667+
if (!result.structuredContent && !result.isError) {
668+
throw new McpError(
669+
ErrorCode.InvalidRequest,
670+
`Tool ${params.name} has an output schema but did not return structured content`
671+
);
672+
}
673+
674+
// Only validate structured content if present (not when there's an error)
675+
if (result.structuredContent) {
676+
try {
677+
// Validate the structured content against the schema
678+
const validationResult = validator(result.structuredContent);
679+
680+
if (!validationResult.valid) {
681+
throw new McpError(
682+
ErrorCode.InvalidParams,
683+
`Structured content does not match the tool's output schema: ${validationResult.errorMessage}`
684+
);
685+
}
686+
} catch (error) {
687+
if (error instanceof McpError) {
688+
throw error;
689+
}
690+
throw new McpError(
691+
ErrorCode.InvalidParams,
692+
`Failed to validate structured content: ${error instanceof Error ? error.message : String(error)}`
693+
);
694+
}
695+
}
696+
}
697+
698+
return result;
655699
}
656700

657701
private isToolTask(toolName: string): boolean {
@@ -662,13 +706,22 @@ export class Client<
662706
return this._cachedKnownTaskTools.has(toolName);
663707
}
664708

709+
/**
710+
* Check if a tool requires task-based execution.
711+
* Unlike isToolTask which includes 'optional' tools, this only checks for 'required'.
712+
*/
713+
private isToolTaskRequired(toolName: string): boolean {
714+
return this._cachedRequiredTaskTools.has(toolName);
715+
}
716+
665717
/**
666718
* Cache validators for tool output schemas.
667719
* Called after listTools() to pre-compile validators for better performance.
668720
*/
669721
private cacheToolMetadata(tools: Tool[]): void {
670722
this._cachedToolOutputValidators.clear();
671723
this._cachedKnownTaskTools.clear();
724+
this._cachedRequiredTaskTools.clear();
672725

673726
for (const tool of tools) {
674727
// If the tool has an outputSchema, create and cache the validator
@@ -682,6 +735,9 @@ export class Client<
682735
if (taskSupport === 'required' || taskSupport === 'optional') {
683736
this._cachedKnownTaskTools.add(tool.name);
684737
}
738+
if (taskSupport === 'required') {
739+
this._cachedRequiredTaskTools.add(tool.name);
740+
}
685741
}
686742
}
687743

0 commit comments

Comments
 (0)