Skip to content

[Bug]: openclaw config validate rejects transport: "stdio" on a command-bearing MCP server that the runtime accepts #95082

Description

@ken-jo

Filed via the Bug report template. Bug type: Behavior bug (incorrect output/state without crash). Beta release blocker: No.

Summary

An MCP server defined with both command and an explicit transport: "stdio" fails openclaw config validate (the schema only allows transport ∈ {sse, streamable-http}), even though the runtime explicitly resolves any command-bearing server as stdio and runs it correctly — so config-validation and runtime behavior disagree for the same config.

Steps to reproduce

  1. Register an MCP server with an explicit stdio transport (mirrors the documented openclaw mcp set shape in docs/cli/mcp.md):
    openclaw mcp set demo '{"command":"npx","args":["-y","@modelcontextprotocol/server-memory"],"transport":"stdio"}'
    
  2. Run openclaw config validate.
  3. Observe validation fails on the transport field — "stdio" is not in the allowed union. Remove transport (or set it to streamable-http) and validation passes; the stdio server also runs fine either way because the runtime infers stdio from command.

Expected behavior

config validate should accept the same config the runtime accepts. The runtime treats any command-bearing server as stdio regardless of the transport value, so an explicit transport: "stdio" alongside command is a valid, runnable config and should not be rejected by validation.

Actual behavior

Validation rejects transport: "stdio". The constraint is in src/config/zod-schema.ts:395:

transport: z.union([z.literal("sse"), z.literal("streamable-http")]).optional(),

McpServerSchema is a z.object({...}).catchall(z.unknown()) (src/config/zod-schema.ts:386-448). The .catchall(z.unknown()) only relaxes unknown keys; it does not loosen the explicitly-declared transport field, so a literal "stdio" fails the union and config validate errors.

Meanwhile the runtime accepts it. resolveMcpTransportConfig (src/agents/mcp-transport-config.ts:199) reads the requested transport via getRequestedTransport (same file, lines 120-129 — accepts any string and lowercases it), then at lines 214-224:

if (stdioLaunch.ok) {
  // A command-bearing server is always treated as stdio even when HTTP-ish
  // aliases are present
  return {
    kind: "stdio",
    transportType: "stdio",
    command: stdioLaunch.config.command,
    ...

So a command + transport: "stdio" server works at runtime but fails config validate — an internal inconsistency between the validator and the resolver.

Why this matters

We hit this while building a cross-platform MCP/hook adapter that generates OpenClaw configs programmatically. A config generator (or an operator copying a remote-server example that includes a transport key, then switching it to a local command) naturally writes transport: "stdio" for a stdio server. That config runs, but config validate reports it as invalid — which is confusing precisely because validation is the tool people trust to tell them whether a config is correct. It is the same class of defect already tracked for other fields where the schema is stricter than the runtime/types (related, not duplicate: #73680 per-agent verboseDefault/elevatedDefault rejected though the resolver/SDK support them; #65305 historyLimit using .positive() instead of .min(0)).

Note on impact (honest scope): the docs already steer users to omit transport for stdio — the "### Stdio transport" section (docs/cli/mcp.md:666-675) lists only command/args/env/cwd and never a transport field, and transport is documented only for remote servers (docs/cli/mcp.md:451; docs/gateway/configuration-reference.md:137-141). So the practical blast radius is mostly external config generators and copy-paste-from-remote-examples — this is primarily a robustness / validate-vs-runtime consistency fix, not a critical break.

Proposed fix

Either of the following keeps config validate consistent with the runtime:

  1. Accept it in the schema (one line). Add "stdio" to the union and let the existing command-precedence resolver handle it:
    transport: z.union([z.literal("stdio"), z.literal("sse"), z.literal("streamable-http")]).optional(),
  2. Targeted validator hint. Keep the union but, when a server has both command and transport: "stdio", emit a specific message ("stdio servers are inferred from command; the transport field is only for remote servers — omit it") instead of the generic union error.

Option 1 is the smaller, runtime-aligned change since resolveMcpTransportConfig already ignores the transport value for command-bearing servers.

Environment

  • OpenClaw version: confirmed against current main (source: src/config/zod-schema.ts:395 and src/agents/mcp-transport-config.ts:214-224).
  • Operating system: cross-platform (config-schema validation; not OS-specific).
  • Model / provider chain: NOT_ENOUGH_INFO (not model- or provider-dependent — this is config-schema validation only).

Offer

Happy to send a small PR (the one-line union change in src/config/zod-schema.ts, plus a validator test mirroring the existing MCP-schema cases) with real-behavior proof: openclaw config validate output on the repro config before vs. after the change. I'll keep it to this single concern and follow the Real-behavior-proof checklist. Let me know which direction (accept "stdio" vs. targeted hint) you prefer before I open it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:linked-pr-openClawSweeper found an open linked pull request for this issue.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions