| 469 | return tools |
| 470 | |
| 471 | async def call_tool( |
| 472 | self, |
| 473 | tool_name: str, |
| 474 | arguments: dict[str, Any] | None = None, |
| 475 | meta: dict[str, Any] | None = None, |
| 476 | ) -> McpToolResult: |
| 477 | params: dict[str, Any] = { |
| 478 | "name": tool_name, |
| 479 | "arguments": arguments or {}, |
| 480 | } |
| 481 | if meta: |
| 482 | params["_meta"] = meta |
| 483 | |
| 484 | # Phase 6a WI-6.1 (gap #8): on Streamable-HTTP session expiry, |
| 485 | # the chapter §"Session Expiry Detection" specifies clear-cache + |
| 486 | # retry-once. Mirrors typescript/src/services/mcp/client.ts: the |
| 487 | # cache is cleared on detection so the next request reconnects |
| 488 | # against a fresh session rather than reusing the expired one. |
| 489 | try: |
| 490 | result = await self._send_request("tools/call", params) |
| 491 | except McpToolCallError as err: |
| 492 | if not is_mcp_session_expired_error(err): |
| 493 | # Regular tool error (invalid params, server-rejected, etc.) — |
| 494 | # propagate untouched. No reconnect, no retry. |
| 495 | raise |
| 496 | await self._recover_from_session_expiry(err, tool_name=tool_name) |
| 497 | # Retry once after the recovery routine returned (it either |
| 498 | # reconnected, or another concurrent caller did, or recovery |
| 499 | # failed and re-raised). A second session-expired here means |
| 500 | # the server is unstable / the retry hit a fresh session that |
| 501 | # already expired — propagate so we don't loop indefinitely. |
| 502 | result = await self._send_request("tools/call", params) |
| 503 | if not result or not isinstance(result, dict): |
| 504 | return McpToolResult() |
| 505 | |
| 506 | is_error = result.get("isError", False) |
| 507 | content = result.get("content", []) |
| 508 | result_meta = result.get("_meta") |
| 509 | structured = result.get("structuredContent") |
| 510 | |
| 511 | if is_error: |
| 512 | error_text = "" |
| 513 | for item in content: |
| 514 | if isinstance(item, dict) and item.get("type") == "text": |
| 515 | error_text += item.get("text", "") |
| 516 | raise McpToolCallError( |
| 517 | error_text or "MCP tool returned an error", |
| 518 | "MCP tool error", |
| 519 | {"_meta": result_meta} if result_meta else None, |
| 520 | ) |
| 521 | |
| 522 | return McpToolResult( |
| 523 | content=content, |
| 524 | is_error=False, |
| 525 | meta=result_meta, |
| 526 | structured_content=structured, |
| 527 | ) |
| 528 | |