diff --git a/docs/migration.md b/docs/migration.md index 881014a930..8a5c5e0994 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -52,6 +52,48 @@ async with http_client: The `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters have been removed from `StreamableHTTPTransport`. Configure these on the `httpx.AsyncClient` instead (see example above). +### 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 which defines URIs as plain strings without 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 diff --git a/examples/servers/everything-server/mcp_everything_server/server.py b/examples/servers/everything-server/mcp_everything_server/server.py index 1f1ee7ecc4..59c60ea654 100644 --- a/examples/servers/everything-server/mcp_everything_server/server.py +++ b/examples/servers/everything-server/mcp_everything_server/server.py @@ -29,7 +29,7 @@ TextContent, TextResourceContents, ) -from pydantic import AnyUrl, BaseModel, Field +from pydantic import BaseModel, Field logger = logging.getLogger(__name__) @@ -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.", ), @@ -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}', ), @@ -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.", ), @@ -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}") diff --git a/examples/servers/simple-pagination/mcp_simple_pagination/server.py b/examples/servers/simple-pagination/mcp_simple_pagination/server.py index 360cbc3cff..3ade505148 100644 --- a/examples/servers/simple-pagination/mcp_simple_pagination/server.py +++ b/examples/servers/simple-pagination/mcp_simple_pagination/server.py @@ -160,7 +160,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: diff --git a/examples/servers/simple-resource/mcp_simple_resource/server.py b/examples/servers/simple-resource/mcp_simple_resource/server.py index 151a23eab4..7c5fd4990b 100644 --- a/examples/servers/simple-resource/mcp_simple_resource/server.py +++ b/examples/servers/simple-resource/mcp_simple_resource/server.py @@ -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 = { @@ -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}", @@ -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 parsed.path is None: 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}") diff --git a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py index 4b2604b9af..bfa9b23727 100644 --- a/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py +++ b/examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py @@ -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 @@ -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", diff --git a/src/mcp/client/session.py b/src/mcp/client/session.py index b61bf0b030..1cb2925e0d 100644 --- a/src/mcp/client/session.py +++ b/src/mcp/client/session.py @@ -332,34 +332,34 @@ 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), + 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), + 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), + params=types.UnsubscribeRequestParams(uri=str(uri)), ) ), types.EmptyResult, diff --git a/src/mcp/server/fastmcp/resources/base.py b/src/mcp/server/fastmcp/resources/base.py index e34b97a820..b91a0e1203 100644 --- a/src/mcp/server/fastmcp/resources/base.py +++ b/src/mcp/server/fastmcp/resources/base.py @@ -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, ) @@ -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) diff --git a/src/mcp/server/fastmcp/resources/types.py b/src/mcp/server/fastmcp/resources/types.py index 5f724301db..791442f87e 100644 --- a/src/mcp/server/fastmcp/resources/types.py +++ b/src/mcp/server/fastmcp/resources/types.py @@ -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 @@ -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 "", diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 26f6148c48..491ff7d0b3 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -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 @@ -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") @@ -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): @@ -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): diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index fe90cd10fa..b6fd3a2e8f 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -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)), ) ) ) diff --git a/src/mcp/types.py b/src/mcp/types.py index cc23e7f6b1..0254fe99f4 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -5,7 +5,6 @@ from typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar from pydantic import BaseModel, ConfigDict, Field, FileUrl, RootModel -from pydantic.networks import AnyUrl, UrlConstraints from typing_extensions import deprecated LATEST_PROTOCOL_VERSION = "2025-11-25" @@ -757,7 +756,7 @@ class Annotations(BaseModel): class Resource(BaseMetadata): """A known resource that the server is capable of reading.""" - uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + uri: str """The URI of this resource.""" description: str | None = None """A description of what this resource represents.""" @@ -828,7 +827,7 @@ class ListResourceTemplatesResult(PaginatedResult): class ReadResourceRequestParams(RequestParams): """Parameters for reading a resource.""" - uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + uri: str """ The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it. @@ -846,7 +845,7 @@ class ReadResourceRequest(Request[ReadResourceRequestParams, Literal["resources/ class ResourceContents(BaseModel): """The contents of a specific resource or sub-resource.""" - uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + uri: str """The URI of this resource.""" mimeType: str | None = None """The MIME type of this resource, if known.""" @@ -896,7 +895,7 @@ class ResourceListChangedNotification( class SubscribeRequestParams(RequestParams): """Parameters for subscribing to a resource.""" - uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + uri: str """ The URI of the resource to subscribe to. The URI can use any protocol; it is up to the server how to interpret it. @@ -917,7 +916,7 @@ class SubscribeRequest(Request[SubscribeRequestParams, Literal["resources/subscr class UnsubscribeRequestParams(RequestParams): """Parameters for unsubscribing from a resource.""" - uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + uri: str """The URI of the resource to unsubscribe from.""" model_config = ConfigDict(extra="allow") @@ -935,7 +934,7 @@ class UnsubscribeRequest(Request[UnsubscribeRequestParams, Literal["resources/un class ResourceUpdatedNotificationParams(NotificationParams): """Parameters for resource update notifications.""" - uri: Annotated[AnyUrl, UrlConstraints(host_required=False)] + uri: str """ The URI of the resource that has been updated. This might be a sub-resource of the one that the client actually subscribed to. diff --git a/tests/issues/test_152_resource_mime_type.py b/tests/issues/test_152_resource_mime_type.py index 2a8cd6202e..ea411ea616 100644 --- a/tests/issues/test_152_resource_mime_type.py +++ b/tests/issues/test_152_resource_mime_type.py @@ -70,9 +70,9 @@ async def test_lowlevel_resource_mime_type(): # Create test resources with specific mime types test_resources = [ - types.Resource(uri=AnyUrl("test://image"), name="test image", mimeType="image/png"), + types.Resource(uri="test://image", name="test image", mimeType="image/png"), types.Resource( - uri=AnyUrl("test://image_bytes"), + uri="test://image_bytes", name="test image bytes", mimeType="image/png", ), @@ -83,7 +83,7 @@ async def handle_list_resources(): return test_resources @server.read_resource() - async def handle_read_resource(uri: AnyUrl): + async def handle_read_resource(uri: str): if str(uri) == "test://image": return [ReadResourceContents(content=base64_string, mime_type="image/png")] elif str(uri) == "test://image_bytes": diff --git a/tests/issues/test_1574_resource_uri_validation.py b/tests/issues/test_1574_resource_uri_validation.py new file mode 100644 index 0000000000..51845ab56f --- /dev/null +++ b/tests/issues/test_1574_resource_uri_validation.py @@ -0,0 +1,85 @@ +"""Tests for issue #1574: Python SDK incorrectly validates Resource URIs. + +The Python SDK uses Pydantic's AnyUrl for URI fields, which rejects relative paths +like 'users/me' that are valid according to the MCP spec and accepted by the +TypeScript SDK. + +The spec defines uri fields as plain strings with no JSON Schema format validation. +""" + +from mcp import types + + +class TestResourceUriValidation: + """Test that Resource URI fields accept all valid MCP URIs.""" + + def test_relative_path_uri(self): + """ + REPRODUCER: Relative paths like 'users/me' should be accepted. + + Currently fails with: + ValidationError: Input should be a valid URL, relative URL without a base + """ + # This should NOT raise - relative paths are valid per MCP spec + resource = types.Resource(name="test", uri="users/me") + assert str(resource.uri) == "users/me" + + def test_custom_scheme_uri(self): + """Custom scheme URIs should be accepted.""" + resource = types.Resource(name="test", uri="custom://resource") + assert str(resource.uri) == "custom://resource" + + def test_file_url(self): + """File URLs should be accepted.""" + resource = types.Resource(name="test", uri="file:///path/to/file") + assert str(resource.uri) == "file:///path/to/file" + + def test_http_url(self): + """HTTP URLs should be accepted.""" + resource = types.Resource(name="test", uri="https://example.com/resource") + assert str(resource.uri) == "https://example.com/resource" + + +class TestReadResourceRequestParamsUri: + """Test that ReadResourceRequestParams.uri accepts all valid MCP URIs.""" + + def test_relative_path_uri(self): + """Relative paths should be accepted in read requests.""" + params = types.ReadResourceRequestParams(uri="users/me") + assert str(params.uri) == "users/me" + + +class TestResourceContentsUri: + """Test that ResourceContents.uri accepts all valid MCP URIs.""" + + def test_relative_path_uri(self): + """Relative paths should be accepted in resource contents.""" + contents = types.TextResourceContents(uri="users/me", text="content") + assert str(contents.uri) == "users/me" + + +class TestSubscribeRequestParamsUri: + """Test that SubscribeRequestParams.uri accepts all valid MCP URIs.""" + + def test_relative_path_uri(self): + """Relative paths should be accepted in subscribe requests.""" + params = types.SubscribeRequestParams(uri="users/me") + assert str(params.uri) == "users/me" + + +class TestUnsubscribeRequestParamsUri: + """Test that UnsubscribeRequestParams.uri accepts all valid MCP URIs.""" + + def test_relative_path_uri(self): + """Relative paths should be accepted in unsubscribe requests.""" + params = types.UnsubscribeRequestParams(uri="users/me") + assert str(params.uri) == "users/me" + + +class TestResourceUpdatedNotificationParamsUri: + """Test that ResourceUpdatedNotificationParams.uri accepts all valid MCP URIs.""" + + def test_relative_path_uri(self): + """Relative paths should be accepted in resource updated notifications.""" + params = types.ResourceUpdatedNotificationParams(uri="users/me") + assert str(params.uri) == "users/me" diff --git a/tests/issues/test_342_base64_encoding.py b/tests/issues/test_342_base64_encoding.py index da56959975..2554fbc735 100644 --- a/tests/issues/test_342_base64_encoding.py +++ b/tests/issues/test_342_base64_encoding.py @@ -13,7 +13,6 @@ from typing import cast import pytest -from pydantic import AnyUrl from mcp.server.lowlevel.helper_types import ReadResourceContents from mcp.server.lowlevel.server import Server @@ -46,7 +45,7 @@ async def test_server_base64_encoding_issue(): # Register a resource handler that returns our test data @server.read_resource() - async def read_resource(uri: AnyUrl) -> list[ReadResourceContents]: + async def read_resource(uri: str) -> list[ReadResourceContents]: return [ReadResourceContents(content=binary_data, mime_type="application/octet-stream")] # Get the handler directly from the server @@ -54,7 +53,7 @@ async def read_resource(uri: AnyUrl) -> list[ReadResourceContents]: # Create a request request = ReadResourceRequest( - params=ReadResourceRequestParams(uri=AnyUrl("test://resource")), + params=ReadResourceRequestParams(uri="test://resource"), ) # Call the handler to get the response diff --git a/tests/server/fastmcp/prompts/test_base.py b/tests/server/fastmcp/prompts/test_base.py index 488bd5002c..84c9712681 100644 --- a/tests/server/fastmcp/prompts/test_base.py +++ b/tests/server/fastmcp/prompts/test_base.py @@ -1,7 +1,6 @@ from typing import Any import pytest -from pydantic import FileUrl from mcp.server.fastmcp.prompts.base import AssistantMessage, Message, Prompt, TextContent, UserMessage from mcp.types import EmbeddedResource, TextResourceContents @@ -95,7 +94,7 @@ async def fn() -> UserMessage: content=EmbeddedResource( type="resource", resource=TextResourceContents( - uri=FileUrl("file://file.txt"), + uri="file://file.txt", text="File contents", mimeType="text/plain", ), @@ -108,7 +107,7 @@ async def fn() -> UserMessage: content=EmbeddedResource( type="resource", resource=TextResourceContents( - uri=FileUrl("file://file.txt"), + uri="file://file.txt", text="File contents", mimeType="text/plain", ), @@ -127,7 +126,7 @@ async def fn() -> list[Message]: content=EmbeddedResource( type="resource", resource=TextResourceContents( - uri=FileUrl("file://file.txt"), + uri="file://file.txt", text="File contents", mimeType="text/plain", ), @@ -143,7 +142,7 @@ async def fn() -> list[Message]: content=EmbeddedResource( type="resource", resource=TextResourceContents( - uri=FileUrl("file://file.txt"), + uri="file://file.txt", text="File contents", mimeType="text/plain", ), @@ -162,7 +161,7 @@ async def fn() -> dict[str, Any]: "content": { "type": "resource", "resource": { - "uri": FileUrl("file://file.txt"), + "uri": "file://file.txt", "text": "File contents", "mimeType": "text/plain", }, @@ -175,7 +174,7 @@ async def fn() -> dict[str, Any]: content=EmbeddedResource( type="resource", resource=TextResourceContents( - uri=FileUrl("file://file.txt"), + uri="file://file.txt", text="File contents", mimeType="text/plain", ), diff --git a/tests/server/fastmcp/resources/test_file_resources.py b/tests/server/fastmcp/resources/test_file_resources.py index c82cf85c5a..0eb24f0632 100644 --- a/tests/server/fastmcp/resources/test_file_resources.py +++ b/tests/server/fastmcp/resources/test_file_resources.py @@ -3,7 +3,6 @@ from tempfile import NamedTemporaryFile import pytest -from pydantic import FileUrl from mcp.server.fastmcp.resources import FileResource @@ -31,7 +30,7 @@ class TestFileResource: def test_file_resource_creation(self, temp_file: Path): """Test creating a FileResource.""" resource = FileResource( - uri=FileUrl(temp_file.as_uri()), + uri=temp_file.as_uri(), name="test", description="test file", path=temp_file, @@ -46,7 +45,7 @@ def test_file_resource_creation(self, temp_file: Path): def test_file_resource_str_path_conversion(self, temp_file: Path): """Test FileResource handles string paths.""" resource = FileResource( - uri=FileUrl(f"file://{temp_file}"), + uri=f"file://{temp_file}", name="test", path=Path(str(temp_file)), ) @@ -57,7 +56,7 @@ def test_file_resource_str_path_conversion(self, temp_file: Path): async def test_read_text_file(self, temp_file: Path): """Test reading a text file.""" resource = FileResource( - uri=FileUrl(f"file://{temp_file}"), + uri=f"file://{temp_file}", name="test", path=temp_file, ) @@ -69,7 +68,7 @@ async def test_read_text_file(self, temp_file: Path): async def test_read_binary_file(self, temp_file: Path): """Test reading a file as binary.""" resource = FileResource( - uri=FileUrl(f"file://{temp_file}"), + uri=f"file://{temp_file}", name="test", path=temp_file, is_binary=True, @@ -82,7 +81,7 @@ def test_relative_path_error(self): """Test error on relative path.""" with pytest.raises(ValueError, match="Path must be absolute"): FileResource( - uri=FileUrl("file:///test.txt"), + uri="file:///test.txt", name="test", path=Path("test.txt"), ) @@ -93,7 +92,7 @@ async def test_missing_file_error(self, temp_file: Path): # Create path to non-existent file missing = temp_file.parent / "missing.txt" resource = FileResource( - uri=FileUrl("file:///missing.txt"), + uri="file:///missing.txt", name="test", path=missing, ) @@ -107,7 +106,7 @@ async def test_permission_error(self, temp_file: Path): # pragma: no cover temp_file.chmod(0o000) # Remove all permissions try: resource = FileResource( - uri=FileUrl(temp_file.as_uri()), + uri=temp_file.as_uri(), name="test", path=temp_file, ) diff --git a/tests/server/fastmcp/resources/test_function_resources.py b/tests/server/fastmcp/resources/test_function_resources.py index 4619fd2e04..61ed44f6c6 100644 --- a/tests/server/fastmcp/resources/test_function_resources.py +++ b/tests/server/fastmcp/resources/test_function_resources.py @@ -1,5 +1,5 @@ import pytest -from pydantic import AnyUrl, BaseModel +from pydantic import BaseModel from mcp.server.fastmcp.resources import FunctionResource @@ -14,7 +14,7 @@ def my_func() -> str: # pragma: no cover return "test content" resource = FunctionResource( - uri=AnyUrl("fn://test"), + uri="fn://test", name="test", description="test function", fn=my_func, @@ -33,7 +33,7 @@ def get_data() -> str: return "Hello, world!" resource = FunctionResource( - uri=AnyUrl("function://test"), + uri="function://test", name="test", fn=get_data, ) @@ -49,7 +49,7 @@ def get_data() -> bytes: return b"Hello, world!" resource = FunctionResource( - uri=AnyUrl("function://test"), + uri="function://test", name="test", fn=get_data, ) @@ -64,7 +64,7 @@ def get_data() -> dict[str, str]: return {"key": "value"} resource = FunctionResource( - uri=AnyUrl("function://test"), + uri="function://test", name="test", fn=get_data, ) @@ -80,7 +80,7 @@ def failing_func() -> str: raise ValueError("Test error") resource = FunctionResource( - uri=AnyUrl("function://test"), + uri="function://test", name="test", fn=failing_func, ) @@ -95,7 +95,7 @@ class MyModel(BaseModel): name: str resource = FunctionResource( - uri=AnyUrl("function://test"), + uri="function://test", name="test", fn=lambda: MyModel(name="test"), ) @@ -114,7 +114,7 @@ def get_data() -> CustomData: return CustomData() resource = FunctionResource( - uri=AnyUrl("function://test"), + uri="function://test", name="test", fn=get_data, ) @@ -129,7 +129,7 @@ async def get_data() -> str: return "Hello, world!" resource = FunctionResource( - uri=AnyUrl("function://test"), + uri="function://test", name="test", fn=get_data, ) @@ -154,7 +154,7 @@ async def get_data() -> str: # pragma: no cover assert resource.description == "get_data returns a string" assert resource.mime_type == "text/plain" assert resource.name == "test" - assert resource.uri == AnyUrl("function://test") + assert resource.uri == "function://test" class TestFunctionResourceMetadata: diff --git a/tests/server/fastmcp/resources/test_resource_manager.py b/tests/server/fastmcp/resources/test_resource_manager.py index 565c816f18..5fd4bc8529 100644 --- a/tests/server/fastmcp/resources/test_resource_manager.py +++ b/tests/server/fastmcp/resources/test_resource_manager.py @@ -2,7 +2,7 @@ from tempfile import NamedTemporaryFile import pytest -from pydantic import AnyUrl, FileUrl +from pydantic import AnyUrl from mcp.server.fastmcp.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate @@ -31,7 +31,7 @@ def test_add_resource(self, temp_file: Path): """Test adding a resource.""" manager = ResourceManager() resource = FileResource( - uri=FileUrl(f"file://{temp_file}"), + uri=f"file://{temp_file}", name="test", path=temp_file, ) @@ -43,7 +43,7 @@ def test_add_duplicate_resource(self, temp_file: Path): """Test adding the same resource twice.""" manager = ResourceManager() resource = FileResource( - uri=FileUrl(f"file://{temp_file}"), + uri=f"file://{temp_file}", name="test", path=temp_file, ) @@ -56,7 +56,7 @@ def test_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCa """Test warning on duplicate resources.""" manager = ResourceManager() resource = FileResource( - uri=FileUrl(f"file://{temp_file}"), + uri=f"file://{temp_file}", name="test", path=temp_file, ) @@ -68,7 +68,7 @@ def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog: pyte """Test disabling warning on duplicate resources.""" manager = ResourceManager(warn_on_duplicate_resources=False) resource = FileResource( - uri=FileUrl(f"file://{temp_file}"), + uri=f"file://{temp_file}", name="test", path=temp_file, ) @@ -81,7 +81,7 @@ async def test_get_resource(self, temp_file: Path): """Test getting a resource by URI.""" manager = ResourceManager() resource = FileResource( - uri=FileUrl(f"file://{temp_file}"), + uri=f"file://{temp_file}", name="test", path=temp_file, ) @@ -120,12 +120,12 @@ def test_list_resources(self, temp_file: Path): """Test listing all resources.""" manager = ResourceManager() resource1 = FileResource( - uri=FileUrl(f"file://{temp_file}"), + uri=f"file://{temp_file}", name="test1", path=temp_file, ) resource2 = FileResource( - uri=FileUrl(f"file://{temp_file}2"), + uri=f"file://{temp_file}2", name="test2", path=temp_file, ) diff --git a/tests/server/fastmcp/resources/test_resources.py b/tests/server/fastmcp/resources/test_resources.py index d617774fa5..6d346786dc 100644 --- a/tests/server/fastmcp/resources/test_resources.py +++ b/tests/server/fastmcp/resources/test_resources.py @@ -1,5 +1,4 @@ import pytest -from pydantic import AnyUrl from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp.resources import FunctionResource, Resource @@ -9,35 +8,35 @@ class TestResourceValidation: """Test base Resource validation.""" - def test_resource_uri_validation(self): - """Test URI validation.""" + def test_resource_uri_accepts_any_string(self): + """Test that URI field accepts any string per MCP spec.""" def dummy_func() -> str: # pragma: no cover return "data" # Valid URI resource = FunctionResource( - uri=AnyUrl("http://example.com/data"), + uri="http://example.com/data", name="test", fn=dummy_func, ) - assert str(resource.uri) == "http://example.com/data" + assert resource.uri == "http://example.com/data" - # Missing protocol - with pytest.raises(ValueError, match="Input should be a valid URL"): - FunctionResource( - uri=AnyUrl("invalid"), - name="test", - fn=dummy_func, - ) + # Relative path - now accepted per MCP spec + resource = FunctionResource( + uri="users/me", + name="test", + fn=dummy_func, + ) + assert resource.uri == "users/me" - # Missing host - with pytest.raises(ValueError, match="Input should be a valid URL"): - FunctionResource( - uri=AnyUrl("http://"), - name="test", - fn=dummy_func, - ) + # Custom scheme + resource = FunctionResource( + uri="custom://resource", + name="test", + fn=dummy_func, + ) + assert resource.uri == "custom://resource" def test_resource_name_from_uri(self): """Test name is extracted from URI if not provided.""" @@ -46,7 +45,7 @@ def dummy_func() -> str: # pragma: no cover return "data" resource = FunctionResource( - uri=AnyUrl("resource://my-resource"), + uri="resource://my-resource", fn=dummy_func, ) assert resource.name == "resource://my-resource" @@ -65,7 +64,7 @@ def dummy_func() -> str: # pragma: no cover # Explicit name takes precedence over URI resource = FunctionResource( - uri=AnyUrl("resource://uri-name"), + uri="resource://uri-name", name="explicit-name", fn=dummy_func, ) @@ -79,14 +78,14 @@ def dummy_func() -> str: # pragma: no cover # Default mime type resource = FunctionResource( - uri=AnyUrl("resource://test"), + uri="resource://test", fn=dummy_func, ) assert resource.mime_type == "text/plain" # Custom mime type resource = FunctionResource( - uri=AnyUrl("resource://test"), + uri="resource://test", fn=dummy_func, mime_type="application/json", ) @@ -100,7 +99,7 @@ class ConcreteResource(Resource): pass with pytest.raises(TypeError, match="abstract method"): - ConcreteResource(uri=AnyUrl("test://test"), name="test") # type: ignore + ConcreteResource(uri="test://test", name="test") # type: ignore class TestResourceAnnotations: @@ -207,7 +206,7 @@ def dummy_func() -> str: # pragma: no cover metadata = {"version": "1.0", "category": "test"} resource = FunctionResource( - uri=AnyUrl("resource://test"), + uri="resource://test", name="test", fn=dummy_func, meta=metadata, @@ -225,7 +224,7 @@ def dummy_func() -> str: # pragma: no cover return "data" resource = FunctionResource( - uri=AnyUrl("resource://test"), + uri="resource://test", name="test", fn=dummy_func, ) diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index b6cd0d5dfa..68adb7ee40 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from pydantic import AnyUrl, BaseModel +from pydantic import BaseModel from starlette.routing import Mount, Route from mcp.server.fastmcp import Context, FastMCP @@ -743,11 +743,11 @@ async def test_text_resource(self): def get_text(): return "Hello, world!" - resource = FunctionResource(uri=AnyUrl("resource://test"), name="test", fn=get_text) + resource = FunctionResource(uri="resource://test", name="test", fn=get_text) mcp.add_resource(resource) async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("resource://test")) + result = await client.read_resource("resource://test") assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Hello, world!" @@ -759,7 +759,7 @@ def get_binary(): return b"Binary data" resource = FunctionResource( - uri=AnyUrl("resource://binary"), + uri="resource://binary", name="binary", fn=get_binary, mime_type="application/octet-stream", @@ -767,7 +767,7 @@ def get_binary(): mcp.add_resource(resource) async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("resource://binary")) + result = await client.read_resource("resource://binary") assert isinstance(result.contents[0], BlobResourceContents) assert result.contents[0].blob == base64.b64encode(b"Binary data").decode() @@ -779,11 +779,11 @@ async def test_file_resource_text(self, tmp_path: Path): text_file = tmp_path / "test.txt" text_file.write_text("Hello from file!") - resource = FileResource(uri=AnyUrl("file://test.txt"), name="test.txt", path=text_file) + resource = FileResource(uri="file://test.txt", name="test.txt", path=text_file) mcp.add_resource(resource) async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("file://test.txt")) + result = await client.read_resource("file://test.txt") assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Hello from file!" @@ -796,7 +796,7 @@ async def test_file_resource_binary(self, tmp_path: Path): binary_file.write_bytes(b"Binary file data") resource = FileResource( - uri=AnyUrl("file://test.bin"), + uri="file://test.bin", name="test.bin", path=binary_file, mime_type="application/octet-stream", @@ -804,7 +804,7 @@ async def test_file_resource_binary(self, tmp_path: Path): mcp.add_resource(resource) async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("file://test.bin")) + result = await client.read_resource("file://test.bin") assert isinstance(result.contents[0], BlobResourceContents) assert result.contents[0].blob == base64.b64encode(b"Binary file data").decode() @@ -822,7 +822,7 @@ def get_data() -> str: # pragma: no cover assert len(resources.resources) == 1 resource = resources.resources[0] assert resource.description == "get_data returns a string" - assert resource.uri == AnyUrl("function://test") + assert resource.uri == "function://test" assert resource.name == "test_get_data" assert resource.mimeType == "text/plain" @@ -870,7 +870,7 @@ def get_data(name: str) -> str: return f"Data for {name}" async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("resource://test/data")) + result = await client.read_resource("resource://test/data") assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Data for test" @@ -895,7 +895,7 @@ def get_data(org: str, repo: str) -> str: return f"Data for {org}/{repo}" async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("resource://cursor/fastmcp/data")) + result = await client.read_resource("resource://cursor/fastmcp/data") assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Data for cursor/fastmcp" @@ -918,7 +918,7 @@ def get_static_data() -> str: return "Static data" async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("resource://static")) + result = await client.read_resource("resource://static") assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "Static data" @@ -958,7 +958,7 @@ def get_csv(user: str) -> str: assert template.mimeType == "text/csv" async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("resource://bob/csv")) + result = await client.read_resource("resource://bob/csv") assert isinstance(result.contents[0], TextResourceContents) assert result.contents[0].text == "csv for bob" @@ -1020,7 +1020,7 @@ def get_data() -> str: return "test data" async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("resource://data")) + result = await client.read_resource("resource://data") # Verify content and metadata in protocol response assert isinstance(result.contents[0], TextResourceContents) @@ -1189,7 +1189,7 @@ def resource_with_context(name: str, ctx: Context[ServerSession, None]) -> str: # Test via client async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("resource://context/test")) + result = await client.read_resource("resource://context/test") assert len(result.contents) == 1 content = result.contents[0] assert isinstance(content, TextResourceContents) @@ -1214,7 +1214,7 @@ def resource_no_context(name: str) -> str: # Test via client async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("resource://nocontext/test")) + result = await client.read_resource("resource://nocontext/test") assert len(result.contents) == 1 content = result.contents[0] assert isinstance(content, TextResourceContents) @@ -1239,7 +1239,7 @@ def resource_custom_ctx(id: str, my_ctx: Context[ServerSession, None]) -> str: # Test via client async with client_session(mcp._mcp_server) as client: - result = await client.read_resource(AnyUrl("resource://custom/123")) + result = await client.read_resource("resource://custom/123") assert len(result.contents) == 1 content = result.contents[0] assert isinstance(content, TextResourceContents) @@ -1442,7 +1442,7 @@ def fn() -> Message: content=EmbeddedResource( type="resource", resource=TextResourceContents( - uri=AnyUrl("file://file.txt"), + uri="file://file.txt", text="File contents", mimeType="text/plain", ), diff --git a/tests/server/fastmcp/test_title.py b/tests/server/fastmcp/test_title.py index 774f8dd63c..da9443eb40 100644 --- a/tests/server/fastmcp/test_title.py +++ b/tests/server/fastmcp/test_title.py @@ -1,7 +1,6 @@ """Integration tests for title field functionality.""" import pytest -from pydantic import AnyUrl from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp.resources import FunctionResource @@ -134,7 +133,7 @@ def get_basic_data() -> str: # pragma: no cover return "Basic data" basic_resource = FunctionResource( - uri=AnyUrl("resource://basic"), + uri="resource://basic", name="basic_resource", description="Basic resource", fn=get_basic_data, @@ -146,7 +145,7 @@ def get_titled_data() -> str: # pragma: no cover return "Titled data" titled_resource = FunctionResource( - uri=AnyUrl("resource://titled"), + uri="resource://titled", name="titled_resource", title="User-Friendly Resource", description="Resource with title", @@ -219,10 +218,10 @@ async def test_get_display_name_utility(): assert get_display_name(tool_with_both) == "Primary Title" # Test other types: title > name - resource = Resource(uri=AnyUrl("file://test"), name="test_res") + resource = Resource(uri="file://test", name="test_res") assert get_display_name(resource) == "test_res" - resource_with_title = Resource(uri=AnyUrl("file://test"), name="test_res", title="Test Resource") + resource_with_title = Resource(uri="file://test", name="test_res", title="Test Resource") assert get_display_name(resource_with_title) == "Test Resource" prompt = Prompt(name="test_prompt") diff --git a/tests/server/lowlevel/test_server_listing.py b/tests/server/lowlevel/test_server_listing.py index 23ac7e4519..60823d967c 100644 --- a/tests/server/lowlevel/test_server_listing.py +++ b/tests/server/lowlevel/test_server_listing.py @@ -3,7 +3,6 @@ import warnings import pytest -from pydantic import AnyUrl from mcp.server import Server from mcp.types import ( @@ -52,8 +51,8 @@ async def test_list_resources_basic() -> None: server = Server("test") test_resources = [ - Resource(uri=AnyUrl("file:///test1.txt"), name="Test 1"), - Resource(uri=AnyUrl("file:///test2.txt"), name="Test 2"), + Resource(uri="file:///test1.txt", name="Test 1"), + Resource(uri="file:///test2.txt", name="Test 2"), ] with warnings.catch_warnings(): diff --git a/tests/server/test_read_resource.py b/tests/server/test_read_resource.py index c31b90c557..75a1f19935 100644 --- a/tests/server/test_read_resource.py +++ b/tests/server/test_read_resource.py @@ -3,7 +3,6 @@ from tempfile import NamedTemporaryFile import pytest -from pydantic import AnyUrl, FileUrl import mcp.types as types from mcp.server.lowlevel.server import ReadResourceContents, Server @@ -27,7 +26,7 @@ async def test_read_resource_text(temp_file: Path): server = Server("test") @server.read_resource() - async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: + async def read_resource(uri: str) -> Iterable[ReadResourceContents]: return [ReadResourceContents(content="Hello World", mime_type="text/plain")] # Get the handler directly from the server @@ -35,7 +34,7 @@ async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: # Create a request request = types.ReadResourceRequest( - params=types.ReadResourceRequestParams(uri=FileUrl(temp_file.as_uri())), + params=types.ReadResourceRequestParams(uri=temp_file.as_uri()), ) # Call the handler @@ -54,7 +53,7 @@ async def test_read_resource_binary(temp_file: Path): server = Server("test") @server.read_resource() - async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: + async def read_resource(uri: str) -> Iterable[ReadResourceContents]: return [ReadResourceContents(content=b"Hello World", mime_type="application/octet-stream")] # Get the handler directly from the server @@ -62,7 +61,7 @@ async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: # Create a request request = types.ReadResourceRequest( - params=types.ReadResourceRequestParams(uri=FileUrl(temp_file.as_uri())), + params=types.ReadResourceRequestParams(uri=temp_file.as_uri()), ) # Call the handler @@ -80,7 +79,7 @@ async def test_read_resource_default_mime(temp_file: Path): server = Server("test") @server.read_resource() - async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: + async def read_resource(uri: str) -> Iterable[ReadResourceContents]: return [ ReadResourceContents( content="Hello World", @@ -93,7 +92,7 @@ async def read_resource(uri: AnyUrl) -> Iterable[ReadResourceContents]: # Create a request request = types.ReadResourceRequest( - params=types.ReadResourceRequestParams(uri=FileUrl(temp_file.as_uri())), + params=types.ReadResourceRequestParams(uri=temp_file.as_uri()), ) # Call the handler diff --git a/tests/shared/test_memory.py b/tests/shared/test_memory.py index ca4368e9f8..10f580a6c5 100644 --- a/tests/shared/test_memory.py +++ b/tests/shared/test_memory.py @@ -1,5 +1,4 @@ import pytest -from pydantic import AnyUrl from typing_extensions import AsyncGenerator from mcp.client.session import ClientSession @@ -16,7 +15,7 @@ def mcp_server() -> Server: async def handle_list_resources(): # pragma: no cover return [ Resource( - uri=AnyUrl("memory://test"), + uri="memory://test", name="Test Resource", description="A test resource", ) diff --git a/tests/shared/test_sse.py b/tests/shared/test_sse.py index 7604450f81..99d84515ef 100644 --- a/tests/shared/test_sse.py +++ b/tests/shared/test_sse.py @@ -5,6 +5,7 @@ from collections.abc import AsyncGenerator, Generator from typing import Any from unittest.mock import AsyncMock, MagicMock, Mock, patch +from urllib.parse import urlparse import anyio import httpx @@ -12,7 +13,6 @@ import uvicorn from httpx_sse import ServerSentEvent from inline_snapshot import snapshot -from pydantic import AnyUrl from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import Response @@ -61,13 +61,14 @@ def __init__(self): super().__init__(SERVER_NAME) @self.read_resource() - async def handle_read_resource(uri: AnyUrl) -> str | bytes: - if uri.scheme == "foobar": - return f"Read {uri.host}" - elif uri.scheme == "slow": + async def handle_read_resource(uri: str) -> str | bytes: + parsed = urlparse(uri) + if parsed.scheme == "foobar": + return f"Read {parsed.netloc}" + if parsed.scheme == "slow": # Simulate a slow resource await anyio.sleep(2.0) - return f"Slow response from {uri.host}" + return f"Slow response from {parsed.netloc}" raise McpError(error=ErrorData(code=404, message="OOPS! no resource with that URI was found")) @@ -254,7 +255,7 @@ async def test_sse_client_happy_request_and_response( initialized_sse_client_session: ClientSession, ) -> None: session = initialized_sse_client_session - response = await session.read_resource(uri=AnyUrl("foobar://should-work")) + response = await session.read_resource(uri="foobar://should-work") assert len(response.contents) == 1 assert isinstance(response.contents[0], TextResourceContents) assert response.contents[0].text == "Read should-work" @@ -266,7 +267,7 @@ async def test_sse_client_exception_handling( ) -> None: session = initialized_sse_client_session with pytest.raises(McpError, match="OOPS! no resource with that URI was found"): - await session.read_resource(uri=AnyUrl("xxx://will-not-work")) + await session.read_resource(uri="xxx://will-not-work") @pytest.mark.anyio @@ -277,12 +278,12 @@ async def test_sse_client_timeout( # pragma: no cover session = initialized_sse_client_session # sanity check that normal, fast responses are working - response = await session.read_resource(uri=AnyUrl("foobar://1")) + response = await session.read_resource(uri="foobar://1") assert isinstance(response, ReadResourceResult) with anyio.move_on_after(3): with pytest.raises(McpError, match="Read timed out"): - response = await session.read_resource(uri=AnyUrl("slow://2")) + response = await session.read_resource(uri="slow://2") # we should receive an error here return diff --git a/tests/shared/test_streamable_http.py b/tests/shared/test_streamable_http.py index 0ed4250533..a0c0e6b4cf 100644 --- a/tests/shared/test_streamable_http.py +++ b/tests/shared/test_streamable_http.py @@ -13,6 +13,7 @@ from collections.abc import Generator from typing import Any from unittest.mock import MagicMock +from urllib.parse import urlparse import anyio import httpx @@ -137,13 +138,14 @@ def __init__(self): self._lock = None # Will be initialized in async context @self.read_resource() - async def handle_read_resource(uri: AnyUrl) -> str | bytes: - if uri.scheme == "foobar": - return f"Read {uri.host}" - elif uri.scheme == "slow": + async def handle_read_resource(uri: str) -> str | bytes: + parsed = urlparse(uri) + if parsed.scheme == "foobar": + return f"Read {parsed.netloc}" + if parsed.scheme == "slow": # Simulate a slow resource await anyio.sleep(2.0) - return f"Slow response from {uri.host}" + return f"Slow response from {parsed.netloc}" raise ValueError(f"Unknown resource: {uri}") @@ -214,7 +216,7 @@ async def handle_call_tool(name: str, args: dict[str, Any]) -> list[TextContent] # When the tool is called, send a notification to test GET stream if name == "test_tool_with_standalone_notification": - await ctx.session.send_resource_updated(uri=AnyUrl("http://test_resource")) + await ctx.session.send_resource_updated(uri="http://test_resource") return [TextContent(type="text", text=f"Called {name}")] elif name == "long_running_with_checkpoints": @@ -368,7 +370,7 @@ async def handle_call_tool(name: str, args: dict[str, Any]) -> list[TextContent] elif name == "tool_with_standalone_stream_close": # Test for GET stream reconnection # 1. Send unsolicited notification via GET stream (no related_request_id) - await ctx.session.send_resource_updated(uri=AnyUrl("http://notification_1")) + await ctx.session.send_resource_updated(uri="http://notification_1") # Small delay to ensure notification is flushed before closing await anyio.sleep(0.1) @@ -381,7 +383,7 @@ async def handle_call_tool(name: str, args: dict[str, Any]) -> list[TextContent] await anyio.sleep(1.5) # 4. Send another notification on the new GET stream connection - await ctx.session.send_resource_updated(uri=AnyUrl("http://notification_2")) + await ctx.session.send_resource_updated(uri="http://notification_2") return [TextContent(type="text", text="Standalone stream close test done")] @@ -1009,7 +1011,7 @@ async def test_streamable_http_client_basic_connection(basic_server: None, basic @pytest.mark.anyio async def test_streamable_http_client_resource_read(initialized_client_session: ClientSession): """Test client resource read functionality.""" - response = await initialized_client_session.read_resource(uri=AnyUrl("foobar://test-resource")) + response = await initialized_client_session.read_resource(uri="foobar://test-resource") assert len(response.contents) == 1 assert response.contents[0].uri == AnyUrl("foobar://test-resource") assert isinstance(response.contents[0], TextResourceContents) @@ -1035,7 +1037,7 @@ async def test_streamable_http_client_tool_invocation(initialized_client_session async def test_streamable_http_client_error_handling(initialized_client_session: ClientSession): """Test error handling in client.""" with pytest.raises(McpError) as exc_info: - await initialized_client_session.read_resource(uri=AnyUrl("unknown://test-error")) + await initialized_client_session.read_resource(uri="unknown://test-error") assert exc_info.value.error.code == 0 assert "Unknown resource: unknown://test-error" in exc_info.value.error.message @@ -1061,7 +1063,7 @@ async def test_streamable_http_client_session_persistence(basic_server: None, ba assert len(tools.tools) == 10 # Read a resource - resource = await session.read_resource(uri=AnyUrl("foobar://test-persist")) + resource = await session.read_resource(uri="foobar://test-persist") assert isinstance(resource.contents[0], TextResourceContents) is True content = resource.contents[0] assert isinstance(content, TextResourceContents) diff --git a/tests/shared/test_ws.py b/tests/shared/test_ws.py index f093cb4927..e24063ffc9 100644 --- a/tests/shared/test_ws.py +++ b/tests/shared/test_ws.py @@ -49,13 +49,16 @@ def __init__(self): super().__init__(SERVER_NAME) @self.read_resource() - async def handle_read_resource(uri: AnyUrl) -> str | bytes: - if uri.scheme == "foobar": - return f"Read {uri.host}" - elif uri.scheme == "slow": + async def handle_read_resource(uri: str) -> str | bytes: + from urllib.parse import urlparse + + parsed = urlparse(uri) + if parsed.scheme == "foobar": + return f"Read {parsed.netloc}" + elif parsed.scheme == "slow": # Simulate a slow resource await anyio.sleep(2.0) - return f"Slow response from {uri.host}" + return f"Slow response from {parsed.netloc}" raise McpError(error=ErrorData(code=404, message="OOPS! no resource with that URI was found"))