Skip to content

feat(workflows): support Codex MCP nodes#1456

Closed
borshyo wants to merge 1 commit intocoleam00:devfrom
borshyo:codex/codex-mcp-workflows
Closed

feat(workflows): support Codex MCP nodes#1456
borshyo wants to merge 1 commit intocoleam00:devfrom
borshyo:codex/codex-mcp-workflows

Conversation

@borshyo
Copy link
Copy Markdown

@borshyo borshyo commented Apr 28, 2026

Summary

  • Problem: Workflow nodes with mcp: were effectively Claude-only. Codex nodes declared mcp: false, validation warned, and Codex workflows could not attach per-node MCP servers such as Figma without configuring them globally outside the workflow.
  • Why it matters: Teams using Codex as their only assistant still need workflow-scoped MCP access for design/code workflows, especially Figma-backed frontend implementation. Requiring global Codex config makes workflows less portable and breaks the per-node capability boundary Archon already provides for Claude.
  • What changed: Adds Codex MCP support by loading the node's MCP JSON, expanding env/header variables (including codebase-scoped env for Codex), translating the config to Codex SDK mcp_servers overrides, and enabling MCP capability metadata for Codex. The shared MCP loader now lives under @archon/providers/mcp/config and supports both Archon's direct server map and the common { "mcpServers": { ... } } wrapper.
  • What did not change (scope boundary): Codex allowed_tools / denied_tools restrictions remain unsupported, so MCP-only sandboxing via allowed_tools: [] is still Claude-only. Pi still ignores mcp. No workflow schema shape, database schema, server API, or Web UI behavior changes.

UX Journey

Before

User                         Archon workflow                 Codex provider
----                         ---------------                 --------------
writes Codex workflow ───▶   node has `mcp: figma.json`
                             validator warns Codex has no MCP support
workflow runs ───────────▶   nodeConfig.mcp is present ───▶  ignored by provider capability path
needs Figma tools ◀───────   tools unavailable unless user configures global Codex MCP

After

User                         Archon workflow                         Codex provider
----                         ---------------                         --------------
writes Codex workflow ───▶   node has `mcp: figma.json`
workflow runs ───────────▶   nodeConfig.mcp is forwarded ─────────▶  [loads MCP JSON]
                                                                  [expands env/header vars]
                                                                  [passes per-call
                                                                   mcp_servers overrides]
needs Figma tools ◀───────   MCP tools are scoped to that node without global Codex config

Architecture Diagram

Before

packages/workflows
  dag-executor.ts
    └─ nodeConfig.mcp ───────────────▶ provider sendQuery()

packages/providers
  claude/provider.ts
    ├─ loadMcpConfig()
    └─ SDK Options.mcpServers + allowedTools wildcards

  codex/provider.ts
    └─ no MCP config translation

  codex/capabilities.ts
    └─ mcp: false

After

packages/workflows
  dag-executor.ts [~]
    └─ nodeConfig.mcp ───────────────▶ provider sendQuery()

packages/providers
  mcp/config.ts [+]
    └═ shared loadMcpConfig()
       - direct server map JSON
       - { "mcpServers": { ... } } wrapper
       - env/header expansion

  claude/provider.ts [~]
    └═ imports shared loadMcpConfig()
       (Claude behavior preserved)

  codex/provider.ts [~]
    └═ imports shared loadMcpConfig()
    └═ converts MCP JSON to Codex `mcp_servers` config overrides
    └═ creates per-call Codex client when config overrides are needed

  codex/capabilities.ts [~]
    └─ mcp: true

Connection inventory:

From To Status Notes
claude/provider.ts providers/mcp/config.ts modified shared loader replaces duplicated Claude-local loader
codex/provider.ts providers/mcp/config.ts new Codex loads per-node MCP JSON at execution time
codex/provider.ts Codex SDK config overrides new passes mcp_servers.<name>.* per call
codex/capabilities.ts provider registry/API capability metadata modified reports mcp: true
workflows/validator.ts capability checks Codex capabilities modified behavior via capability no longer warns for Codex mcp:
Docs workflow authoring/MCP guides modified removes Claude-only wording and documents Codex caveat for tool restrictions

Label Snapshot

  • Risk: risk: medium
  • Size: size: M
  • Scope: workflows|providers|docs|tests
  • Module: providers:codex-mcp

Change Metadata

  • Change type: feature
  • Primary scope: multi

Linked Issue

  • Closes #
  • Related #
  • Depends on #
  • Supersedes #

No linked issue — this closes a provider capability gap for Codex-based workflow users.

Validation Evidence (required)

Commands and result summary:

bun run validate
# exit 0
# includes:
# - bun run check:bundled
# - bun run type-check
# - bun run lint --max-warnings 0
# - bun run format:check
# - bun run test

Additional targeted coverage included in the full suite:

bun test packages/providers/src/codex/provider.test.ts
# 52 pass, 0 fail

bun test packages/workflows/src/dag-executor.test.ts
# 197 pass, 0 fail

bun --filter @archon/workflows test
# exit 0
  • Evidence provided: full local bun run validate completed successfully after this commit. New tests cover Codex mcp_servers override construction, request-env expansion, surfacing workflow-configured MCP client errors, mcpServers wrapper loading, and Codex capability/validator behavior.
  • If any command is intentionally skipped: none.

Security Impact (required)

  • New permissions/capabilities? Yes — Codex workflow nodes can now attach MCP servers explicitly configured by a node's mcp: file.
  • New external network calls? Yes, indirectly — if the MCP config points at an HTTP/SSE endpoint or starts a local MCP process that calls a network service. Archon itself only reads the config and passes it to the Codex SDK.
  • Secrets/tokens handling changed? Yes — Codex MCP env/header values can now be expanded from Archon's process env plus codebase-scoped env vars, matching the existing workflow env injection model.
  • File system access scope changed? Yes — Codex provider now reads the per-node MCP config file that workflow authors opt into via mcp:.
  • If any Yes, describe risk and mitigation:
    • The capability is opt-in per workflow node and scoped to the configured MCP JSON path.
    • Missing env vars are reported by name only, not by secret value.
    • headers are translated to Codex http_headers; values are not logged.
    • User-level plugin MCP connection noise is still filtered at the workflow layer unless the workflow configured MCP itself.

