fix: change Resource URI fields from AnyUrl to str (#1863)

This commit is contained in:
Felix Weinberger 2026-01-16 08:58:57 +00:00 • committed by GitHub
commit cfb2909631
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 343 additions and 179 deletions

View file

@ -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 @@ class TestRenderPrompt:
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 @@ class TestRenderPrompt:
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 @@ class TestRenderPrompt:
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 @@ class TestRenderPrompt:
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 @@ class TestRenderPrompt:
"content": {
"type": "resource",
"resource": {
"uri": FileUrl("file://file.txt"),
"uri": "file://file.txt",
"text": "File contents",
"mimeType": "text/plain",
},
@ -175,7 +174,7 @@ class TestRenderPrompt:
content=EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri=FileUrl("file://file.txt"),
uri="file://file.txt",
text="File contents",
mimeType="text/plain",
),

View file

@ -3,7 +3,6 @@ from pathlib import Path
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 @@ class TestFileResource:
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 @@ class TestFileResource:
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 @@ class TestFileResource:
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 @@ class TestFileResource:
"""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 @@ class TestFileResource:
# 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 @@ class TestFileResource:
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,
)

View file

@ -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 @@ class TestFunctionResource:
return "test content"
resource = FunctionResource(
uri=AnyUrl("fn://test"),
uri="fn://test",
name="test",
description="test function",
fn=my_func,
@ -33,7 +33,7 @@ class TestFunctionResource:
return "Hello, world!"
resource = FunctionResource(
uri=AnyUrl("function://test"),
uri="function://test",
name="test",
fn=get_data,
)
@ -49,7 +49,7 @@ class TestFunctionResource:
return b"Hello, world!"
resource = FunctionResource(
uri=AnyUrl("function://test"),
uri="function://test",
name="test",
fn=get_data,
)
@ -64,7 +64,7 @@ class TestFunctionResource:
return {"key": "value"}
resource = FunctionResource(
uri=AnyUrl("function://test"),
uri="function://test",
name="test",
fn=get_data,
)
@ -80,7 +80,7 @@ class TestFunctionResource:
raise ValueError("Test error")
resource = FunctionResource(
uri=AnyUrl("function://test"),
uri="function://test",
name="test",
fn=failing_func,
)
@ -95,7 +95,7 @@ class TestFunctionResource:
name: str
resource = FunctionResource(
uri=AnyUrl("function://test"),
uri="function://test",
name="test",
fn=lambda: MyModel(name="test"),
)
@ -114,7 +114,7 @@ class TestFunctionResource:
return CustomData()
resource = FunctionResource(
uri=AnyUrl("function://test"),
uri="function://test",
name="test",
fn=get_data,
)
@ -129,7 +129,7 @@ class TestFunctionResource:
return "Hello, world!"
resource = FunctionResource(
uri=AnyUrl("function://test"),
uri="function://test",
name="test",
fn=get_data,
)
@ -154,7 +154,7 @@ class TestFunctionResource:
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:

View file

@ -2,7 +2,7 @@ from pathlib import Path
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 @@ class TestResourceManager:
"""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 @@ class TestResourceManager:
"""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 @@ class TestResourceManager:
"""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 @@ class TestResourceManager:
"""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 @@ class TestResourceManager:
"""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 @@ class TestResourceManager:
"""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,
)

View file

@ -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 @@ from mcp.types import Annotations
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 @@ class TestResourceValidation:
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 @@ class TestResourceValidation:
# 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 @@ class TestResourceValidation:
# 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 TestResourceValidation:
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 @@ class TestResourceMetadata:
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 @@ class TestResourceMetadata:
return "data"
resource = FunctionResource(
uri=AnyUrl("resource://test"),
uri="resource://test",
name="test",
fn=dummy_func,
)

View file

@ -4,7 +4,7 @@ from typing import Any
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 @@ class TestServerResources:
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 @@ class TestServerResources:
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 @@ class TestServerResources:
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 @@ class TestServerResources:
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 @@ class TestServerResources:
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 @@ class TestServerResources:
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 @@ class TestServerResources:
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 @@ class TestServerResourceTemplates:
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 @@ class TestServerResourceTemplates:
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 @@ class TestServerResourceTemplates:
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 @@ class TestServerResourceTemplates:
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 @@ class TestServerResourceMetadata:
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 @@ class TestContextInjection:
# 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 @@ class TestContextInjection:
# 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 @@ class TestContextInjection:
# 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 @@ class TestServerPrompts:
content=EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri=AnyUrl("file://file.txt"),
uri="file://file.txt",
text="File contents",
mimeType="text/plain",
),

View file

@ -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 @@ async def test_resource_title():
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 @@ async def test_resource_title():
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")