Executes plan steps by routing tool calls to MCP servers.
| 45 | |
| 46 | |
| 47 | class Executor: |
| 48 | """Executes plan steps by routing tool calls to MCP servers.""" |
| 49 | |
| 50 | def __init__( |
| 51 | self, |
| 52 | llm: LLMBackend, |
| 53 | server_paths: dict[str, Path | str] | None = None, |
| 54 | ) -> None: |
| 55 | self._llm = llm |
| 56 | self._server_paths = ( |
| 57 | DEFAULT_SERVER_PATHS if server_paths is None else server_paths |
| 58 | ) |
| 59 | |
| 60 | async def get_server_descriptions(self) -> dict[str, str]: |
| 61 | """Query each registered MCP server and return formatted tool signatures.""" |
| 62 | descriptions: dict[str, str] = {} |
| 63 | for name, path in self._server_paths.items(): |
| 64 | try: |
| 65 | tools = await _list_tools(path) |
| 66 | lines = [] |
| 67 | for t in tools: |
| 68 | params = ", ".join( |
| 69 | f"{p['name']}: {p['type']}{'?' if not p['required'] else ''}" |
| 70 | for p in t.get("parameters", []) |
| 71 | ) |
| 72 | lines.append(f" - {t['name']}({params}): {t['description']}") |
| 73 | descriptions[name] = "\n".join(lines) |
| 74 | except Exception as exc: # noqa: BLE001 |
| 75 | descriptions[name] = f" (unavailable: {exc})" |
| 76 | return descriptions |
| 77 | |
| 78 | async def execute_plan(self, plan: Plan, question: str) -> list[StepResult]: |
| 79 | """Execute all plan steps in dependency order.""" |
| 80 | ordered = plan.resolved_order() |
| 81 | total = len(ordered) |
| 82 | |
| 83 | # Pre-fetch tool schemas for all servers referenced in the plan so that |
| 84 | # _resolve_args_with_llm can include exact parameter names in its prompt. |
| 85 | server_names = {step.server for step in ordered} |
| 86 | tool_schemas: dict[str, dict[str, str]] = {} # server -> {tool_name -> sig} |
| 87 | for name in server_names: |
| 88 | path = self._server_paths.get(name) |
| 89 | if path is None: |
| 90 | continue |
| 91 | try: |
| 92 | tools = await _list_tools(path) |
| 93 | tool_schemas[name] = { |
| 94 | t["name"]: ", ".join( |
| 95 | f"{p['name']}: {p['type']}{'?' if not p['required'] else ''}" |
| 96 | for p in t.get("parameters", []) |
| 97 | ) |
| 98 | for t in tools |
| 99 | } |
| 100 | except Exception: # noqa: BLE001 |
| 101 | tool_schemas[name] = {} |
| 102 | |
| 103 | context: dict[int, StepResult] = {} |
| 104 | results: list[StepResult] = [] |
no outgoing calls