From b53e0902991b43d1e9f32774849b5c87b4d4c178 Mon Sep 17 00:00:00 2001
From: Henry Wildermuth <hmwildermuth@gmail.com>
Date: Thu, 13 Feb 2025 14:21:50 -0800
Subject: [PATCH] Update URL validation to allow file and other nonstandard
 schemas

---
 .../mcp_simple_resource/server.py             |  7 +++---
 src/mcp/types.py                              | 24 ++++++++++++-------
 2 files changed, 19 insertions(+), 12 deletions(-)

diff --git a/examples/servers/simple-resource/mcp_simple_resource/server.py b/examples/servers/simple-resource/mcp_simple_resource/server.py
index 11ba5692..0ec1d926 100644
--- a/examples/servers/simple-resource/mcp_simple_resource/server.py
+++ b/examples/servers/simple-resource/mcp_simple_resource/server.py
@@ -2,7 +2,7 @@
 import click
 import mcp.types as types
 from mcp.server.lowlevel import Server
-from pydantic import AnyUrl
+from pydantic import FileUrl
 
 SAMPLE_RESOURCES = {
     "greeting": "Hello! This is a sample text resource.",
@@ -26,7 +26,7 @@ def main(port: int, transport: str) -> int:
     async def list_resources() -> list[types.Resource]:
         return [
             types.Resource(
-                uri=AnyUrl(f"file:///{name}.txt"),
+                uri=FileUrl(f"file:///{name}.txt"),
                 name=name,
                 description=f"A sample text resource named {name}",
                 mimeType="text/plain",
@@ -35,8 +35,7 @@ async def list_resources() -> list[types.Resource]:
         ]
 
     @app.read_resource()
-    async def read_resource(uri: AnyUrl) -> str | bytes:
-        assert uri.path is not None
+    async def read_resource(uri: FileUrl) -> str | bytes:
         name = uri.path.replace(".txt", "").lstrip("/")
 
         if name not in SAMPLE_RESOURCES:
diff --git a/src/mcp/types.py b/src/mcp/types.py
index d1157aa6..7d867bd3 100644
--- a/src/mcp/types.py
+++ b/src/mcp/types.py
@@ -1,7 +1,15 @@
-from typing import Annotated, Any, Callable, Generic, Literal, TypeAlias, TypeVar
+from typing import (
+    Annotated,
+    Any,
+    Callable,
+    Generic,
+    Literal,
+    TypeAlias,
+    TypeVar,
+)
 
 from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel
-from pydantic.networks import AnyUrl
+from pydantic.networks import AnyUrl, UrlConstraints
 
 """
 Model Context Protocol bindings for Python
@@ -353,7 +361,7 @@ class Annotations(BaseModel):
 class Resource(BaseModel):
     """A known resource that the server is capable of reading."""
 
-    uri: AnyUrl
+    uri: Annotated[AnyUrl, UrlConstraints(host_required=False)]
     """The URI of this resource."""
     name: str
     """A human-readable name for this resource."""
@@ -415,7 +423,7 @@ class ListResourceTemplatesResult(PaginatedResult):
 class ReadResourceRequestParams(RequestParams):
     """Parameters for reading a resource."""
 
-    uri: AnyUrl
+    uri: Annotated[AnyUrl, UrlConstraints(host_required=False)]
     """
     The URI of the resource to read. The URI can use any protocol; it is up to the
     server how to interpret it.
@@ -433,7 +441,7 @@ class ReadResourceRequest(Request):
 class ResourceContents(BaseModel):
     """The contents of a specific resource or sub-resource."""
 
-    uri: AnyUrl
+    uri: Annotated[AnyUrl, UrlConstraints(host_required=False)]
     """The URI of this resource."""
     mimeType: str | None = None
     """The MIME type of this resource, if known."""
@@ -476,7 +484,7 @@ class ResourceListChangedNotification(Notification):
 class SubscribeRequestParams(RequestParams):
     """Parameters for subscribing to a resource."""
 
-    uri: AnyUrl
+    uri: Annotated[AnyUrl, UrlConstraints(host_required=False)]
     """
     The URI of the resource to subscribe to. The URI can use any protocol; it is up to
     the server how to interpret it.
@@ -497,7 +505,7 @@ class SubscribeRequest(Request):
 class UnsubscribeRequestParams(RequestParams):
     """Parameters for unsubscribing from a resource."""
 
-    uri: AnyUrl
+    uri: Annotated[AnyUrl, UrlConstraints(host_required=False)]
     """The URI of the resource to unsubscribe from."""
     model_config = ConfigDict(extra="allow")
 
@@ -515,7 +523,7 @@ class UnsubscribeRequest(Request):
 class ResourceUpdatedNotificationParams(NotificationParams):
     """Parameters for resource update notifications."""
 
-    uri: AnyUrl
+    uri: Annotated[AnyUrl, UrlConstraints(host_required=False)]
     """
     The URI of the resource that has been updated. This might be a sub-resource of the
     one that the client actually subscribed to.