From a13de1af54a9ff16d359f50cdb105945305cde38 Mon Sep 17 00:00:00 2001 From: sbobryshev Date: Wed, 10 Sep 2025 21:06:01 +0300 Subject: [PATCH] Switch to immutable types and add `typing.Final` for constants per PEP 591 --- src/mcp/cli/claude.py | 4 ++-- src/mcp/client/stdio/__init__.py | 19 +++++++++++++------ src/mcp/client/streamable_http.py | 15 ++++++++------- src/mcp/server/auth/routes.py | 10 +++++----- src/mcp/server/streamable_http.py | 15 ++++++++------- src/mcp/shared/version.py | 4 +++- src/mcp/types.py | 19 +++++++++---------- 7 files changed, 48 insertions(+), 38 deletions(-) diff --git a/src/mcp/cli/claude.py b/src/mcp/cli/claude.py index 6a2effa3b..dca57196e 100644 --- a/src/mcp/cli/claude.py +++ b/src/mcp/cli/claude.py @@ -5,13 +5,13 @@ import shutil import sys from pathlib import Path -from typing import Any +from typing import Any, Final from mcp.server.fastmcp.utilities.logging import get_logger logger = get_logger(__name__) -MCP_PACKAGE = "mcp[cli]" +MCP_PACKAGE: Final = "mcp[cli]" def get_claude_config_path() -> Path | None: diff --git a/src/mcp/client/stdio/__init__.py b/src/mcp/client/stdio/__init__.py index e3532e988..6259a87cc 100644 --- a/src/mcp/client/stdio/__init__.py +++ b/src/mcp/client/stdio/__init__.py @@ -3,7 +3,7 @@ import sys from contextlib import asynccontextmanager from pathlib import Path -from typing import Literal, TextIO +from typing import Final, Literal, TextIO import anyio import anyio.lowlevel @@ -25,8 +25,8 @@ logger = logging.getLogger(__name__) # Environment variables to inherit by default -DEFAULT_INHERITED_ENV_VARS = ( - [ +DEFAULT_INHERITED_ENV_VARS: Final = ( + ( "APPDATA", "HOMEDRIVE", "HOMEPATH", @@ -39,13 +39,20 @@ "TEMP", "USERNAME", "USERPROFILE", - ] + ) if sys.platform == "win32" - else ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"] + else ( + "HOME", + "LOGNAME", + "PATH", + "SHELL", + "TERM", + "USER", + ) ) # Timeout for process termination before falling back to force kill -PROCESS_TERMINATION_TIMEOUT = 2.0 +PROCESS_TERMINATION_TIMEOUT: Final = 2.0 def get_default_environment() -> dict[str, str]: diff --git a/src/mcp/client/streamable_http.py b/src/mcp/client/streamable_http.py index 57df64705..f6e278bb8 100644 --- a/src/mcp/client/streamable_http.py +++ b/src/mcp/client/streamable_http.py @@ -11,6 +11,7 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from datetime import timedelta +from typing import Final import anyio import httpx @@ -39,15 +40,15 @@ StreamReader = MemoryObjectReceiveStream[SessionMessage] GetSessionIdCallback = Callable[[], str | None] -MCP_SESSION_ID = "mcp-session-id" -MCP_PROTOCOL_VERSION = "mcp-protocol-version" -LAST_EVENT_ID = "last-event-id" -CONTENT_TYPE = "content-type" -ACCEPT = "accept" +MCP_SESSION_ID: Final = "mcp-session-id" +MCP_PROTOCOL_VERSION: Final = "mcp-protocol-version" +LAST_EVENT_ID: Final = "last-event-id" +CONTENT_TYPE: Final = "content-type" +ACCEPT: Final = "accept" -JSON = "application/json" -SSE = "text/event-stream" +JSON: Final = "application/json" +SSE: Final = "text/event-stream" class StreamableHTTPError(Exception): diff --git a/src/mcp/server/auth/routes.py b/src/mcp/server/auth/routes.py index bce32df52..2a9f0d3c3 100644 --- a/src/mcp/server/auth/routes.py +++ b/src/mcp/server/auth/routes.py @@ -1,5 +1,5 @@ from collections.abc import Awaitable, Callable -from typing import Any +from typing import Any, Final from pydantic import AnyHttpUrl from starlette.middleware.cors import CORSMiddleware @@ -46,10 +46,10 @@ def validate_issuer_url(url: AnyHttpUrl): raise ValueError("Issuer URL must not have a query string") -AUTHORIZATION_PATH = "/authorize" -TOKEN_PATH = "/token" -REGISTRATION_PATH = "/register" -REVOCATION_PATH = "/revoke" +AUTHORIZATION_PATH: Final = "/authorize" +TOKEN_PATH: Final = "/token" +REGISTRATION_PATH: Final = "/register" +REVOCATION_PATH: Final = "/revoke" def cors_middleware( diff --git a/src/mcp/server/streamable_http.py b/src/mcp/server/streamable_http.py index b45d742b0..d4580f028 100644 --- a/src/mcp/server/streamable_http.py +++ b/src/mcp/server/streamable_http.py @@ -15,6 +15,7 @@ from contextlib import asynccontextmanager from dataclasses import dataclass from http import HTTPStatus +from typing import Final import anyio from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream @@ -48,20 +49,20 @@ # Header names -MCP_SESSION_ID_HEADER = "mcp-session-id" -MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version" -LAST_EVENT_ID_HEADER = "last-event-id" +MCP_SESSION_ID_HEADER: Final = "mcp-session-id" +MCP_PROTOCOL_VERSION_HEADER: Final = "mcp-protocol-version" +LAST_EVENT_ID_HEADER: Final = "last-event-id" # Content types -CONTENT_TYPE_JSON = "application/json" -CONTENT_TYPE_SSE = "text/event-stream" +CONTENT_TYPE_JSON: Final = "application/json" +CONTENT_TYPE_SSE: Final = "text/event-stream" # Special key for the standalone GET stream -GET_STREAM_KEY = "_GET_stream" +GET_STREAM_KEY: Final = "_GET_stream" # Session ID validation pattern (visible ASCII characters ranging from 0x21 to 0x7E) # Pattern ensures entire string contains only valid characters by using ^ and $ anchors -SESSION_ID_PATTERN = re.compile(r"^[\x21-\x7E]+$") +SESSION_ID_PATTERN: Final = re.compile(r"^[\x21-\x7E]+$") # Type aliases StreamId = str diff --git a/src/mcp/shared/version.py b/src/mcp/shared/version.py index 23c46d04b..ade68581b 100644 --- a/src/mcp/shared/version.py +++ b/src/mcp/shared/version.py @@ -1,3 +1,5 @@ +from typing import Final + from mcp.types import LATEST_PROTOCOL_VERSION -SUPPORTED_PROTOCOL_VERSIONS: list[str] = ["2024-11-05", "2025-03-26", LATEST_PROTOCOL_VERSION] +SUPPORTED_PROTOCOL_VERSIONS: Final = ("2024-11-05", "2025-03-26", LATEST_PROTOCOL_VERSION) diff --git a/src/mcp/types.py b/src/mcp/types.py index 62feda87a..de554ba25 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -1,5 +1,5 @@ from collections.abc import Callable -from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar +from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel from pydantic.networks import AnyUrl, UrlConstraints @@ -23,7 +23,7 @@ not separate types in the schema. """ -LATEST_PROTOCOL_VERSION = "2025-06-18" +LATEST_PROTOCOL_VERSION: Final = "2025-06-18" """ The default negotiated version of the Model Context Protocol when no version is specified. @@ -31,7 +31,7 @@ specific version if none is provided by the client. See section "Protocol Version Header" at https://modelcontextprotocol.io/specification """ -DEFAULT_NEGOTIATED_VERSION = "2025-03-26" +DEFAULT_NEGOTIATED_VERSION: Final = "2025-03-26" ProgressToken = str | int Cursor = str @@ -147,15 +147,14 @@ class JSONRPCResponse(BaseModel): # SDK error codes -CONNECTION_CLOSED = -32000 +CONNECTION_CLOSED: Final = -32000 # REQUEST_TIMEOUT = -32001 # the typescript sdk uses this - # Standard JSON-RPC error codes -PARSE_ERROR = -32700 -INVALID_REQUEST = -32600 -METHOD_NOT_FOUND = -32601 -INVALID_PARAMS = -32602 -INTERNAL_ERROR = -32603 +PARSE_ERROR: Final = -32700 +INVALID_REQUEST: Final = -32600 +METHOD_NOT_FOUND: Final = -32601 +INVALID_PARAMS: Final = -32602 +INTERNAL_ERROR: Final = -32603 class ErrorData(BaseModel):