Uh oh!
There was an error while loading. Please reload this page.
feat(mcp): conductor mcp serve — expose workflows as MCP tools (#432) - #491
Draft
Jason Robert (jrob5756) wants to merge 18 commits into
Draft
feat(mcp): conductor mcp serve — expose workflows as MCP tools (#432)#491Jason Robert (jrob5756) wants to merge 18 commits into
conductor mcp serve — expose workflows as MCP tools (#432)#491Jason Robert (jrob5756) wants to merge 18 commits into
Conversation
Adds the engineering/architecture solution design for exposing a user's registry workflows as MCP tools, so any MCP host (Claude Code, VS Code, Cursor) can invoke a governed, routed, budget-capped, checkpointed workflow as a typed tool call instead of driving the CLI through a hand-written skill. Covers problem statement, goals/non-goals, requirements, the proposed design, alternatives considered, dependencies, impact, security, risks, and open questions. Absorbs and supersedes #135. Every claim about Conductor's own code is cited by file and symbol against `main` at 0554517; MCP spec and Python SDK claims are checked against primary sources, with several inherited from the source issue corrected inline. Two load-bearing SDK behaviours were verified empirically across both major versions (DD0). Filed under docs/projects/<project>/ to match the existing design docs (aca, fleet-manager, web-ui, agent-sdk). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Consumes the already-reviewed solution design and breaks it into 14 epics (E1-E14): bounding the `mcp` SDK dependency, the terminal run record and its retention, surfacing completed runs in `status` / `fleet list` / History, registry index fields and parse cache, the `mcp:` workflow block, the catalogue builder, the stdio server, detached invocation with a bounded wait, run lifecycle tools, `introspect` / `diagnose`, discovery above the tool cap, the `doctor` MCP section, and docs. Every path, symbol, and line reference is grounded against the tree at b6c5b11, and the pinned mcp 1.28.1 SDK surface was exercised in this repo's own venv rather than taken from the design's report. Four gaps the design left open were put to a stakeholder and are recorded as plan-level decisions R1-R4. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bound the mcp SDK dependency to >=1.28.1,<2 in pyproject.toml to prevent a lock refresh from silently pulling mcp 2.x, which renamed the camelCase Tool.inputSchema/CallToolResult.isError attributes read by the existing MCP client (mcp/manager.py:207) — a runtime AttributeError the module's except ImportError guard cannot catch. - Re-resolved uv.lock (mcp stays pinned at 1.28.1) - Added tests/test_mcp/test_sdk_bound.py regression suite asserting the SDK attribute surface and the declared specifier bound - Documented the fix in CHANGELOG.md under Unreleased/Fixed - Marked all five E1 tasks (E1-T1..T5) and acceptance criteria as DONE in docs/projects/mcp-server/conductor-mcp.plan.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add TerminalRunRecord (frozen dataclass, fully absence-tolerant from_dict) and its read/write/remove API in fleet/records.py, stored under run_records_dir()/terminal/ — a subdirectory deliberately invisible to the three functions that non-recursively glob run_records_dir(). Wire the write into both run_workflow_async and resume_workflow_async in cli/run.py: each captures its terminal status/output/error on every exit path (clean success, explicit WorkflowTerminated, or an unexpected exception) and writes the tombstone in the existing finally block, immediately before the live record is removed, using a never-raising helper. Token/cost/unpriced- agent totals are read unconditionally from engine.get_execution_summary()['usage']. A resumed run replaces its predecessor's terminal record rather than duplicating it. A process that is kill -9'd (or otherwise dies before the finally runs) is documented and tested to leave no terminal record. Mark E2 DONE in the MCP server plan (epic status, all seven task rows, acceptance criteria). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bound terminal run records by [fleet.retention].keep_last, pruned in the same sweep as the event log they point at, so a run_id resolves completely or not at all. - Terminal run records are treated as a fourth companion of their events log (alongside .bg.stderr.log/.bg.stdout.log), matched by the run_id embedded in the events log's filename - Added an orphan-only sweep for terminal records whose events log has already disappeared, sorted newest-first by ended_at, sharing keep_last and the keep_last < 1 guard - Liveness sourced from the same _live_event_log_paths() call already made by the main sweep (no second read_run_records() call) - Fixed run_id extraction regex to correctly handle the full alphanumeric/hyphen/underscore run_id contract instead of only hex characters Files: src/conductor/fleet/retention.py, tests/test_fleet/test_retention.py, docs/configuration.md, docs/fleet.md, docs/projects/mcp-server/conductor-mcp.plan.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- conductor status and conductor fleet list now surface recently-completed runs alongside live ones, bounded by [fleet.retention].keep_last, with a new --live flag on both to restore the pre-change 'live only' scope - HistoryEntry gained output/error_type/error_message fields, enriched by joining a matching TerminalRunRecord by run_id after the existing single-pass log scan - Fleet Manager TUI History screen surfaces failure reason / rendered output via row selection, extending the existing replay-command notification - Fixed: completed rows in conductor fleet list's table now show started_at (not ended_at) in the Started column Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- T1: optional input/mcp fields on WorkflowInfo (config/schema.py, registry/index.py) - T2: SHA-keyed parse cache (_meta/<sha>/tools.json + tools.complete sentinel) via save_parsed_tools/load_parsed_tools (registry/cache.py) - T3: offline ref->SHA pointer (_meta/_refs/<slug>.json) modelled on plugins/fetch.py (registry/cache.py) - T4: allow_network seam on fetch_workflow/fetch_workflow_adhoc/resolve_and_fetch resolving from cache and raising typed RegistryError on a cold pointer - T5: comprehensive tests including the load-bearing "every function in registry/github.py patched to raise" test (tests/test_registry/test_cache.py) - Fix: corrected test_a_build_that_ignores_the_fields_still_loads to exercise a genuine legacy (pre-E5) model ignoring unknown fields, per review feedback Includes a minimal McpConfig prerequisite type in config/schema.py for E5-T1; E6 itself remains unimplemented. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Wire WorkflowDef.mcp onto the existing McpConfig model in config/schema.py - Add validator cross-checks for _wait_seconds reserved-input collision and unslugifiable workflow names (_validate_mcp_exposure, slugify_workflow_name) - Add _report_mcp(...) CLI reporting modelled on _report_plugins - Document the mcp: block in docs/workflow-syntax.md - Add examples/mcp-serve.yaml - Add tests/test_config/test_mcp_block.py and extend tests/test_cli/test_validate.py - Mark E6 DONE in docs/projects/mcp-server/conductor-mcp.plan.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…nning Turns registry/CLI configuration into a frozen, immutable list of mcp.types.Tool objects at startup with zero process launching and zero network I/O on a warm cache. - options.py: frozen ServeOptions dataclass holding every startup argument (registries, workflow_dirs, allow, deny, toolsets, max_direct_tools, max_wait_seconds, tool_prefix, max_concurrent_runs, introspect_full). - naming.py: slugify() delegates to config.validator.slugify_workflow_name; build_tool_names() computes base slugs, qualifies all colliding identities with their registry (never only the loser, DD10), and applies --tool-prefix last. - sanitize.py: sanitize_description() strips control chars, invisible/ bidi-override Unicode, and instruction-marker shapes, and hard-caps length at 500 chars (NFR4). - toolgen.py: maps all 5 InputDef types to JSON Schema, injects the reserved _wait_seconds parameter, rejects workflows that declare _wait_seconds themselves, and publishes no outputSchema (DD5). - pinning.py: Pin dataclass (sha for GitHub registries, content hash for path registries and --workflow-dir); recheck helpers report drift without mutating catalogue state (DD6, DD3). - catalogue.py: build_catalogue() wires the four-rung exposure ladder and three-tier schema ladder together, degrading unparseable workflows to a permissive schema instead of dropping them (NFR2). Marks E7 and all E7-T1..T10 tasks/acceptance-criteria DONE in docs/projects/mcp-server/conductor-mcp.plan.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds `conductor mcp serve` Typer sub-app wired to the low-level mcp.server.lowlevel.Server, publishing a byte-identical tools/list response built from the frozen E7 catalogue over stdio (DD3). Keeps stdout protocol-pure by routing all server-side messages, including the FR10 startup summary (exposed tool/workflow counts, collisions, degraded schemas), through a dedicated stderr console (DD9). - New src/conductor/cli/mcp.py: `mcp serve` sub-app with --registry, --allow, --deny, --workflow-dir, --toolsets, --max-direct-tools, --max-wait-seconds, --tool-prefix, --max-concurrent-runs, and --introspect-full flags - New src/conductor/mcp/serve/server.py: catalogue -> lowlevel Server wiring, stdio transport, startup summary - Server reports Conductor's own package version in serverInfo.version - FR10 collision summary names every distinct registry/workflow pair, including same-registry collisions - Registered mcp sub-app on the root app (Environment panel) - Tests: in-memory stream pair drive of initialize/tools/list with cross-connection identity check, CLI flag/help/stdout-purity tests Marks epic E8 DONE in docs/projects/mcp-server/conductor-mcp.plan.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…5, G3, G4, DD2, R3) - Add build_typed_launch_inputs to fleet/launch.py for JSON-typed MCP inputs - Extend max_concurrent_runs docstring in mcp/serve/options.py - Add src/conductor/mcp/serve/invoke.py: tool dispatch, launch_background invocation (web_port=0, hardcoded skip_gates=False per DD11), FR5 wait resolution, bounded polling loop, result shaping (structuredContent + text, resource_link spill per NFR6), and LaunchTracker for R3 concurrency bounding - Add tests/test_mcp/test_serve_invoke.py - Mark E9 DONE in docs/projects/mcp-server/conductor-mcp.plan.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add conductor_run_status, conductor_await_run, conductor_cancel_run, and conductor_list_runs plus the shared resolve_run/RunLookup three-source resolver in src/conductor/mcp/serve/runs.py. - resolve_run(run_id) tries a live run (read_run_record + derive_run_summary), then read_terminal_record, then find_event_log_for_run as a crash fallback, naming which source answered. - conductor_run_status shapes each source into a uniform status dict, including gate prompt/options/option_details and approval URL when at a gate. - conductor_await_run bounds polling at 2s cadence, returns early on terminal or at-gate status, and emits progress notifications when a token/sender are supplied. - conductor_cancel_run reuses cli/app.py::stop_records for the graceful stop ladder, reporting stopped/failed/already_terminal honestly. - conductor_list_runs unions live and terminal run records, dedupes by run_id (live wins), and filters by status/workflow. Updates docs/projects/mcp-server/conductor-mcp.plan.md: E10 and E10-T1..T6 marked DONE, acceptance criteria checked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… R4)
Add two opt-in MCP toolsets, both off by default (DD3):
- introspect (conductor_run_events, conductor_node_detail,
conductor_plan_tree) in src/conductor/mcp/serve/introspect.py
- diagnose (conductor_doctor, conductor_validate_workflow,
conductor_run_logs) in src/conductor/mcp/serve/diagnose.py
All six are thin adapters over existing Fleet Manager / diagnostics /
validate machinery. R4's redaction posture is applied specifically where
raw tool-call payloads live: conductor_run_events replaces
agent_tool_start.arguments and agent_tool_complete.result with
{name, status, byte_size} unless --introspect-full is set, while
conductor_node_detail returns prompt/output in full and is proven (not
assumed) to never carry a tool payload. conductor_run_logs follows DD12:
ResourceLink content blocks plus bounded per-file metadata, never file
bytes.
- --toolsets validation added at ServeOptions construction, rejecting
unknown toolset names at startup and surfacing the enabled set in the
startup stderr summary (E11-T1).
- Wired tools/list and tools/call for both toolsets in server.py, gated
on options.toolsets.
- Rejected tool-name collisions between generated workflow tools and the
introspect/diagnose tool set at build_server time.
- Fixed invoke.py's workflow-dir resolution to fail closed
(UnknownToolError) rather than ambiguously rescanning when a recorded
source path disappears.
- Fixed conductor_run_events' event_types argument handling to
distinguish an explicit empty filter from an absent one.
Updates docs/projects/mcp-server/conductor-mcp.plan.md: E11 and
E11-T1..T6 marked DONE, acceptance criteria checked.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>- discovery.py: conductor_find_workflow(query) searches the frozen catalogue's own name/description/registry fields; conductor_run_workflow dispatches through invoke_workflow_tool, the same invocation layer a generated per-workflow tool uses, so a path-shaped/URL-shaped/registry- source-shaped `name` is refused the same way any unrecognized tool name is (NFR3) -- there is no separate shape check to bypass. - server.py: build_server now acts on catalogue.mode (decided once, at startup, by E7's build_catalogue): direct mode publishes the catalogue's per-workflow tools as before; discovery mode publishes the fixed conductor_find_workflow/conductor_run_workflow pair instead, never both. Both tools/list and tools/call are gated on the mode captured in build_server's closure, so it can never vary within or across a connection (DD3). One LaunchTracker is shared per server process (R3). - tests/test_mcp/test_serve_discovery.py: search/dispatch behavior, path- shaped-name refusal, above/below-cap tool-list and tool-call gating, mode stability across repeated calls/connections, and the startup log naming the exposed count and --max-direct-tools threshold. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- conductor_find_workflow: bound result count (_MAX_RESULTS=25) and report total count/truncation - conductor_run_workflow: validate flattened inputs against the resolved catalogue entry's inputSchema before dispatch, raising LaunchError on mismatch (parity with direct-mode tools) - build_server: scope collision checks to tools actually published together, so discovery mode no longer checks against hidden catalogue names - build_server/_call_tool/_dispatch_discovery_tool: thread the request's progressToken and send_progress_notification into the discovery pair's dispatch for end-to-end progress reporting Resolves all four round-one review blockers. Focused discovery suite (31 tests) and full MCP suite (327 tests) pass; ruff and ty clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a new `mcp` section to `conductor doctor` that shows what `conductor mcp serve` would expose as MCP tools, without starting a server or attaching a host. - Added `McpServeDiagnostic` + `gather_mcp_serve()` in providers/diagnostics.py, wrapping the existing E7 mcp.serve.catalogue.build_catalogue() pipeline offline (allow_network=False), wired into gather()/ALL_SECTIONS/DoctorReport. - Added McpServeToolInfo, McpServeCollision, McpServeRejectedWorkflow, and McpServeFailedRegistry dataclasses with to_dict() serialization. - Added a thin Rich-table renderer `_render_mcp_serve()` in cli/doctor.py following the existing `_render_registries` convention, covering tools, collisions, rejected workflows, and failed registries. - Updated `conductor doctor --help` text/examples in cli/app.py to mention the mcp section. - Added FailedRegistry dataclass and Catalogue.failed_registries field in mcp/serve/catalogue.py; threaded a failed_registries accumulator through build_catalogue/_collect_registry_candidates so a whole-registry resolution failure is captured structurally instead of only logged. - Marked E13 DONE in the MCP server plan document. Targeted suite: 382 tests passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Documents the previously-undocumented conductor mcp serve feature (epics E1-E13): a new user-facing guide (docs/mcp-server.md) with host configuration snippets, the exposure ladder, toolsets, the mcp: workflow block, run lifecycle, and an explicit Limits section; a disambiguation/cross-link with the existing MCP client docs (docs/mcp-tools.md); a CLI reference entry for conductor mcp serve (docs/cli-reference.md); AGENTS.md architecture updates; and CHANGELOG entries. Also fixes two bugs surfaced during doc verification: - direct-mode generated workflow tools are now dispatchable through tools/call (previously listed but uncallable) - the default runs toolset (conductor_run_status/await_run/cancel_run/ list_runs) is now both listed in tools/list and dispatched in tools/call, matching documented default behavior make check and make validate-examples are green; make test is green except one pre-existing, environment-dependent failure unrelated to this epic's changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
E14-T4 replaced the `- **web/**: Real-time web dashboard for workflow visualization` bullet with the new `- **mcp/**:` bullet instead of adding it, orphaning auth.py/server.py/frontend/static (all of which live in src/conductor/web/) as children of the mcp/ section and leaving web/ undocumented as a top-level package. Restore the web/ bullet and drop the stray doubled blank line left by the insertion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes#432.
Adds
conductor mcp serve: a stdio MCP server that exposes registered Conductor workflows as MCP tools to any MCP-compatible host (Claude Code, VS Code, Cursor, your own agent), with run introspection and diagnostics.What this does
Zero-edit exposure. Every workflow in every configured registry is exposed by default, with a typed
inputSchemaderived from its owninput:block. No workflow needs editing to be reachable — a workflow with nomcp:block is exposed identically to one declaring the defaults explicitly.Invocation is always a real detached run. A tool call forks
conductor runvia the existinglaunch_backgroundpath — the server never executes a workflow in-process. It waits up to a bounded per-call timeout (--max-wait-seconds) and then returns either a handle (run_id+ dashboardurl) or the completed run's output inline, spilling to aresource_linkonce serialized output exceeds 50 KB. Human gates are never auto-skipped: a run that reaches one parks and reports its dashboard approval URL until a person resolves it.Run lifecycle tools (
conductor_run_status/await_run/cancel_run/list_runs) answer for arun_idbefore, during, at a gate, and after a run has finished. Answering after it finished is what motivated the terminal run record (below).Optional toolsets, off by default:
introspect(event query, per-step detail, plan tree) anddiagnose(doctor/validateequivalents, links — never contents — to raw logs). Raw tool-call payloads are withheld unless--introspect-full.Degrades rather than overflows. A registry above
--max-direct-tools(default 25) publishes a two-tool discovery pair (conductor_find_workflow/conductor_run_workflow) instead of failing or blowing past a host's tool-count limit. The mode is decided once at startup and never varies within or across a connection.Notable side-effects on existing surfaces
conductor statusandconductor fleet listnow also list recently-completed runs — a deliberate contract change. Previously both meant "alive right now" and a finished run vanished the instant its process exited, which left the MCP run tools with nothing to answer from. Every run now writes a terminal record on exit;--liverestores the exact previous scope, andstatus --json --livedrops the additivecompletedkey entirely so existing scripted consumers ofpayload["running"]are unaffected.conductor doctorgains anmcpsection showing whatmcp servewould expose (tools, collisions, rejected workflows, failed registries) without starting a server.mcpSDK dependency is now bounded below 2.0 (mcp>=1.28.1,<2).mcp2.0.0 renamed the camelCase attributes the existing MCP client reads (Tool.inputSchema→input_schema), so any install whose lock had floated to 2.x had a client that connected and then raisedAttributeErroron every tool listing — MCP tools were silently non-functional.New config
workflow.mcp:— per-workflowexpose/mode/read_only/destructive/estimated_minutes. Every field defaults to current behavior; an unknown key inside the block is aconductor validateschema error rather than a silently ignored typo. Seeexamples/mcp-serve.yaml.Shape of the change
70 files, ~19k insertions. New package
src/conductor/mcp/serve/(catalogue, naming, pinning, toolgen, invoke, runs, introspect, diagnose, discovery, server, options, sanitize) pluscli/mcp.py. Roughly half the diff is tests (tests/test_mcp/test_serve_*.py,tests/test_registry/,tests/test_fleet/test_terminal_records.py,tests/test_cli/test_doctor.py).Design and implementation plan are committed alongside the code:
docs/projects/mcp-server/conductor-mcp.design.mddocs/projects/mcp-server/conductor-mcp.plan.md(epics E1–E14, all marked done)User-facing docs:
docs/mcp-server.md(with an explicit Limits section for what v1 deliberately does not do — nooutputSchema, stdio only, no Streamable HTTP), cross-linked with the existing client-sidedocs/mcp-tools.md.Status
Draft.
make checkandmake validate-examplesare green;make testis green except one pre-existing, environment-dependent failure unrelated to this work.Not yet rebased — the branch is currently ~28 commits behind
main. I'll bring it up to date before marking ready for review.