Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,12 @@ and output parity** but the following are managed by the SDK rather than
Conductor:

- **Retry and error handling**: The `claude-agent-sdk` package does **not** retry API failures (429s, 5xx, network errors) internally — its built-in retry logic covers only filesystem operations. Conductor wraps SDK errors in `ProviderError` and uses `stop_reason` / error subtype to set `is_retryable`, so workflow-level `retry:` configuration drives all retry behavior. Plan for transient failures with explicit `retry:` blocks in your workflow.
- **Tool execution**: Tools and MCP servers are managed by the `claude` CLI's own configuration. The provider rejects workflow-level `runtime.mcp_servers` at the factory and refuses any non-empty per-agent `tools:` list (workflow tool names do not translate to CLI tool IDs). An agent with `tools: []` runs with no tools; omitting `tools:` grants the full `claude_code` preset.
- **MCP servers** (issue #335): workflow-level `runtime.mcp_servers` **are** supported. `_translate_mcp_servers` maps each `MCPServerDef`-derived dict onto the SDK's `McpStdioServerConfig` / `McpHttpServerConfig` / `McpSSEServerConfig` shapes, and the provider passes them via `ClaudeAgentOptions`. Four details are load-bearing:
- Translation runs once in `__init__` rather than per `execute` call. Note providers are constructed **lazily** (`ProviderRegistry.get_provider` ← `WorkflowEngine._get_executor_for_agent`), so a bad server config surfaces when the first agent on this provider runs, **not** at `conductor validate` — which does not inspect per-server `tools:` filters at all.
- The config is written to a `0600` temp file (`_write_mcp_config`) and passed **by path**. Passing the dict would make the SDK serialize it into a `--mcp-config <json>` argv element, publishing resolved stdio `env` values and http/sse `Authorization` headers to anything that can read `/proc/<pid>/cmdline`. The write happens **inside** `execute`'s `try`, so the `finally` reclaims the file on every exit path; the finally also `aclose()`s the SDK iterator first, so the `claude` subprocess is gone before its config file is. The file must use the `{"mcpServers": {...}}` envelope — the CLI rejects a bare mapping.
- `strict_mcp_config=True` is set **unconditionally**, including when the workflow declares no servers: otherwise the CLI loads project `.mcp.json`, user-global, and plugin-provided servers, and `permission_mode` bypasses approval for whatever they expose.
- A narrowing per-server `tools:` filter (anything other than the default `["*"]`) is **refused**, not ignored: forwarding the server unfiltered would grant more tools than declared, the same security regression that justifies refusing the per-agent allowlist. A dropped `timeout` only warns, since losing it cannot widen tool access.
- **Tool execution**: Per-agent `tools:` allowlists remain unsupported (`workflow_tools_passthrough=False`). The provider refuses any non-empty per-agent list because workflow tool names do not translate to CLI tool IDs. Note the SDK's `tools` option governs **built-in** tools only, and `allowed_tools` is a permission auto-approve list rather than an availability filter — so honoring an allowlist would require a permission-mode redesign, not just a name mapping. An agent with `tools: []` runs with no built-in tools (MCP servers still attach); omitting `tools:` grants the full `claude_code` preset.
- **Runtime config**: `temperature` and `max_tokens` are rejected at the factory — the CLI controls sampling behavior.

#### `aca.py` parity notes
Expand Down Expand Up @@ -298,9 +303,11 @@ it will pick up the developer's real token (see
`config/validator.py` rejects **any** explicit `tools:` on an
`aca`-backed agent, not just a non-empty one (review follow-up, #284
E7). This mirrors the same declared carve-out on `claude_agent_sdk.py`
and `hermes.py`, except those declare `mcp_tools=False` (nothing is ever
and `hermes.py`. `hermes.py` declares `mcp_tools=False` (nothing is ever
forwarded regardless of the list), so `tools: []` genuinely disables all
tools and stays valid for them.
tools and stays valid for it. `claude_agent_sdk.py` now behaves like
`aca` whenever the workflow declares `mcp_servers`, and like `hermes`
when it does not.
- **`working_dir=False`**: this capability field means "applies the
generic, host-resolved `agent.working_dir` / `runtime.working_dir`" — a
host filesystem path the engine resolves against the workflow file's
Expand Down
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased](https://github.com/microsoft/conductor/compare/v0.1.26...HEAD)

### Added

- **`claude-agent-sdk` provider now supports MCP servers** — workflow-level
`runtime.mcp_servers` are translated to the SDK's own `stdio` / `http` /
`sse` config shapes and passed through `ClaudeAgentOptions`, so an agent can
use custom MCP tool servers *and* the built-in `claude_code` tool preset at
the same time. Previously the two were mutually exclusive: the provider
declared `mcp_tools=False` and the factory rejected any workflow declaring
MCP servers. The generated config is written to a `0600` temp file and
passed by path so resolved `env` values and `Authorization` headers never
reach the `claude` CLI's command line, and `strict_mcp_config` is always
enabled so ambient project/user MCP config cannot inject undeclared servers.
A narrowing per-server `tools:` filter has no SDK equivalent and is refused
(when the first agent on this provider runs) rather than silently ignored.
See
[`examples/claude-agent-sdk-mcp.yaml`](examples/claude-agent-sdk-mcp.yaml)
and [`docs/mcp-tools.md`](docs/mcp-tools.md).
([#335](https://github.com/microsoft/conductor/issues/335))

### Fixed

- **`tools: []` no longer fails validation when no MCP servers are declared** —
the capability cross-check rejected an explicit empty allowlist against any
provider with `mcp_tools=True` and `workflow_tools_passthrough=False` (such
as `aca`), even when the workflow declared no `mcp_servers` and therefore had
nothing to forward. The check is now gated on MCP servers actually being
configured.
([#335](https://github.com/microsoft/conductor/issues/335))

### Changed

- The `claude-agent-sdk` optional dependency floor is now
`claude-agent-sdk>=0.2.82` — the 0.2.x line is what Conductor tests against.
([#335](https://github.com/microsoft/conductor/issues/335))
- `claude-agent-sdk` agents no longer inherit ambient MCP configuration.
Conductor now always sets `strict_mcp_config`, so a project `.mcp.json`,
user-global settings, or plugin-provided servers are ignored and only
servers declared in `runtime.mcp_servers` attach. Workflows that relied on
Claude Code's own MCP settings must declare those servers in the workflow.
([#335](https://github.com/microsoft/conductor/issues/335))

## [0.1.26](https://github.com/microsoft/conductor/compare/v0.1.25...v0.1.26) - 2026-07-27

### Added
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,9 @@ workflow:
default_model: claude-sonnet-5
```

Requires the `claude` CLI to be installed and authenticated. Install the SDK: `uv add 'claude-agent-sdk>=0.1.0'`
Requires the `claude` CLI to be installed and authenticated. Install the SDK: `uv add 'claude-agent-sdk>=0.2.82'`

> **Note:** The `claude-agent-sdk` provider delegates tool and MCP management to the `claude` CLI; workflow-level tool/MCP config is **not** bridged into it. `runtime.mcp_servers` is rejected at the factory, and a workflow-level `tools:` block is rejected at `conductor validate` for any agent that omits `tools:` (it would otherwise inherit a list the CLI can't map). Omit `tools:` to grant the full `claude_code` preset, set an agent's `tools: []` to disable all tools, and configure MCP servers through your Claude Code settings instead.
> **Note:** `runtime.mcp_servers` is supported — servers are translated into the SDK's own MCP config and attach alongside the built-in `claude_code` preset (a narrowing per-server `tools:` filter is refused, since the SDK cannot enforce one). Per-agent tool allowlists are not bridged: a workflow-level `tools:` block is rejected at `conductor validate` for any agent that omits `tools:` (it would otherwise inherit a list the CLI can't map). Omit `tools:` to grant the full `claude_code` preset; an agent's `tools: []` disables the built-in tools, though declared MCP servers still attach.

### Using Hermes (Experimental)

Expand Down
27 changes: 20 additions & 7 deletions docs/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ mcp_servers:

The configuration fields are the same as `http`.

> **Provider note:** The Claude provider only supports `stdio` servers. The `http` and `sse` types are supported by the Copilot provider only.
> **Provider note:** The Claude provider only supports `stdio` servers. The `http` and `sse` types are supported by the Copilot and Claude Agent SDK providers.

## Configuration Reference

Expand Down Expand Up @@ -326,12 +326,12 @@ workflow:

| Feature | Copilot | Claude | Claude Agent SDK | Hermes |
|---|---|---|---|---|
| stdio servers | ✅ | ✅ | | ❌ |
| http servers | ✅ | ❌ | | ❌ |
| sse servers | ✅ | ❌ | | ❌ |
| Tool filtering | ✅ | ✅ | ❌ | ❌ |
| OAuth auto-auth | ✅ | N/A | | ❌ |
| env var passing | ⚠️ Bug ([#163](https://github.com/github/copilot-sdk/issues/163)) | ✅ | | ❌ |
| stdio servers | ✅ | ✅ | | ❌ |
| http servers | ✅ | ❌ | | ❌ |
| sse servers | ✅ | ❌ | | ❌ |
| Tool filtering | ✅ | ✅ | ❌ (refused) | ❌ |
| OAuth auto-auth | ✅ | N/A | | ❌ |
| env var passing | ⚠️ Bug ([#163](https://github.com/github/copilot-sdk/issues/163)) | ✅ | | ❌ |
| Tool output limits | ✅ (native SDK) | ✅ (conductor-side) | ✅ (native CLI env var) | N/A |

### Copilot Provider
Expand All @@ -349,6 +349,19 @@ The Claude provider uses Conductor's built-in `MCPManager` to spawn and manage M

HTTP and SSE servers are not supported with the Claude provider. If configured, a warning is logged and the server is skipped.

### Claude Agent SDK Provider

The Claude Agent SDK provider translates each server into the SDK's own MCP config shape and passes it to the `claude` CLI, which owns server lifecycle and tool execution. All three transport types are supported, and MCP tools attach *alongside* the built-in `claude_code` tool preset — see [`examples/claude-agent-sdk-mcp.yaml`](../examples/claude-agent-sdk-mcp.yaml).

Two behaviors are specific to this provider:

- **Per-server `tools:` filters are refused.** The SDK's MCP config has no equivalent field, so a narrowing filter cannot be enforced. Rather than forward the server unfiltered — granting more tools than the workflow declared — Conductor raises a `ProviderError` the first time an agent on this provider runs. Note `conductor validate` does not catch this today. Keep the default `tools: ["*"]`.
- **Only declared servers are reachable.** Conductor sets `strict_mcp_config`, so a project `.mcp.json` or user-global MCP setting cannot add servers the workflow never declared.

The generated config is written to a `0600` temp file and passed to the CLI by path, so resolved `env` values and `Authorization` headers stay out of the process command line. A fresh file is written and deleted per agent execution.

A per-server `timeout` has no SDK equivalent and is dropped with a warning.

## Examples

### Web Search
Expand Down
2 changes: 1 addition & 1 deletion docs/providers/aca.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ agents:
| Capability | Value | Notes |
|---|---|---|
| `mcp_tools` | ✅ `True` | Full `mcp_servers` forwarded — runner-image contract. |
| `workflow_tools_passthrough` | ❌ **`False`** | The per-agent `tools:` allowlist is forwarded to the runner in the request body, but the in-container `CopilotProvider` it wraps never applies that list to the SDK session — every tool/MCP server available to the session is callable regardless of the declared allowlist. Combined with `mcp_tools=True` (below), there is no allowlist value the runner can honor — not even `tools: []` — so `conductor validate` rejects any explicit `tools:` on an `aca`-backed agent. This is a known, allowed experimental carve-out (the same gap `claude_agent_sdk` and `hermes` already declare, though those declare `mcp_tools=False` so `tools: []` stays valid for them). |
| `workflow_tools_passthrough` | ❌ **`False`** | The per-agent `tools:` allowlist is forwarded to the runner in the request body, but the in-container `CopilotProvider` it wraps never applies that list to the SDK session — every tool/MCP server available to the session is callable regardless of the declared allowlist. Combined with `mcp_tools=True` (below), there is no allowlist value the runner can honor — not even `tools: []` — so `conductor validate` rejects any explicit `tools:` on an `aca`-backed agent. This is a known, allowed experimental carve-out (the same gap `claude_agent_sdk` and `hermes` already declare; `hermes` declares `mcp_tools=False` so `tools: []` stays valid for it, and `claude_agent_sdk` behaves like `aca` here only when the workflow declares `mcp_servers`). |
| `streaming_events` | ✅ `True` | Single streaming request relays event frames incrementally. |
| `agent_reasoning_events` | ✅ `True` | Runner forwards reasoning frames from the inner provider. |
| `reasoning_effort` | ✅ Copilot's full tuple | Inner provider (Copilot) translates reasoning effort natively. |
Expand Down
12 changes: 6 additions & 6 deletions docs/providers/comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ This guide helps you choose between GitHub Copilot, Anthropic Claude, Claude Age
| **Setup** | GitHub auth | API key | `claude` CLI auth | API key (model-provider's key) |
| **Model Selection** | GPT-5.2, o1 | Haiku, Sonnet, Opus | Haiku, Sonnet, Opus | Any OpenRouter-style model |
| **Streaming** | Yes | No (Phase 1) | Yes | Yes |
| **Tool Support** | Yes (MCP, all types) | Yes (MCP, stdio only) | Yes (built-in, CLI-managed) | Yes (hermes toolsets) |
| **MCP Servers** | Yes | Yes (stdio) | No | No |
| **Tool Support** | Yes (MCP, all types) | Yes (MCP, stdio only) | Yes (MCP + built-in preset) | Yes (hermes toolsets) |
| **MCP Servers** | Yes | Yes (stdio) | Yes (all types) | No |
| **Reasoning / Extended Thinking** | Yes (`reasoning_effort` on session) | Yes (extended `thinking` budget) | Inherits from CLI config | Yes (`reasoning_config`) |
| **Speed** | Fast | Fast | Fast | Depends on model |
| **Output Quality** | Excellent | Excellent | Excellent | Depends on model |
Expand All @@ -25,7 +25,7 @@ This guide helps you choose between GitHub Copilot, Anthropic Claude, Claude Age
| **Tool Output Limits** | native SDK spill (large_output) | conductor-side truncation+spill | native CLI env var | N/A |

> **About the experimental tier.** `claude-agent-sdk` and `hermes` declare
> specific capability carve-outs (e.g. no MCP servers). `conductor validate`
> specific capability carve-outs (e.g. no per-agent tools allowlist). `conductor validate`
> catches workflows that depend on those features against these providers,
> and the CLI prints a one-time banner when the workflow runs. See
> [docs/providers/experimental.md](./experimental.md) for the stability
Expand Down Expand Up @@ -123,10 +123,10 @@ agents:

### Important: Tools and MCP Servers

The `claude-agent-sdk` provider does not bridge workflow-level tools/MCP into the CLI. Concretely:
The `claude-agent-sdk` provider bridges MCP servers into the CLI, but not per-agent tool allowlists. Concretely:

- `runtime.mcp_servers` — **rejected at the factory** with a clear error. Translation to the CLI's MCP configuration is not implemented. Configure MCP servers through your Claude Code settings instead.
- Per-agent `tools: []` — disables all tools for that agent.
- `runtime.mcp_servers` — **supported**. Servers are translated into the SDK's MCP config and attach alongside the built-in preset. Only declared servers attach: Conductor sets `strict_mcp_config`, so ambient Claude Code MCP settings are ignored. A narrowing per-server `tools:` filter is refused, since the SDK has no equivalent field.
- Per-agent `tools: []` — disables the built-in tools for that agent. Declared MCP servers still attach, so this combination is rejected at `conductor validate` when the workflow declares `mcp_servers`.
- Per-agent `tools: [list]` — **refused loudly**. Workflow tool names do not translate to Claude CLI tool IDs; silently passing them through would risk granting the wrong native tool.
- Workflow-level `tools:` combined with an agent that omits `tools:` — **rejected at `conductor validate`**. The agent would otherwise inherit that non-empty list at runtime and hit the same refusal with a confusing message. Remove the workflow-level `tools:` (so omitting `tools:` grants the preset) or set the agent's `tools: []`.
- Omitting `tools:` entirely (with no workflow-level `tools:`) — grants the full `claude_code` preset (filesystem, bash, web), matching the bare `claude` CLI experience.
Expand Down
9 changes: 5 additions & 4 deletions docs/providers/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ prints a one-time banner per provider:
```text
┌─────────────────────────────────────────────────────────────────────┐
│ ⚠ Experimental provider in use: claude-agent-sdk │
│ (claude-agent-sdk>=0.1.0) maintained by @lesandiz (best-effort) │
│ Limitations: no MCP servers, no per-agent tools allowlist, │
│ reasoning_effort ignored, no checkpoint resume. │
│ (claude-agent-sdk>=0.2.82) maintained by @lesandiz (best-effort) │
│ Limitations: no per-agent tools allowlist, reasoning_effort │
│ ignored, structured output via prompt injection, no checkpoint │
│ resume, working_dir ignored. │
│ See docs/providers/experimental.md for stability policy. │
└─────────────────────────────────────────────────────────────────────┘
```
Expand Down Expand Up @@ -98,7 +99,7 @@ adopting one does not inflate the install surface for others.

| Provider | Upstream pin | Maintainer | Capability carve-outs |
|---|---|---|---|
| `claude-agent-sdk` | `claude-agent-sdk>=0.1.0` | `@lesandiz (best-effort)` | no `mcp_tools`, no `workflow_tools_passthrough`, no `reasoning_effort`, `prompt_injection` structured output, no `checkpoint_resume`, no `working_dir` |
| `claude-agent-sdk` | `claude-agent-sdk>=0.2.82` | `@lesandiz (best-effort)` | no `workflow_tools_passthrough`, no `reasoning_effort`, `prompt_injection` structured output, no `checkpoint_resume`, no `working_dir`. Supports `mcp_tools` as of [#335](https://github.com/microsoft/conductor/issues/335), except that a narrowing per-server `tools:` filter is refused (no SDK equivalent). |
| `hermes` | `hermes-agent` | `(community contribution)` | no `mcp_tools`, `prompt_injection` structured output, no `working_dir` |
| `aca` | `azure-identity>=1.19.0` | `(unassigned)` | no `workflow_tools_passthrough` (the wrapped in-container `CopilotProvider` never applies the `tools:` allowlist to the SDK session), no `working_dir` (only the separate, container-relative `sandbox.working_dir` is honored — not the generic host-resolved field), `prompt_injection` structured output (inherits the inner Copilot provider), no `checkpoint_resume` (ephemeral sandbox sessions, no volume mount). Declares `interrupt`/`max_session_seconds` as `True`, but the shipped runner MVP doesn't fully back either yet — see [Known Gaps](./aca.md#known-gaps-runner-mvp). |

Expand Down
Loading
Loading