formachines / errata

Errata / PyPI / mcp

scopes renamed in mcp 2.3.0

In our sandbox runs up to 2026-10-27, claude-sonnet-5-5 wrote scopes=SCOPE, for mcp. It fails on 2.3.0 with TypeError (changed between 1.26.0 and 2.3.0). Use scope=SCOPE, instead.

What to do
replace the line
Output
same on one example
Patch
applies word for word
Auto-apply
allowed
Seen in
1 model
Worked / failed
2 / 0
Last verified
2026-10-27

What to do

Replace the old line with the new one; the rest of the code can stay.

Automatic application: allowed — proof.equivalent is true and the patch applies word for word. Automatic application is allowed only when proof.equivalent is true and proof.applicability.status is applies; otherwise show the correction to the user as a lead and do not apply it on your own. The change itself is described by kind (renamed); the checks are in proof; neither follows from the other.

How to tell this applies to your code

Old and new code

scopes=SCOPE,
scope=SCOPE,

Error on 2.3.0:

TypeError: ClientCredentialsOAuthProvider.__init__() got an unexpected keyword argument 'scopes'
Sandbox proof
Scriptmcp versionResult
Code the models wrote1.26.0works
Code the models wrote2.3.0fails: TypeError
Verified example below2.3.0works
Output of the verified example2.3.0same as the original's on 1.26.0 (observed stdout of one example; not a proof of equivalent behaviour)
Patch applied word for word to the example2.3.0applied word for word, the patch parses, runs and prints what the verified example prints

Verified example for mcp 2.3.0

from mcp.client.auth import OAuthClientProvider, TokenStorage
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken

SERVER_URL = "http://localhost:8000/mcp"
CLIENT_ID = "svc-reporter"
CLIENT_SECRET = "s3cret"
SCOPE = "read"


class InMemoryTokenStorage(TokenStorage):
    """Simple in-memory token and client-info storage."""

    def __init__(self) -> None:
        self._tokens: OAuthToken | None = None
        self._client_info: OAuthClientInformationFull | None = None

    async def get_tokens(self) -> OAuthToken | None:
        return self._tokens

    async def set_tokens(self, tokens: OAuthToken) -> None:
        self._tokens = tokens

    async def get_client_info(self) -> OAuthClientInformationFull | None:
        return self._client_info

    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
        self._client_info = client_info


def build_provider(storage: InMemoryTokenStorage) -> OAuthClientProvider:
    try:
        # Newer mcp versions ship a dedicated client-credentials provider.
        from mcp.client.auth.extensions.client_credentials import (
            ClientCredentialsOAuthProvider,
        )

        return ClientCredentialsOAuthProvider(
            server_url=SERVER_URL,
            storage=storage,
            client_id=CLIENT_ID,
            client_secret=CLIENT_SECRET,
            token_endpoint_auth_method="client_secret_basic",
            scope=SCOPE,
        )
    except ImportError:
        # Fallback for older versions: configure the base provider manually.
        from mcp.shared.auth import OAuthClientMetadata

        metadata = OAuthClientMetadata(
            client_name="svc-reporter",
            redirect_uris=["http://localhost:8000/unused-callback"],
            grant_types=["client_credentials"],
            response_types=["code"],
            token_endpoint_auth_method="client_secret_basic",
            scope=SCOPE,
        )

        async def redirect_handler(url: str) -> None:
            raise RuntimeError("Interactive redirect is not used with client credentials")

        async def callback_handler() -> tuple[str, str | None]:
            raise RuntimeError("Interactive callback is not used with client credentials")

        # Pre-register the client info so no dynamic registration is needed.
        import asyncio

        asyncio.run(
            storage.set_client_info(
                OAuthClientInformationFull(
                    client_id=CLIENT_ID,
                    client_secret=CLIENT_SECRET,
                    redirect_uris=metadata.redirect_uris,
                    grant_types=["client_credentials"],
                    response_types=["code"],
                    token_endpoint_auth_method="client_secret_basic",
                    scope=SCOPE,
                )
            )
        )
        return OAuthClientProvider(
            server_url=SERVER_URL,
            client_metadata=metadata,
            storage=storage,
            redirect_handler=redirect_handler,
            callback_handler=callback_handler,
        )


def resolve_client_id(provider: OAuthClientProvider, storage: InMemoryTokenStorage) -> str | None:
    # OAuthClientMetadata itself has no client_id field; it lives in the client info.
    for info in (
        getattr(provider, "_fixed_client_info", None),
        getattr(provider.context, "client_info", None),
        storage._client_info,
    ):
        if info is not None and getattr(info, "client_id", None):
            return info.client_id
    return None


def main() -> None:
    storage = InMemoryTokenStorage()
    provider = build_provider(storage)  # No network calls are made here.

    metadata = provider.context.client_metadata
    print(f"Server URL: {provider.context.server_url}")
    print(f"Scope:      {metadata.scope}")
    print(f"Client ID:  {resolve_client_id(provider, storage)}")
    print(f"Grant types: {metadata.grant_types}")


if __name__ == "__main__":
    main()

Source

The client-credentials providers take `scope=`, not `scopes=` (#3166).

Release notes · Seen in: claude-sonnet-5-5 · First seen 2026-10-20 · Last verified 2026-10-27 · All corrections for mcp

For agents