Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2044,8 +2044,6 @@ For servers that need to handle large datasets, the low-level server provides pa
Example of implementing pagination with MCP server decorators.
"""

from pydantic import AnyUrl

import mcp.types as types
from mcp.server.lowlevel import Server

Expand All @@ -2070,7 +2068,7 @@ async def list_resources_paginated(request: types.ListResourcesRequest) -> types

# Get page of resources
page_items = [
types.Resource(uri=AnyUrl(f"resource://items/{item}"), name=item, description=f"Description for {item}")
types.Resource(uri=f"resource://items/{item}", name=item, description=f"Description for {item}")
for item in ITEMS[start:end]
]

Expand Down
43 changes: 43 additions & 0 deletions docs/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,49 @@ result = await session.list_resources(params=PaginatedRequestParams(cursor="next
result = await session.list_tools(params=PaginatedRequestParams(cursor="next_page_token"))
```

### Resource URI type changed from `AnyUrl` to `str`

The `uri` field on resource-related types now uses `str` instead of Pydantic's `AnyUrl`. This aligns with the [MCP specification schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) which defines URIs as plain strings (`uri: string`) without strict URL validation. This change allows relative paths like `users/me` that were previously rejected.

**Before (v1):**

```python
from pydantic import AnyUrl
from mcp.types import Resource

# Required wrapping in AnyUrl
resource = Resource(name="test", uri=AnyUrl("users/me")) # Would fail validation
```

**After (v2):**

```python
from mcp.types import Resource

# Plain strings accepted
resource = Resource(name="test", uri="users/me") # Works
resource = Resource(name="test", uri="custom://scheme") # Works
resource = Resource(name="test", uri="https://example.com") # Works
```

If your code passes `AnyUrl` objects to URI fields, convert them to strings:

```python
# If you have an AnyUrl from elsewhere
uri = str(my_any_url) # Convert to string
```

Affected types:

- `Resource.uri`
- `ReadResourceRequestParams.uri`
- `ResourceContents.uri` (and subclasses `TextResourceContents`, `BlobResourceContents`)
- `SubscribeRequestParams.uri`
- `UnsubscribeRequestParams.uri`
- `ResourceUpdatedNotificationParams.uri`

The `ClientSession.read_resource()`, `subscribe_resource()`, and `unsubscribe_resource()` methods now accept both `str` and `AnyUrl` for backwards compatibility.

## Deprecations

<!-- Add deprecations below -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
TextContent,
TextResourceContents,
)
from pydantic import AnyUrl, BaseModel, Field
from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -114,7 +114,7 @@ def test_embedded_resource() -> list[EmbeddedResource]:
EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri=AnyUrl("test://embedded-resource"),
uri="test://embedded-resource",
mimeType="text/plain",
text="This is an embedded resource content.",
),
Expand All @@ -131,7 +131,7 @@ def test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedR
EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri=AnyUrl("test://mixed-content-resource"),
uri="test://mixed-content-resource",
mimeType="application/json",
text='{"test": "data", "value": 123}',
),
Expand Down Expand Up @@ -372,7 +372,7 @@ def test_prompt_with_embedded_resource(resourceUri: str) -> list[UserMessage]:
content=EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri=AnyUrl(resourceUri),
uri=resourceUri,
mimeType="text/plain",
text="Embedded resource content for testing.",
),
Expand Down Expand Up @@ -402,13 +402,13 @@ async def handle_set_logging_level(level: str) -> None:
# For conformance testing, we just acknowledge the request


async def handle_subscribe(uri: AnyUrl) -> None:
async def handle_subscribe(uri: str) -> None:
"""Handle resource subscription"""
resource_subscriptions.add(str(uri))
logger.info(f"Subscribed to resource: {uri}")


async def handle_unsubscribe(uri: AnyUrl) -> None:
async def handle_unsubscribe(uri: str) -> None:
"""Handle resource unsubscription"""
resource_subscriptions.discard(str(uri))
logger.info(f"Unsubscribed from resource: {uri}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import click
import mcp.types as types
from mcp.server.lowlevel import Server
from pydantic import AnyUrl
from starlette.requests import Request

# Sample data - in real scenarios, this might come from a database
Expand All @@ -27,7 +26,7 @@

SAMPLE_RESOURCES = [
types.Resource(
uri=AnyUrl(f"file:///path/to/resource_{i}.txt"),
uri=f"file:///path/to/resource_{i}.txt",
name=f"resource_{i}",
description=f"This is sample resource number {i}",
)
Expand Down Expand Up @@ -160,7 +159,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentB

# Implement read_resource handler
@app.read_resource()
async def read_resource(uri: AnyUrl) -> str:
async def read_resource(uri: str) -> str:
# Find the resource in our sample data
resource = next((r for r in SAMPLE_RESOURCES if r.uri == uri), None)
if not resource:
Expand Down
12 changes: 7 additions & 5 deletions examples/servers/simple-resource/mcp_simple_resource/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import mcp.types as types
from mcp.server.lowlevel import Server
from mcp.server.lowlevel.helper_types import ReadResourceContents
from pydantic import AnyUrl, FileUrl
from starlette.requests import Request

SAMPLE_RESOURCES = {
Expand Down Expand Up @@ -37,7 +36,7 @@ def main(port: int, transport: str) -> int:
async def list_resources() -> list[types.Resource]:
return [
types.Resource(
uri=FileUrl(f"file:///{name}.txt"),
uri=f"file:///{name}.txt",
name=name,
title=SAMPLE_RESOURCES[name]["title"],
description=f"A sample text resource named {name}",
Expand All @@ -47,10 +46,13 @@ async def list_resources() -> list[types.Resource]:
]

@app.read_resource()
async def read_resource(uri: AnyUrl):
if uri.path is None:
async def read_resource(uri: str):
from urllib.parse import urlparse

parsed = urlparse(uri)
if not parsed.path:
raise ValueError(f"Invalid resource path: {uri}")
name = uri.path.replace(".txt", "").lstrip("/")
name = parsed.path.replace(".txt", "").lstrip("/")

if name not in SAMPLE_RESOURCES:
raise ValueError(f"Unknown resource: {uri}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import mcp.types as types
from mcp.server.lowlevel import Server
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from pydantic import AnyUrl
from starlette.applications import Starlette
from starlette.middleware.cors import CORSMiddleware
from starlette.routing import Mount
Expand Down Expand Up @@ -74,7 +73,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentB

# This will send a resource notificaiton though standalone SSE
# established by GET request
await ctx.session.send_resource_updated(uri=AnyUrl("http:///test_resource"))
await ctx.session.send_resource_updated(uri="http:///test_resource")
return [
types.TextContent(
type="text",
Expand Down
4 changes: 1 addition & 3 deletions examples/snippets/servers/pagination_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
Example of implementing pagination with MCP server decorators.
"""

