PrefectHQ/fastmcp · error · TypeError

Protocol mode for server {name!r} must be a string

Error message

Protocol mode for server {name!r} must be a string

What it means

ClientGroup.from_config reads each server's optional `mode` key (falling back to the group default). Because the value comes from untyped config (model_extra), it is validated: if a server's effective mode is not a string, this TypeError is raised naming the offending server. Mode selects the protocol era (legacy vs modern) for that client.

Source

Thrown at fastmcp_slim/fastmcp/client/group.py:82

        cls,
        config: MCPConfig | dict[str, Any],
        *,
        default_mode: ConnectMode = "auto",
    ) -> ClientGroup:
        """Create one independent client for each configured server.

        A server entry may include a FastMCP-specific ``mode`` field. It applies
        only to that server; entries without one use ``default_mode``.
        """
        parsed = (
            config if isinstance(config, MCPConfig) else MCPConfig.from_dict(config)
        )
        clients: dict[str, Client[Any]] = {}

        for name, server in parsed.mcpServers.items():
            configured_mode = (server.model_extra or {}).get("mode", default_mode)
            if not isinstance(configured_mode, str):
                raise TypeError(f"Protocol mode for server {name!r} must be a string")
            clients[name] = Client(server.to_transport(), mode=configured_mode)

        return cls(clients)

    @property
    def protocol_versions(self) -> dict[str, str | None]:
        return {name: client.protocol_version for name, client in self._clients.items()}

    async def __aenter__(self) -> ClientGroup:
        if self._exit_stack is not None:
            raise RuntimeError("ClientGroup is already connected")

        # Claim the stack before the first await so a concurrent entry hits the
        # guard above instead of racing past it and overwriting this one.
        stack = contextlib.AsyncExitStack()
        self._exit_stack = stack
        await stack.__aenter__()

View on GitHub (pinned to 1f02114297)

Solutions

  1. Fix the config so each server's mode is a string, e.g. "mode": "legacy"
  2. Remove the mode key to inherit the (string) default_mode
  3. Validate the parsed config before calling from_config

Example fix

// before
{"mcpServers": {"local": {"command": "python", "args": ["srv.py"], "mode": 1}}}

// after
{"mcpServers": {"local": {"command": "python", "args": ["srv.py"], "mode": "legacy"}}}
Defensive patterns

Strategy: validation

Validate before calling

mode = (server.model_extra or {}).get("mode", default_mode)
if not isinstance(mode, str):
    raise TypeError(f"Server {name!r}: mode must be a string, got {type(mode).__name__}")

Type guard

def is_valid_mode(v: object) -> TypeGuard[str]:
    return isinstance(v, str)

Try / catch

try:
    group = ClientGroup.from_config(path)
except TypeError as e:
    if "must be a string" in str(e):
        logger.error("bad config: %s", e)
    raise

Prevention

When it happens

Trigger: A config where a server entry has `"mode": 2`, `"mode": null` with no string default, or any non-string value; a non-string default_mode combined with servers lacking a mode key.

Common situations: YAML/JSON typo like `mode: [legacy]` or an accidentally numeric mode; programmatically generated config inserting a wrong-typed mode; misunderstanding that mode must be a string like 'legacy' or 'server'.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/7b6744c6f7c47516. Report an issue: GitHub.