PrefectHQ/fastmcp · error · ValueError

cache_scope requires cache_ttl; a scope without a TTL does n

Error message

cache_scope requires cache_ttl; a scope without a TTL does not enable caching

What it means

build_cache_hints raises ValueError when cache_scope is provided to FastMCP(...) but cache_ttl is None. The client gates caching on the presence of a TTL, so a scope alone would be silently meaningless — the library rejects it instead of ignoring it.

Source

Thrown at fastmcp_slim/fastmcp/server/caching.py:50

) -> dict[CacheableMethod, CacheHint] | None:
    """Build the per-method `CacheHint` map for the SDK low-level server.

    `cache_ttl` is in seconds and is converted to the wire's milliseconds. When
    `cache_ttl` is `None` the server emits no hint, so its wire output is
    identical to a server that never set one; a `cache_scope` given without a
    `cache_ttl` is meaningless (the client gates caching on the presence of a
    TTL) and is rejected rather than silently ignored.

    Returns `None` when no hint is set, or a map applying the same hint to every
    SDK-cacheable method otherwise.

    Raises:
        ValueError: If `cache_ttl` is not positive, or if `cache_scope` is set
            without `cache_ttl`.
    """
    if cache_ttl is None:
        if cache_scope is not None:
            raise ValueError(
                "cache_scope requires cache_ttl; a scope without a TTL does not "
                "enable caching"
            )
        return None
    if cache_ttl <= 0:
        raise ValueError(f"cache_ttl must be a positive integer, got {cache_ttl}")
    hint = CacheHint(ttl_ms=cache_ttl * 1000, scope=cache_scope or "private")
    return dict.fromkeys(get_args(CacheableMethod), hint)

View on GitHub (pinned to 1f02114297)

Solutions

  1. Provide a positive integer cache_ttl in seconds alongside cache_scope, e.g. FastMCP(cache_ttl=300, cache_scope="public").
  2. If caching is not wanted, remove cache_scope as well — pass neither.
  3. Validate your config loading so cache_ttl defaults to an integer, not None, when scope is configured.
  4. Catch ValueError at server-construction time and fail fast with a clear config error.

Example fix

// before
mcp = FastMCP("server", cache_scope="public")  # ValueError
// after
mcp = FastMCP("server", cache_ttl=300, cache_scope="public")
Defensive patterns

Strategy: validation

Validate before calling

def check_cache_config(ttl: int | None, scope: str | None) -> None:
    if scope is not None and ttl is None:
        raise ValueError("cache_scope requires cache_ttl")
    if ttl is not None and ttl <= 0:
        raise ValueError("cache_ttl must be positive")

Type guard

def has_valid_cache_hint(ttl: int | None, scope: str | None) -> bool:
    return scope is None or (isinstance(ttl, int) and ttl > 0)

Try / catch

try:
    mcp = FastMCP("server", cache_ttl=cfg.ttl, cache_scope=cfg.scope)
except ValueError as e:
    raise ConfigError(f"invalid cache settings: {e}") from e

Prevention

When it happens

Trigger: FastMCP(cache_scope="public") without cache_ttl; programmatically calling build_cache_hints(None, "private"); config where cache_ttl is parsed out (e.g. env var missing yields None) but cache_scope is set.

Common situations: Setting cache_scope from a config file while forgetting cache_ttl; typo like cache_ttl="0" string coercion failing upstream and arriving as None; migrating from an older FastMCP version where scope behaved differently.

Related errors


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