Skip to content

Commit 3d4e767

Browse files
committed
feat(workflows): support Codex MCP nodes
1 parent 287bb35 commit 3d4e767

20 files changed

Lines changed: 624 additions & 146 deletions

packages/cli/src/commands/workflow.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,54 @@ describe('workflowRunCommand', () => {
691691
'hello world',
692692
'claude',
693693
'/test/path',
694-
'assist'
694+
'assist',
695+
{}
696+
);
697+
});
698+
699+
it('uses the workflow provider for title generation', async () => {
700+
const { discoverWorkflowsWithConfig } = await import('@archon/workflows/workflow-discovery');
701+
const { executeWorkflow } = await import('@archon/workflows/executor');
702+
const conversationDb = await import('@archon/core/db/conversations');
703+
const codebaseDb = await import('@archon/core/db/codebases');
704+
const core = await import('@archon/core');
705+
706+
(discoverWorkflowsWithConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
707+
workflows: [
708+
makeTestWorkflowWithSource({
709+
name: 'figma-mcp-smoke',
710+
description: 'Smoke test Figma MCP',
711+
provider: 'codex',
712+
}),
713+
],
714+
errors: [],
715+
});
716+
(conversationDb.getOrCreateConversation as ReturnType<typeof mock>).mockResolvedValueOnce({
717+
id: 'conv-123',
718+
ai_assistant_type: 'claude',
719+
});
720+
(core.loadConfig as ReturnType<typeof mock>).mockResolvedValueOnce({
721+
assistant: 'claude',
722+
assistants: { codex: { model: 'gpt-5.4' } },
723+
defaults: {},
724+
});
725+
(codebaseDb.findCodebaseByDefaultCwd as ReturnType<typeof mock>).mockResolvedValueOnce(null);
726+
(conversationDb.updateConversation as ReturnType<typeof mock>).mockResolvedValueOnce(undefined);
727+
(executeWorkflow as ReturnType<typeof mock>).mockResolvedValueOnce({
728+
success: true,
729+
workflowRunId: 'run-123',
730+
});
731+
(core.generateAndSetTitle as ReturnType<typeof mock>).mockClear();
732+
733+
await workflowRunCommand('/test/path', 'figma-mcp-smoke', 'check figma', { noWorktree: true });
734+
735+
expect(core.generateAndSetTitle).toHaveBeenCalledWith(
736+
'conv-123',
737+
'check figma',
738+
'codex',
739+
'/test/path',
740+
'figma-mcp-smoke',
741+
{ model: 'gpt-5.4' }
695742
);
696743
});
697744

packages/cli/src/commands/workflow.ts

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ import { createLogger, getArchonHome } from '@archon/paths';
1414
import { join } from 'node:path';
1515
import { createWorkflowDeps } from '@archon/core/workflows/store-adapter';
1616
import { discoverWorkflowsWithConfig } from '@archon/workflows/workflow-discovery';
17+
import { inferProviderFromModel } from '@archon/workflows/model-validation';
1718
import { resolveWorkflowName } from '@archon/workflows/router';
1819
import { executeWorkflow } from '@archon/workflows/executor';
1920
import {
2021
getWorkflowEventEmitter,
2122
type WorkflowEmitterEvent,
2223
} from '@archon/workflows/event-emitter';
23-
import type { WorkflowLoadResult } from '@archon/workflows/schemas/workflow';
24+
import type { WorkflowDefinition, WorkflowLoadResult } from '@archon/workflows/schemas/workflow';
2425
import type { WorkflowRun } from '@archon/workflows/schemas/workflow-run';
2526
import {
2627
approveWorkflow,
@@ -129,6 +130,22 @@ function buildRegistrationFailureError(action: string, error: Error): Error {
129130
);
130131
}
131132

