Skip to content

Commit 76f1ac8

Browse files
committed
Validate metadata keys in types.RequestParams.Meta
1 parent f021dec commit 76f1ac8

File tree

3 files changed

+109
-1
lines changed

3 files changed

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

0 commit comments

Comments
 (0)