from pydantic import AnyUrl

import mcp.types as types
from mcp.server.lowlevel import Server

Expand All @@ -28,7 +26,7 @@ async def list_resources_paginated(request: types.ListResourcesRequest) -> types

# Get page of resources
page_items = [
types.Resource(uri=AnyUrl(f"resource://items/{item}"), name=item, description=f"Description for {item}")
types.Resource(uri=f"resource://items/{item}", name=item, description=f"Description for {item}")
for item in ITEMS[start:end]
]

Expand Down
12 changes: 6 additions & 6 deletions src/mcp/client/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,24 +279,24 @@ async def list_resource_templates(
types.ListResourceTemplatesResult,
)

async def read_resource(self, uri: AnyUrl) -> types.ReadResourceResult:
async def read_resource(self, uri: str | AnyUrl) -> types.ReadResourceResult:
"""Send a resources/read request."""
return await self.send_request(
types.ClientRequest(types.ReadResourceRequest(params=types.ReadResourceRequestParams(uri=uri))),
types.ClientRequest(types.ReadResourceRequest(params=types.ReadResourceRequestParams(uri=str(uri)))),
types.ReadResourceResult,
)

async def subscribe_resource(self, uri: AnyUrl) -> types.EmptyResult:
async def subscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult:
"""Send a resources/subscribe request."""
return await self.send_request( # pragma: no cover
types.ClientRequest(types.SubscribeRequest(params=types.SubscribeRequestParams(uri=uri))),
types.ClientRequest(types.SubscribeRequest(params=types.SubscribeRequestParams(uri=str(uri)))),
types.EmptyResult,
)

