refactor: move mcp sdk converters to wf_sources_mcp
This commit is contained in:
@@ -290,6 +290,9 @@ implementation state.
|
|||||||
types (`BackendAdapter`, `ToolExecutor`, `ToolCallResult`) now live in
|
types (`BackendAdapter`, `ToolExecutor`, `ToolCallResult`) now live in
|
||||||
`wf_sources_mcp.sdk`, with `wf_mcp.sdk` and `wf_mcp.runtime.protocols`
|
`wf_sources_mcp.sdk`, with `wf_mcp.sdk` and `wf_mcp.runtime.protocols`
|
||||||
retained as compatibility shims.
|
retained as compatibility shims.
|
||||||
|
Fifth `wf_sources_mcp` slice complete: MCP SDK conversion helpers now live
|
||||||
|
in `wf_sources_mcp.sdk.converters`, with `wf_mcp.sdk.converters` retained
|
||||||
|
as a compatibility shim.
|
||||||
The `wf-mcp` script is now a legacy/special-purpose MCP entrypoint, not the
|
The `wf-mcp` script is now a legacy/special-purpose MCP entrypoint, not the
|
||||||
preferred durable workflow server. New product paths should target
|
preferred durable workflow server. New product paths should target
|
||||||
`wf-rpc-server` plus neutral `wf_config`/`wf_server` composition, then keep
|
`wf-rpc-server` plus neutral `wf_config`/`wf_server` composition, then keep
|
||||||
|
|||||||
@@ -0,0 +1,517 @@
|
|||||||
|
# wf_sources_mcp SDK Converters Slice Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Move MCP SDK conversion helpers into `wf_sources_mcp.sdk.converters` so adapter/runtime code can later move without depending on `wf_mcp.sdk.converters`.
|
||||||
|
|
||||||
|
**Architecture:** `wf_sources_mcp` owns upstream MCP conversion logic from MCP SDK models into workflow-source DTOs. `wf_mcp.sdk.converters` remains a compatibility shim. This slice moves pure conversion functions only; it does not move `McpSdkAdapter`, runtime sessions, broker discovery, or upstream transport services.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, MCP SDK model types, pytest, Ruff, basedpyright, `src/` package layout.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
Move only:
|
||||||
|
|
||||||
|
- `tool_to_discovered`
|
||||||
|
- `resource_to_discovered`
|
||||||
|
- `prompt_to_discovered`
|
||||||
|
- `tool_result_to_call_result`
|
||||||
|
- `workflow_output_schema_from_mcp_tool_schema`
|
||||||
|
|
||||||
|
Do not move:
|
||||||
|
|
||||||
|
- `McpSdkAdapter`
|
||||||
|
- `PersistentMcpSession`
|
||||||
|
- `PersistentSessionFactory`
|
||||||
|
- `McpRuntimePool`
|
||||||
|
- `discover_connection_capabilities`
|
||||||
|
- `snapshot_from_specs`
|
||||||
|
- `UpstreamTransportService`
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
Create:
|
||||||
|
|
||||||
|
- `src/wf_sources_mcp/sdk/converters.py` — canonical converter helpers.
|
||||||
|
- `tests/wf_sources_mcp/test_sdk_converters.py` — canonical converter tests.
|
||||||
|
|
||||||
|
Modify:
|
||||||
|
|
||||||
|
- `src/wf_sources_mcp/sdk/__init__.py` — export converter helpers if convenient.
|
||||||
|
- `src/wf_mcp/sdk/converters.py` — compatibility shim.
|
||||||
|
- `src/wf_mcp/sdk/adapter.py` — import converters from canonical path.
|
||||||
|
- `src/wf_mcp/runtime/session.py` — import `tool_result_to_call_result` from canonical path.
|
||||||
|
- `src/wf_mcp/broker/catalog.py` — import `workflow_output_schema_from_mcp_tool_schema` from canonical path.
|
||||||
|
- `tests/wf_mcp/test_compat_imports.py` — shim identity test.
|
||||||
|
- `tests/wf_sources_mcp/test_import_direction_guard.py` — guard against old converter imports.
|
||||||
|
- `docs/current_roadmap.md` — mark converter slice complete.
|
||||||
|
- `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md` — mark converter slice complete.
|
||||||
|
|
||||||
|
After implementation, move this plan to:
|
||||||
|
|
||||||
|
- `docs/historical/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-converters-slice.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Create Canonical Converter Module
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/wf_sources_mcp/sdk/converters.py`
|
||||||
|
- Modify: `src/wf_sources_mcp/sdk/__init__.py`
|
||||||
|
- Test: `tests/wf_sources_mcp/test_sdk_converters.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create converter module**
|
||||||
|
|
||||||
|
Create `src/wf_sources_mcp/sdk/converters.py` by moving the current implementation from `src/wf_mcp/sdk/converters.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from mcp.types import CallToolResult as McpCallToolResult
|
||||||
|
from mcp.types import Prompt as McpPrompt
|
||||||
|
from mcp.types import Resource as McpResource
|
||||||
|
from mcp.types import Tool as McpTool
|
||||||
|
|
||||||
|
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||||
|
from wf_sources_mcp.sdk import ToolCallResult
|
||||||
|
|
||||||
|
|
||||||
|
def tool_to_discovered(tool: McpTool) -> DiscoveredTool:
|
||||||
|
"""Convert an MCP SDK tool into the source discovery model."""
|
||||||
|
output_schema = workflow_output_schema_from_mcp_tool_schema(tool.outputSchema)
|
||||||
|
display_name = (
|
||||||
|
tool.annotations.title
|
||||||
|
if tool.annotations is not None and tool.annotations.title
|
||||||
|
else tool.title
|
||||||
|
)
|
||||||
|
return DiscoveredTool(
|
||||||
|
name=tool.name,
|
||||||
|
title=display_name,
|
||||||
|
description=tool.description,
|
||||||
|
input_schema=tool.inputSchema,
|
||||||
|
output_schema=output_schema,
|
||||||
|
outcomes=("ok", "error"),
|
||||||
|
metadata=tool.model_dump(by_alias=True, mode="json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def workflow_output_schema_from_mcp_tool_schema(
|
||||||
|
schema: dict[str, Any] | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return the MCP tool output schema without inventing workflow fields.
|
||||||
|
|
||||||
|
MCP tools without structured output expose raw content blocks. Those blocks
|
||||||
|
can be text, images, resource links, or mixed results, so this layer must not
|
||||||
|
pretend there is a stable top-level ``text`` field. Workflow authors should
|
||||||
|
add an explicit wrapper/extraction node for the block shape they expect.
|
||||||
|
"""
|
||||||
|
return schema or {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"content": {"type": "array"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resource_to_discovered(resource: McpResource) -> DiscoveredResource:
|
||||||
|
"""Convert an MCP SDK resource into the source discovery model."""
|
||||||
|
local_name = resource.name or str(resource.uri)
|
||||||
|
return DiscoveredResource(
|
||||||
|
uri=str(resource.uri),
|
||||||
|
name=local_name,
|
||||||
|
title=resource.title,
|
||||||
|
description=resource.description,
|
||||||
|
mime_type=resource.mimeType,
|
||||||
|
metadata=resource.model_dump(by_alias=True, mode="json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_to_discovered(prompt: McpPrompt) -> DiscoveredPrompt:
|
||||||
|
"""Convert an MCP SDK prompt into the source discovery model."""
|
||||||
|
arguments = [
|
||||||
|
argument.model_dump(by_alias=True, mode="json")
|
||||||
|
for argument in prompt.arguments or []
|
||||||
|
]
|
||||||
|
return DiscoveredPrompt(
|
||||||
|
name=prompt.name,
|
||||||
|
title=prompt.title,
|
||||||
|
description=prompt.description,
|
||||||
|
arguments=arguments,
|
||||||
|
metadata=prompt.model_dump(by_alias=True, mode="json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tool_result_to_call_result(result: McpCallToolResult) -> ToolCallResult:
|
||||||
|
"""Convert an MCP SDK tool call result into the adapter result model."""
|
||||||
|
if result.structuredContent is not None:
|
||||||
|
output = result.structuredContent
|
||||||
|
else:
|
||||||
|
output: dict[str, Any] = {
|
||||||
|
"content": [item.model_dump(by_alias=True) for item in result.content]
|
||||||
|
}
|
||||||
|
return ToolCallResult(
|
||||||
|
outcome="error" if result.isError else "ok",
|
||||||
|
output=output,
|
||||||
|
meta=result.meta or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"prompt_to_discovered",
|
||||||
|
"resource_to_discovered",
|
||||||
|
"tool_result_to_call_result",
|
||||||
|
"tool_to_discovered",
|
||||||
|
"workflow_output_schema_from_mcp_tool_schema",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Export converter helpers**
|
||||||
|
|
||||||
|
Update `src/wf_sources_mcp/sdk/__init__.py` to import and export:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .converters import (
|
||||||
|
prompt_to_discovered,
|
||||||
|
resource_to_discovered,
|
||||||
|
tool_result_to_call_result,
|
||||||
|
tool_to_discovered,
|
||||||
|
workflow_output_schema_from_mcp_tool_schema,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add those names to `__all__`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add canonical converter tests**
|
||||||
|
|
||||||
|
Create `tests/wf_sources_mcp/test_sdk_converters.py` by copying `tests/wf_mcp/test_sdk_converters.py`, but change imports to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_sources_mcp.sdk.converters import (
|
||||||
|
tool_result_to_call_result,
|
||||||
|
tool_to_discovered,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the four existing test bodies unchanged.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run canonical converter tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_sources_mcp/test_sdk_converters.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: 4 tests pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Replace Old Converter Module With Shim
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_mcp/sdk/converters.py`
|
||||||
|
- Modify: `tests/wf_mcp/test_compat_imports.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace old converter module**
|
||||||
|
|
||||||
|
Set `src/wf_mcp/sdk/converters.py` to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""Compatibility shim for MCP SDK converter helpers.
|
||||||
|
|
||||||
|
Canonical implementation lives in `wf_sources_mcp.sdk.converters`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from wf_sources_mcp.sdk.converters import (
|
||||||
|
prompt_to_discovered,
|
||||||
|
resource_to_discovered,
|
||||||
|
tool_result_to_call_result,
|
||||||
|
tool_to_discovered,
|
||||||
|
workflow_output_schema_from_mcp_tool_schema,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"prompt_to_discovered",
|
||||||
|
"resource_to_discovered",
|
||||||
|
"tool_result_to_call_result",
|
||||||
|
"tool_to_discovered",
|
||||||
|
"workflow_output_schema_from_mcp_tool_schema",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add shim identity test**
|
||||||
|
|
||||||
|
Append to `tests/wf_mcp/test_compat_imports.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_wf_mcp_sdk_converter_shim_reexports_wf_sources_mcp_converters() -> None:
|
||||||
|
from wf_mcp.sdk.converters import tool_result_to_call_result as compat_tool_result
|
||||||
|
from wf_mcp.sdk.converters import tool_to_discovered as compat_tool_to_discovered
|
||||||
|
from wf_mcp.sdk.converters import (
|
||||||
|
workflow_output_schema_from_mcp_tool_schema as compat_output_schema,
|
||||||
|
)
|
||||||
|
from wf_sources_mcp.sdk.converters import (
|
||||||
|
tool_result_to_call_result,
|
||||||
|
tool_to_discovered,
|
||||||
|
workflow_output_schema_from_mcp_tool_schema,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert compat_tool_result is tool_result_to_call_result
|
||||||
|
assert compat_tool_to_discovered is tool_to_discovered
|
||||||
|
assert compat_output_schema is workflow_output_schema_from_mcp_tool_schema
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run compatibility tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_mcp/test_compat_imports.py tests/wf_mcp/test_sdk_converters.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: compatibility tests and old converter tests pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Rewrite Production Imports to Canonical Converter Path
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_mcp/sdk/adapter.py`
|
||||||
|
- Modify: `src/wf_mcp/runtime/session.py`
|
||||||
|
- Modify: `src/wf_mcp/broker/catalog.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update adapter imports**
|
||||||
|
|
||||||
|
In `src/wf_mcp/sdk/adapter.py`, replace:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from .converters import (
|
||||||
|
prompt_to_discovered,
|
||||||
|
resource_to_discovered,
|
||||||
|
tool_result_to_call_result,
|
||||||
|
tool_to_discovered,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_sources_mcp.sdk.converters import (
|
||||||
|
prompt_to_discovered,
|
||||||
|
resource_to_discovered,
|
||||||
|
tool_result_to_call_result,
|
||||||
|
tool_to_discovered,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update runtime session import**
|
||||||
|
|
||||||
|
In `src/wf_mcp/runtime/session.py`, replace:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ..sdk.converters import tool_result_to_call_result
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_sources_mcp.sdk.converters import tool_result_to_call_result
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update broker catalog import**
|
||||||
|
|
||||||
|
In `src/wf_mcp/broker/catalog.py`, replace:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ..sdk.converters import workflow_output_schema_from_mcp_tool_schema
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_sources_mcp.sdk.converters import (
|
||||||
|
workflow_output_schema_from_mcp_tool_schema,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Confirm production imports no longer use old converter path**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "wf_mcp\\.sdk\\.converters|\\.sdk\\.converters|\\.\\.sdk\\.converters" src
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: only `src/wf_mcp/sdk/converters.py` shim may mention `wf_sources_mcp.sdk.converters`; no production code imports old converter path.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run focused production tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_mcp/test_sdk_adapter.py tests/wf_mcp/test_sdk_converters.py tests/wf_mcp/test_stateful_runtime.py tests/wf_mcp/service/test_catalog.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all focused tests pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Strengthen Import-Direction Guard
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/wf_sources_mcp/test_import_direction_guard.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add old converter module to forbidden guard**
|
||||||
|
|
||||||
|
Append this test to `tests/wf_sources_mcp/test_import_direction_guard.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_wf_sources_mcp_does_not_import_old_sdk_converter_module() -> None:
|
||||||
|
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||||
|
forbidden = {
|
||||||
|
"wf_mcp.sdk.converters",
|
||||||
|
}
|
||||||
|
violations: list[str] = []
|
||||||
|
|
||||||
|
for py_file in sorted(root.rglob("*.py")):
|
||||||
|
rel = py_file.relative_to(root.parent)
|
||||||
|
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
|
||||||
|
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
|
||||||
|
violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
|
||||||
|
elif isinstance(node, ast.Import):
|
||||||
|
for alias in node.names:
|
||||||
|
if alias.name in forbidden:
|
||||||
|
violations.append(f"{module}:{node.lineno}: import {alias.name}")
|
||||||
|
|
||||||
|
assert violations == [], (
|
||||||
|
"wf_sources_mcp still imports old wf_mcp SDK converter module:\n"
|
||||||
|
+ "\n".join(f" {violation}" for violation in violations)
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run guard tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_sources_mcp/test_import_direction_guard.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: guard tests pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Docs Status and Plan Archival
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/current_roadmap.md`
|
||||||
|
- Modify: `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`
|
||||||
|
- Move: `docs/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-converters-slice.md` to `docs/historical/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-converters-slice.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update roadmap**
|
||||||
|
|
||||||
|
In `docs/current_roadmap.md`, under the MCP package split section, add:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Fifth `wf_sources_mcp` slice complete: MCP SDK conversion helpers now live
|
||||||
|
in `wf_sources_mcp.sdk.converters`, with `wf_mcp.sdk.converters` retained
|
||||||
|
as a compatibility shim.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update long-lived API boundary spec**
|
||||||
|
|
||||||
|
In `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`, update the `wf_sources_mcp` status list so it includes:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
5. Complete: MCP SDK conversion helpers moved to `wf_sources_mcp.sdk.converters`, with `wf_mcp.sdk.converters` retained as a shim.
|
||||||
|
6. Upstream transport/discovery/session services.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Move completed plan to historical**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git mv docs/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-converters-slice.md docs/historical/superpowers/plans/2026-06-07-wf-sources-mcp-sdk-converters-slice.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `git status --short` shows an `R` rename for this plan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Final Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- All changed files
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run focused extraction tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_sources_mcp tests/wf_mcp/test_compat_imports.py tests/wf_mcp/test_sdk_adapter.py tests/wf_mcp/test_sdk_converters.py tests/wf_mcp/test_stateful_runtime.py tests/wf_mcp/service/test_catalog.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all focused tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run lint and type checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ruff check src tests
|
||||||
|
uv run basedpyright --level error src
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: Ruff reports `All checks passed!`; basedpyright reports `0 errors`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run full suite**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: full suite passes with current skip/xfail counts. If it times out locally, rerun with a longer timeout before reporting.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Review remaining old converter imports**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "wf_mcp\\.sdk\\.converters|from wf_mcp\\.sdk\\.converters|from \\.\\.sdk\\.converters|from \\.sdk\\.converters" src tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: remaining occurrences are compatibility shims or tests intentionally exercising old import paths.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Report**
|
||||||
|
|
||||||
|
Report:
|
||||||
|
|
||||||
|
- files created/modified
|
||||||
|
- focused/full verification output
|
||||||
|
- whether `wf_sources_mcp.sdk.converters` owns all converter helpers
|
||||||
|
- whether `wf_mcp.sdk.converters` remains as a shim
|
||||||
|
- deviations from this plan
|
||||||
|
|
||||||
|
Do not commit unless the user explicitly asks. If committing, use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "refactor: move mcp sdk converters to wf_sources_mcp"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- Spec coverage: moves the pure conversion helpers needed before moving SDK adapter/runtime sessions.
|
||||||
|
- Placeholder scan: no `TODO`, `TBD`, or unspecified test steps.
|
||||||
|
- Type consistency: function names and behavior match current `wf_mcp.sdk.converters` exactly, with only import ownership changing.
|
||||||
@@ -92,7 +92,8 @@ First slices should move leaf modules only and leave `wf_mcp` re-export shims:
|
|||||||
2. Complete: MCP source registry models/conversion moved to `wf_sources_mcp.source_registry`, with `wf_mcp.source_registry` retained as a shim.
|
2. Complete: MCP source registry models/conversion moved to `wf_sources_mcp.source_registry`, with `wf_mcp.source_registry` retained as a shim.
|
||||||
3. Complete: upstream MCP catalog/discovery DTOs moved to `wf_sources_mcp.catalog`, with `wf_mcp.capabilities` and `wf_mcp.catalog.models` retained as shims.
|
3. Complete: upstream MCP catalog/discovery DTOs moved to `wf_sources_mcp.catalog`, with `wf_mcp.capabilities` and `wf_mcp.catalog.models` retained as shims.
|
||||||
4. Complete: upstream SDK protocol/result types moved to `wf_sources_mcp.sdk`, with `wf_mcp.sdk` and `wf_mcp.runtime.protocols` retained as shims.
|
4. Complete: upstream SDK protocol/result types moved to `wf_sources_mcp.sdk`, with `wf_mcp.sdk` and `wf_mcp.runtime.protocols` retained as shims.
|
||||||
5. Upstream transport/discovery/session services.
|
5. Complete: MCP SDK conversion helpers moved to `wf_sources_mcp.sdk.converters`, with `wf_mcp.sdk.converters` retained as a shim.
|
||||||
|
6. Upstream transport/discovery/session services.
|
||||||
|
|
||||||
Each slice should add import-direction tests so the new source-provider package
|
Each slice should add import-direction tests so the new source-provider package
|
||||||
does not depend on `wf_mcp.workflow_surface`, `wf_mcp.admin_surface`,
|
does not depend on `wf_mcp.workflow_surface`, `wf_mcp.admin_surface`,
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ from wf_sources_mcp.catalog import (
|
|||||||
DiscoveredResource,
|
DiscoveredResource,
|
||||||
)
|
)
|
||||||
from wf_sources_mcp.catalog.models import CatalogSnapshot
|
from wf_sources_mcp.catalog.models import CatalogSnapshot
|
||||||
|
from wf_sources_mcp.sdk.converters import (
|
||||||
|
workflow_output_schema_from_mcp_tool_schema,
|
||||||
|
)
|
||||||
|
|
||||||
from ..connections import qualify_node_name
|
from ..connections import qualify_node_name
|
||||||
from ..sdk.converters import workflow_output_schema_from_mcp_tool_schema
|
|
||||||
|
|
||||||
|
|
||||||
def snapshot_from_specs(
|
def snapshot_from_specs(
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ from mcp.client.session import ClientSession
|
|||||||
from mcp.types import CallToolResult
|
from mcp.types import CallToolResult
|
||||||
|
|
||||||
from wf_sources_mcp.sdk import ToolCallResult
|
from wf_sources_mcp.sdk import ToolCallResult
|
||||||
|
from wf_sources_mcp.sdk.converters import tool_result_to_call_result
|
||||||
|
|
||||||
from ..auth import AuthRecord
|
from ..auth import AuthRecord
|
||||||
from ..models import ConnectionConfig
|
from ..models import ConnectionConfig
|
||||||
from ..sdk.converters import tool_result_to_call_result
|
|
||||||
|
|
||||||
RawToolCaller = Callable[[str, dict[str, Any]], Awaitable[CallToolResult]]
|
RawToolCaller = Callable[[str, dict[str, Any]], Awaitable[CallToolResult]]
|
||||||
|
|
||||||
|
|||||||
@@ -20,15 +20,15 @@ from pydantic import AnyUrl
|
|||||||
from wf_sources_mcp.auth import AuthRecord, mcp_auth_env, mcp_auth_headers
|
from wf_sources_mcp.auth import AuthRecord, mcp_auth_env, mcp_auth_headers
|
||||||
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||||
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
|
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
|
||||||
|
from wf_sources_mcp.sdk.converters import (
|
||||||
from ..models import ConnectionConfig
|
|
||||||
from .converters import (
|
|
||||||
prompt_to_discovered,
|
prompt_to_discovered,
|
||||||
resource_to_discovered,
|
resource_to_discovered,
|
||||||
tool_result_to_call_result,
|
tool_result_to_call_result,
|
||||||
tool_to_discovered,
|
tool_to_discovered,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from ..models import ConnectionConfig
|
||||||
|
|
||||||
|
|
||||||
class McpSdkAdapter(BackendAdapter):
|
class McpSdkAdapter(BackendAdapter):
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
|||||||
@@ -1,89 +1,22 @@
|
|||||||
|
"""Compatibility shim for MCP SDK converter helpers.
|
||||||
|
|
||||||
|
Canonical implementation lives in `wf_sources_mcp.sdk.converters`.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from wf_sources_mcp.sdk.converters import (
|
||||||
|
prompt_to_discovered,
|
||||||
from mcp.types import CallToolResult as McpCallToolResult
|
resource_to_discovered,
|
||||||
from mcp.types import Prompt as McpPrompt
|
tool_result_to_call_result,
|
||||||
from mcp.types import Resource as McpResource
|
tool_to_discovered,
|
||||||
from mcp.types import Tool as McpTool
|
workflow_output_schema_from_mcp_tool_schema,
|
||||||
|
|
||||||
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
|
||||||
from wf_sources_mcp.sdk import ToolCallResult
|
|
||||||
|
|
||||||
|
|
||||||
def tool_to_discovered(tool: McpTool) -> DiscoveredTool:
|
|
||||||
"""Convert an MCP SDK tool into the broker discovery model."""
|
|
||||||
output_schema = workflow_output_schema_from_mcp_tool_schema(tool.outputSchema)
|
|
||||||
display_name = (
|
|
||||||
tool.annotations.title
|
|
||||||
if tool.annotations is not None and tool.annotations.title
|
|
||||||
else tool.title
|
|
||||||
)
|
|
||||||
return DiscoveredTool(
|
|
||||||
name=tool.name,
|
|
||||||
title=display_name,
|
|
||||||
description=tool.description,
|
|
||||||
input_schema=tool.inputSchema,
|
|
||||||
output_schema=output_schema,
|
|
||||||
outcomes=("ok", "error"),
|
|
||||||
metadata=tool.model_dump(by_alias=True, mode="json"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
def workflow_output_schema_from_mcp_tool_schema(
|
"prompt_to_discovered",
|
||||||
schema: dict[str, Any] | None,
|
"resource_to_discovered",
|
||||||
) -> dict[str, Any]:
|
"tool_result_to_call_result",
|
||||||
"""Return the MCP tool output schema without inventing workflow fields.
|
"tool_to_discovered",
|
||||||
|
"workflow_output_schema_from_mcp_tool_schema",
|
||||||
MCP tools without structured output expose raw content blocks. Those blocks
|
|
||||||
can be text, images, resource links, or mixed results, so wf_mcp must not
|
|
||||||
pretend there is a stable top-level ``text`` field. Workflow authors should
|
|
||||||
add an explicit wrapper/extraction node for the block shape they expect.
|
|
||||||
"""
|
|
||||||
return schema or {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {"content": {"type": "array"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def resource_to_discovered(resource: McpResource) -> DiscoveredResource:
|
|
||||||
"""Convert an MCP SDK resource into the broker discovery model."""
|
|
||||||
local_name = resource.name or str(resource.uri)
|
|
||||||
return DiscoveredResource(
|
|
||||||
uri=str(resource.uri),
|
|
||||||
name=local_name,
|
|
||||||
title=resource.title,
|
|
||||||
description=resource.description,
|
|
||||||
mime_type=resource.mimeType,
|
|
||||||
metadata=resource.model_dump(by_alias=True, mode="json"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def prompt_to_discovered(prompt: McpPrompt) -> DiscoveredPrompt:
|
|
||||||
"""Convert an MCP SDK prompt into the broker discovery model."""
|
|
||||||
arguments = [
|
|
||||||
argument.model_dump(by_alias=True, mode="json")
|
|
||||||
for argument in prompt.arguments or []
|
|
||||||
]
|
]
|
||||||
return DiscoveredPrompt(
|
|
||||||
name=prompt.name,
|
|
||||||
title=prompt.title,
|
|
||||||
description=prompt.description,
|
|
||||||
arguments=arguments,
|
|
||||||
metadata=prompt.model_dump(by_alias=True, mode="json"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def tool_result_to_call_result(result: McpCallToolResult) -> ToolCallResult:
|
|
||||||
"""Convert an MCP SDK tool call result into the adapter result model."""
|
|
||||||
if result.structuredContent is not None:
|
|
||||||
output = result.structuredContent
|
|
||||||
else:
|
|
||||||
output: dict[str, Any] = {
|
|
||||||
"content": [item.model_dump(by_alias=True) for item in result.content]
|
|
||||||
}
|
|
||||||
return ToolCallResult(
|
|
||||||
outcome="error" if result.isError else "ok",
|
|
||||||
output=output,
|
|
||||||
meta=result.meta or {},
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,9 +1,21 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .converters import (
|
||||||
|
prompt_to_discovered,
|
||||||
|
resource_to_discovered,
|
||||||
|
tool_result_to_call_result,
|
||||||
|
tool_to_discovered,
|
||||||
|
workflow_output_schema_from_mcp_tool_schema,
|
||||||
|
)
|
||||||
from .protocols import BackendAdapter, ToolCallResult, ToolExecutor
|
from .protocols import BackendAdapter, ToolCallResult, ToolExecutor
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"BackendAdapter",
|
"BackendAdapter",
|
||||||
"ToolCallResult",
|
"ToolCallResult",
|
||||||
"ToolExecutor",
|
"ToolExecutor",
|
||||||
|
"prompt_to_discovered",
|
||||||
|
"resource_to_discovered",
|
||||||
|
"tool_result_to_call_result",
|
||||||
|
"tool_to_discovered",
|
||||||
|
"workflow_output_schema_from_mcp_tool_schema",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from mcp.types import CallToolResult as McpCallToolResult
|
||||||
|
from mcp.types import Prompt as McpPrompt
|
||||||
|
from mcp.types import Resource as McpResource
|
||||||
|
from mcp.types import Tool as McpTool
|
||||||
|
|
||||||
|
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||||
|
|
||||||
|
from .protocols import ToolCallResult
|
||||||
|
|
||||||
|
|
||||||
|
def tool_to_discovered(tool: McpTool) -> DiscoveredTool:
|
||||||
|
"""Convert an MCP SDK tool into the source discovery model."""
|
||||||
|
output_schema = workflow_output_schema_from_mcp_tool_schema(tool.outputSchema)
|
||||||
|
display_name = (
|
||||||
|
tool.annotations.title
|
||||||
|
if tool.annotations is not None and tool.annotations.title
|
||||||
|
else tool.title
|
||||||
|
)
|
||||||
|
return DiscoveredTool(
|
||||||
|
name=tool.name,
|
||||||
|
title=display_name,
|
||||||
|
description=tool.description,
|
||||||
|
input_schema=tool.inputSchema,
|
||||||
|
output_schema=output_schema,
|
||||||
|
outcomes=("ok", "error"),
|
||||||
|
metadata=tool.model_dump(by_alias=True, mode="json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def workflow_output_schema_from_mcp_tool_schema(
|
||||||
|
schema: dict[str, Any] | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return the MCP tool output schema without inventing workflow fields.
|
||||||
|
|
||||||
|
MCP tools without structured output expose raw content blocks. Those blocks
|
||||||
|
can be text, images, resource links, or mixed results, so this layer must not
|
||||||
|
pretend there is a stable top-level ``text`` field. Workflow authors should
|
||||||
|
add an explicit wrapper/extraction node for the block shape they expect.
|
||||||
|
"""
|
||||||
|
return schema or {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"content": {"type": "array"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def resource_to_discovered(resource: McpResource) -> DiscoveredResource:
|
||||||
|
"""Convert an MCP SDK resource into the source discovery model."""
|
||||||
|
local_name = resource.name or str(resource.uri)
|
||||||
|
return DiscoveredResource(
|
||||||
|
uri=str(resource.uri),
|
||||||
|
name=local_name,
|
||||||
|
title=resource.title,
|
||||||
|
description=resource.description,
|
||||||
|
mime_type=resource.mimeType,
|
||||||
|
metadata=resource.model_dump(by_alias=True, mode="json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def prompt_to_discovered(prompt: McpPrompt) -> DiscoveredPrompt:
|
||||||
|
"""Convert an MCP SDK prompt into the source discovery model."""
|
||||||
|
arguments = [
|
||||||
|
argument.model_dump(by_alias=True, mode="json")
|
||||||
|
for argument in prompt.arguments or []
|
||||||
|
]
|
||||||
|
return DiscoveredPrompt(
|
||||||
|
name=prompt.name,
|
||||||
|
title=prompt.title,
|
||||||
|
description=prompt.description,
|
||||||
|
arguments=arguments,
|
||||||
|
metadata=prompt.model_dump(by_alias=True, mode="json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def tool_result_to_call_result(result: McpCallToolResult) -> ToolCallResult:
|
||||||
|
"""Convert an MCP SDK tool call result into the adapter result model."""
|
||||||
|
if result.structuredContent is not None:
|
||||||
|
output = result.structuredContent
|
||||||
|
else:
|
||||||
|
output: dict[str, Any] = {
|
||||||
|
"content": [item.model_dump(by_alias=True) for item in result.content]
|
||||||
|
}
|
||||||
|
return ToolCallResult(
|
||||||
|
outcome="error" if result.isError else "ok",
|
||||||
|
output=output,
|
||||||
|
meta=result.meta or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"prompt_to_discovered",
|
||||||
|
"resource_to_discovered",
|
||||||
|
"tool_result_to_call_result",
|
||||||
|
"tool_to_discovered",
|
||||||
|
"workflow_output_schema_from_mcp_tool_schema",
|
||||||
|
]
|
||||||
@@ -124,3 +124,26 @@ def test_wf_mcp_runtime_protocol_shim_reexports_wf_sources_mcp_tool_executor() -
|
|||||||
|
|
||||||
assert CompatRuntimeToolExecutor is ToolExecutor
|
assert CompatRuntimeToolExecutor is ToolExecutor
|
||||||
assert CompatProtocolToolExecutor is ToolExecutor
|
assert CompatProtocolToolExecutor is ToolExecutor
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_mcp_sdk_converter_shim_reexports_wf_sources_mcp_converters() -> None:
|
||||||
|
from wf_mcp.sdk.converters import prompt_to_discovered as compat_prompt
|
||||||
|
from wf_mcp.sdk.converters import resource_to_discovered as compat_resource
|
||||||
|
from wf_mcp.sdk.converters import tool_result_to_call_result as compat_tool_result
|
||||||
|
from wf_mcp.sdk.converters import tool_to_discovered as compat_tool_to_discovered
|
||||||
|
from wf_mcp.sdk.converters import (
|
||||||
|
workflow_output_schema_from_mcp_tool_schema as compat_output_schema,
|
||||||
|
)
|
||||||
|
from wf_sources_mcp.sdk.converters import (
|
||||||
|
prompt_to_discovered,
|
||||||
|
resource_to_discovered,
|
||||||
|
tool_result_to_call_result,
|
||||||
|
tool_to_discovered,
|
||||||
|
workflow_output_schema_from_mcp_tool_schema,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert compat_prompt is prompt_to_discovered
|
||||||
|
assert compat_resource is resource_to_discovered
|
||||||
|
assert compat_tool_result is tool_result_to_call_result
|
||||||
|
assert compat_tool_to_discovered is tool_to_discovered
|
||||||
|
assert compat_output_schema is workflow_output_schema_from_mcp_tool_schema
|
||||||
|
|||||||
@@ -100,3 +100,28 @@ def test_wf_sources_mcp_does_not_import_old_sdk_protocol_modules() -> None:
|
|||||||
"wf_sources_mcp still imports old wf_mcp SDK/runtime protocol modules:\n"
|
"wf_sources_mcp still imports old wf_mcp SDK/runtime protocol modules:\n"
|
||||||
+ "\n".join(f" {violation}" for violation in violations)
|
+ "\n".join(f" {violation}" for violation in violations)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_sources_mcp_does_not_import_old_sdk_converter_module() -> None:
|
||||||
|
root = Path(__file__).resolve().parents[2] / "src" / "wf_sources_mcp"
|
||||||
|
forbidden = {
|
||||||
|
"wf_mcp.sdk.converters",
|
||||||
|
}
|
||||||
|
violations: list[str] = []
|
||||||
|
|
||||||
|
for py_file in sorted(root.rglob("*.py")):
|
||||||
|
rel = py_file.relative_to(root.parent)
|
||||||
|
module = str(rel.with_suffix("")).replace("/", ".").replace("\\", ".")
|
||||||
|
tree = ast.parse(py_file.read_text(encoding="utf-8"), filename=str(py_file))
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if isinstance(node, ast.ImportFrom) and node.module in forbidden:
|
||||||
|
violations.append(f"{module}:{node.lineno}: from {node.module} import ...")
|
||||||
|
elif isinstance(node, ast.Import):
|
||||||
|
for alias in node.names:
|
||||||
|
if alias.name in forbidden:
|
||||||
|
violations.append(f"{module}:{node.lineno}: import {alias.name}")
|
||||||
|
|
||||||
|
assert violations == [], (
|
||||||
|
"wf_sources_mcp still imports old wf_mcp SDK converter module:\n"
|
||||||
|
+ "\n".join(f" {violation}" for violation in violations)
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from mcp.types import CallToolResult, TextContent, Tool
|
||||||
|
|
||||||
|
from wf_sources_mcp.sdk.converters import tool_result_to_call_result, tool_to_discovered
|
||||||
|
|
||||||
|
|
||||||
|
def test_tool_without_output_schema_exposes_raw_content_schema() -> None:
|
||||||
|
tool = Tool(
|
||||||
|
name="echo",
|
||||||
|
inputSchema={"type": "object", "properties": {}},
|
||||||
|
)
|
||||||
|
|
||||||
|
discovered = tool_to_discovered(tool)
|
||||||
|
|
||||||
|
properties = discovered.output_schema["properties"]
|
||||||
|
assert properties["content"]["type"] == "array"
|
||||||
|
assert "text" not in properties
|
||||||
|
|
||||||
|
|
||||||
|
def test_tool_with_content_only_output_schema_stays_raw() -> None:
|
||||||
|
tool = Tool(
|
||||||
|
name="echo",
|
||||||
|
inputSchema={"type": "object", "properties": {}},
|
||||||
|
outputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Raw MCP content blocks.",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["content"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
discovered = tool_to_discovered(tool)
|
||||||
|
|
||||||
|
properties = discovered.output_schema["properties"]
|
||||||
|
assert properties["content"]["type"] == "array"
|
||||||
|
assert "text" not in properties
|
||||||
|
assert discovered.output_schema["required"] == ["content"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tool_result_single_text_content_block_stays_in_content() -> None:
|
||||||
|
result = CallToolResult(
|
||||||
|
content=[TextContent(type="text", text="Echo: hello")],
|
||||||
|
)
|
||||||
|
|
||||||
|
converted = tool_result_to_call_result(result)
|
||||||
|
|
||||||
|
assert converted.outcome == "ok"
|
||||||
|
assert "text" not in converted.output
|
||||||
|
assert converted.output["content"][0]["type"] == "text"
|
||||||
|
assert converted.output["content"][0]["text"] == "Echo: hello"
|
||||||
|
|
||||||
|
|
||||||
|
def test_tool_result_structured_content_is_not_rewritten() -> None:
|
||||||
|
result = CallToolResult(
|
||||||
|
content=[TextContent(type="text", text="ignored")],
|
||||||
|
structuredContent={"value": "structured"},
|
||||||
|
)
|
||||||
|
|
||||||
|
converted = tool_result_to_call_result(result)
|
||||||
|
|
||||||
|
assert converted.output["value"] == "structured"
|
||||||
|
assert "text" not in converted.output
|
||||||
Reference in New Issue
Block a user