Skip to content

Commit 3e44a6a

Browse files
committed
Validate metadata keys in types.RequestParams.Meta
1 parent f021dec commit 3e44a6a

File tree

3 files changed

+110
-1
lines changed

3 files changed

+110
-1
lines changed

src/mcp/types.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import re
12
from collections.abc import Callable
23
from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar
34

4-
from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel
5+
from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel, model_validator
56
from pydantic.networks import AnyUrl, UrlConstraints
67
from typing_extensions import deprecated
78

@@ -52,6 +53,49 @@ class Meta(BaseModel):
5253

5354
model_config = ConfigDict(extra="allow")
5455

56+
@model_validator(mode="before")
57+
@classmethod
58+
def validate_metadata_keys(cls, data: Any) -> Any:
59+
"""
60+
Validate if metadata keys follows the protocol specification
61+
See section "General fields" at https://modelcontextprotocol.io/specification/
62+
"""
63+
for metadata_key in data.keys():
64+
key_parts = metadata_key.split("/")
65+
66+
match len(key_parts):
67+
case 1:
68+
cls._validate_metadata_name(key_parts[0])
69+
70+
case 2:
71+
cls._validate_metadata_prefix(key_parts[0])
72+
cls._validate_metadata_name(key_parts[1])
73+
74+
case _:
75+
raise ValueError(f"The metadata key {metadata_key} does not comply with MCP specification")
76+
77+
return data
78+
79+
@classmethod
80+
def _validate_metadata_prefix(cls, prefix: str):
81+
if len(prefix) == 0:
82+
raise ValueError(
83+
"One of the metadata keys is empty, and therefore does not comply with MCP specification"
84+
)
85+
86+
for label in prefix.split("."):
87+
cls._validate_prefix_label(prefix=prefix, label=label)
88+
89+
@classmethod
90+
def _validate_metadata_name(cls, name: str):
91+
if re.match(r"^[a-zA-Z0-9]([a-zA-Z0-9-._]*[a-zA-Z0-9])?$", name) is None:
92+
raise ValueError(f"The metadata name {name} does not comply with MCP specification")
93+
94+
@classmethod
95+
def _validate_prefix_label(cls, label: str, prefix: str):
96+
if re.match(r"^[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$", label) is None:
97+
raise ValueError(f"The label {label} inside of prefix {prefix} does not comply with MCP specification")
98+
5599
meta: Meta | None = Field(alias="_meta", default=None)
56100

57101

tests/types/__init__.py

Whitespace-only changes.

tests/types/test_meta.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import pytest
2+
3+
from mcp import types
4+
5+
6+
@pytest.mark.parametrize(
7+
argnames="key",
8+
argvalues=[
9+
# Simple keys without reserved prefix
10+
"clientId",
11+
"request-id",
12+
"api_version",
13+
"product-id",
14+
"x-correlation-id",
15+
"my-key",
16+
"info",
17+
"data-1",
18+
"label-key",
19+
# Keys with reserved prefix
20+
"modelcontextprotocol.io/request-id",
21+
"mcp.dev/debug-mode",
22+
"api.modelcontextprotocol.org/api-version",
23+
"tools.mcp.com/validation-status",
24+
"my-company.mcp.io/internal-flag",
25+
"modelcontextprotocol.io/a",
26+
"mcp.dev/b-c",
27+
# Keys with non-reserved prefix
28+
"my-app.com/user-preferences",
29+
"internal.api/tracking-id",
30+
"org.example/resource-type",
31+
"custom.domain/status",
32+
],
33+
)
34+
def test_metadata_valid_keys(key: str):
35+
"""
36+
Asserts that valid metadata keys does not raise ValueErrors
37+
"""
38+
types.RequestParams.Meta(**{key: "value"})
39+
40+
41+
@pytest.mark.parametrize(
42+
argnames="key",
43+
argvalues=[
44+
# Invalid key names (without prefix)
45+
"-leading-hyphen",
46+
"trailing-hyphen-",
47+
"with space",
48+
"key/with/slash",
49+
"no@special-chars",
50+
"...",
51+
# Invalid prefixes
52+
"mcp.123/key",
53+
"my.custom./key",
54+
"my-app.com//key",
55+
# Invalid combination of prefix and name
56+
"mcp.dev/-invalid",
57+
"org.example/invalid-name-",
58+
],
59+
)
60+
def test_metadata_invalid_keys(key: str):
61+
"""
62+
Asserts that invalid metadata keys raise ValueErrors
63+
"""
64+
with pytest.raises(ValueError):
65+
types.RequestParams.Meta(**{key: "value"})

0 commit comments

Comments
 (0)