async def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult:
async def unsubscribe_resource(self, uri: str | AnyUrl) -> types.EmptyResult:
"""Send a resources/unsubscribe request."""
return await self.send_request( # pragma: no cover
types.ClientRequest(types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=uri))),
types.ClientRequest(types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=str(uri)))),
types.EmptyResult,
)

Expand Down
6 changes: 2 additions & 4 deletions src/mcp/server/fastmcp/resources/base.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
"""Base classes and interfaces for FastMCP resources."""

import abc
from typing import Annotated, Any
from typing import Any

from pydantic import (
AnyUrl,
BaseModel,
ConfigDict,
Field,
UrlConstraints,
ValidationInfo,
field_validator,
)
Expand All @@ -21,7 +19,7 @@ class Resource(BaseModel, abc.ABC):

model_config = ConfigDict(validate_default=True)

uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] = Field(default=..., description="URI of the resource")
uri: str = Field(default=..., description="URI of the resource")
name: str | None = Field(description="Name of the resource", default=None)
title: str | None = Field(description="Human-readable title of the resource", default=None)
description: str | None = Field(description="Description of the resource", default=None)
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/server/fastmcp/resources/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import httpx
import pydantic
import pydantic_core
from pydantic import AnyUrl, Field, ValidationInfo, validate_call
from pydantic import Field, ValidationInfo, validate_call

from mcp.server.fastmcp.resources.base import Resource
from mcp.types import Annotations, Icon
Expand Down Expand Up @@ -94,7 +94,7 @@ def from_function(
fn = validate_call(fn)

return cls(
uri=AnyUrl(uri),
uri=uri,
name=func_name,
title=title,
description=description or fn.__doc__ or "",
Expand Down
7 changes: 3 additions & 4 deletions src/mcp/server/lowlevel/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ async def main():
import anyio
import jsonschema
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from pydantic import AnyUrl
from typing_extensions import TypeVar

import mcp.types as types
Expand Down Expand Up @@ -337,7 +336,7 @@ async def handler(_: Any):

def read_resource(self):
def decorator(
func: Callable[[AnyUrl], Awaitable[str | bytes | Iterable[ReadResourceContents]]],
func: Callable[[str], Awaitable[str | bytes | Iterable[ReadResourceContents]]],
):
logger.debug("Registering handler for ReadResourceRequest")

Expand Down Expand Up @@ -412,7 +411,7 @@ async def handler(req: types.SetLevelRequest):
return decorator

def subscribe_resource(self): # pragma: no cover
def decorator(func: Callable[[AnyUrl], Awaitable[None]]):
def decorator(func: Callable[[str], Awaitable[None]]):
logger.debug("Registering handler for SubscribeRequest")

async def handler(req: types.SubscribeRequest):
Expand All @@ -425,7 +424,7 @@ async def handler(req: types.SubscribeRequest):
return decorator

def unsubscribe_resource(self): # pragma: no cover
def decorator(func: Callable[[AnyUrl], Awaitable[None]]):
def decorator(func: Callable[[str], Awaitable[None]]):
logger.debug("Registering handler for UnsubscribeRequest")

async def handler(req: types.UnsubscribeRequest):
Expand Down
4 changes: 2 additions & 2 deletions src/mcp/server/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,12 @@ async def send_log_message(
related_request_id,
)

async def send_resource_updated(self, uri: AnyUrl) -> None: # pragma: no cover
async def send_resource_updated(self, uri: str | AnyUrl) -> None: # pragma: no cover
"""Send a resource updated notification."""
await self.send_notification(
types.ServerNotification(
types.ResourceUpdatedNotification(
params=types.ResourceUpdatedNotificationParams(uri=uri),
params=types.ResourceUpdatedNotificationParams(uri=str(uri)),
)
)
)
Expand Down
Loading