From 5aea389bfc74c2397ea56876fd3f3c0fce3e6efd Mon Sep 17 00:00:00 2001 From: Samuel Chenatti Date: Wed, 24 Sep 2025 12:11:42 -0300 Subject: [PATCH] Validate metadata keys in types.RequestParams.Meta --- src/mcp/types.py | 46 +++++++++++++++++++++++++++- tests/types/__init__.py | 0 tests/types/test_meta.py | 65 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 tests/types/__init__.py create mode 100644 tests/types/test_meta.py diff --git a/src/mcp/types.py b/src/mcp/types.py index 62feda87a..f99eb8d3b 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -1,7 +1,8 @@ +import re from collections.abc import Callable from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar -from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel +from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel, model_validator from pydantic.networks import AnyUrl, UrlConstraints from typing_extensions import deprecated @@ -52,6 +53,49 @@ class Meta(BaseModel): model_config = ConfigDict(extra="allow") + @model_validator(mode="before") + @classmethod + def validate_metadata_keys(cls, data: Any) -> Any: + """ + Validate if metadata keys follows the protocol specification + See section "General fields" at https://modelcontextprotocol.io/specification/ + """ + for metadata_key in data.keys(): + key_parts = metadata_key.split("/") + + match len(key_parts): + case 1: + cls._validate_metadata_name(key_parts[0]) + + case 2: + cls._validate_metadata_prefix(key_parts[0]) + cls._validate_metadata_name(key_parts[1]) + + case _: + raise ValueError(f"The metadata key {metadata_key} does not comply with MCP specification") + + return data + + @classmethod + def _validate_metadata_prefix(cls, prefix: str): + if len(prefix) == 0: + raise ValueError( + "One of the metadata keys is empty, and therefore does not comply with MCP specification" + ) + + for label in prefix.split("."): + cls._validate_prefix_label(prefix=prefix, label=label) + + @classmethod + def _validate_metadata_name(cls, name: str): + if re.match(r"^[a-zA-Z0-9]([a-zA-Z0-9-._]*[a-zA-Z0-9])?$", name) is None: + raise ValueError(f"The metadata name {name} does not comply with MCP specification") + + @classmethod + def _validate_prefix_label(cls, label: str, prefix: str): + if re.match(r"^[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$", label) is None: + raise ValueError(f"The label {label} inside of prefix {prefix} does not comply with MCP specification") + meta: Meta | None = Field(alias="_meta", default=None) diff --git a/tests/types/__init__.py b/tests/types/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/types/test_meta.py b/tests/types/test_meta.py new file mode 100644 index 000000000..3c3fb8148 --- /dev/null +++ b/tests/types/test_meta.py @@ -0,0 +1,65 @@ +import pytest + +from mcp import types + + +@pytest.mark.parametrize( + argnames="key", + argvalues=[ + # Simple keys without reserved prefix + "clientId", + "request-id", + "api_version", + "product-id", + "x-correlation-id", + "my-key", + "info", + "data-1", + "label-key", + # Keys with reserved prefix + "modelcontextprotocol.io/request-id", + "mcp.dev/debug-mode", + "api.modelcontextprotocol.org/api-version", + "tools.mcp.com/validation-status", + "my-company.mcp.io/internal-flag", + "modelcontextprotocol.io/a", + "mcp.dev/b-c", + # Keys with non-reserved prefix + "my-app.com/user-preferences", + "internal.api/tracking-id", + "org.example/resource-type", + "custom.domain/status", + ], +) +def test_metadata_valid_keys(key: str): + """ + Asserts that valid metadata keys does not raise ValueErrors + """ + types.RequestParams.Meta(**{key: "value"}) + + +@pytest.mark.parametrize( + argnames="key", + argvalues=[ + # Invalid key names (without prefix) + "-leading-hyphen", + "trailing-hyphen-", + "with space", + "key/with/slash", + "no@special-chars", + "...", + # Invalid prefixes + "mcp.123/key", + "my.custom./key", + "my-app.com//key", + # Invalid combination of prefix and name + "mcp.dev/-invalid", + "org.example/invalid-name-", + ], +) +def test_metadata_invalid_keys(key: str): + """ + Asserts that invalid metadata keys raise ValueErrors + """ + with pytest.raises(ValueError): + types.RequestParams.Meta(**{key: "value"})