Compatibility / Migration

  • Backward compatible? Yes — existing Claude MCP workflows keep working; Codex workflows that did not set mcp: are unchanged.
  • Config/env changes? No required changes. Existing Archon-style MCP JSON works; common { "mcpServers": { ... } } JSON is also accepted.
  • Database migration needed? No.
  • If yes, exact upgrade steps: none.

Human Verification (required)

What was personally validated beyond CI:

  • Verified scenarios:
    • Codex provider receives nodeConfig.mcp, loads JSON, and passes per-call config.mcp_servers to the Codex SDK.
    • $VAR_NAME expansion uses request/codebase env over process env when provided.
    • headers in Archon MCP JSON maps to Codex http_headers.
    • mcpServers wrapper JSON loads as the server map.
    • Codex provider capability reports mcp: true, and workflow resource validation no longer warns for Codex MCP nodes.
    • Full bun run validate passes locally.
  • Edge cases checked:
    • Missing env vars become empty strings and produce a warning.
    • Non-object MCP server entries are skipped with a warning.
    • Invalid/missing MCP config files still fail clearly through the shared loader.
    • Local user ~/.archon/workflows no longer pollutes loader/dag unit tests due test-local ARCHON_HOME isolation.
  • What was not verified:
    • A live Figma MCP server end-to-end through a real Codex workflow. The provider integration is tested against the mocked Codex SDK boundary; live MCP server behavior depends on the user's MCP command/URL and Codex CLI runtime.

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: @archon/providers Codex/Claude provider internals, workflow MCP validation tests, docs. No server route, database, adapter, isolation, or Web UI state changes.
  • Potential unintended effects:
    • Codex SDK config key compatibility is delegated to the SDK/CLI mcp_servers config parser; unsupported server fields are not passed except for a conservative allowlist.
    • Codex allowed_tools: [] still does not enforce MCP-only sandboxing, so docs explicitly call out that caveat.
    • Moving loadMcpConfig out of claude/provider.ts changes an internal import path; claude/index.ts re-exports it for compatibility.
  • Guardrails/monitoring for early detection:
    • Provider tests assert exact Codex constructor config.
    • Workflow validator tests assert Codex no longer emits unsupported-MCP warnings.
    • MCP connection errors from workflow-configured servers are surfaced to users instead of being hidden as plugin noise.

Rollback Plan (required)

  • Fast rollback command/path: git revert 858e8a99 on dev.
  • Feature flags or config toggles: none.
  • Observable failure symptoms:
    • Codex workflows with mcp: fail to start due Codex config parsing errors.
    • Workflow-configured MCP server errors appear in node output.
    • Missing env warnings appear before a Codex node runs.

Risks and Mitigations

  • Risk: Codex CLI changes its MCP config key shape or rejects a field this PR passes through.
    • Mitigation: the implementation uses the SDK-supported generic config override path and a conservative MCP field allowlist; tests assert the generated config shape.
  • Risk: Users assume allowed_tools: [] now enforces MCP-only sandboxing for Codex.
    • Mitigation: docs explicitly state Codex tool restrictions remain unsupported and should not be used for Codex sandboxing.
  • Risk: Workflow MCP configs copied from other tools use the mcpServers wrapper instead of Archon's direct map.
    • Mitigation: the shared loader now accepts both formats.

Summary by CodeRabbit

  • New Features

    • Codex now supports per-node MCP JSON configuration (parity with Claude); MCP wrapper format accepted.
    • Title generation now resolves and passes provider/assistant-specific configuration when deriving titles.
  • Behavior / Bug Fixes

    • MCP client connection errors are surfaced to the user stream as system messages.
    • Provider tool-wiring and sandboxing behavior clarified: enforcement of allowed-tools varies by provider.
  • Documentation

    • Guides and troubleshooting updated with MCP compatibility and clearer env-var expansion semantics.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Centralizes MCP JSON loading into a shared module, adds MCP support and per-node MCP passthrough to the Codex provider (including error surfacing and env precedence), updates exports/tests/docs, and removes the Claude-local MCP loader in favor of the shared loader.

Changes

Cohort / File(s) Summary
Documentation
packages/docs-web/src/content/docs/guides/authoring-workflows.md, packages/docs-web/src/content/docs/guides/mcp-servers.md
Clarified that mcp: is supported by both Claude and Codex; documented { "mcpServers": ... } wrapper, env expansion semantics, provider tool wiring, sandboxing caveats, and updated troubleshooting.
Shared MCP loader
packages/providers/src/mcp/config.ts
New loadMcpConfig + LoadedMcpConfig: resolves path, parses top-level object or { mcpServers: { ... } }, expands $VARS from provided env source, records missing vars, and normalizes/warns on malformed entries.
Package exports
packages/providers/package.json, packages/providers/src/index.ts, packages/providers/src/claude/index.ts
Added ./mcp/config subpath export; re-exported loadMcpConfig/types from package entry; moved Claude's loadMcpConfig re-export to the new module.
Claude provider
packages/providers/src/claude/provider.ts
Removed inlined MCP loader; now calls shared loadMcpConfig and continues applying mcpServers, allowedTools, and missing-var warnings as before.
Codex provider & capabilities
packages/providers/src/codex/capabilities.ts, packages/providers/src/codex/provider.ts
Enabled mcp: true; during sendQuery loads per-node MCP, maps into config.mcp_servers (headershttp_headers), accepts codex config overrides, logs server metadata, surfaces MCP client errors as system stream chunks, and emits missing-var warnings.
Tests & mocks
packages/providers/src/codex/provider.test.ts, packages/providers/src/registry.test.ts, packages/workflows/src/dag-executor.test.ts, packages/workflows/src/loader.test.ts, packages/workflows/src/validator.test.ts
Updated capability assertions to mcp: true; added Codex MCP regression tests (config loading, env precedence, error streaming); adjusted imports/mocks to use new loader; improved env isolation.
Workflow docs/comment
packages/workflows/src/dag-executor.ts
Updated inline comment to reference @archon/providers/mcp/config as load source for MCP config.
CLI / Title generation
packages/cli/src/commands/workflow.ts, packages/cli/src/commands/workflow.test.ts, packages/core/src/services/title-generator.ts, packages/core/src/services/title-generator.test.ts
Title generation now resolves assistant/provider from workflow and forwards assistantConfig into title-generation sendQuery; tests updated to assert forwarded config.
Workflows package export
packages/workflows/package.json
Added ./model-validation export subpath.

