call method
Implementation
Future<dynamic> call({
required Client client,
required String tool,
required Map<String, dynamic> params,
String? scope,
}) async {
_logger.debug('Tool call', {'tool': tool, 'params': params});
// Try in-process first. If the tool is registered we skip the
// external MCP forward and resolve it directly.
if (resolveInProcess(tool, scope: scope) != null) {
return _callInProcessForRuntime(tool, params, scope: scope);
}
final List<Tool> tools;
try {
tools = await client.listTools();
} catch (e, st) {
_logger.logError('listTools failed', e, st, {'tool': tool});
throw ToolExecutionException(tool, cause: e);
}
if (!tools.any((t) => t.name == tool)) {
throw ToolNotFoundException(
tool,
tools.map((t) => t.name).toList(),
);
}
final CallToolResult result;
try {
result = await client.callTool(tool, params);
} catch (e, st) {
_logger.logError('callTool failed', e, st, {'tool': tool});
throw ToolExecutionException(tool, cause: e);
}
_logger
.debug('Tool result', {'tool': tool, 'items': result.content.length});
// The server refused the call (a rate limit, a rule): the runtime must see
// a failure and fire the document's `onError`. Read as a payload, the
// refusal text is not JSON, came back null, and the runtime took the call
// for a success — the button did nothing and said nothing.
if (result.isError == true) {
final text = result.content.whereType<TextContent>().map((c) => c.text);
return <String, dynamic>{
'content': <Map<String, dynamic>>[
<String, dynamic>{'type': 'text', 'text': text.join('\n')},
],
'isError': true,
};
}
if (result.content.isEmpty) return null;
final first = result.content.first;
if (first is! TextContent) return null;
try {
return jsonDecode(first.text);
} catch (e) {
_logger.warn(
'Failed to parse tool response',
{
'tool': tool,
'text': first.text,
},
e);
return null;
}
}