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
- 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"}'
- Run
openclaw config validate.
- 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:
- 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(),
- 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.
Summary
An MCP server defined with both
commandand an explicittransport: "stdio"failsopenclaw config validate(the schema only allowstransport∈ {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
openclaw mcp setshape in docs/cli/mcp.md):openclaw config validate.transportfield —"stdio"is not in the allowed union. Removetransport(or set it tostreamable-http) and validation passes; the stdio server also runs fine either way because the runtime infers stdio fromcommand.Expected behavior
config validateshould accept the same config the runtime accepts. The runtime treats any command-bearing server as stdio regardless of thetransportvalue, so an explicittransport: "stdio"alongsidecommandis a valid, runnable config and should not be rejected by validation.Actual behavior
Validation rejects
transport: "stdio". The constraint is insrc/config/zod-schema.ts:395:McpServerSchemais az.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-declaredtransportfield, so a literal"stdio"fails the union andconfig validateerrors.Meanwhile the runtime accepts it.
resolveMcpTransportConfig(src/agents/mcp-transport-config.ts:199) reads the requested transport viagetRequestedTransport(same file, lines 120-129 — accepts any string and lowercases it), then at lines 214-224:So a
command+transport: "stdio"server works at runtime but failsconfig 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
transportkey, then switching it to a local command) naturally writestransport: "stdio"for a stdio server. That config runs, butconfig validatereports 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-agentverboseDefault/elevatedDefaultrejected though the resolver/SDK support them; #65305historyLimitusing.positive()instead of.min(0)).Note on impact (honest scope): the docs already steer users to omit
transportfor stdio — the "### Stdio transport" section (docs/cli/mcp.md:666-675) lists onlycommand/args/env/cwdand never atransportfield, andtransportis 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 validateconsistent with the runtime:"stdio"to the union and let the existing command-precedence resolver handle it:commandandtransport: "stdio", emit a specific message ("stdio servers are inferred fromcommand; thetransportfield is only for remote servers — omit it") instead of the generic union error.Option 1 is the smaller, runtime-aligned change since
resolveMcpTransportConfigalready ignores the transport value for command-bearing servers.Environment
main(source: src/config/zod-schema.ts:395 and src/agents/mcp-transport-config.ts:214-224).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 validateoutput 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.