create-a-workflow! end to end usage!
This commit is contained in:
@@ -0,0 +1,439 @@
|
|||||||
|
# Unified MCP Surface And Protocol Proxy 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:** Converge broker mode and transparent proxy mode into one MCP server surface that supports direct upstream capability projection, stable workflow/admin tools, protocol-level proxying, and local broker notifications.
|
||||||
|
|
||||||
|
**Architecture:** Use one service/config/store layer and explicit projection layers. The unified server should expose upstream tools/resources/prompts directly when enabled, expose stable local workflow/admin tools, and route bidirectional MCP protocol features through well-named proxy components rather than ad hoc tool wrappers.
|
||||||
|
|
||||||
|
**Tech Stack:** FastMCP 3.x, MCP Python SDK, existing `WfMcpService`, transparent proxy runtime, capability source registry, pytest, basedpyright.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why This Plan Exists
|
||||||
|
|
||||||
|
The current project has two partial MCP surfaces:
|
||||||
|
|
||||||
|
- Broker mode exposes stable control/workflow tools such as
|
||||||
|
`create_workflow_artifact_from_plan`, `validate_workflow_deployment`, and
|
||||||
|
`run_workflow_deployment`.
|
||||||
|
- Transparent proxy mode exposes upstream capabilities directly through MCP
|
||||||
|
`tools/list` and `tools/call`.
|
||||||
|
|
||||||
|
That split is not enough. A real MCP proxy has more than tools:
|
||||||
|
|
||||||
|
- resources
|
||||||
|
- prompts
|
||||||
|
- tool calls
|
||||||
|
- notifications
|
||||||
|
- progress
|
||||||
|
- logging
|
||||||
|
- resource subscriptions
|
||||||
|
- elicitation
|
||||||
|
- sampling
|
||||||
|
- tasks
|
||||||
|
- capability/list-changed events
|
||||||
|
|
||||||
|
The unified server must not be a "tools-only proxy." It needs a protocol plan
|
||||||
|
that decides what is pass-through, what is projected, what is local, what is
|
||||||
|
unsupported, and what needs explicit client/server capability negotiation.
|
||||||
|
|
||||||
|
## Definitions
|
||||||
|
|
||||||
|
### Upstream Server
|
||||||
|
|
||||||
|
An MCP server configured as a connection, such as `everything.default`,
|
||||||
|
`context7.default`, or `serena.default`.
|
||||||
|
|
||||||
|
### Downstream Client
|
||||||
|
|
||||||
|
The MCP client connected to our server, such as Codex or MCP Inspector.
|
||||||
|
|
||||||
|
### Local Capability
|
||||||
|
|
||||||
|
A capability implemented by this project, such as `wf.workflow.run_deployment`
|
||||||
|
or `wf.admin.list_connections`.
|
||||||
|
|
||||||
|
### Proxy Capability
|
||||||
|
|
||||||
|
A capability discovered from an upstream server and projected through our
|
||||||
|
server, such as `everything.default_echo`.
|
||||||
|
|
||||||
|
### Protocol Proxy
|
||||||
|
|
||||||
|
Bidirectional routing for MCP protocol messages that are not ordinary
|
||||||
|
tools/resources/prompts list/read/get calls. Examples: sampling requests,
|
||||||
|
elicitation requests, progress notifications, task status notifications, and
|
||||||
|
resource update notifications.
|
||||||
|
|
||||||
|
## Target Surface
|
||||||
|
|
||||||
|
One MCP server instance should expose both proxy capabilities and local
|
||||||
|
capabilities.
|
||||||
|
|
||||||
|
```text
|
||||||
|
upstream tools:
|
||||||
|
everything.default_echo
|
||||||
|
context7.default_query-docs
|
||||||
|
|
||||||
|
upstream resources:
|
||||||
|
everything.default.instructions.md
|
||||||
|
|
||||||
|
upstream prompts:
|
||||||
|
everything.default.simple-prompt
|
||||||
|
|
||||||
|
stable workflow tools:
|
||||||
|
wf.workflow.list_artifacts
|
||||||
|
wf.workflow.create_artifact_from_plan
|
||||||
|
wf.workflow.save_deployment
|
||||||
|
wf.workflow.validate_deployment
|
||||||
|
wf.workflow.run_deployment
|
||||||
|
|
||||||
|
admin tools, only when explicitly enabled:
|
||||||
|
wf.admin.list_connections
|
||||||
|
wf.admin.refresh_connection_catalog
|
||||||
|
wf.admin.list_proxy_tools
|
||||||
|
wf.admin.reload_config
|
||||||
|
```
|
||||||
|
|
||||||
|
Compatibility broker tool names such as `get_planner_catalog` can remain during
|
||||||
|
migration, but namespaced tools should become the recommended interface.
|
||||||
|
|
||||||
|
## Protocol Coverage Matrix
|
||||||
|
|
||||||
|
| MCP Feature | Near-Term Behavior | Long-Term Behavior | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `tools/list` | Project upstream tools plus local workflow/admin tools | Same, with pagination/search | Already partially supported in transparent proxy and broker separately. |
|
||||||
|
| `tools/call` | Forward upstream tool calls and execute local tools | Same, with tasks/progress support | Local tools should share handlers across modes. |
|
||||||
|
| `resources/list` | Project upstream resources | Same, plus local docs/resources | Resources-as-tools remains optional compatibility. |
|
||||||
|
| `resources/read` | Forward upstream reads | Same, with subscriptions | Needs namespacing and URI/local-name mapping. |
|
||||||
|
| `prompts/list` | Project upstream prompts | Same, plus local authoring manuals | Prompts-as-tools remains optional compatibility. |
|
||||||
|
| `prompts/get` | Forward upstream prompt rendering | Same | Must preserve arguments and metadata. |
|
||||||
|
| `notifications/progress` | Forward where the SDK/server surface supports it | First-class run/proxy progress bus | Local workflow runs should emit progress later. |
|
||||||
|
| `notifications/resources/updated` | Not reliable yet | Proxy subscriptions with lifecycle tracking | Requires subscription ownership and reload behavior. |
|
||||||
|
| `notifications/resources/list_changed` | Emit local changed events after refresh/reload if supported | Also forward upstream changes | Needed when tools/resources/prompts change. |
|
||||||
|
| `notifications/tools/list_changed` | Emit after config reload/catalog refresh if supported | Same | Important for clients that refresh tools. |
|
||||||
|
| `notifications/prompts/list_changed` | Emit after prompt catalog changes if supported | Same | Same shape as tool/resource changed. |
|
||||||
|
| `notifications/message` / logging | Proxy upstream logging where supported | Add local broker logging notifications | Everything server has logging examples. |
|
||||||
|
| Elicitation | Do not fake it as a tool | Route upstream elicitation to downstream client | Requires bidirectional request routing and capability checks. |
|
||||||
|
| Sampling | Do not fake it as a tool | Route upstream sampling to downstream client | Requires downstream client sampling support. |
|
||||||
|
| Tasks | Do not invent custom primary API | Use MCP Tasks for long-running runs where supported | Custom `start_run` only as compatibility fallback. |
|
||||||
|
| Ping | Support local ping and keep upstream health separately | Same | Upstream ping should be a health/admin operation, not necessarily forwarded blindly. |
|
||||||
|
|
||||||
|
## Current Risks
|
||||||
|
|
||||||
|
- FastMCP mount/unmount lifecycle is not complete enough for safe dynamic
|
||||||
|
unmount of all proxied capabilities.
|
||||||
|
- Transparent reload is currently best-effort.
|
||||||
|
- Bidirectional upstream requests such as elicitation/sampling require access to
|
||||||
|
the downstream client session, not just an upstream SDK client.
|
||||||
|
- Notifications must be scoped: a resource update from `everything.default`
|
||||||
|
should not look like a local broker config update.
|
||||||
|
- Clients vary. Codex may not immediately refresh tool lists. Inspector may show
|
||||||
|
more protocol features. The proxy must be robust even when clients ignore
|
||||||
|
optional notifications.
|
||||||
|
|
||||||
|
## Required Boundaries
|
||||||
|
|
||||||
|
### Shared Service Layer
|
||||||
|
|
||||||
|
`WfMcpService` or a sibling service owns:
|
||||||
|
|
||||||
|
- configured connections
|
||||||
|
- adapters
|
||||||
|
- auth/catalog stores
|
||||||
|
- capability sources
|
||||||
|
- workflow artifact store
|
||||||
|
- events
|
||||||
|
|
||||||
|
It should not own FastMCP decorators directly.
|
||||||
|
|
||||||
|
### Projection Layer
|
||||||
|
|
||||||
|
Projection modules register MCP-visible capabilities:
|
||||||
|
|
||||||
|
- upstream tools/resources/prompts
|
||||||
|
- local workflow tools
|
||||||
|
- local admin tools
|
||||||
|
- local docs/prompts/resources
|
||||||
|
|
||||||
|
Projection modules call shared handlers. They should not contain business logic.
|
||||||
|
|
||||||
|
### Protocol Routing Layer
|
||||||
|
|
||||||
|
Protocol routing handles bidirectional features:
|
||||||
|
|
||||||
|
- upstream-to-downstream elicitation
|
||||||
|
- upstream-to-downstream sampling
|
||||||
|
- upstream/local notifications
|
||||||
|
- tasks/progress
|
||||||
|
- subscriptions
|
||||||
|
|
||||||
|
This layer should be explicit. Do not bury protocol routing inside a generic
|
||||||
|
`call_tool` helper.
|
||||||
|
|
||||||
|
### Event/Notification Bus
|
||||||
|
|
||||||
|
Local events should be emitted once and then projected to:
|
||||||
|
|
||||||
|
- stored broker events
|
||||||
|
- MCP notifications where the client supports them
|
||||||
|
- future UI/dashboard streams
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
connection_registered
|
||||||
|
catalog_refresh_started
|
||||||
|
catalog_refresh_completed
|
||||||
|
tool_call_started
|
||||||
|
tool_call_completed
|
||||||
|
workflow_artifact_saved
|
||||||
|
workflow_deployment_saved
|
||||||
|
workflow_run_started
|
||||||
|
workflow_run_progress
|
||||||
|
workflow_run_completed
|
||||||
|
source_enabled
|
||||||
|
source_disabled
|
||||||
|
config_reloaded
|
||||||
|
```
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Complete `2026-05-12-workflow-artifact-hardening.md`.
|
||||||
|
- Keep artifact operations in shared callable functions, not duplicated
|
||||||
|
FastMCP decorators.
|
||||||
|
- Keep config mutation/admin operations behind explicit admin exposure.
|
||||||
|
- Document client capability assumptions for elicitation, sampling, tasks, and
|
||||||
|
notifications.
|
||||||
|
- Add tests against the fixture MCP server and the everything server where
|
||||||
|
practical.
|
||||||
|
|
||||||
|
## Phase 1: Inventory And Adapter Reality Check
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/mcp_protocol_proxy_inventory.md`
|
||||||
|
- Inspect: `src/wf_mcp/sdk/adapter.py`
|
||||||
|
- Inspect: `src/wf_mcp/transparent_proxy/runtime.py`
|
||||||
|
- Inspect: `src/wf_mcp/broker/artifact_tools.py`
|
||||||
|
- Test: no new tests required in this phase
|
||||||
|
|
||||||
|
- [ ] List which MCP SDK APIs are currently wrapped by `BackendAdapter`.
|
||||||
|
- [ ] List which FastMCP server APIs we currently use.
|
||||||
|
- [ ] List which features everything-server exposes that we can test:
|
||||||
|
- progress
|
||||||
|
- logging
|
||||||
|
- resource updates
|
||||||
|
- elicitation
|
||||||
|
- sampling
|
||||||
|
- tasks
|
||||||
|
- [ ] Identify SDK gaps before implementation. If an MCP feature is not
|
||||||
|
accessible through FastMCP/MCP SDK at our current version, document it instead
|
||||||
|
of inventing a fake abstraction.
|
||||||
|
|
||||||
|
## Phase 2: Extract Shared Workflow/Admin Handlers
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/wf_mcp/workflow_surface/handlers.py`
|
||||||
|
- Create: `src/wf_mcp/admin_surface/handlers.py`
|
||||||
|
- Modify: `src/wf_mcp/broker/artifact_tools.py`
|
||||||
|
- Modify: `src/wf_mcp/broker/tools.py`
|
||||||
|
- Modify: `src/wf_mcp/transparent_proxy/admin.py`
|
||||||
|
- Test: `tests/wf_mcp/test_broker_server.py`
|
||||||
|
- Test: `tests/wf_mcp/test_transparent_proxy.py`
|
||||||
|
|
||||||
|
- [ ] Move workflow artifact list/save/inspect/validate/run logic into shared
|
||||||
|
handler functions/classes.
|
||||||
|
- [ ] Move admin list/refresh/config/reload logic into shared handler
|
||||||
|
functions/classes.
|
||||||
|
- [ ] Keep broker compatibility tool names working.
|
||||||
|
- [ ] Keep transparent proxy admin tool names working.
|
||||||
|
- [ ] Do not change behavior in this phase; only remove duplicated logic and
|
||||||
|
create a single implementation path.
|
||||||
|
|
||||||
|
## Phase 3: Unified Server Factory
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/wf_mcp/server/unified.py`
|
||||||
|
- Modify: `src/wf_mcp/cli.py`
|
||||||
|
- Modify: `src/wf_mcp/broker/server.py`
|
||||||
|
- Test: `tests/wf_mcp/test_unified_server.py`
|
||||||
|
|
||||||
|
- [ ] Build one FastMCP server from `BrokerConfig`.
|
||||||
|
- [ ] Register local workflow tools with namespaced names:
|
||||||
|
- `wf.workflow.list_artifacts`
|
||||||
|
- `wf.workflow.create_artifact_from_plan`
|
||||||
|
- `wf.workflow.save_artifact`
|
||||||
|
- `wf.workflow.list_deployments`
|
||||||
|
- `wf.workflow.save_deployment`
|
||||||
|
- `wf.workflow.validate_deployment`
|
||||||
|
- `wf.workflow.run_deployment`
|
||||||
|
- [ ] Register admin tools only when admin exposure is enabled.
|
||||||
|
- [ ] Project upstream tools using the transparent proxy path.
|
||||||
|
- [ ] Keep existing `broker` and `proxy` CLI modes during migration.
|
||||||
|
- [ ] Add `unified` CLI mode.
|
||||||
|
- [ ] Do not make unified default until manual Inspector/Codex tests pass.
|
||||||
|
|
||||||
|
## Phase 4: Namespacing And Collision Policy
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_mcp/shared/names.py`
|
||||||
|
- Test: `tests/wf_mcp/test_names.py`
|
||||||
|
- Test: `tests/wf_mcp/test_unified_server.py`
|
||||||
|
|
||||||
|
- [ ] Use `wf.workflow.*` for stable workflow tools.
|
||||||
|
- [ ] Use `wf.admin.*` for privileged admin/control tools.
|
||||||
|
- [ ] Keep `wf.mcp.*` for workflow runtime helpers, not admin.
|
||||||
|
- [ ] Keep upstream proxy names collision-safe.
|
||||||
|
- [ ] Reject configured connection ids that collide with reserved local
|
||||||
|
namespaces.
|
||||||
|
- [ ] Decide whether compatibility broker names remain visible by default in
|
||||||
|
unified mode. Recommended: yes during migration, no after migration.
|
||||||
|
|
||||||
|
## Phase 5: Tool/Resource/Prompt Projection Parity
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_mcp/transparent_proxy/runtime.py`
|
||||||
|
- Modify: unified server files from Phase 3.
|
||||||
|
- Test: `tests/wf_mcp/test_unified_server.py`
|
||||||
|
|
||||||
|
- [ ] Ensure upstream tools and stable workflow/admin tools both appear in
|
||||||
|
`tools/list`.
|
||||||
|
- [ ] Ensure upstream resources appear in `resources/list` and can be read.
|
||||||
|
- [ ] Ensure upstream prompts appear in `prompts/list` and can be rendered.
|
||||||
|
- [ ] Ensure resources-as-tools and prompts-as-tools remain optional projection
|
||||||
|
modes, not the only way to access resources/prompts.
|
||||||
|
- [ ] Ensure search/pagination includes stable local tools and upstream tools.
|
||||||
|
|
||||||
|
## Phase 6: Local Notification Bus
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/wf_mcp/events/bus.py`
|
||||||
|
- Modify: `src/wf_mcp/broker/events.py`
|
||||||
|
- Modify: `src/wf_mcp/broker/service/core.py`
|
||||||
|
- Modify: unified server files from Phase 3.
|
||||||
|
- Test: `tests/wf_mcp/test_events.py`
|
||||||
|
|
||||||
|
- [ ] Introduce an in-process event bus abstraction.
|
||||||
|
- [ ] Keep the existing stored `McpEvent` list as one subscriber/sink.
|
||||||
|
- [ ] Add event kinds for workflow artifacts and deployments:
|
||||||
|
- `workflow_artifact_saved`
|
||||||
|
- `workflow_deployment_saved`
|
||||||
|
- `workflow_run_started`
|
||||||
|
- `workflow_run_completed`
|
||||||
|
- `workflow_run_failed`
|
||||||
|
- [ ] Add event kinds for capability changes:
|
||||||
|
- `source_enabled`
|
||||||
|
- `source_disabled`
|
||||||
|
- `catalog_changed`
|
||||||
|
- `tools_changed`
|
||||||
|
- `resources_changed`
|
||||||
|
- `prompts_changed`
|
||||||
|
- [ ] Do not emit MCP notifications yet unless the server/session API is
|
||||||
|
clearly available. This phase creates the source of truth.
|
||||||
|
|
||||||
|
## Phase 7: MCP Notifications
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: unified server files from Phase 3.
|
||||||
|
- Modify: `src/wf_mcp/events/bus.py`
|
||||||
|
- Test: `tests/wf_mcp/test_unified_server.py`
|
||||||
|
|
||||||
|
- [ ] Emit MCP list-changed notifications when local or upstream catalogs
|
||||||
|
change, if supported:
|
||||||
|
- `notifications/tools/list_changed`
|
||||||
|
- `notifications/resources/list_changed`
|
||||||
|
- `notifications/prompts/list_changed`
|
||||||
|
- [ ] Emit local workflow progress notifications where supported.
|
||||||
|
- [ ] Proxy upstream logging notifications where supported.
|
||||||
|
- [ ] Ensure clients that ignore notifications can still poll/list manually.
|
||||||
|
- [ ] Add tests that assert notifications are requested/emitted through whatever
|
||||||
|
FastMCP/MCP SDK surface is available. If no testable surface exists, document
|
||||||
|
the limitation in `docs/mcp_protocol_proxy_inventory.md`.
|
||||||
|
|
||||||
|
## Phase 8: Elicitation And Sampling Routing
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/wf_mcp/protocol/elicitation.py`
|
||||||
|
- Create: `src/wf_mcp/protocol/sampling.py`
|
||||||
|
- Modify: SDK adapter/session layer if supported.
|
||||||
|
- Test: `tests/wf_mcp/test_protocol_proxy.py`
|
||||||
|
|
||||||
|
- [ ] Determine how upstream MCP SDK exposes server-to-client elicitation
|
||||||
|
requests.
|
||||||
|
- [ ] Determine how FastMCP exposes downstream client elicitation responses.
|
||||||
|
- [ ] Route upstream elicitation requests to the downstream client only when the
|
||||||
|
downstream client advertised support.
|
||||||
|
- [ ] Route upstream sampling requests to the downstream client only when the
|
||||||
|
downstream client advertised support.
|
||||||
|
- [ ] Preserve request ids/correlation ids so responses return to the correct
|
||||||
|
upstream session.
|
||||||
|
- [ ] Return structured unsupported diagnostics when routing is impossible.
|
||||||
|
- [ ] Do not convert elicitation/sampling into normal tools as the primary
|
||||||
|
behavior.
|
||||||
|
|
||||||
|
## Phase 9: Tasks And Long-Running Workflow Runs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/wf_mcp/workflow_surface/runs.py`
|
||||||
|
- Modify: unified server files from Phase 3.
|
||||||
|
- Test: `tests/wf_mcp/test_workflow_tasks.py`
|
||||||
|
|
||||||
|
- [ ] Prefer MCP Tasks for long-running `wf.workflow.run_deployment` when the
|
||||||
|
client/server support task execution.
|
||||||
|
- [ ] Keep synchronous run behavior for short/manual local tests.
|
||||||
|
- [ ] Add a compatibility run store only if MCP Tasks are unavailable or
|
||||||
|
insufficient for Codex/Inspector.
|
||||||
|
- [ ] Map workflow interrupts to task status such as `input_required` only after
|
||||||
|
the runtime supports the needed resume model.
|
||||||
|
- [ ] Do not implement durable scheduling/cron here.
|
||||||
|
|
||||||
|
## Phase 10: Mode Migration
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/wf_mcp_architecture.md`
|
||||||
|
- Modify: `docs/wf_mcp_capability_sources.md`
|
||||||
|
- Modify: `src/wf_mcp/cli.py`
|
||||||
|
- Test: `tests/wf_mcp/test_cli.py`
|
||||||
|
|
||||||
|
- [ ] Document unified mode as the recommended local mode once it passes manual
|
||||||
|
Codex and Inspector checks.
|
||||||
|
- [ ] Keep broker/proxy modes as compatibility modes.
|
||||||
|
- [ ] Mark compatibility broker tool names as legacy once namespaced local tools
|
||||||
|
work in unified mode.
|
||||||
|
- [ ] Do not delete compatibility modes until tests cover every important
|
||||||
|
surface.
|
||||||
|
|
||||||
|
## Manual Verification Checklist
|
||||||
|
|
||||||
|
- [ ] Codex can list upstream tools.
|
||||||
|
- [ ] Codex can call `everything.default_echo` or equivalent upstream tool.
|
||||||
|
- [ ] Codex can call `wf.workflow.list_artifacts`.
|
||||||
|
- [ ] Codex can create an artifact from a plan.
|
||||||
|
- [ ] Codex can save a deployment.
|
||||||
|
- [ ] Codex can validate and run a deployment.
|
||||||
|
- [ ] Inspector shows upstream resources and prompts.
|
||||||
|
- [ ] Inspector shows local workflow tools with useful names/descriptions.
|
||||||
|
- [ ] If everything-server elicitation is triggered, the proxy either routes it
|
||||||
|
correctly or returns a clear unsupported diagnostic.
|
||||||
|
- [ ] If everything-server progress/logging is triggered, the proxy either
|
||||||
|
forwards it correctly or documents why not.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No native `wf_core` subgraph implementation.
|
||||||
|
- No UI/dashboard implementation.
|
||||||
|
- No cron/scheduler implementation.
|
||||||
|
- No hidden conversion of every protocol feature into a tool.
|
||||||
|
- No pretending unsupported protocol features are proxied.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
The project can run one local MCP server that gives an LLM client both:
|
||||||
|
|
||||||
|
- direct access to upstream MCP capabilities
|
||||||
|
- stable workflow/admin capabilities implemented by this project
|
||||||
|
|
||||||
|
The implementation must be explicit about unsupported protocol features and must
|
||||||
|
have a path to proxy elicitation, sampling, notifications, and tasks without
|
||||||
|
duplicating mode-specific logic.
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Workflow Artifact Hardening 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:** Make saved workflow artifacts safe enough to rely on before merging MCP server modes or exposing workflow execution more broadly.
|
||||||
|
|
||||||
|
**Architecture:** Keep `wf_artifacts` provider-neutral. `wf_mcp` may project artifact operations, but validation, storage, artifact creation, and diagnostics should stay in `wf_artifacts` where possible.
|
||||||
|
|
||||||
|
**Tech Stack:** Python 3.14, Pydantic v2, pytest, `wf_core.Workflow` validation, existing `wf_mcp` broker service.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why This Comes First
|
||||||
|
|
||||||
|
The live Codex probe proved the artifact execution path works, but it also found
|
||||||
|
a real weakness: `create_workflow_artifact_from_plan` saved an invalid
|
||||||
|
`state_schema` shape, and the error surfaced only when `run_workflow_deployment`
|
||||||
|
tried to compile the workflow.
|
||||||
|
|
||||||
|
Before unifying broker and transparent proxy modes, artifact creation should
|
||||||
|
reject invalid plans earlier and return structured diagnostics.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This plan hardens saved workflow creation and dependency validation. It does not
|
||||||
|
merge MCP server modes and does not add native subgraphs.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
### Task 1: Validate Plan Shape During Artifact Creation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_artifacts/factory.py`
|
||||||
|
- Test: `tests/artifacts/test_factory.py`
|
||||||
|
|
||||||
|
- [ ] Add a failing test proving `create_workflow_artifact_from_plan` rejects a
|
||||||
|
plan that cannot become a `wf_core.Workflow`.
|
||||||
|
- [ ] Implement validation by constructing `wf_core.Workflow.model_validate`
|
||||||
|
from the plan fields.
|
||||||
|
- [ ] Keep the validation dependency one-way: `wf_artifacts` may import
|
||||||
|
`wf_core`, but `wf_core` must not import `wf_artifacts`.
|
||||||
|
- [ ] Return `ValueError` with a concise message containing the failing field
|
||||||
|
path or Pydantic error message.
|
||||||
|
- [ ] Run `uv run --with pytest pytest tests\artifacts\test_factory.py -q`.
|
||||||
|
|
||||||
|
### Task 2: Add Artifact Creation Diagnostics
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_artifacts/models.py`
|
||||||
|
- Modify: `src/wf_artifacts/factory.py`
|
||||||
|
- Test: `tests/artifacts/test_factory.py`
|
||||||
|
|
||||||
|
- [ ] Decide whether creation failures should raise exceptions only or also
|
||||||
|
expose a `validate_workflow_artifact_plan(...) -> list[DependencyDiagnostic]`
|
||||||
|
style function.
|
||||||
|
- [ ] Recommended v1: add a separate `validate_workflow_artifact_plan(plan)`
|
||||||
|
that returns structured diagnostics, while the factory still raises on errors.
|
||||||
|
- [ ] Add tests for missing `input_schema`, missing `output_schema`, invalid
|
||||||
|
state schema, and missing start node.
|
||||||
|
|
||||||
|
### Task 3: Validate Direct Workflow Dependencies
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_artifacts/validation.py`
|
||||||
|
- Test: `tests/artifacts/test_validation.py`
|
||||||
|
|
||||||
|
- [ ] Add artifact-store-aware validation for `workflow_dependencies`.
|
||||||
|
- [ ] Validate exact artifact-version pins.
|
||||||
|
- [ ] Return `dependency_missing` when a child artifact version does not exist.
|
||||||
|
- [ ] Return `dependency_cycle` when direct/transitive workflow artifact
|
||||||
|
dependencies cycle.
|
||||||
|
- [ ] Do not copy child dependency snapshots into the parent.
|
||||||
|
|
||||||
|
### Task 4: Improve Capability Contract Checking
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_artifacts/validation.py`
|
||||||
|
- Test: `tests/artifacts/test_validation.py`
|
||||||
|
- Optional: `src/wf_mcp/broker/artifact_tools.py`
|
||||||
|
|
||||||
|
- [ ] Preserve current hash comparison behavior.
|
||||||
|
- [ ] Add tests for kind mismatch, for example required `tool` but available
|
||||||
|
`node_spec`.
|
||||||
|
- [ ] Decide whether `node_spec` can satisfy `tool` when the provider is an MCP
|
||||||
|
wrapper. Recommended: no implicit kind coercion in `wf_artifacts`; adapters
|
||||||
|
should present the available kind they mean to expose.
|
||||||
|
- [ ] Add diagnostics with `code="capability_kind_mismatch"`.
|
||||||
|
|
||||||
|
### Task 5: Document Current Runtime Limitations In Tool Responses
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/wf_mcp/broker/artifact_tools.py`
|
||||||
|
- Test: `tests/wf_mcp/test_broker_server.py`
|
||||||
|
|
||||||
|
- [ ] Keep interrupting artifacts rejected by `run_workflow_deployment`.
|
||||||
|
- [ ] Add `repair_hint` text explaining native subgraphs/nested resume are not
|
||||||
|
implemented yet.
|
||||||
|
- [ ] Add a test proving unsupported interrupt artifacts return a diagnostic
|
||||||
|
instead of raising.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- [ ] `uv run --with pytest pytest tests\artifacts -q`
|
||||||
|
- [ ] `uv run --with pytest pytest tests\wf_mcp\test_broker_server.py -q`
|
||||||
|
- [ ] `uv run --with pytest pytest -q`
|
||||||
|
- [ ] `uv run ruff check src tests examples main.py`
|
||||||
|
- [ ] `uv run basedpyright src tests examples main.py --level error`
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- No native `wf_core` subgraph implementation.
|
||||||
|
- No MCP Tasks implementation.
|
||||||
|
- No run history persistence.
|
||||||
|
- No broker/transparent mode merge in this plan.
|
||||||
@@ -3,6 +3,7 @@ from .catalog import (
|
|||||||
artifact_catalog_entry,
|
artifact_catalog_entry,
|
||||||
artifact_node_name,
|
artifact_node_name,
|
||||||
)
|
)
|
||||||
|
from .factory import create_workflow_artifact_from_plan
|
||||||
from .models import (
|
from .models import (
|
||||||
AvailableCapability,
|
AvailableCapability,
|
||||||
AvailableSource,
|
AvailableSource,
|
||||||
@@ -30,5 +31,6 @@ __all__ = [
|
|||||||
"WorkflowDeployment",
|
"WorkflowDeployment",
|
||||||
"artifact_catalog_entry",
|
"artifact_catalog_entry",
|
||||||
"artifact_node_name",
|
"artifact_node_name",
|
||||||
|
"create_workflow_artifact_from_plan",
|
||||||
"validate_deployment_dependencies",
|
"validate_deployment_dependencies",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
from .models import JsonObject, RequiredCapability, WorkflowArtifact
|
||||||
|
|
||||||
|
|
||||||
|
def create_workflow_artifact_from_plan(
|
||||||
|
*,
|
||||||
|
artifact_id: str,
|
||||||
|
version: int,
|
||||||
|
title: str,
|
||||||
|
plan: JsonObject,
|
||||||
|
outcomes: tuple[str, ...],
|
||||||
|
description: str | None = None,
|
||||||
|
required_capabilities: Mapping[str, RequiredCapability] | None = None,
|
||||||
|
created_from_catalog_version: str | None = None,
|
||||||
|
) -> WorkflowArtifact:
|
||||||
|
"""Create an immutable artifact from a declarative workflow plan."""
|
||||||
|
return WorkflowArtifact(
|
||||||
|
id=artifact_id,
|
||||||
|
version=version,
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
input_schema=_required_object_field(plan, "input_schema"),
|
||||||
|
output_schema=_required_object_field(plan, "output_schema"),
|
||||||
|
outcomes=outcomes,
|
||||||
|
plan=plan,
|
||||||
|
required_capabilities=dict(required_capabilities or {}),
|
||||||
|
created_from_catalog_version=created_from_catalog_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _required_object_field(plan: JsonObject, field_name: str) -> JsonObject:
|
||||||
|
value = plan.get(field_name)
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise ValueError(f"workflow plan is missing object field {field_name!r}")
|
||||||
|
return value
|
||||||
@@ -8,8 +8,10 @@ from wf_artifacts import (
|
|||||||
AvailableSource,
|
AvailableSource,
|
||||||
DependencyDiagnostic,
|
DependencyDiagnostic,
|
||||||
DiagnosticSeverity,
|
DiagnosticSeverity,
|
||||||
|
RequiredCapability,
|
||||||
WorkflowArtifact,
|
WorkflowArtifact,
|
||||||
WorkflowDeployment,
|
WorkflowDeployment,
|
||||||
|
create_workflow_artifact_from_plan as build_workflow_artifact_from_plan,
|
||||||
validate_deployment_dependencies,
|
validate_deployment_dependencies,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,6 +44,39 @@ def register_artifact_tools(server: FastMCP, service: WfMcpService) -> None:
|
|||||||
"saved": True,
|
"saved": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@server.tool()
|
||||||
|
async def create_workflow_artifact_from_plan(
|
||||||
|
artifact_id: str,
|
||||||
|
version: int,
|
||||||
|
title: str,
|
||||||
|
plan: dict[str, Any],
|
||||||
|
outcomes: list[str],
|
||||||
|
description: str | None = None,
|
||||||
|
required_capabilities: dict[str, dict[str, Any]] | None = None,
|
||||||
|
created_from_catalog_version: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if service.artifact_store is None:
|
||||||
|
raise KeyError("workflow artifact store is not configured")
|
||||||
|
workflow_artifact = build_workflow_artifact_from_plan(
|
||||||
|
artifact_id=artifact_id,
|
||||||
|
version=version,
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
plan=plan,
|
||||||
|
outcomes=tuple(outcomes),
|
||||||
|
required_capabilities={
|
||||||
|
name: RequiredCapability.model_validate(capability)
|
||||||
|
for name, capability in (required_capabilities or {}).items()
|
||||||
|
},
|
||||||
|
created_from_catalog_version=created_from_catalog_version,
|
||||||
|
)
|
||||||
|
service.artifact_store.save_artifact(workflow_artifact)
|
||||||
|
return {
|
||||||
|
"artifact_id": workflow_artifact.id,
|
||||||
|
"version": workflow_artifact.version,
|
||||||
|
"saved": True,
|
||||||
|
}
|
||||||
|
|
||||||
@server.tool()
|
@server.tool()
|
||||||
async def inspect_workflow_artifact(
|
async def inspect_workflow_artifact(
|
||||||
artifact_id: str,
|
artifact_id: str,
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from wf_artifacts import RequiredCapability, create_workflow_artifact_from_plan
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_workflow_artifact_from_plan_derives_boundary_schemas() -> None:
|
||||||
|
artifact = create_workflow_artifact_from_plan(
|
||||||
|
artifact_id="echo",
|
||||||
|
version=1,
|
||||||
|
title="Echo",
|
||||||
|
description="Echo through a saved plan.",
|
||||||
|
plan=_plan(),
|
||||||
|
outcomes=("done",),
|
||||||
|
required_capabilities={
|
||||||
|
"demo.echo_tool": RequiredCapability(
|
||||||
|
logical_source="demo",
|
||||||
|
capability_name="echo_tool",
|
||||||
|
kind="node_spec",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
created_from_catalog_version="catalog-1",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert artifact.id == "echo"
|
||||||
|
assert artifact.version == 1
|
||||||
|
assert artifact.title == "Echo"
|
||||||
|
assert artifact.input_schema["properties"]["text"]["type"] == "string"
|
||||||
|
assert artifact.output_schema["properties"]["echoed"]["type"] == "string"
|
||||||
|
assert artifact.outcomes == ("done",)
|
||||||
|
assert artifact.plan["name"] == "echo"
|
||||||
|
assert "demo.echo_tool" in artifact.required_capabilities
|
||||||
|
assert artifact.created_from_catalog_version == "catalog-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_workflow_artifact_from_plan_rejects_missing_boundary_schema() -> None:
|
||||||
|
plan = _plan()
|
||||||
|
plan.pop("output_schema")
|
||||||
|
|
||||||
|
try:
|
||||||
|
create_workflow_artifact_from_plan(
|
||||||
|
artifact_id="echo",
|
||||||
|
version=1,
|
||||||
|
title="Echo",
|
||||||
|
plan=plan,
|
||||||
|
outcomes=("done",),
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
assert "output_schema" in str(exc)
|
||||||
|
else:
|
||||||
|
raise AssertionError("expected missing output_schema to be rejected")
|
||||||
|
|
||||||
|
|
||||||
|
def _plan() -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"name": "echo",
|
||||||
|
"input_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"text": {"type": "string"}},
|
||||||
|
"required": ["text"],
|
||||||
|
},
|
||||||
|
"state_schema": {"fields": {"echoed": {"type": "string"}}},
|
||||||
|
"output_schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {"echoed": {"type": "string"}},
|
||||||
|
"required": ["echoed"],
|
||||||
|
},
|
||||||
|
"start": "echo",
|
||||||
|
"nodes": [],
|
||||||
|
"edges": [],
|
||||||
|
}
|
||||||
@@ -283,6 +283,48 @@ def test_broker_saves_workflow_artifact() -> None:
|
|||||||
assert loaded.title == "Summarize Docs"
|
assert loaded.title == "Summarize Docs"
|
||||||
|
|
||||||
|
|
||||||
|
def test_broker_creates_workflow_artifact_from_plan() -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
|
local_temp_root() / "broker_create_artifact_from_plan"
|
||||||
|
)
|
||||||
|
service = WfMcpService(
|
||||||
|
store=FileStore(local_temp_root() / "broker_create_artifact_mcp_store"),
|
||||||
|
artifact_store=artifact_store,
|
||||||
|
)
|
||||||
|
server = create_broker_server(service)
|
||||||
|
|
||||||
|
_content, structured = asyncio.run(
|
||||||
|
server.call_tool(
|
||||||
|
"create_workflow_artifact_from_plan",
|
||||||
|
{
|
||||||
|
"artifact_id": "echo",
|
||||||
|
"version": 1,
|
||||||
|
"title": "Echo",
|
||||||
|
"description": "Echo through a saved plan.",
|
||||||
|
"plan": _echo_artifact().plan,
|
||||||
|
"outcomes": ["done"],
|
||||||
|
"required_capabilities": {
|
||||||
|
"demo.echo_tool": {
|
||||||
|
"logical_source": "demo",
|
||||||
|
"capability_name": "echo_tool",
|
||||||
|
"kind": "node_spec",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_from_catalog_version": "catalog-1",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
payload = cast(dict[str, Any], cast(object, structured))
|
||||||
|
loaded = artifact_store.get_artifact("echo", 1)
|
||||||
|
|
||||||
|
assert payload["artifact_id"] == "echo"
|
||||||
|
assert payload["version"] == 1
|
||||||
|
assert payload["saved"] is True
|
||||||
|
assert loaded.input_schema["properties"]["text"]["type"] == "string"
|
||||||
|
assert loaded.output_schema["properties"]["echoed"]["type"] == "string"
|
||||||
|
assert loaded.required_capabilities["demo.echo_tool"].logical_source == "demo"
|
||||||
|
|
||||||
|
|
||||||
def test_broker_saves_and_lists_workflow_deployments() -> None:
|
def test_broker_saves_and_lists_workflow_deployments() -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
local_temp_root() / "broker_save_deployments"
|
local_temp_root() / "broker_save_deployments"
|
||||||
|
|||||||
Reference in New Issue
Block a user