crewai - ✅(Solved) Fix Suggest: add command allowlist validation for MCP stdio transport [1 pull requests, 1 comments, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
crewAIInc/crewAI#5080Fetched 2026-04-08 01:35:24
View on GitHub
Comments
1
Participants
1
Timeline
6
Reactions
0
Participants
Timeline (top)
cross-referenced ×3referenced ×3

The MCP stdio transport (StdioTransport) accepts arbitrary command and args parameters and executes them as subprocesses without validation against an allowlist. Adding an optional command allowlist would provide a defense-in-depth layer against configuration-driven command injection, especially as MCP server discovery becomes more dynamic.

Affected files:

  • lib/crewai/src/crewai/mcp/transports/stdio.py (lines 34-84)

Root Cause

  • The MCP specification supports dynamic server discovery, where server configurations may come from external sources (marketplace, shared configs, agent-generated configs)
  • As CrewAI's MCP ecosystem grows, the risk of configuration injection increases
  • An allowlist is a simple, zero-overhead check that prevents unexpected executables from being launched
  • Users who need custom commands can explicitly expand the allowlist or set allowed_commands=None

Fix Action

Fixed

PR fix notes

PR #5081: feat: add command allowlist validation for MCP stdio transport

Description (problem / solution / changelog)

Summary

Addresses #5080 — adds an optional allowed_commands parameter to StdioTransport that validates the command basename against an allowlist before spawning a subprocess. This provides a defense-in-depth layer against configuration-driven command injection as MCP server discovery becomes more dynamic.

Changes:

  • StdioTransport.__init__ now accepts allowed_commands: Set[str] | None (default: DEFAULT_ALLOWED_COMMANDS)
  • DEFAULT_ALLOWED_COMMANDS = frozenset({"python", "python3", "node", "npx", "uvx", "deno"})
  • MCPServerStdio config model gains a matching allowed_commands field
  • MCPToolResolver._create_transport forwards the field to StdioTransport
  • Validation uses os.path.basename(command) so full paths like /usr/bin/python3 resolve correctly
  • Users can opt out with allowed_commands=None or supply a custom set

Review & Testing Checklist for Human

  • Breaking change assessment: The default allowlist is enabled by default. Any existing user passing a command not in {python, python3, node, npx, uvx, deno} (e.g., bun, ruby, java, docker, custom binaries) will get a ValueError at construction time. Decide whether this should default to None (opt-in) or the allowlist (opt-out) to avoid breaking existing workflows.
  • Default allowlist completeness: Should bun, docker, ruby, java, go, cargo, or other runtimes be included? The current list covers the most common MCP server runtimes but may miss edge cases.
  • Cross-platform os.path.basename behavior: On POSIX, Windows-style backslash paths (e.g., C:\Python311\python) won't have their basename extracted correctly. The test documents this as requiring allowed_commands=None. Verify this is acceptable.
  • Manual integration test: Try constructing a StdioTransport or MCPServerStdio with an allowed command, a blocked command, and allowed_commands=None to confirm the behavior matches expectations end-to-end.

Notes

  • Validation happens at construction time (not at connect()), so invalid commands fail fast.
  • 29 new tests cover: default allowlist, blocked commands, full/relative path resolution, opt-out via None, custom allowlists, error messages, config model integration, and tool resolver forwarding.
  • All 63 existing MCP tests continue to pass.
  • CI is green across all Python versions (3.10–3.13), lint, format, and type-checker.

Link to Devin session: https://app.devin.ai/sessions/a00b11dee1e947b2bfcda49db63036d3

Changed files

Code Example

def __init__(
    self,
    command: str,
    args: list[str] | None = None,
    env: dict[str, str] | None = None,
    **kwargs: Any,
) -> None:
    super().__init__(**kwargs)
    self.command = command       # Any command string
    self.args = args or []      # Any arguments
    self.env = env or {}

---

server_params = StdioServerParameters(
    command=self.command,        # Passed directly to subprocess
    args=self.args,
    env=process_env if process_env else None,
)
self._transport_context = stdio_client(server_params)

---

# Default allowlist for common MCP server runtimes
DEFAULT_ALLOWED_COMMANDS = frozenset({
    "python", "python3", "node", "npx", "uvx", "deno",
})

class StdioTransport(BaseTransport):
    def __init__(
        self,
        command: str,
        args: list[str] | None = None,
        env: dict[str, str] | None = None,
        allowed_commands: frozenset[str] | None = DEFAULT_ALLOWED_COMMANDS,
        **kwargs: Any,
    ) -> None:
        super().__init__(**kwargs)

        if allowed_commands is not None:
            base_command = os.path.basename(command)
            if base_command not in allowed_commands:
                raise ValueError(
                    f"Command '{command}' is not in the allowed commands list: "
                    f"{sorted(allowed_commands)}. Pass allowed_commands=None to disable this check."
                )

        self.command = command
        self.args = args or []
        self.env = env or {}
RAW_BUFFERClick to expand / collapse

Suggest: add command allowlist validation for MCP stdio transport

Summary

The MCP stdio transport (StdioTransport) accepts arbitrary command and args parameters and executes them as subprocesses without validation against an allowlist. Adding an optional command allowlist would provide a defense-in-depth layer against configuration-driven command injection, especially as MCP server discovery becomes more dynamic.

Affected files:

  • lib/crewai/src/crewai/mcp/transports/stdio.py (lines 34-84)

Current Behavior

stdio.py:32-50:

def __init__(
    self,
    command: str,
    args: list[str] | None = None,
    env: dict[str, str] | None = None,
    **kwargs: Any,
) -> None:
    super().__init__(**kwargs)
    self.command = command       # Any command string
    self.args = args or []      # Any arguments
    self.env = env or {}

stdio.py:79-84:

server_params = StdioServerParameters(
    command=self.command,        # Passed directly to subprocess
    args=self.args,
    env=process_env if process_env else None,
)
self._transport_context = stdio_client(server_params)

The command parameter is passed directly to StdioServerParameters which spawns a subprocess. There is no validation of what commands are allowed.

Proposed Enhancement

Add an optional allowed_commands parameter that restricts which executables can be launched:

# Default allowlist for common MCP server runtimes
DEFAULT_ALLOWED_COMMANDS = frozenset({
    "python", "python3", "node", "npx", "uvx", "deno",
})

class StdioTransport(BaseTransport):
    def __init__(
        self,
        command: str,
        args: list[str] | None = None,
        env: dict[str, str] | None = None,
        allowed_commands: frozenset[str] | None = DEFAULT_ALLOWED_COMMANDS,
        **kwargs: Any,
    ) -> None:
        super().__init__(**kwargs)

        if allowed_commands is not None:
            base_command = os.path.basename(command)
            if base_command not in allowed_commands:
                raise ValueError(
                    f"Command '{command}' is not in the allowed commands list: "
                    f"{sorted(allowed_commands)}. Pass allowed_commands=None to disable this check."
                )

        self.command = command
        self.args = args or []
        self.env = env or {}

Why This Matters

  • The MCP specification supports dynamic server discovery, where server configurations may come from external sources (marketplace, shared configs, agent-generated configs)
  • As CrewAI's MCP ecosystem grows, the risk of configuration injection increases
  • An allowlist is a simple, zero-overhead check that prevents unexpected executables from being launched
  • Users who need custom commands can explicitly expand the allowlist or set allowed_commands=None

Design Considerations

  • Default allowlist: Include common runtimes (python, node, npx, uvx, deno). Cover 95% of use cases.
  • Opt-out: Users who need to run custom binaries can pass allowed_commands=None or extend the set
  • Path resolution: Validate against basename only (not full path) to be platform-agnostic
  • No breaking change: The default allowlist should cover all currently documented MCP server examples

Alternatives Considered

  • No validation (current): Simple but offers no protection against misconfiguration
  • Full path validation: Too restrictive, breaks cross-platform usage
  • Blocklist approach: Harder to maintain, easy to bypass — allowlist is more robust

extent analysis

Fix Plan

To address the issue, we need to implement command allowlist validation for the MCP stdio transport. Here are the steps:

  • Update the StdioTransport class to include an allowed_commands parameter with a default allowlist of common MCP server runtimes.
  • Validate the command parameter against the allowed_commands set in the __init__ method.
  • Raise a ValueError if the command is not in the allowed list.

Code Changes

import os

# Default allowlist for common MCP server runtimes
DEFAULT_ALLOWED_COMMANDS = frozenset({
    "python", "python3", "node", "npx", "uvx", "deno",
})

class StdioTransport(BaseTransport):
    def __init__(
        self,
        command: str,
        args: list[str] | None = None,
        env: dict[str, str] | None = None,
        allowed_commands: frozenset[str] | None = DEFAULT_ALLOWED_COMMANDS,
        **kwargs: Any,
    ) -> None:
        super().__init__(**kwargs)

        if allowed_commands is not None:
            base_command = os.path.basename(command)
            if base_command not in allowed_commands:
                raise ValueError(
                    f"Command '{command}' is not in the allowed commands list: "
                    f"{sorted(allowed_commands)}. Pass allowed_commands=None to disable this check."
                )

        self.command = command
        self.args = args or []
        self.env = env or {}

Verification

To verify the fix, test the StdioTransport class with different commands and allowed commands sets. For example:

# Test with a valid command
transport = StdioTransport("python", allowed_commands=DEFAULT_ALLOWED_COMMANDS)

# Test with an invalid command
try:
    transport = StdioTransport("custom_command", allowed_commands=DEFAULT_ALLOWED_COMMANDS)
except ValueError as e:
    print(e)

# Test with a custom allowed commands set
custom_allowed_commands = frozenset({"custom_command"})
transport = StdioTransport("custom_command", allowed_commands=custom_allowed_commands)

Extra Tips

  • Make sure to update the documentation to reflect the new allowed_commands parameter and its default value.
  • Consider adding a configuration option to allow users to customize the default allowlist.
  • Review the code for any potential security vulnerabilities and address them accordingly.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING