refactor: move mcp discovered tool specs helper
This commit is contained in:
@@ -348,6 +348,9 @@ implementation state.
|
|||||||
- Completed: MCP discovered-tool wrapper generation (`wrap_discovered_tool`)
|
- Completed: MCP discovered-tool wrapper generation (`wrap_discovered_tool`)
|
||||||
now lives in `wf_sources_mcp.tool_wrappers`. `wf_mcp.workflow` remains a
|
now lives in `wf_sources_mcp.tool_wrappers`. `wf_mcp.workflow` remains a
|
||||||
compatibility shim; broker discovery imports the canonical wrapper.
|
compatibility shim; broker discovery imports the canonical wrapper.
|
||||||
|
- Completed: neutral `specs_from_discovered_tools` now lives in
|
||||||
|
`wf_sources_mcp.discovery`. `wf_mcp.broker.discovery` remains as the
|
||||||
|
broker adapter for `ConnectionConfig` and `McpEvent` projection.
|
||||||
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
|
||||||
|
|||||||
+508
@@ -0,0 +1,508 @@
|
|||||||
|
# MCP Specs From Discovered Tools Move 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 neutral `specs_from_discovered_tools` logic from `wf_mcp.broker.discovery` into `wf_sources_mcp.discovery`, leaving broker `McpEvent` projection in the compatibility adapter.
|
||||||
|
|
||||||
|
**Architecture:** `wf_sources_mcp.discovery.specs_from_discovered_tools` will accept `McpSourceConnection`, `AuthRecord`, `ToolExecutor`, `DiscoveredTool` list, and neutral `ToolWrapperEventSink`. `wf_mcp.broker.discovery.specs_from_discovered_tools` remains as the broker compatibility wrapper: it converts `ConnectionConfig` to `McpSourceConnection` and projects neutral `ToolWrapperEvent` into broker `McpEvent`.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, `wf_authoring.NodeSpec`, `wf_sources_mcp` typed connection/auth/sdk/tool wrapper DTOs, pytest, ruff, basedpyright.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hard Boundaries
|
||||||
|
|
||||||
|
- Do not remove `wf_mcp.broker.discovery.specs_from_discovered_tools`; keep it as a compatibility adapter.
|
||||||
|
- Do not change broker event kinds or payload shapes.
|
||||||
|
- Do not change generated `NodeSpec` behavior.
|
||||||
|
- Do not import `wf_mcp` from `src/wf_sources_mcp/discovery.py`.
|
||||||
|
- Keep `discover_connection_capabilities` in `wf_sources_mcp.discovery`.
|
||||||
|
- Do not touch `UpstreamTransportService` behavior beyond imports if needed.
|
||||||
|
- Do not commit unless the caller explicitly asks for a commit.
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
- Modify `src/wf_sources_mcp/discovery.py`: add canonical `specs_from_discovered_tools`.
|
||||||
|
- Modify `src/wf_sources_mcp/__init__.py`: export `specs_from_discovered_tools`.
|
||||||
|
- Modify `src/wf_mcp/broker/discovery.py`: import canonical specs helper under an alias and keep the broker adapter.
|
||||||
|
- Create or modify `tests/wf_sources_mcp/test_discovery.py`: add canonical specs tests.
|
||||||
|
- Modify `tests/wf_mcp/test_compat_imports.py`: add/extend compatibility test for broker adapter if useful.
|
||||||
|
- Modify docs: `docs/current_roadmap.md` and `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`.
|
||||||
|
- Move this plan to `docs/historical/superpowers/plans/` after implementation is verified.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add Canonical Specs Tests
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/wf_sources_mcp/test_discovery.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add imports needed for specs tests**
|
||||||
|
|
||||||
|
In `tests/wf_sources_mcp/test_discovery.py`, extend imports:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_authoring import build_async_registry
|
||||||
|
from wf_core import RuntimeContext
|
||||||
|
from wf_sources_mcp.discovery import (
|
||||||
|
discover_connection_capabilities,
|
||||||
|
specs_from_discovered_tools,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep existing imports for `AuthRecord`, `DiscoveredTool`, `McpSourceConnection`, `ToolCallResult`, and `BackendAdapter`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add a recording executor fake**
|
||||||
|
|
||||||
|
Append after existing adapter fakes:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class _RecordingExecutor:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.calls: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
async def call_tool(
|
||||||
|
self,
|
||||||
|
connection: McpSourceConnection,
|
||||||
|
auth: AuthRecord | None,
|
||||||
|
tool_name: str,
|
||||||
|
payload: dict[str, Any],
|
||||||
|
) -> ToolCallResult:
|
||||||
|
self.calls.append(
|
||||||
|
{
|
||||||
|
"connection": connection,
|
||||||
|
"auth": auth,
|
||||||
|
"tool_name": tool_name,
|
||||||
|
"payload": payload,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return ToolCallResult(
|
||||||
|
outcome="ok",
|
||||||
|
output={"content": [{"type": "text", "text": "Echo: hello"}]},
|
||||||
|
meta={"duration_ms": 3},
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add canonical specs behavior test**
|
||||||
|
|
||||||
|
Append:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def test_specs_from_discovered_tools_wraps_tools_with_neutral_events() -> None:
|
||||||
|
executor = _RecordingExecutor()
|
||||||
|
events = []
|
||||||
|
connection = _connection()
|
||||||
|
specs = specs_from_discovered_tools(
|
||||||
|
connection=connection,
|
||||||
|
auth=None,
|
||||||
|
executor=executor,
|
||||||
|
tools=[
|
||||||
|
DiscoveredTool(
|
||||||
|
name="echo",
|
||||||
|
title="Echo",
|
||||||
|
description="Echo input",
|
||||||
|
input_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"message": {"type": "string"}},
|
||||||
|
"required": ["message"],
|
||||||
|
},
|
||||||
|
output_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"content": {"type": "array"}},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
emit_event=events.append,
|
||||||
|
)
|
||||||
|
handler = build_async_registry(*specs)["echo"]
|
||||||
|
|
||||||
|
result = await handler(
|
||||||
|
{"message": "hello"},
|
||||||
|
RuntimeContext(current_node_id="echo"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["outcome"] == "ok"
|
||||||
|
assert result["output"]["content"][0]["text"] == "Echo: hello"
|
||||||
|
assert executor.calls[0]["connection"] is connection
|
||||||
|
assert executor.calls[0]["tool_name"] == "echo"
|
||||||
|
assert executor.calls[0]["payload"] == {"message": "hello"}
|
||||||
|
assert [event.kind for event in events] == [
|
||||||
|
"tool_call_started",
|
||||||
|
"tool_call_completed",
|
||||||
|
]
|
||||||
|
assert events[0].capability_id == "demo.default.echo"
|
||||||
|
assert events[1].payload == {"outcome": "ok", "meta": {"duration_ms": 3}}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add package-root export assertion**
|
||||||
|
|
||||||
|
Append:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_specs_from_discovered_tools_exports_from_package_root() -> None:
|
||||||
|
from wf_sources_mcp import specs_from_discovered_tools as root_specs_from_tools
|
||||||
|
from wf_sources_mcp.discovery import specs_from_discovered_tools
|
||||||
|
|
||||||
|
assert root_specs_from_tools is specs_from_discovered_tools
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run tests and verify failure before implementation**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_sources_mcp/test_discovery.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: fail because `wf_sources_mcp.discovery.specs_from_discovered_tools` is not defined/exported yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add Canonical `specs_from_discovered_tools`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_sources_mcp/discovery.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add imports**
|
||||||
|
|
||||||
|
In `src/wf_sources_mcp/discovery.py`, add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_authoring import NodeSpec
|
||||||
|
from wf_sources_mcp.tool_events import ToolWrapperEventSink
|
||||||
|
from wf_sources_mcp.tool_wrappers import wrap_discovered_tool
|
||||||
|
from wf_sources_mcp.sdk import BackendAdapter, ToolExecutor
|
||||||
|
```
|
||||||
|
|
||||||
|
If `BackendAdapter` is already imported, merge the import:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_sources_mcp.sdk import BackendAdapter, ToolExecutor
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add canonical function**
|
||||||
|
|
||||||
|
Append below `discover_connection_capabilities`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def specs_from_discovered_tools(
|
||||||
|
*,
|
||||||
|
connection: McpSourceConnection,
|
||||||
|
auth: AuthRecord | None,
|
||||||
|
executor: ToolExecutor,
|
||||||
|
tools: list[DiscoveredTool],
|
||||||
|
emit_event: ToolWrapperEventSink | None = None,
|
||||||
|
) -> list[NodeSpec[Any, Any]]:
|
||||||
|
return [
|
||||||
|
wrap_discovered_tool(
|
||||||
|
connection=connection,
|
||||||
|
auth=auth,
|
||||||
|
executor=executor,
|
||||||
|
tool=tool,
|
||||||
|
emit_event=emit_event,
|
||||||
|
)
|
||||||
|
for tool in tools
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update `__all__`**
|
||||||
|
|
||||||
|
Change:
|
||||||
|
|
||||||
|
```python
|
||||||
|
__all__ = ["DiscoveredConnectionCapabilities", "discover_connection_capabilities"]
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
__all__ = [
|
||||||
|
"DiscoveredConnectionCapabilities",
|
||||||
|
"discover_connection_capabilities",
|
||||||
|
"specs_from_discovered_tools",
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run canonical discovery tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_sources_mcp/test_discovery.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: tests still fail only if package-root export is not wired yet. Direct module tests should pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Export `specs_from_discovered_tools` From Package Root
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_sources_mcp/__init__.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add lazy export**
|
||||||
|
|
||||||
|
Add `"specs_from_discovered_tools"` to `__all__`.
|
||||||
|
|
||||||
|
In the existing discovery `__getattr__` branch, include this name:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if name in {
|
||||||
|
"DiscoveredConnectionCapabilities",
|
||||||
|
"discover_connection_capabilities",
|
||||||
|
"specs_from_discovered_tools",
|
||||||
|
}:
|
||||||
|
from . import discovery
|
||||||
|
|
||||||
|
return getattr(discovery, name)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run canonical discovery tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_sources_mcp/test_discovery.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all tests pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Make Broker Discovery a Compatibility Adapter
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_mcp/broker/discovery.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Import canonical specs helper under an alias**
|
||||||
|
|
||||||
|
Change imports:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_sources_mcp.discovery import (
|
||||||
|
DiscoveredConnectionCapabilities,
|
||||||
|
discover_connection_capabilities,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_sources_mcp.discovery import (
|
||||||
|
DiscoveredConnectionCapabilities,
|
||||||
|
discover_connection_capabilities,
|
||||||
|
specs_from_discovered_tools as source_specs_from_discovered_tools,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Remove now-unneeded imports**
|
||||||
|
|
||||||
|
Remove:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from wf_authoring import NodeSpec
|
||||||
|
from wf_sources_mcp.catalog import DiscoveredTool
|
||||||
|
from wf_sources_mcp.tool_wrappers import wrap_discovered_tool
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from typing import Any
|
||||||
|
from collections.abc import Callable
|
||||||
|
from wf_sources_mcp.connections import mcp_source_connection_from_connection_config
|
||||||
|
from wf_sources_mcp.sdk import ToolExecutor
|
||||||
|
from wf_sources_mcp.tool_events import ToolWrapperEvent
|
||||||
|
```
|
||||||
|
|
||||||
|
If basedpyright needs the return type annotation, keep `NodeSpec` and `DiscoveredTool`; otherwise prefer keeping the public signature unchanged:
|
||||||
|
|
||||||
|
```python
|
||||||
|
) -> list[NodeSpec[Any, Any]]:
|
||||||
|
```
|
||||||
|
|
||||||
|
In that case, keep `from wf_authoring import NodeSpec` and `from wf_sources_mcp.catalog import DiscoveredTool`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Replace function body to delegate to canonical helper**
|
||||||
|
|
||||||
|
Inside `specs_from_discovered_tools`, keep `source_connection` and `emit_tool_event`, then replace the list comprehension with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return source_specs_from_discovered_tools(
|
||||||
|
connection=source_connection,
|
||||||
|
auth=auth,
|
||||||
|
executor=executor,
|
||||||
|
tools=tools,
|
||||||
|
emit_event=emit_tool_event if emit_event is not None else None,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run broker event behavior test**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_mcp/service/test_events.py::test_service_records_tool_call_events -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Add Compatibility Test for Broker Adapter
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/wf_mcp/test_compat_imports.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add assertion that broker adapter remains distinct but callable**
|
||||||
|
|
||||||
|
Append:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def test_wf_mcp_broker_discovery_keeps_specs_adapter() -> None:
|
||||||
|
from wf_mcp.broker.discovery import specs_from_discovered_tools as broker_specs
|
||||||
|
from wf_sources_mcp.discovery import specs_from_discovered_tools as source_specs
|
||||||
|
|
||||||
|
assert broker_specs is not source_specs
|
||||||
|
assert broker_specs.__name__ == "specs_from_discovered_tools"
|
||||||
|
```
|
||||||
|
|
||||||
|
This is intentionally not an identity test because the broker function adapts `ConnectionConfig` and broker `McpEvent`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run compatibility tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_mcp/test_compat_imports.py::test_wf_mcp_broker_discovery_keeps_specs_adapter -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: pass.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Update Docs and Archive Plan
|
||||||
|
|
||||||
|
**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-08-wf-sources-mcp-specs-from-discovered-tools.md` to `docs/historical/superpowers/plans/2026-06-08-wf-sources-mcp-specs-from-discovered-tools.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update `docs/current_roadmap.md`**
|
||||||
|
|
||||||
|
Under the `wf_sources_mcp` cleanup section, add:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- Completed: neutral `specs_from_discovered_tools` now lives in
|
||||||
|
`wf_sources_mcp.discovery`. `wf_mcp.broker.discovery` remains as the
|
||||||
|
broker adapter for `ConnectionConfig` and `McpEvent` projection.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Update `docs/superpowers/specs/2026-06-03-long-lived-workflow-api-boundary.md`**
|
||||||
|
|
||||||
|
Add a completed numbered item before the pending upstream transport/discovery/session services item:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
20. Complete: neutral `specs_from_discovered_tools` moved to
|
||||||
|
`wf_sources_mcp.discovery`. `wf_mcp.broker.discovery` remains as the broker
|
||||||
|
adapter for legacy `ConnectionConfig` input and `McpEvent` projection.
|
||||||
|
```
|
||||||
|
|
||||||
|
If numbering differs because new items landed meanwhile, keep the completed item before the broad pending item and renumber.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Archive the plan**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git mv docs/superpowers/plans/2026-06-08-wf-sources-mcp-specs-from-discovered-tools.md docs/historical/superpowers/plans/2026-06-08-wf-sources-mcp-specs-from-discovered-tools.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `git status --short` shows an `R` rename for the plan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Final Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- No code edits unless verification finds a real issue.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run focused tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_sources_mcp/test_discovery.py tests/wf_sources_mcp/test_tool_wrappers.py tests/wf_sources_mcp/test_import_direction_guard.py tests/wf_mcp/test_compat_imports.py tests/wf_mcp/service/test_events.py::test_service_records_tool_call_events tests/wf_mcp/service/test_catalog.py tests/wf_mcp/service/test_upstream_transport.py -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all selected tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run source-provider tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run pytest tests/wf_sources_mcp -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all `wf_sources_mcp` tests pass.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run lint**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run ruff check src/wf_sources_mcp/discovery.py src/wf_mcp/broker/discovery.py tests/wf_sources_mcp/test_discovery.py tests/wf_mcp/test_compat_imports.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `All checks passed!`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run typecheck**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run basedpyright --level error src/wf_sources_mcp/discovery.py src/wf_mcp/broker/discovery.py tests/wf_sources_mcp/test_discovery.py tests/wf_mcp/test_compat_imports.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `0 errors, 0 warnings, 0 notes`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Check remaining broker discovery import usage**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rg -n "specs_from_discovered_tools|wf_mcp\\.broker\\.discovery|from \\.discovery import" src tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected:
|
||||||
|
|
||||||
|
- `src/wf_sources_mcp/discovery.py` owns neutral `specs_from_discovered_tools`.
|
||||||
|
- `src/wf_mcp/broker/discovery.py` owns broker adapter `specs_from_discovered_tools`.
|
||||||
|
- `src/wf_mcp/broker/service/upstream_transport.py` may still import broker adapter.
|
||||||
|
- `src/wf_sources_mcp` must not import `wf_mcp.broker.discovery`.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Check whitespace**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no whitespace errors. CRLF warnings on Windows are acceptable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expected Final Report
|
||||||
|
|
||||||
|
The implementer should report:
|
||||||
|
|
||||||
|
- Files created, modified, and moved.
|
||||||
|
- Exact verification commands and pass/fail output.
|
||||||
|
- Confirmation that neutral `specs_from_discovered_tools` is canonical in `wf_sources_mcp.discovery`.
|
||||||
|
- Confirmation that `wf_mcp.broker.discovery.specs_from_discovered_tools` remains as a broker adapter.
|
||||||
|
- Confirmation that broker tool-call events still emit `McpEvent`.
|
||||||
|
- Any deviations from this plan.
|
||||||
|
|
||||||
|
Do not claim "full suite passed" unless the full suite was actually run.
|
||||||
@@ -145,7 +145,10 @@ First slices should move leaf modules only and leave `wf_mcp` re-export shims:
|
|||||||
moved to `wf_sources_mcp.tool_wrappers`, with `wf_mcp.workflow` retained as
|
moved to `wf_sources_mcp.tool_wrappers`, with `wf_mcp.workflow` retained as
|
||||||
a compatibility shim. `specs_from_discovered_tools` remains in `wf_mcp`
|
a compatibility shim. `specs_from_discovered_tools` remains in `wf_mcp`
|
||||||
as the broker event projection adapter.
|
as the broker event projection adapter.
|
||||||
20. Upstream transport/discovery/session services.
|
20. Complete: neutral `specs_from_discovered_tools` moved to
|
||||||
|
`wf_sources_mcp.discovery`. `wf_mcp.broker.discovery` remains as the broker
|
||||||
|
adapter for legacy `ConnectionConfig` input and `McpEvent` projection.
|
||||||
|
21. 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`,
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ from wf_sources_mcp.discovery import (
|
|||||||
DiscoveredConnectionCapabilities,
|
DiscoveredConnectionCapabilities,
|
||||||
discover_connection_capabilities,
|
discover_connection_capabilities,
|
||||||
)
|
)
|
||||||
|
from wf_sources_mcp.discovery import (
|
||||||
|
specs_from_discovered_tools as source_specs_from_discovered_tools,
|
||||||
|
)
|
||||||
from wf_sources_mcp.sdk import ToolExecutor
|
from wf_sources_mcp.sdk import ToolExecutor
|
||||||
from wf_sources_mcp.tool_events import ToolWrapperEvent
|
from wf_sources_mcp.tool_events import ToolWrapperEvent
|
||||||
from wf_sources_mcp.tool_wrappers import wrap_discovered_tool
|
|
||||||
|
|
||||||
from ..auth import AuthRecord
|
from ..auth import AuthRecord
|
||||||
from ..models import ConnectionConfig
|
from ..models import ConnectionConfig
|
||||||
@@ -45,16 +47,13 @@ def specs_from_discovered_tools(
|
|||||||
if emit_event is not None:
|
if emit_event is not None:
|
||||||
emit_event(_project_tool_wrapper_event(event))
|
emit_event(_project_tool_wrapper_event(event))
|
||||||
|
|
||||||
return [
|
return source_specs_from_discovered_tools(
|
||||||
wrap_discovered_tool(
|
connection=source_connection,
|
||||||
connection=source_connection,
|
auth=auth,
|
||||||
auth=auth,
|
executor=executor,
|
||||||
executor=executor,
|
tools=tools,
|
||||||
tool=tool,
|
emit_event=emit_tool_event if emit_event is not None else None,
|
||||||
emit_event=emit_tool_event if emit_event is not None else None,
|
)
|
||||||
)
|
|
||||||
for tool in tools
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ __all__ = [
|
|||||||
"model_from_schema",
|
"model_from_schema",
|
||||||
"neutral_auth_from_mcp",
|
"neutral_auth_from_mcp",
|
||||||
"registry_entry_to_connection_config",
|
"registry_entry_to_connection_config",
|
||||||
|
"specs_from_discovered_tools",
|
||||||
"tool_call_completed_event",
|
"tool_call_completed_event",
|
||||||
"tool_call_started_event",
|
"tool_call_started_event",
|
||||||
"ToolWrapperEvent",
|
"ToolWrapperEvent",
|
||||||
@@ -106,6 +107,7 @@ def __getattr__(name: str) -> object:
|
|||||||
if name in {
|
if name in {
|
||||||
"DiscoveredConnectionCapabilities",
|
"DiscoveredConnectionCapabilities",
|
||||||
"discover_connection_capabilities",
|
"discover_connection_capabilities",
|
||||||
|
"specs_from_discovered_tools",
|
||||||
}:
|
}:
|
||||||
from . import discovery
|
from . import discovery
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ from typing import Any, TypeVar
|
|||||||
from mcp import McpError
|
from mcp import McpError
|
||||||
from mcp.types import METHOD_NOT_FOUND
|
from mcp.types import METHOD_NOT_FOUND
|
||||||
|
|
||||||
|
from wf_authoring import NodeSpec
|
||||||
from wf_sources_mcp.auth import AuthRecord
|
from wf_sources_mcp.auth import AuthRecord
|
||||||
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||||
from wf_sources_mcp.connections import McpSourceConnection
|
from wf_sources_mcp.connections import McpSourceConnection
|
||||||
from wf_sources_mcp.sdk import BackendAdapter
|
from wf_sources_mcp.sdk import BackendAdapter, ToolExecutor
|
||||||
|
from wf_sources_mcp.tool_events import ToolWrapperEventSink
|
||||||
|
from wf_sources_mcp.tool_wrappers import wrap_discovered_tool
|
||||||
|
|
||||||
_CapabilityT = TypeVar("_CapabilityT")
|
_CapabilityT = TypeVar("_CapabilityT")
|
||||||
|
|
||||||
@@ -74,4 +77,28 @@ def _root_exception(exc: BaseException) -> BaseException:
|
|||||||
return current
|
return current
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["DiscoveredConnectionCapabilities", "discover_connection_capabilities"]
|
def specs_from_discovered_tools(
|
||||||
|
*,
|
||||||
|
connection: McpSourceConnection,
|
||||||
|
auth: AuthRecord | None,
|
||||||
|
executor: ToolExecutor,
|
||||||
|
tools: list[DiscoveredTool],
|
||||||
|
emit_event: ToolWrapperEventSink | None = None,
|
||||||
|
) -> list[NodeSpec[Any, Any]]:
|
||||||
|
return [
|
||||||
|
wrap_discovered_tool(
|
||||||
|
connection=connection,
|
||||||
|
auth=auth,
|
||||||
|
executor=executor,
|
||||||
|
tool=tool,
|
||||||
|
emit_event=emit_event,
|
||||||
|
)
|
||||||
|
for tool in tools
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DiscoveredConnectionCapabilities",
|
||||||
|
"discover_connection_capabilities",
|
||||||
|
"specs_from_discovered_tools",
|
||||||
|
]
|
||||||
|
|||||||
@@ -237,3 +237,11 @@ def test_wf_mcp_workflow_wrapper_shim_reexports_wf_sources_mcp_tool_wrapper() ->
|
|||||||
|
|
||||||
assert compat_package_wrap is wrap_discovered_tool
|
assert compat_package_wrap is wrap_discovered_tool
|
||||||
assert compat_module_wrap is wrap_discovered_tool
|
assert compat_module_wrap is wrap_discovered_tool
|
||||||
|
|
||||||
|
|
||||||
|
def test_wf_mcp_broker_discovery_keeps_specs_adapter() -> None:
|
||||||
|
from wf_mcp.broker.discovery import specs_from_discovered_tools as broker_specs
|
||||||
|
from wf_sources_mcp.discovery import specs_from_discovered_tools as source_specs
|
||||||
|
|
||||||
|
assert broker_specs is not source_specs
|
||||||
|
assert broker_specs.__name__ == "specs_from_discovered_tools"
|
||||||
|
|||||||
@@ -6,10 +6,15 @@ import pytest
|
|||||||
from mcp import McpError
|
from mcp import McpError
|
||||||
from mcp.types import ErrorData
|
from mcp.types import ErrorData
|
||||||
|
|
||||||
|
from wf_authoring import build_async_registry
|
||||||
|
from wf_core import RuntimeContext
|
||||||
from wf_sources_mcp.auth import AuthRecord
|
from wf_sources_mcp.auth import AuthRecord
|
||||||
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
from wf_sources_mcp.catalog import DiscoveredPrompt, DiscoveredResource, DiscoveredTool
|
||||||
from wf_sources_mcp.connections import McpSourceConnection
|
from wf_sources_mcp.connections import McpSourceConnection
|
||||||
from wf_sources_mcp.discovery import discover_connection_capabilities
|
from wf_sources_mcp.discovery import (
|
||||||
|
discover_connection_capabilities,
|
||||||
|
specs_from_discovered_tools,
|
||||||
|
)
|
||||||
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
|
from wf_sources_mcp.sdk import BackendAdapter, ToolCallResult
|
||||||
from wf_sources_mcp.transports import StdioSourceTransport
|
from wf_sources_mcp.transports import StdioSourceTransport
|
||||||
|
|
||||||
@@ -194,6 +199,85 @@ def test_backend_adapter_static_shape() -> None:
|
|||||||
assert adapter is not None
|
assert adapter is not None
|
||||||
|
|
||||||
|
|
||||||
|
class _RecordingExecutor:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.calls: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
async def call_tool(
|
||||||
|
self,
|
||||||
|
connection: McpSourceConnection,
|
||||||
|
auth: AuthRecord | None,
|
||||||
|
tool_name: str,
|
||||||
|
payload: dict[str, Any],
|
||||||
|
) -> ToolCallResult:
|
||||||
|
self.calls.append(
|
||||||
|
{
|
||||||
|
"connection": connection,
|
||||||
|
"auth": auth,
|
||||||
|
"tool_name": tool_name,
|
||||||
|
"payload": payload,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return ToolCallResult(
|
||||||
|
outcome="ok",
|
||||||
|
output={"content": [{"type": "text", "text": "Echo: hello"}]},
|
||||||
|
meta={"duration_ms": 3},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_specs_from_discovered_tools_wraps_tools_with_neutral_events() -> None:
|
||||||
|
executor = _RecordingExecutor()
|
||||||
|
events: list[Any] = []
|
||||||
|
connection = _connection()
|
||||||
|
specs = specs_from_discovered_tools(
|
||||||
|
connection=connection,
|
||||||
|
auth=None,
|
||||||
|
executor=executor,
|
||||||
|
tools=[
|
||||||
|
DiscoveredTool(
|
||||||
|
name="echo",
|
||||||
|
title="Echo",
|
||||||
|
description="Echo input",
|
||||||
|
input_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"message": {"type": "string"}},
|
||||||
|
"required": ["message"],
|
||||||
|
},
|
||||||
|
output_schema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"content": {"type": "array"}},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
emit_event=events.append,
|
||||||
|
)
|
||||||
|
handler = build_async_registry(*specs)["echo"]
|
||||||
|
|
||||||
|
result = await handler(
|
||||||
|
{"message": "hello"},
|
||||||
|
RuntimeContext(current_node_id="echo"),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["outcome"] == "ok"
|
||||||
|
assert result["output"]["content"][0]["text"] == "Echo: hello"
|
||||||
|
assert executor.calls[0]["connection"] is connection
|
||||||
|
assert executor.calls[0]["tool_name"] == "echo"
|
||||||
|
assert executor.calls[0]["payload"] == {"message": "hello"}
|
||||||
|
assert [event.kind for event in events] == [
|
||||||
|
"tool_call_started",
|
||||||
|
"tool_call_completed",
|
||||||
|
]
|
||||||
|
assert events[0].capability_id == "demo.default.echo"
|
||||||
|
assert events[1].payload == {"outcome": "ok", "meta": {"duration_ms": 3}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_specs_from_discovered_tools_exports_from_package_root() -> None:
|
||||||
|
from wf_sources_mcp import specs_from_discovered_tools as root_specs_from_tools
|
||||||
|
from wf_sources_mcp.discovery import specs_from_discovered_tools
|
||||||
|
|
||||||
|
assert root_specs_from_tools is specs_from_discovered_tools
|
||||||
|
|
||||||
|
|
||||||
def test_discovery_symbols_export_from_package_root() -> None:
|
def test_discovery_symbols_export_from_package_root() -> None:
|
||||||
from wf_sources_mcp import (
|
from wf_sources_mcp import (
|
||||||
DiscoveredConnectionCapabilities as RootDiscoveredConnectionCapabilities,
|
DiscoveredConnectionCapabilities as RootDiscoveredConnectionCapabilities,
|
||||||
|
|||||||
Reference in New Issue
Block a user