draft verification MCP
This commit is contained in:
@@ -510,10 +510,12 @@ Concrete MCP sequence:
|
|||||||
6. Use focused helpers such as `wf.workflow.set_draft_name` or
|
6. Use focused helpers such as `wf.workflow.set_draft_name` or
|
||||||
`wf.workflow.set_draft_route`, or call `wf.workflow.patch_draft_workspace`
|
`wf.workflow.set_draft_route`, or call `wf.workflow.patch_draft_workspace`
|
||||||
with the current `revision` for arbitrary JSON Patch edits.
|
with the current `revision` for arbitrary JSON Patch edits.
|
||||||
7. `wf.workflow.create_artifact_from_workspace` after validation is clean.
|
7. `wf.workflow.validate_draft_workspace` if capabilities changed or you want
|
||||||
8. `wf.workflow.save_deployment`, then `validate_deployment`, then
|
to refresh diagnostics without editing the draft.
|
||||||
|
8. `wf.workflow.create_artifact_from_workspace` after validation is clean.
|
||||||
|
9. `wf.workflow.save_deployment`, then `validate_deployment`, then
|
||||||
`run_deployment`.
|
`run_deployment`.
|
||||||
9. `wf.workflow.delete_draft_workspace` when the mutable authoring session is no
|
10. `wf.workflow.delete_draft_workspace` when the mutable authoring session is no
|
||||||
longer needed.
|
longer needed.
|
||||||
|
|
||||||
`create_artifact_from_workspace` also uses a `request` object:
|
`create_artifact_from_workspace` also uses a `request` object:
|
||||||
|
|||||||
@@ -352,6 +352,7 @@ resending the full draft each turn.
|
|||||||
| List existing draft sessions | `wf.workflow.list_draft_workspaces` |
|
| List existing draft sessions | `wf.workflow.list_draft_workspaces` |
|
||||||
| Fetch current draft workspace | `wf.workflow.get_draft_workspace` |
|
| Fetch current draft workspace | `wf.workflow.get_draft_workspace` |
|
||||||
| Patch current draft workspace | `wf.workflow.patch_draft_workspace` |
|
| Patch current draft workspace | `wf.workflow.patch_draft_workspace` |
|
||||||
|
| Refresh validation without changing revision | `wf.workflow.validate_draft_workspace` |
|
||||||
| Change common draft fields without JSON Patch | `wf.workflow.set_draft_name`, `wf.workflow.set_draft_route`, `wf.workflow.set_step_input_map`, `wf.workflow.set_step_output_map` |
|
| Change common draft fields without JSON Patch | `wf.workflow.set_draft_name`, `wf.workflow.set_draft_route`, `wf.workflow.set_step_input_map`, `wf.workflow.set_step_output_map` |
|
||||||
| Save final workspace as artifact | `wf.workflow.create_artifact_from_workspace` |
|
| Save final workspace as artifact | `wf.workflow.create_artifact_from_workspace` |
|
||||||
| Clean up a draft workspace | `wf.workflow.delete_draft_workspace` |
|
| Clean up a draft workspace | `wf.workflow.delete_draft_workspace` |
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ _SEARCH_ALWAYS_VISIBLE_TOOL_NAMES = [
|
|||||||
"wf.workflow.get_draft_workspace",
|
"wf.workflow.get_draft_workspace",
|
||||||
"wf.workflow.delete_draft_workspace",
|
"wf.workflow.delete_draft_workspace",
|
||||||
"wf.workflow.patch_draft_workspace",
|
"wf.workflow.patch_draft_workspace",
|
||||||
|
"wf.workflow.validate_draft_workspace",
|
||||||
"wf.workflow.set_draft_name",
|
"wf.workflow.set_draft_name",
|
||||||
"wf.workflow.set_draft_route",
|
"wf.workflow.set_draft_route",
|
||||||
"wf.workflow.set_step_input_map",
|
"wf.workflow.set_step_input_map",
|
||||||
|
|||||||
@@ -419,6 +419,20 @@ class WorkflowSurfaceHandlers:
|
|||||||
"status": "deleted" if deleted else "not_found",
|
"status": "deleted" if deleted else "not_found",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def validate_draft_workspace(self, *, workspace_id: str) -> dict[str, Any]:
|
||||||
|
"""Refresh stored validation status without changing draft revision."""
|
||||||
|
store = self._draft_store()
|
||||||
|
workspace = store.get_workspace(workspace_id)
|
||||||
|
validation = await self.validate_draft(draft=workspace.draft)
|
||||||
|
refreshed = workspace.model_copy(
|
||||||
|
update={
|
||||||
|
"status": validation["status"],
|
||||||
|
"diagnostics": validation["diagnostics"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
store.save_workspace(refreshed)
|
||||||
|
return get_draft_workspace_record(store, workspace_id=workspace_id)
|
||||||
|
|
||||||
async def patch_draft_workspace(
|
async def patch_draft_workspace(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -115,6 +115,12 @@ class PatchDraftWorkspaceRequest(BaseModel):
|
|||||||
patch: JsonPatchOperations
|
patch: JsonPatchOperations
|
||||||
|
|
||||||
|
|
||||||
|
class ValidateDraftWorkspaceRequest(BaseModel):
|
||||||
|
"""Typed MCP request for refreshing one workspace validation status."""
|
||||||
|
|
||||||
|
workspace_id: WorkspaceId
|
||||||
|
|
||||||
|
|
||||||
class SetDraftNameRequest(BaseModel):
|
class SetDraftNameRequest(BaseModel):
|
||||||
"""Typed MCP request for changing the workflow draft name."""
|
"""Typed MCP request for changing the workflow draft name."""
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from .models import (
|
|||||||
SetDraftRouteRequest,
|
SetDraftRouteRequest,
|
||||||
SetStepInputMapRequest,
|
SetStepInputMapRequest,
|
||||||
SetStepOutputMapRequest,
|
SetStepOutputMapRequest,
|
||||||
|
ValidateDraftWorkspaceRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -277,6 +278,21 @@ def register_workflow_tools(server: FastMCP[Any], service: WfMcpService) -> None
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@server.tool(
|
||||||
|
name="wf.workflow.validate_draft_workspace",
|
||||||
|
title="Validate Draft Workspace",
|
||||||
|
description=(
|
||||||
|
"Refresh validation status for one draft workspace without changing "
|
||||||
|
"its revision."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def validate_draft_workspace(
|
||||||
|
request: ValidateDraftWorkspaceRequest,
|
||||||
|
) -> DraftWorkspaceResult:
|
||||||
|
return DraftWorkspaceResult.model_validate(
|
||||||
|
await handlers.validate_draft_workspace(workspace_id=request.workspace_id)
|
||||||
|
)
|
||||||
|
|
||||||
@server.tool(
|
@server.tool(
|
||||||
name="wf.workflow.set_draft_name",
|
name="wf.workflow.set_draft_name",
|
||||||
title="Set Draft Name",
|
title="Set Draft Name",
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ def test_server_exposes_upstream_admin_and_workflow_tools() -> None:
|
|||||||
assert "wf.workflow.get_draft_workspace" in names
|
assert "wf.workflow.get_draft_workspace" in names
|
||||||
assert "wf.workflow.delete_draft_workspace" in names
|
assert "wf.workflow.delete_draft_workspace" in names
|
||||||
assert "wf.workflow.patch_draft_workspace" in names
|
assert "wf.workflow.patch_draft_workspace" in names
|
||||||
|
assert "wf.workflow.validate_draft_workspace" in names
|
||||||
assert "wf.workflow.set_draft_name" in names
|
assert "wf.workflow.set_draft_name" in names
|
||||||
assert "wf.workflow.set_draft_route" in names
|
assert "wf.workflow.set_draft_route" in names
|
||||||
assert "wf.workflow.set_step_input_map" in names
|
assert "wf.workflow.set_step_input_map" in names
|
||||||
@@ -198,6 +199,7 @@ def test_server_search_mode_pins_stable_control_and_workflow_tools() -> None:
|
|||||||
assert "wf.workflow.get_draft_workspace" in names
|
assert "wf.workflow.get_draft_workspace" in names
|
||||||
assert "wf.workflow.delete_draft_workspace" in names
|
assert "wf.workflow.delete_draft_workspace" in names
|
||||||
assert "wf.workflow.patch_draft_workspace" in names
|
assert "wf.workflow.patch_draft_workspace" in names
|
||||||
|
assert "wf.workflow.validate_draft_workspace" in names
|
||||||
assert "wf.workflow.set_draft_name" in names
|
assert "wf.workflow.set_draft_name" in names
|
||||||
assert "wf.workflow.set_draft_route" in names
|
assert "wf.workflow.set_draft_route" in names
|
||||||
assert "wf.workflow.set_step_input_map" in names
|
assert "wf.workflow.set_step_input_map" in names
|
||||||
|
|||||||
@@ -492,6 +492,39 @@ def test_workflow_surface_patch_helpers_update_draft_workspace() -> None:
|
|||||||
assert fetched["draft"]["steps"]["echo"]["out"] == {"echoed": "state.echoed"}
|
assert fetched["draft"]["steps"]["echo"]["out"] == {"echoed": "state.echoed"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_workflow_surface_validates_draft_workspace_with_live_outcomes() -> None:
|
||||||
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
|
local_temp_root() / "surface_workspace_validate"
|
||||||
|
)
|
||||||
|
service = WfMcpService(
|
||||||
|
store=FileStore(local_temp_root() / "surface_workspace_validate_mcp"),
|
||||||
|
artifact_store=artifact_store,
|
||||||
|
)
|
||||||
|
service.register_connection(
|
||||||
|
ConnectionConfig(id="demo.personal", server="demo", account="personal")
|
||||||
|
)
|
||||||
|
service.register_specs("demo.personal", echo_tool)
|
||||||
|
handlers = WorkflowSurfaceHandlers(service)
|
||||||
|
draft = _echo_draft()
|
||||||
|
draft["routes"]["echo"] = {"typo": "__end__"}
|
||||||
|
asyncio.run(
|
||||||
|
handlers.create_draft_workspace(
|
||||||
|
workspace_id="echo_draft",
|
||||||
|
draft=draft,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = asyncio.run(
|
||||||
|
handlers.validate_draft_workspace(workspace_id="echo_draft")
|
||||||
|
)
|
||||||
|
fetched = asyncio.run(handlers.get_draft_workspace(workspace_id="echo_draft"))
|
||||||
|
|
||||||
|
assert payload["revision"] == 1
|
||||||
|
assert payload["status"] == "invalid"
|
||||||
|
assert payload["diagnostics"][0]["code"] == "unknown_outcome"
|
||||||
|
assert fetched["status"] == "invalid"
|
||||||
|
|
||||||
|
|
||||||
def test_workflow_surface_patches_draft_workspace_by_revision() -> None:
|
def test_workflow_surface_patches_draft_workspace_by_revision() -> None:
|
||||||
artifact_store = FileWorkflowArtifactStore(
|
artifact_store = FileWorkflowArtifactStore(
|
||||||
local_temp_root() / "surface_workspace_patch"
|
local_temp_root() / "surface_workspace_patch"
|
||||||
|
|||||||
Reference in New Issue
Block a user