Skip to content

Commit d360e8d

Browse files
Tapan Chughclaude
andcommitted
refactor: Update URI schemes and improve filter_by_prefix signature
- Change URI schemes from tool:// and prompt:// to mcp://tools and mcp://prompts - Add MCP_SCHEME constant and use it for URL validation - Add model validators to ensure URIs start with correct prefixes - Improve filter_by_prefix to accept AnyUrl | str, removing need for str() casts - Make Prompt description field optional (matching main branch) - Update all tests to use scheme constants instead of hardcoded values This provides a more standard URI format where 'mcp' is the scheme and 'tools'/'prompts' are path segments, while also simplifying the API. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8238b0a commit d360e8d

File tree

10 files changed

+41
-24
lines changed

10 files changed

+41
-24
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,7 +1125,7 @@ server = Server("example-server", lifespan=server_lifespan)
11251125

11261126

11271127
@server.list_tools()
1128-
async def handle_list_tools(request: types.ListToolsRequest) -> list[types.Tool]:
1128+
async def handle_list_tools(_) -> list[types.Tool]:
11291129
"""List available tools."""
11301130
return [
11311131
types.Tool(
@@ -1207,7 +1207,7 @@ server = Server("example-server")
12071207

12081208

12091209
@server.list_prompts()
1210-
async def handle_list_prompts(request: types.ListPromptsRequest) -> list[types.Prompt]:
1210+
async def handle_list_prompts(_) -> list[types.Prompt]:
12111211
"""List available prompts."""
12121212
return [
12131213
types.Prompt(
@@ -1286,7 +1286,7 @@ server = Server("example-server")
12861286

12871287

12881288
@server.list_tools()
1289-
async def list_tools(request: types.ListToolsRequest) -> list[types.Tool]:
1289+
async def list_tools(_) -> list[types.Tool]:
12901290
"""List available tools with structured output schemas."""
12911291
return [
12921292
types.Tool(

src/mcp/server/fastmcp/prompts/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class Prompt(BaseModel):
6060
name: str = Field(description="Name of the prompt")
6161
uri: str = Field(description="URI of the prompt")
6262
title: str | None = Field(None, description="Human-readable title of the prompt")
63-
description: str = Field(description="Description of what the prompt does")
63+
description: str | None = Field(None, description="Description of what the prompt does")
6464
arguments: list[PromptArgument] | None = Field(None, description="Arguments that can be passed to the prompt")
6565
fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)
6666

src/mcp/server/fastmcp/prompts/manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def get_prompt(self, name: str) -> Prompt | None:
2828
def list_prompts(self, prefix: str | None = None) -> list[Prompt]:
2929
"""List all registered prompts, optionally filtered by URI prefix."""
3030
prompts = list(self._prompts.values())
31-
prompts = filter_by_prefix(prompts, prefix, lambda p: str(p.uri))
31+
prompts = filter_by_prefix(prompts, prefix, lambda p: p.uri)
3232
logger.debug("Listing prompts", extra={"count": len(prompts), "prefix": prefix})
3333
return prompts
3434

src/mcp/server/fastmcp/prompts/prompt_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ def get_prompt(self, name: str) -> Prompt | None:
3737
def list_prompts(self, prefix: str | None = None) -> list[Prompt]:
3838
"""List all registered prompts, optionally filtered by URI prefix."""
3939
prompts = list(self._prompts.values())
40-
prompts = filter_by_prefix(prompts, prefix, lambda p: str(p.uri))
40+
prompts = filter_by_prefix(prompts, prefix, lambda p: p.uri)
4141
logger.debug("Listing prompts", extra={"count": len(prompts), "prefix": prefix})
4242
return prompts

src/mcp/server/fastmcp/resources/resource_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ async def get_resource(self, uri: AnyUrl | str) -> Resource | None:
9090
def list_resources(self, prefix: str | None = None) -> list[Resource]:
9191
"""List all registered resources, optionally filtered by URI prefix."""
9292
resources = list(self._resources.values())
93-
resources = filter_by_prefix(resources, prefix, lambda r: str(r.uri))
93+
resources = filter_by_prefix(resources, prefix, lambda r: r.uri)
9494
logger.debug("Listing resources", extra={"count": len(resources), "prefix": prefix})
9595
return resources
9696

src/mcp/server/fastmcp/tools/tool_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def get_tool(self, name: str) -> Tool | None:
4747
def list_tools(self, prefix: str | None = None) -> list[Tool]:
4848
"""List all registered tools, optionally filtered by URI prefix."""
4949
tools = list(self._tools.values())
50-
tools = filter_by_prefix(tools, prefix, lambda t: str(t.uri))
50+
tools = filter_by_prefix(tools, prefix, lambda t: t.uri)
5151
logger.debug("Listing tools", extra={"count": len(tools), "prefix": prefix})
5252
return tools
5353

src/mcp/server/fastmcp/uri_utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from collections.abc import Callable
44
from typing import TypeVar
55

6+
from pydantic import AnyUrl
7+
68
from mcp.types import PROMPT_SCHEME, TOOL_SCHEME
79

810
T = TypeVar("T")
@@ -33,7 +35,7 @@ def normalize_to_prompt_uri(name_or_uri: str) -> str:
3335
return normalize_to_uri(name_or_uri, PROMPT_SCHEME)
3436

3537

36-
def filter_by_prefix(items: list[T], prefix: str | None, uri_getter: Callable[[T], str]) -> list[T]:
38+
def filter_by_prefix(items: list[T], prefix: str | None, uri_getter: Callable[[T], AnyUrl | str]) -> list[T]:
3739
"""Filter items by URI prefix.
3840
3941
Args:
@@ -50,7 +52,7 @@ def filter_by_prefix(items: list[T], prefix: str | None, uri_getter: Callable[[T
5052
# Filter items where the URI starts with the prefix
5153
filtered: list[T] = []
5254
for item in items:
53-
uri = uri_getter(item)
55+
uri = str(uri_getter(item))
5456
if uri.startswith(prefix):
5557
# If prefix ends with a separator, we already have a proper boundary
5658
if prefix.endswith(("/", "?", "#")):

src/mcp/types.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
from typing_extensions import deprecated
77

88
# URI scheme constants
9-
TOOL_SCHEME = "tool:/"
10-
PROMPT_SCHEME = "prompt:/"
9+
MCP_SCHEME = "mcp"
10+
TOOL_SCHEME = f"{MCP_SCHEME}://tools"
11+
PROMPT_SCHEME = f"{MCP_SCHEME}://prompts"
1112

1213
"""
1314
Model Context Protocol bindings for Python
@@ -641,9 +642,7 @@ class PromptArgument(BaseModel):
641642
class Prompt(BaseMetadata):
642643
"""A prompt or prompt template that the server offers."""
643644

644-
uri: (
645-
Annotated[AnyUrl, UrlConstraints(allowed_schemes=[PROMPT_SCHEME.rstrip(":/ ")], host_required=False)] | None
646-
) = None
645+
uri: Annotated[AnyUrl, UrlConstraints(allowed_schemes=[MCP_SCHEME], host_required=False)] | None = None
647646
"""URI for the prompt. Auto-generated if not provided."""
648647
description: str | None = None
649648
"""An optional description of what this prompt provides."""
@@ -662,6 +661,15 @@ def __init__(self, **data: Any) -> None:
662661
data["uri"] = AnyUrl(f"{PROMPT_SCHEME}/{data['name']}")
663662
super().__init__(**data)
664663

664+
@model_validator(mode="after")
665+
def validate_prompt_uri(self) -> "Prompt":
666+
"""Validate that prompt URI starts with the correct prefix."""
667+
if self.uri is not None:
668+
uri_str = str(self.uri)
669+
if not uri_str.startswith(f"{PROMPT_SCHEME}/"):
670+
raise ValueError(f"Prompt URI must start with {PROMPT_SCHEME}/")
671+
return self
672+
665673

666674
class ListPromptsResult(ListResult):
667675
"""The server's response to a prompts/list request from the client."""
@@ -870,9 +878,7 @@ class ToolAnnotations(BaseModel):
870878
class Tool(BaseMetadata):
871879
"""Definition for a tool the client can call."""
872880

873-
uri: Annotated[AnyUrl, UrlConstraints(allowed_schemes=[TOOL_SCHEME.rstrip(":/ ")], host_required=False)] | None = (
874-
None
875-
)
881+
uri: Annotated[AnyUrl, UrlConstraints(allowed_schemes=[MCP_SCHEME], host_required=False)] | None = None
876882
"""URI for the tool. Auto-generated if not provided."""
877883
description: str | None = None
878884
"""A human-readable description of the tool."""
@@ -898,6 +904,15 @@ def __init__(self, **data: Any) -> None:
898904
data["uri"] = AnyUrl(f"{TOOL_SCHEME}/{data['name']}")
899905
super().__init__(**data)
900906

907+
@model_validator(mode="after")
908+
def validate_tool_uri(self) -> "Tool":
909+
"""Validate that tool URI starts with the correct prefix."""
910+
if self.uri is not None:
911+
uri_str = str(self.uri)
912+
if not uri_str.startswith(f"{TOOL_SCHEME}/"):
913+
raise ValueError(f"Tool URI must start with {TOOL_SCHEME}/")
914+
return self
915+
901916

902917
class ListToolsResult(ListResult):
903918
"""The server's response to a tools/list request from the client."""

tests/server/fastmcp/test_tool_manager.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class AddArguments(ArgModelBase):
6262
# warn on duplicate tools
6363
with caplog.at_level(logging.WARNING):
6464
manager = ToolManager(True, tools=[original_tool, original_tool])
65-
assert "Tool already exists: tool://sum" in caplog.text
65+
assert f"Tool already exists: {TOOL_SCHEME}/sum" in caplog.text
6666

6767
@pytest.mark.anyio
6868
async def test_async_function(self):
@@ -163,7 +163,7 @@ def f(x: int) -> int:
163163
manager.add_tool(f)
164164
with caplog.at_level(logging.WARNING):
165165
manager.add_tool(f)
166-
assert "Tool already exists: tool://f" in caplog.text
166+
assert f"Tool already exists: {TOOL_SCHEME}/f" in caplog.text
167167

168168
def test_disable_warn_on_duplicate_tools(self, caplog):
169169
"""Test disabling warning on duplicate tools."""
@@ -351,7 +351,7 @@ def math_multiply(a: int, b: int) -> int:
351351
multiply_tool.uri = f"{TOOL_SCHEME}/custom/math/multiply"
352352
manager._tools[str(multiply_tool.uri)] = multiply_tool
353353

354-
# Call by default URI (tool://function_name)
354+
# Call by default URI (TOOL_SCHEME/function_name)
355355
result = await manager.call_tool(f"{TOOL_SCHEME}/math_add", {"a": 5, "b": 3})
356356
assert result == 8
357357

tests/test_types.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_resource_uri():
5858
assert resource.name == "test"
5959
assert str(resource.uri) == "file://test.txt/" # AnyUrl adds trailing slash
6060

61-
# Should reject tool:// and prompt:// schemes
61+
# Should reject TOOL_SCHEME and PROMPT_SCHEME schemes
6262
with pytest.raises(ValueError, match="reserved schemes"):
6363
Resource(name="test", uri=AnyUrl(f"{TOOL_SCHEME}/test"))
6464

@@ -68,7 +68,7 @@ def test_resource_uri():
6868

6969
def test_tool_uri_validation():
7070
"""Test that Tool requires URI with tool scheme."""
71-
# Tool requires URI with tool:// scheme
71+
# Tool requires URI with TOOL_SCHEME
7272
tool = Tool(name="calculator", inputSchema={"type": "object"}, uri=f"{TOOL_SCHEME}/calculator")
7373
assert tool.name == "calculator"
7474
assert str(tool.uri) == f"{TOOL_SCHEME}/calculator"
@@ -80,7 +80,7 @@ def test_tool_uri_validation():
8080

8181
def test_prompt_uri_validation():
8282
"""Test that Prompt requires URI with prompt scheme."""
83-
# Prompt requires URI with prompt:// scheme
83+
# Prompt requires URI with PROMPT_SCHEME
8484
prompt = Prompt(name="greeting", uri=f"{PROMPT_SCHEME}/greeting")
8585
assert prompt.name == "greeting"
8686
assert str(prompt.uri) == f"{PROMPT_SCHEME}/greeting"

0 commit comments

Comments
 (0)