133+
/**
134+
* Resolve the provider used for CLI conversation titles from the workflow itself.
135+
* This keeps auxiliary title generation aligned with workflow execution instead
136+
* of falling back to a stale conversation default.
137+
*/
138+
function resolveTitleAssistantType(
139+
workflow: WorkflowDefinition,
140+
defaultAssistant: string | undefined,
141+
conversationAssistant: string | undefined
142+
): string {
143+
const fallbackAssistant = defaultAssistant ?? conversationAssistant ?? 'claude';
144+
if (workflow.provider) return workflow.provider;
145+
if (workflow.model) return inferProviderFromModel(workflow.model, fallbackAssistant);
146+
return fallbackAssistant;
147+
}
148+
132149
/** Render a workflow event to stderr as a progress line. Called only when --quiet is not set. */
133150
function renderWorkflowEvent(event: WorkflowEmitterEvent, verbose: boolean): void {
134151
switch (event.type) {
@@ -636,12 +653,20 @@ export async function workflowRunCommand(
636653
}
637654

638655
// Auto-generate title for CLI workflow conversations (fire-and-forget)
656+
const workflowConfig = await loadConfig(cwd);
657+
const titleAssistantType = resolveTitleAssistantType(
658+
workflow,
659+
workflowConfig.assistant,
660+
conversation.ai_assistant_type
661+
);
662+
const titleAssistantConfig = workflowConfig.assistants?.[titleAssistantType] ?? {};
639663
void generateAndSetTitle(
640664
conversation.id,
641665
userMessage,
642-
conversation.ai_assistant_type,
666+
titleAssistantType,
643667
workingCwd,
644-
workflowName
668+
workflowName,
669+
titleAssistantConfig
645670
);
646671

647672
// Register cleanup handlers for graceful termination

packages/core/src/services/title-generator.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const mockSendQuery = mock(async function* (): AsyncGenerator<MessageChunk> {
2727
prompt: string,
2828
cwd: string,
2929
resumeSessionId?: string,
30-
options?: { model?: string; tools?: string[] }
30+
options?: { model?: string; tools?: string[]; assistantConfig?: Record<string, unknown> }
3131
) => AsyncGenerator<MessageChunk>
3232
>;
3333

@@ -177,6 +177,24 @@ describe('title-generator', () => {
177177
expect(optionsArg.nodeConfig?.allowed_tools).toEqual([]);
178178
});
179179

180+
test('passes assistantConfig through to the provider', async () => {
181+
const assistantConfig = { model: 'gpt-5.4', modelReasoningEffort: 'medium' };
182+
183+
await generateAndSetTitle(
184+
'conv-12',
185+
'Some message',
186+
'codex',
187+
'/tmp',
188+
'figma-mcp-smoke',
189+
assistantConfig
190+
);
191+
192+
const optionsArg = mockSendQuery.mock.calls[0][3] as {
193+
assistantConfig?: Record<string, unknown>;
194+
};
195+
expect(optionsArg.assistantConfig).toEqual(assistantConfig);
196+
});
197+
180198
test('handles double failure gracefully (AI fails + fallback DB write fails)', async () => {
181199
mockSendQuery.mockImplementation(async function* (): AsyncGenerator<MessageChunk> {
182200
throw new Error('AI failure');

packages/core/src/services/title-generator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ const MAX_TITLE_LENGTH = 100;
2929
* @param assistantType - Provider identifier (e.g. 'claude', 'codex')
3030
* @param cwd - Working directory for the AI client
3131
* @param workflowName - Optional workflow name for additional context
32+
* @param assistantConfig - Optional provider-specific defaults for the selected assistant
3233
*/
3334
export async function generateAndSetTitle(
3435
conversationDbId: string,
3536
userMessage: string,
3637
assistantType: string,
3738
cwd: string,
38-
workflowName?: string
39+
workflowName?: string,
40+
assistantConfig?: Record<string, unknown>
3941
): Promise<void> {
4042
try {
4143
getLog().debug({ conversationDbId, assistantType }, 'title.generate_started');
@@ -52,6 +54,7 @@ export async function generateAndSetTitle(
5254

5355
for await (const chunk of client.sendQuery(titlePrompt, cwd, undefined, {
5456
model: titleModel,
57+
assistantConfig,
5558
nodeConfig: { allowed_tools: [] }, // No tool access — pure text generation
5659
})) {
5760
if (chunk.type === 'assistant') {

packages/docs-web/src/content/docs/guides/authoring-workflows.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ nodes:
165165
provider: claude # Per-node provider override
166166
model: haiku # Per-node model override
167167
# hooks: # Optional: per-node SDK hook callbacks (Claude only) — see hooks guide
168-
# mcp: .archon/mcp/servers.json # Optional: per-node MCP servers (Claude only)
168+
# mcp: .archon/mcp/servers.json # Optional: per-node MCP servers (Codex and Claude)
169169
# skills: [remotion-best-practices] # Optional: per-node skills (Claude only) — see skills guide
170170
```
171171

@@ -1173,7 +1173,7 @@ Before deploying a workflow:
11731173
8. **`allowed_tools` / `denied_tools`** — restrict tools per node (Claude only, SDK-enforced)
11741174
9. **`retry:`** — auto-retries transient errors (default: 2 retries / 3 total attempts, 3 s backoff); customize per node
11751175
10. **`hooks`** — attach SDK hook callbacks to Claude nodes for tool control and context injection
1176-
11. **`mcp:`** — attach per-node MCP servers via JSON config (Claude only)
1176+
11. **`mcp:`** — attach per-node MCP servers via JSON config (Codex and Claude)
11771177
12. **`skills:`** — preload skills into Claude nodes for domain expertise
11781178
13. **`agents:`** — inline Claude sub-agent definitions invokable via the `Task` tool
11791179
14. **`effort` / `thinking`** — control reasoning depth and thinking mode per node or workflow (Claude only)

packages/docs-web/src/content/docs/guides/mcp-servers.md

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ DAG workflow nodes support a `mcp` field that attaches MCP (Model Context Protoc
1313
servers to individual nodes. Each node gets exactly the external tools it needs —
1414
GitHub, Linear, Postgres, etc. — without over-provisioning.
1515

16-
**Claude only** — Codex nodes will warn and ignore the `mcp` field.
16+
MCP works with Codex and Claude workflow nodes. Pi nodes still warn and ignore
17+
the `mcp` field.
1718

1819
## Quick Start
1920

@@ -50,6 +51,9 @@ to the AI, and it shuts down when the node completes.
5051
MCP config files are JSON objects where each key is a server name and the value
5152
is a server configuration. Three transport types are supported:
5253
54+
Archon also accepts the common wrapper format `{ "mcpServers": { ... } }`; this
55+
is useful when copying config from tools that already export MCP JSON.
56+
5357
### stdio (default)
5458

5559
Runs a local process. This is the most common type.
@@ -119,8 +123,9 @@ Connects to an SSE endpoint.
119123

120124
## Environment Variable Expansion
121125

122-
Values in `env` and `headers` fields support `$VAR_NAME` references that are
123-
expanded from `process.env` at execution time.
126+
Values in `env` and `headers` fields support `$VAR_NAME` references. They are
127+
expanded from Archon's process environment at execution time. Codex workflow
128+
nodes also include codebase-scoped env vars in that expansion.
124129

125130
```json
126131
{
@@ -165,21 +170,23 @@ A single config file can define multiple servers:
165170
}
166171
```
167172

168-
## Automatic Tool Wildcards
173+
## Provider Tool Wiring
169174

170-
When a node loads MCP servers, tool wildcards are automatically added to `allowedTools`.
171-
For servers named `github` and `postgres`, the node gets:
175+
Claude nodes automatically add tool wildcards to `allowedTools`. For servers
176+
named `github` and `postgres`, the node gets:
172177

173178
- `mcp__github__*`
174179
- `mcp__postgres__*`
175180

176-
This means all tools from those servers are immediately available without manually
177-
listing them. The wildcards merge with any existing `allowed_tools` on the node.
181+
Codex nodes pass the same MCP config as per-node `mcp_servers` overrides to the
182+
Codex SDK, so the servers are available for that node without requiring global
183+
`~/.codex/config.toml` setup.
178184

179185
## MCP-Only Nodes
180186

181-
Combine `mcp` with `allowed_tools: []` to create nodes that can only use MCP tools
182-
and have no access to built-in tools (Bash, Read, Write, etc.):
187+
For providers that support tool restrictions, combine `mcp` with
188+
`allowed_tools: []` to create nodes that can only use MCP tools and have no
189+
access to built-in tools (Bash, Read, Write, etc.):
183190

184191
```yaml
185192
nodes:
@@ -190,7 +197,9 @@ nodes:
190197
```
191198

192199
This is useful for sandboxing — the AI can only interact through the MCP server
193-
and cannot touch the filesystem or run shell commands.
200+
and cannot touch the filesystem or run shell commands. Codex currently does not
201+
support Archon's `allowed_tools` / `denied_tools` restrictions, so this pattern
202+
is enforced for Claude nodes but not Codex nodes.
194203

195204
## Connection Failure Handling
196205

@@ -205,12 +214,12 @@ MCP server connection failed: github (failed)
205214
The node continues executing but without the tools from the failed server.
206215
Check your config file path, server command, and environment variables if this happens.
207216

208-
User-level Claude plugin MCPs inherited from `~/.claude/` (e.g. `telegram`,
209-
`notion`) routinely fail to connect inside the headless workflow subprocess
210-
and are **not** surfaced here — they're not actionable for the workflow author.
211-
They appear only in debug logs as `dag.mcp_plugin_connection_suppressed`. Run
212-
the CLI with `--verbose` (or set `LOG_LEVEL=debug` on the server) if you need
213-
to see them.
217+
User-level plugin MCPs inherited from provider-specific user config routinely
218+
fail to connect inside headless workflow subprocesses and are **not** surfaced
219+
when the workflow did not configure MCP itself — they're not actionable for the
220+
workflow author. They appear only in debug logs as
221+
`dag.mcp_plugin_connection_suppressed` for Claude workflows. Run the CLI with
222+
`--verbose` (or set `LOG_LEVEL=debug` on the server) if you need to see them.
214223

215224
## Workflow Examples
216225

@@ -368,8 +377,8 @@ bun run cli workflow run archon-smart-pr-review "Review PR #123"
368377

369378
## Limitations
370379

371-
- **Claude only** — Codex nodes warn and ignore the `mcp` field. Configure MCP
372-
servers globally in the Codex CLI config instead.
380+
- **Codex tool restrictions** — Codex nodes support `mcp`, but Archon's
381+
`allowed_tools` / `denied_tools` restrictions are still ignored by Codex.
373382
- **Haiku model** — Tool search (lazy loading for many tools) is not supported on
374383
Haiku. You'll see a warning. Consider using Sonnet or Opus for MCP nodes.
375384
- **No load-time validation** — The MCP config file is read at execution time, not
@@ -386,8 +395,8 @@ bun run cli workflow run archon-smart-pr-review "Review PR #123"
386395
| `MCP config must be a JSON object` | Top-level value is array or string | Wrap in `{ "server-name": { ... } }` |
387396
| `undefined env vars: VAR_NAME` | Environment variable not set | Export the variable or add it to your `.env` |
388397
| `MCP server connection failed` | Server process crashed or URL unreachable | Check command/URL, test the server standalone |
389-
| Plugin MCP missing from workflow output | User-level plugin MCPs (from `~/.claude/`) are filtered out of workflow warnings | Run with `--verbose` and look for `dag.mcp_plugin_connection_suppressed` |
390-
| `mcp config but uses Codex` | Node resolved to Codex provider | Set `provider: claude` on the node or switch default |
398+
| Plugin MCP missing from workflow output | User-level plugin MCPs are filtered out of workflow warnings | Run with `--verbose` and look for provider MCP debug logs |
399+
| `allowed_tools` ignored with Codex | Codex provider does not support Archon's tool restrictions yet | Do not rely on `allowed_tools: []` for Codex sandboxing |
391400
| `Haiku model with MCP servers` | Haiku doesn't support tool search | Use `model: sonnet` or `model: opus` instead |
392401

393402
## Finding MCP Servers

packages/providers/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"./codex/provider": "./src/codex/provider.ts",
1414
"./codex/config": "./src/codex/config.ts",
1515
"./codex/binary-resolver": "./src/codex/binary-resolver.ts",
16+
"./mcp/config": "./src/mcp/config.ts",
1617
"./community/pi": "./src/community/pi/index.ts",
1718
"./errors": "./src/errors.ts",
1819
"./registry": "./src/registry.ts"
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
11
export { ClaudeProvider } from './provider';
22
export { parseClaudeConfig, type ClaudeProviderDefaults } from './config';
3-
export {
4-
loadMcpConfig,
5-
buildSDKHooksFromYAML,
6-
withFirstMessageTimeout,
7-
getProcessUid,
8-
} from './provider';
3+
export { loadMcpConfig } from '../mcp/config';
4+
export { buildSDKHooksFromYAML, withFirstMessageTimeout, getProcessUid } from './provider';

0 commit comments

Comments
 (0)