Sequence Diagram(s)

sequenceDiagram
    rect rgba(220,220,255,0.5)
    participant User
    participant WorkflowRunner as Workflow
    participant Provider as Codex Provider
    participant MCP as MCP Loader
    participant CodexSDK as Codex SDK
    end
    User->>Workflow: trigger node execution (nodeConfig.mcp)
    Workflow->>Provider: sendQuery(..., nodeConfig)
    Provider->>MCP: loadMcpConfig(mcpPath, cwd, envSource)
    MCP-->>Provider: servers, serverNames, missingVars
    Provider->>CodexSDK: create client with config + mcp_servers
    CodexSDK-->>Provider: client stream / errors
    Provider->>Workflow: emit result chunks
    Provider->>Workflow: emit MCP client errors as system chunks
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through configs, near and far,
One loader now shines like a star,
Claude and Codex dance in tune,
Vars expand beneath the moon,
A tiny rabbit claps—hip hooray! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding Codex MCP node support, which directly corresponds to the core objective of enabling per-node MCP functionality for the Codex provider.
Description check ✅ Passed The PR description provides comprehensive coverage of all template sections including summary, UX journey, architecture diagrams with module inventory, security impact, validation evidence, compatibility, human verification, side effects, rollback plan, and risk mitigations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 858e8a9958

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

