|
| 1 | +import re |
1 | 2 | from collections.abc import Callable
|
2 | 3 | from typing import Annotated, Any, Generic, Literal, TypeAlias, TypeVar
|
3 | 4 |
|
4 |
| -from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel |
| 5 | +from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel, model_validator |
5 | 6 | from pydantic.networks import AnyUrl, UrlConstraints
|
6 | 7 | from typing_extensions import deprecated
|
7 | 8 |
|
@@ -52,6 +53,49 @@ class Meta(BaseModel):
|
52 | 53 |
|
53 | 54 | model_config = ConfigDict(extra="allow")
|
54 | 55 |
|
| 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 | + |
55 | 99 | meta: Meta | None = Field(alias="_meta", default=None)
|
56 | 100 |
|
57 | 101 |
|
|
0 commit comments