for (const warning of providerWarnings) {
yield { type: 'system', content: `Warning: ${warning.message}` };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prefix Codex provider warnings with ⚠️

The new Codex MCP warning chunks are emitted as Warning: ... here, but workflow execution only forwards provider system messages that start with ⚠️ (or the MCP failure prefix) in packages/workflows/src/dag-executor.ts (msg.content.startsWith('⚠️') branch). As a result, MCP env-missing warnings introduced in this change are silently dropped during workflow runs, which hides actionable misconfiguration and makes MCP auth failures hard to diagnose.

Useful? React with 👍 / 👎.

Comment on lines +85 to +87
const keys = Object.keys(parsed);
if (keys.length !== 1 || keys[0] !== 'mcpServers') {
return parsed;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Unwrap mcpServers even when config has extra top-level keys

This wrapper detection only normalizes when the top-level object is exactly { "mcpServers": ... }; if a copied config includes metadata keys (for example $schema alongside mcpServers), normalization is skipped and providers treat mcpServers as a server name instead of the server map. In Codex this can drop all MCP servers from mcp_servers overrides, so workflows run without the intended MCP tools despite a seemingly valid wrapped config.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
packages/providers/src/codex/provider.test.ts (1)

789-899: Make the MCP config assertions less brittle.

These checks pin the entire config object, so they’ll start failing if Codex gains unrelated per-call options alongside mcp_servers. A nested expect.objectContaining around the MCP payload would keep the test focused on the behavior you care about.

Suggested adjustment
-        expect(MockCodex).toHaveBeenCalledWith(
-          expect.objectContaining({
-            config: {
-              mcp_servers: {
-                figma: {
-                  url: 'http://127.0.0.1:3845/mcp',
-                  http_headers: { Authorization: 'Bearer token-from-process' },
-                  startup_timeout_sec: 20,
-                },
-                local: {
-                  command: 'npx',
-                  args: ['-y', 'figma-mcp'],
-                  env: { TOKEN: 'token-from-process' },
-                },
-              },
-            },
-          })
-        );
+        expect(MockCodex).toHaveBeenCalledWith(
+          expect.objectContaining({
+            config: expect.objectContaining({
+              mcp_servers: expect.objectContaining({
+                figma: expect.objectContaining({
+                  url: 'http://127.0.0.1:3845/mcp',
+                }),
+                local: expect.objectContaining({
+                  command: 'npx',
+                }),
+              }),
+            }),
+          })
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/codex/provider.test.ts` around lines 789 - 899, The
assertions pin the entire config object; update the two MockCodex expectations
so they only assert the mcp_servers portion by nesting expect.objectContaining:
replace expect.objectContaining({ config: { mcp_servers: { ... } } }) with
expect.objectContaining({ config: expect.objectContaining({ mcp_servers:
expect.objectContaining({ /* server entries like figma/local or figma only */ })
}) }) and likewise use expect.objectContaining for the inner server entries
(figma, local) in both tests to avoid brittleness when Codex adds other per-call
options.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/docs-web/src/content/docs/guides/authoring-workflows.md`:
- Around line 1176-1177: Update the Node Fields table row that currently
contains the "Claude only" note to reflect the new behavior: change the
annotation for the mcp field (the table row referencing "mcp" / "MCP") so it
indicates MCP servers can be attached to both Codex and Claude nodes (and keep
the "skills" note as only for Claude if that remains true); locate the table row
containing the literal "Claude only" and replace that text with a short,
consistent note such as "Codex and Claude" or otherwise match the summary
bullets for `mcp:` and `skills:` so the table no longer contradicts the summary.

In `@packages/docs-web/src/content/docs/guides/mcp-servers.md`:
- Around line 175-176: The doc uses the internal camelCase field name
allowedTools but the guide is user-facing and elsewhere uses the YAML-facing
allowed_tools; update the text and any examples to use allowed_tools instead of
allowedTools (search for occurrences of allowedTools and replace with
allowed_tools) and ensure the surrounding explanation and sample YAML reflect
the snake_case user-facing field name consistently.

In `@packages/providers/src/codex/provider.ts`:
- Around line 334-335: The current filter uses a case-sensitive check
errorEvent.message.includes('MCP client'), causing misses for variants like "Mcp
client" or "mcp client"; update the conditional in the error-emitting block so
it does a case-insensitive match (e.g., compare lowercased strings or use a
case-insensitive regex) when deciding whether to suppress MCP client messages,
referencing the surfaceMcpClientErrors flag and errorEvent.message to preserve
existing behavior.

In `@packages/providers/src/mcp/config.ts`:
- Around line 81-96: normalizeMcpConfig currently unwraps a single-key {
"mcpServers": ... } but silently allows mixed formats; change it to explicitly
reject any config that contains "mcpServers" alongside any other top-level keys
(e.g., throw an Error if keys.includes('mcpServers') && keys.length > 1) so
mixed wrapper-and-metadata formats fail fast; keep the existing behavior for the
pure single-key wrapper (keys.length === 1 && keys[0] === 'mcpServers') and for
configs that do not include 'mcpServers' at all, and mention/consider downstream
callers like expandEnvVars when forming the error message to make debugging
clear.

---

Nitpick comments:
In `@packages/providers/src/codex/provider.test.ts`:
- Around line 789-899: The assertions pin the entire config object; update the
two MockCodex expectations so they only assert the mcp_servers portion by
nesting expect.objectContaining: replace expect.objectContaining({ config: {
mcp_servers: { ... } } }) with expect.objectContaining({ config:
expect.objectContaining({ mcp_servers: expect.objectContaining({ /* server
entries like figma/local or figma only */ }) }) }) and likewise use
expect.objectContaining for the inner server entries (figma, local) in both
tests to avoid brittleness when Codex adds other per-call options.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9f974646-842d-4651-a22c-df0a41d3e9bf

📥 Commits

Reviewing files that changed from the base of the PR and between 287bb35 and 858e8a9.

📒 Files selected for processing (15)
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md
  • packages/docs-web/src/content/docs/guides/mcp-servers.md
  • packages/providers/package.json
  • packages/providers/src/claude/index.ts
  • packages/providers/src/claude/provider.ts
  • packages/providers/src/codex/capabilities.ts
  • packages/providers/src/codex/provider.test.ts
  • packages/providers/src/codex/provider.ts
  • packages/providers/src/index.ts
  • packages/providers/src/mcp/config.ts
  • packages/providers/src/registry.test.ts
  • packages/workflows/src/dag-executor.test.ts
  • packages/workflows/src/dag-executor.ts
  • packages/workflows/src/loader.test.ts
  • packages/workflows/src/validator.test.ts

Comment thread packages/docs-web/src/content/docs/guides/authoring-workflows.md
Comment on lines +175 to +176
Claude nodes automatically add tool wildcards to `allowedTools`. For servers
named `github` and `postgres`, the node gets:
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use user-facing field name in docs (allowed_tools)

Line 175 references allowedTools (internal casing), but this guide is workflow-author facing and otherwise uses allowed_tools. Keeping one form avoids confusion in YAML authoring.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~176-~176: The official name of this software platform is spelled with a capital “H”.
Context: ...ds to allowedTools. For servers named github and postgres, the node gets: - `mcp...

(GITHUB)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs-web/src/content/docs/guides/mcp-servers.md` around lines 175 -
176, The doc uses the internal camelCase field name allowedTools but the guide
is user-facing and elsewhere uses the YAML-facing allowed_tools; update the text
and any examples to use allowed_tools instead of allowedTools (search for
occurrences of allowedTools and replace with allowed_tools) and ensure the
surrounding explanation and sample YAML reflect the snake_case user-facing field
name consistently.

Comment on lines +334 to 335
if (surfaceMcpClientErrors || !errorEvent.message.includes('MCP client')) {
yield { type: 'system', content: `⚠️ ${errorEvent.message}` };
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Make MCP-client error filtering case-insensitive

Line 334 uses a case-sensitive includes('MCP client'). If the SDK emits variants like Mcp client / mcp client, filtering behavior becomes inconsistent.

Suggested fix
-      if (surfaceMcpClientErrors || !errorEvent.message.includes('MCP client')) {
+      const isMcpClientError = errorEvent.message.toLowerCase().includes('mcp client');
+      if (surfaceMcpClientErrors || !isMcpClientError) {
         yield { type: 'system', content: `⚠️ ${errorEvent.message}` };
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (surfaceMcpClientErrors || !errorEvent.message.includes('MCP client')) {
yield { type: 'system', content: `⚠️ ${errorEvent.message}` };
const isMcpClientError = errorEvent.message.toLowerCase().includes('mcp client');
if (surfaceMcpClientErrors || !isMcpClientError) {
yield { type: 'system', content: `⚠️ ${errorEvent.message}` };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/codex/provider.ts` around lines 334 - 335, The current
filter uses a case-sensitive check errorEvent.message.includes('MCP client'),
causing misses for variants like "Mcp client" or "mcp client"; update the
conditional in the error-emitting block so it does a case-insensitive match
(e.g., compare lowercased strings or use a case-insensitive regex) when deciding
whether to suppress MCP client messages, referencing the surfaceMcpClientErrors
flag and errorEvent.message to preserve existing behavior.

Comment on lines +81 to +96
function normalizeMcpConfig(
parsed: Record<string, unknown>,
mcpPath: string
): Record<string, unknown> {
const keys = Object.keys(parsed);
if (keys.length !== 1 || keys[0] !== 'mcpServers') {
return parsed;
}

const servers = parsed.mcpServers;
if (typeof servers !== 'object' || servers === null || Array.isArray(servers)) {
throw new Error(`MCP config field "mcpServers" must be a JSON object: ${mcpPath}`);
}

return servers as Record<string, unknown>;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual file to understand context
cat -n packages/providers/src/mcp/config.ts | head -120

Repository: coleam00/Archon

Length of output: 4461


🏁 Script executed:

# Search for tests related to normalizeMcpConfig
find . -type f -name "*.test.ts" -o -name "*.spec.ts" | xargs rg "normalizeMcpConfig" -l

Repository: coleam00/Archon

Length of output: 43


🏁 Script executed:

# Look for any MCP specification or documentation
find . -type f \( -name "*.md" -o -name "*.txt" \) | xargs rg -l "mcpServers\|MCP config" 2>/dev/null | head -10

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

# Search for all usages of normalizeMcpConfig
rg "normalizeMcpConfig" --type ts --type tsx -B 3 -A 3

Repository: coleam00/Archon

Length of output: 87


🏁 Script executed:

# Look for any MCP-related documentation or schema files
fd -e json -e schema | xargs rg -l "mcpServers" 2>/dev/null

Repository: coleam00/Archon

Length of output: 43


🏁 Script executed:

# Continue reading the config.ts file to see complete context
cat -n packages/providers/src/mcp/config.ts | sed -n '96,160p'

Repository: coleam00/Archon

Length of output: 1678


🏁 Script executed:

# Search for test files in the mcp directory
find packages/providers/src/mcp -type f \( -name "*.test.ts" -o -name "*.spec.ts" \) 2>/dev/null

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

# Search for usages of normalizeMcpConfig with correct syntax
rg "normalizeMcpConfig" -t ts -B 3 -A 3 2>/dev/null

Repository: coleam00/Archon

Length of output: 1078


🏁 Script executed:

# Check if there are any example or documentation files
find packages/providers -type f -name "*.json" -o -name "*.md" | head -20

Repository: coleam00/Archon

Length of output: 124


🏁 Script executed:

# Look for MCP-related documentation or examples
rg "mcpServers|MCP config" --type ts -B 2 -A 2 | head -100

Repository: coleam00/Archon

Length of output: 7186


🏁 Script executed:

# Look at the test cases more closely to understand intended behavior
cat packages/workflows/src/dag-executor.test.ts | grep -A 15 "loads standard mcpServers-wrapped"

Repository: coleam00/Archon

Length of output: 766


🏁 Script executed:

# Search for any comments or documentation about MCP config format
rg "mcpServers|MCP.*config.*format|MCP.*structure" packages/providers/src/mcp/ -B 2 -A 2

Repository: coleam00/Archon

Length of output: 823


🏁 Script executed:

# Check if there are examples or documentation about MCP config shape
find . -name "*.md" -o -name "README*" | xargs rg -l "mcp" 2>/dev/null | head -5

Repository: coleam00/Archon

Length of output: 343


🏁 Script executed:

# Look at how the loaded config is actually used after normalization
rg "expanded\|servers" packages/providers/src/mcp/config.ts -B 2 -A 2

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

# Search for any validator or schema that might define expected MCP structure
find packages -name "*mcp*" -type f | head -20

Repository: coleam00/Archon

Length of output: 116


🏁 Script executed:

# Read the MCP servers documentation
cat packages/docs-web/src/content/docs/guides/mcp-servers.md

Repository: coleam00/Archon

Length of output: 13041


🏁 Script executed:

# Look for examples in the docs about configuration format
rg "mcpServers|{.*command" packages/docs-web/src/content/docs/guides/mcp-servers.md -B 2 -A 5

Repository: coleam00/Archon

Length of output: 167


🏁 Script executed:

# Check if there are example config files in the repository
find . -name "*.json" -path "*mcp*" -o -name "*.json" -path "*example*" | head -10

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

# Look for any tests that specifically test the normalizeMcpConfig behavior with edge cases
rg -A 30 "normalizeMcpConfig|loads standard mcpServers" packages/workflows/src/dag-executor.test.ts

Repository: coleam00/Archon

Length of output: 1420


🏁 Script executed:

# Let's see what happens downstream when expandEnvVars processes a mixed object
cat -n packages/providers/src/mcp/config.ts | sed -n '47,79p'

Repository: coleam00/Archon

Length of output: 1352


🏁 Script executed:

# Check the full flow to see if there's error handling for malformed server configs
cat -n packages/providers/src/mcp/config.ts | sed -n '24,45p'

Repository: coleam00/Archon

Length of output: 889


🏁 Script executed:

# Let's check what the actual behavior would be with a mixed format
# by examining if there are any tests covering this edge case
rg -A 20 "test.*normalizeMcp|it.*wrapper|it.*mixed" packages/workflows/src/dag-executor.test.ts

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

# Check if there's any validation elsewhere that rejects mixed formats
rg "mcpServers" packages/ -B 3 -A 3 --type ts | grep -A 5 -B 5 "metadata\|additional\|extra"

Repository: coleam00/Archon

Length of output: 41


🏁 Script executed:

# Let me verify the exact behavior with a practical scenario
# Check if there are any real-world examples or edge case tests
rg "mcpServers" packages/workflows/src/dag-executor.test.ts -B 5 -A 10

Repository: coleam00/Archon

Length of output: 831


🏁 Script executed:

# Check if there are any comments explaining the strict behavior
rg -B 5 -A 5 "keys.length !== 1|only.*top-level" packages/providers/src/mcp/config.ts

Repository: coleam00/Archon

Length of output: 405


🏁 Script executed:

# Look at the docstring for normalizeMcpConfig
cat -n packages/providers/src/mcp/config.ts | sed -n '75,96p'

Repository: coleam00/Archon

Length of output: 789


🏁 Script executed:

# Let me verify the actual handling by simulating the mixed format scenario
python3 << 'EOF'
# Simulate what happens with mixed format: { mcpServers: {...}, metadata: "foo" }
mixed = { "mcpServers": { "github": { "command": "npx" } }, "metadata": "extra" }
keys = list(mixed.keys())
print(f"keys.length = {len(keys)}")
print(f"keys[0] = {keys[0]}")
print(f"Unwrap condition (len == 1 AND key == 'mcpServers'): {len(keys) == 1 and keys[0] == 'mcpServers'}")
print(f"Would return as-is: {not (len(keys) == 1 and keys[0] == 'mcpServers')}")
print(f"\nAfter returning as-is, expandEnvVars would iterate:")
for key, val in mixed.items():
    is_obj = isinstance(val, dict)
    print(f"  key='{key}', value_is_object={is_obj}, would_skip={not is_obj}")
EOF

Repository: coleam00/Archon

Length of output: 349


Consider explicitly rejecting mixed wrapper-and-metadata formats to fail fast.

While documentation only specifies two supported formats (direct server map or single-key { "mcpServers": ... } wrapper), the current code silently mishandles mixed formats like { "mcpServers": {...}, "metadata": "..." } by treating the wrapper itself as a server name in expandEnvVars(). This violates the guideline to "prefer throwing early with a clear error for unsupported or unsafe states." Either validate that the unwrapped config is being used correctly, or explicitly reject configurations with extraneous top-level keys alongside mcpServers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/mcp/config.ts` around lines 81 - 96,
normalizeMcpConfig currently unwraps a single-key { "mcpServers": ... } but
silently allows mixed formats; change it to explicitly reject any config that
contains "mcpServers" alongside any other top-level keys (e.g., throw an Error
if keys.includes('mcpServers') && keys.length > 1) so mixed wrapper-and-metadata
formats fail fast; keep the existing behavior for the pure single-key wrapper
(keys.length === 1 && keys[0] === 'mcpServers') and for configs that do not
include 'mcpServers' at all, and mention/consider downstream callers like
expandEnvVars when forming the error message to make debugging clear.

@borshyo borshyo force-pushed the codex/codex-mcp-workflows branch from 858e8a9 to 3d4e767 Compare April 28, 2026 06:32
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
packages/docs-web/src/content/docs/guides/mcp-servers.md (1)

175-176: ⚠️ Potential issue | 🟡 Minor

Use the user-facing field name (allowed_tools) in docs.

Line 175 currently uses allowedTools, which conflicts with the YAML-facing name used elsewhere in this guide.

Suggested fix
-Claude nodes automatically add tool wildcards to `allowedTools`. For servers
+Claude nodes automatically add tool wildcards to `allowed_tools`. For servers
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/docs-web/src/content/docs/guides/mcp-servers.md` around lines 175 -
176, The docs line uses the code-facing identifier allowedTools but should use
the user-facing YAML name allowed_tools; update the sentence that currently
reads "Claude nodes automatically add tool wildcards to `allowedTools`" (the
surrounding example mentioning servers named `github` and `postgres`) to use
`allowed_tools` instead so terminology matches the rest of the guide.
packages/providers/src/codex/provider.ts (1)

334-335: ⚠️ Potential issue | 🟡 Minor

Make MCP-client error filtering case-insensitive.

Line 334 uses a case-sensitive match, so variants like mcp client / Mcp client can bypass intended filtering behavior.

Suggested fix
-      if (surfaceMcpClientErrors || !errorEvent.message.includes('MCP client')) {
+      const isMcpClientError = errorEvent.message.toLowerCase().includes('mcp client');
+      if (surfaceMcpClientErrors || !isMcpClientError) {
         yield { type: 'system', content: `⚠️ ${errorEvent.message}` };
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/codex/provider.ts` around lines 334 - 335, The current
conditional in the Codex provider uses a case-sensitive check
errorEvent.message.includes('MCP client'), which lets variants like "mcp client"
bypass filtering; update the check used in the if block that references
surfaceMcpClientErrors and errorEvent.message so it performs a case-insensitive
match (e.g., compare errorEvent.message.toLowerCase().includes('mcp client') or
use a case-insensitive regex) and ensure you guard against a missing/undefined
message before calling toLowerCase(); keep the rest of the logic (yielding
system message) unchanged.
packages/providers/src/mcp/config.ts (1)

85-96: ⚠️ Potential issue | 🟠 Major

Reject mixed top-level mcpServers + sibling keys explicitly.

If mcpServers appears alongside other top-level keys, the loader currently treats the file as a direct map and can misinterpret the wrapper itself as a server entry instead of failing fast.

Suggested fix
 function normalizeMcpConfig(
   parsed: Record<string, unknown>,
   mcpPath: string
 ): Record<string, unknown> {
   const keys = Object.keys(parsed);
+  if (keys.includes('mcpServers') && keys.length > 1) {
+    throw new Error(
+      `MCP config cannot mix top-level "mcpServers" with other keys: ${mcpPath}`
+    );
+  }
   if (keys.length !== 1 || keys[0] !== 'mcpServers') {
     return parsed;
   }
As per coding guidelines: "Prefer throwing early with a clear error for unsupported or unsafe states; never silently swallow errors (Fail Fast + Explicit Errors)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/mcp/config.ts` around lines 85 - 96, The loader
currently returns parsed when keys.length !== 1 || keys[0] !== 'mcpServers',
which allows files that contain mcpServers plus other top-level keys to be
treated as valid; change the logic to detect the mixed-case and throw instead:
if keys.includes('mcpServers') && keys.length > 1 throw a clear Error mentioning
mcpServers and mcpPath; otherwise keep the existing branches (return parsed for
unrelated structures, and later validate parsed.mcpServers as an object).
Reference symbols: parsed, keys, mcpServers, mcpPath.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/cli/src/commands/workflow.ts`:
- Around line 655-670: The change made title generation a blocking step by
awaiting loadConfig(cwd); instead restore fire-and-forget behavior by moving
config loading and title generation into an unawaited async task that catches
and swallows errors: create an inline async wrapper (or helper) that calls
loadConfig, resolveTitleAssistantType, determines titleAssistantConfig, then
calls generateAndSetTitle(conversation.id, ...), and invoke it without await;
ensure any exceptions from loadConfig or generateAndSetTitle are caught/logged
but do not propagate to the main workflowRunCommand path.

In `@packages/providers/src/mcp/config.ts`:
- Around line 31-34: The current branch in the config parsing logic (where val
and key are handled) should not coerce non-string env/headers values; replace
the getLog().warn + String(val) coercion with an explicit thrown error that
includes the key name and the actual typeof val (e.g., throw new Error(`Invalid
config: expected string for ${key}, got ${typeof val}`)); update the code path
around result[key] assignment in the same function so invalid types immediately
fail rather than being coerced, and ensure any callers of this config loader
will surface that error (preserve existing getLog() usage only for context, not
for swallowing the error).
- Around line 57-60: The current check in config.ts allows arrays because typeof
returns "object"; update the validation for serverConfig (use
Array.isArray(serverConfig)) and treat arrays as invalid by throwing a clear
error (include serverName and the actual value/type) instead of
warning/continuing; replace or augment the block that currently calls
getLog().warn({ serverName, valueType: typeof serverConfig },
'mcp_server_config_not_object') so that when serverConfig is null, not an
object, or an array you throw a descriptive Error describing the unsupported
array-valued server config for serverName.

---

Duplicate comments:
In `@packages/docs-web/src/content/docs/guides/mcp-servers.md`:
- Around line 175-176: The docs line uses the code-facing identifier
allowedTools but should use the user-facing YAML name allowed_tools; update the
sentence that currently reads "Claude nodes automatically add tool wildcards to
`allowedTools`" (the surrounding example mentioning servers named `github` and
`postgres`) to use `allowed_tools` instead so terminology matches the rest of
the guide.

In `@packages/providers/src/codex/provider.ts`:
- Around line 334-335: The current conditional in the Codex provider uses a
case-sensitive check errorEvent.message.includes('MCP client'), which lets
variants like "mcp client" bypass filtering; update the check used in the if
block that references surfaceMcpClientErrors and errorEvent.message so it
performs a case-insensitive match (e.g., compare
errorEvent.message.toLowerCase().includes('mcp client') or use a
case-insensitive regex) and ensure you guard against a missing/undefined message
before calling toLowerCase(); keep the rest of the logic (yielding system
message) unchanged.

In `@packages/providers/src/mcp/config.ts`:
- Around line 85-96: The loader currently returns parsed when keys.length !== 1
|| keys[0] !== 'mcpServers', which allows files that contain mcpServers plus
other top-level keys to be treated as valid; change the logic to detect the
mixed-case and throw instead: if keys.includes('mcpServers') && keys.length > 1
throw a clear Error mentioning mcpServers and mcpPath; otherwise keep the
existing branches (return parsed for unrelated structures, and later validate
parsed.mcpServers as an object). Reference symbols: parsed, keys, mcpServers,
mcpPath.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8b00d546-63b8-4492-a936-c4ff1c886d8a

📥 Commits

Reviewing files that changed from the base of the PR and between 858e8a9 and 3d4e767.

📒 Files selected for processing (20)
  • packages/cli/src/commands/workflow.test.ts
  • packages/cli/src/commands/workflow.ts
  • packages/core/src/services/title-generator.test.ts
  • packages/core/src/services/title-generator.ts
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md
  • packages/docs-web/src/content/docs/guides/mcp-servers.md
  • packages/providers/package.json
  • packages/providers/src/claude/index.ts
  • packages/providers/src/claude/provider.ts
  • packages/providers/src/codex/capabilities.ts
  • packages/providers/src/codex/provider.test.ts
  • packages/providers/src/codex/provider.ts
  • packages/providers/src/index.ts
  • packages/providers/src/mcp/config.ts
  • packages/providers/src/registry.test.ts
  • packages/workflows/package.json
  • packages/workflows/src/dag-executor.test.ts
  • packages/workflows/src/dag-executor.ts
  • packages/workflows/src/loader.test.ts
  • packages/workflows/src/validator.test.ts
✅ Files skipped from review due to trivial changes (7)
  • packages/workflows/package.json
  • packages/providers/src/codex/capabilities.ts
  • packages/providers/src/index.ts
  • packages/providers/package.json
  • packages/workflows/src/dag-executor.ts
  • packages/providers/src/claude/index.ts
  • packages/docs-web/src/content/docs/guides/authoring-workflows.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/workflows/src/validator.test.ts
  • packages/providers/src/registry.test.ts
  • packages/workflows/src/dag-executor.test.ts
  • packages/workflows/src/loader.test.ts
  • packages/providers/src/codex/provider.test.ts

Comment on lines 655 to 670
// Auto-generate title for CLI workflow conversations (fire-and-forget)
const workflowConfig = await loadConfig(cwd);
const titleAssistantType = resolveTitleAssistantType(
workflow,
workflowConfig.assistant,
conversation.ai_assistant_type
);
const titleAssistantConfig = workflowConfig.assistants?.[titleAssistantType] ?? {};
void generateAndSetTitle(
conversation.id,
userMessage,
conversation.ai_assistant_type,
titleAssistantType,
workingCwd,
workflowName
workflowName,
titleAssistantConfig
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't make title generation a hard dependency of workflowRunCommand.

loadConfig(cwd) now runs on the main execution path, so a missing or malformed .archon/config.yaml will abort the workflow before it even starts. That regresses the previous fire-and-forget behavior for titles.

Suggested fix
-  const workflowConfig = await loadConfig(cwd);
-  const titleAssistantType = resolveTitleAssistantType(
-    workflow,
-    workflowConfig.assistant,
-    conversation.ai_assistant_type
-  );
-  const titleAssistantConfig = workflowConfig.assistants?.[titleAssistantType] ?? {};
+  let workflowConfig;
+  try {
+    workflowConfig = await loadConfig(cwd);
+  } catch (error) {
+    getLog().warn({ err: error as Error, cwd }, 'workflow.title_config_load_failed');
+  }
+
+  const titleAssistantType = resolveTitleAssistantType(
+    workflow,
+    workflowConfig?.assistant,
+    conversation.ai_assistant_type
+  );
+  const titleAssistantConfig = workflowConfig?.assistants?.[titleAssistantType] ?? {};
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Auto-generate title for CLI workflow conversations (fire-and-forget)
const workflowConfig = await loadConfig(cwd);
const titleAssistantType = resolveTitleAssistantType(
workflow,
workflowConfig.assistant,
conversation.ai_assistant_type
);
const titleAssistantConfig = workflowConfig.assistants?.[titleAssistantType] ?? {};
void generateAndSetTitle(
conversation.id,
userMessage,
conversation.ai_assistant_type,
titleAssistantType,
workingCwd,
workflowName
workflowName,
titleAssistantConfig
);
// Auto-generate title for CLI workflow conversations (fire-and-forget)
let workflowConfig;
try {
workflowConfig = await loadConfig(cwd);
} catch (error) {
getLog().warn({ err: error as Error, cwd }, 'workflow.title_config_load_failed');
}
const titleAssistantType = resolveTitleAssistantType(
workflow,
workflowConfig?.assistant,
conversation.ai_assistant_type
);
const titleAssistantConfig = workflowConfig?.assistants?.[titleAssistantType] ?? {};
void generateAndSetTitle(
conversation.id,
userMessage,
titleAssistantType,
workingCwd,
workflowName,
titleAssistantConfig
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/workflow.ts` around lines 655 - 670, The change
made title generation a blocking step by awaiting loadConfig(cwd); instead
restore fire-and-forget behavior by moving config loading and title generation
into an unawaited async task that catches and swallows errors: create an inline
async wrapper (or helper) that calls loadConfig, resolveTitleAssistantType,
determines titleAssistantConfig, then calls generateAndSetTitle(conversation.id,
...), and invoke it without await; ensure any exceptions from loadConfig or
generateAndSetTitle are caught/logged but do not propagate to the main
workflowRunCommand path.

Comment on lines +31 to +34
if (typeof val !== 'string') {
getLog().warn({ key, valueType: typeof val }, 'mcp_env_value_coerced_to_string');
result[key] = String(val);
continue;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not coerce non-string env/headers values; fail with a clear error.

String coercion here hides invalid config and can turn malformed secrets/headers into unusable values without surfacing a hard failure.

Suggested fix
 function expandEnvVarsInRecord(
   record: Record<string, unknown>,
   missingVars: string[],
-  envSource: EnvSource
+  envSource: EnvSource,
+  fieldPath: string
 ): Record<string, string> {
   const result: Record<string, string> = {};
   for (const [key, val] of Object.entries(record)) {
     if (typeof val !== 'string') {
-      getLog().warn({ key, valueType: typeof val }, 'mcp_env_value_coerced_to_string');
-      result[key] = String(val);
-      continue;
+      throw new Error(`MCP config ${fieldPath}.${key} must be a string`);
     }
-      server.env = expandEnvVarsInRecord(
+      server.env = expandEnvVarsInRecord(
         server.env as Record<string, unknown>,
         missingVars,
-        envSource
+        envSource,
+        `${serverName}.env`
       );
...
-      server.headers = expandEnvVarsInRecord(
+      server.headers = expandEnvVarsInRecord(
         server.headers as Record<string, unknown>,
         missingVars,
-        envSource
+        envSource,
+        `${serverName}.headers`
       );
As per coding guidelines: "Prefer throwing early with a clear error for unsupported or unsafe states; never silently swallow errors (Fail Fast + Explicit Errors)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/mcp/config.ts` around lines 31 - 34, The current
branch in the config parsing logic (where val and key are handled) should not
coerce non-string env/headers values; replace the getLog().warn + String(val)
coercion with an explicit thrown error that includes the key name and the actual
typeof val (e.g., throw new Error(`Invalid config: expected string for ${key},
got ${typeof val}`)); update the code path around result[key] assignment in the
same function so invalid types immediately fail rather than being coerced, and
ensure any callers of this config loader will surface that error (preserve
existing getLog() usage only for context, not for swallowing the error).

Comment on lines +57 to +60
if (typeof serverConfig !== 'object' || serverConfig === null) {
getLog().warn({ serverName, valueType: typeof serverConfig }, 'mcp_server_config_not_object');
continue;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reject array-valued server configs during validation.

Arrays currently pass the object check and flow through as pseudo-objects, which weakens entry validation and can produce malformed provider config.

Suggested fix
-    if (typeof serverConfig !== 'object' || serverConfig === null) {
-      getLog().warn({ serverName, valueType: typeof serverConfig }, 'mcp_server_config_not_object');
-      continue;
+    if (
+      typeof serverConfig !== 'object' ||
+      serverConfig === null ||
+      Array.isArray(serverConfig)
+    ) {
+      throw new Error(
+        `MCP server "${serverName}" must be a JSON object`
+      );
     }
As per coding guidelines: "Prefer throwing early with a clear error for unsupported or unsafe states; never silently swallow errors (Fail Fast + Explicit Errors)".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/mcp/config.ts` around lines 57 - 60, The current check
in config.ts allows arrays because typeof returns "object"; update the
validation for serverConfig (use Array.isArray(serverConfig)) and treat arrays
as invalid by throwing a clear error (include serverName and the actual
value/type) instead of warning/continuing; replace or augment the block that
currently calls getLog().warn({ serverName, valueType: typeof serverConfig },
'mcp_server_config_not_object') so that when serverConfig is null, not an
object, or an array you throw a descriptive Error describing the unsupported
array-valued server config for serverName.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant