Skip to content

Commit e4614ba

Browse files
author
Tapan Chugh
committed
refactor: Replace single prefix filter with multi-path URI filters
1 parent cf7103e commit e4614ba

File tree

14 files changed

+1164
-155
lines changed

14 files changed

+1164
-155
lines changed

src/mcp/.types.py.~undo-tree~

Lines changed: 881 additions & 0 deletions
Large diffs are not rendered by default.

src/mcp/client/session.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -221,11 +221,13 @@ async def set_logging_level(self, level: types.LoggingLevel) -> types.EmptyResul
221221
types.EmptyResult,
222222
)
223223

224-
async def list_resources(self, prefix: str | None = None, cursor: str | None = None) -> types.ListResourcesResult:
224+
async def list_resources(
225+
self, filters: types.ListFilters | None = None, cursor: str | None = None
226+
) -> types.ListResourcesResult:
225227
"""Send a resources/list request."""
226228
params = None
227-
if cursor is not None or prefix is not None:
228-
params = types.ListRequestParams(prefix=prefix, cursor=cursor)
229+
if cursor is not None or filters is not None:
230+
params = types.ListRequestParams(filters=filters, cursor=cursor)
229231
return await self.send_request(
230232
types.ClientRequest(
231233
types.ListResourcesRequest(
@@ -238,13 +240,13 @@ async def list_resources(self, prefix: str | None = None, cursor: str | None = N
238240

239241
async def list_resource_templates(
240242
self,
241-
prefix: str | None = None,
243+
filters: types.ListFilters | None = None,
242244
cursor: str | None = None,
243245
) -> types.ListResourceTemplatesResult:
244246
"""Send a resources/templates/list request."""
245247
params = None
246-
if cursor is not None or prefix is not None:
247-
params = types.ListRequestParams(prefix=prefix, cursor=cursor)
248+
if cursor is not None or filters is not None:
249+
params = types.ListRequestParams(filters=filters, cursor=cursor)
248250
return await self.send_request(
249251
types.ClientRequest(
250252
types.ListResourceTemplatesRequest(
@@ -342,11 +344,13 @@ async def _validate_tool_result(self, name: str, result: types.CallToolResult) -
342344
except SchemaError as e:
343345
raise RuntimeError(f"Invalid schema for tool {name}: {e}")
344346

345-
async def list_prompts(self, prefix: str | None = None, cursor: str | None = None) -> types.ListPromptsResult:
347+
async def list_prompts(
348+
self, filters: types.ListFilters | None = None, cursor: str | None = None
349+
) -> types.ListPromptsResult:
346350
"""Send a prompts/list request."""
347351
params = None
348-
if cursor is not None or prefix is not None:
349-
params = types.ListRequestParams(prefix=prefix, cursor=cursor)
352+
if cursor is not None or filters is not None:
353+
params = types.ListRequestParams(filters=filters, cursor=cursor)
350354
return await self.send_request(
351355
types.ClientRequest(
352356
types.ListPromptsRequest(
@@ -394,11 +398,13 @@ async def complete(
394398
types.CompleteResult,
395399
)
396400

397-
async def list_tools(self, prefix: str | None = None, cursor: str | None = None) -> types.ListToolsResult:
401+
async def list_tools(
402+
self, filters: types.ListFilters | None = None, cursor: str | None = None
403+
) -> types.ListToolsResult:
398404
"""Send a tools/list request."""
399405
params = None
400-
if cursor is not None or prefix is not None:
401-
params = types.ListRequestParams(prefix=prefix, cursor=cursor)
406+
if cursor is not None or filters is not None:
407+
params = types.ListRequestParams(filters=filters, cursor=cursor)
402408
result = await self.send_request(
403409
types.ClientRequest(
404410
types.ListToolsRequest(

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from typing import Any
44

55
from mcp.server.fastmcp.prompts.base import Message, Prompt
6-
from mcp.server.fastmcp.uri_utils import filter_by_prefix, normalize_to_prompt_uri
6+
from mcp.server.fastmcp.uri_utils import filter_by_uri_paths, normalize_to_prompt_uri
77
from mcp.server.fastmcp.utilities.logging import get_logger
88

99
logger = get_logger(__name__)
@@ -25,11 +25,11 @@ def get_prompt(self, name: str) -> Prompt | None:
2525
uri = self._normalize_to_uri(name)
2626
return self._prompts.get(uri)
2727

28-
def list_prompts(self, prefix: str | None = None) -> list[Prompt]:
29-
"""List all registered prompts, optionally filtered by URI prefix."""
28+
def list_prompts(self, uri_paths: list[str] | None = None) -> list[Prompt]:
29+
"""List all registered prompts, optionally filtered by URI paths."""
3030
prompts = list(self._prompts.values())
31-
prompts = filter_by_prefix(prompts, prefix, lambda p: p.uri)
32-
logger.debug("Listing prompts", extra={"count": len(prompts), "prefix": prefix})
31+
prompts = filter_by_uri_paths(prompts, uri_paths, lambda p: p.uri)
32+
logger.debug("Listing prompts", extra={"count": len(prompts), "uri_paths": uri_paths})
3333
return prompts
3434

3535
def add_prompt(

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Prompt management functionality."""
22

33
from mcp.server.fastmcp.prompts.base import Prompt
4-
from mcp.server.fastmcp.uri_utils import filter_by_prefix, normalize_to_prompt_uri
4+
from mcp.server.fastmcp.uri_utils import filter_by_uri_paths, normalize_to_prompt_uri
55
from mcp.server.fastmcp.utilities.logging import get_logger
66

77
logger = get_logger(__name__)
@@ -34,9 +34,9 @@ def get_prompt(self, name: str) -> Prompt | None:
3434
uri = self._normalize_to_uri(name)
3535
return self._prompts.get(uri)
3636

37-
def list_prompts(self, prefix: str | None = None) -> list[Prompt]:
38-
"""List all registered prompts, optionally filtered by URI prefix."""
37+
def list_prompts(self, uri_paths: list[str] | None = None) -> list[Prompt]:
38+
"""List all registered prompts, optionally filtered by URI paths."""
3939
prompts = list(self._prompts.values())
40-
prompts = filter_by_prefix(prompts, prefix, lambda p: p.uri)
41-
logger.debug("Listing prompts", extra={"count": len(prompts), "prefix": prefix})
40+
prompts = filter_by_uri_paths(prompts, uri_paths, lambda p: p.uri)
41+
logger.debug("Listing prompts", extra={"count": len(prompts), "uri_paths": uri_paths})
4242
return prompts

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

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from mcp.server.fastmcp.resources.base import Resource
99
from mcp.server.fastmcp.resources.templates import ResourceTemplate
10-
from mcp.server.fastmcp.uri_utils import filter_by_prefix
10+
from mcp.server.fastmcp.uri_utils import filter_by_uri_paths
1111
from mcp.server.fastmcp.utilities.logging import get_logger
1212

1313
logger = get_logger(__name__)
@@ -87,20 +87,26 @@ async def get_resource(self, uri: AnyUrl | str) -> Resource | None:
8787

8888
raise ValueError(f"Unknown resource: {uri}")
8989

90-
def list_resources(self, prefix: str | None = None) -> list[Resource]:
91-
"""List all registered resources, optionally filtered by URI prefix."""
90+
def list_resources(self, uri_paths: list[str] | None = None) -> list[Resource]:
91+
"""List all registered resources, optionally filtered by URI paths."""
9292
resources = list(self._resources.values())
93-
resources = filter_by_prefix(resources, prefix, lambda r: r.uri)
94-
logger.debug("Listing resources", extra={"count": len(resources), "prefix": prefix})
93+
resources = filter_by_uri_paths(resources, uri_paths, lambda r: r.uri)
94+
logger.debug("Listing resources", extra={"count": len(resources), "uri_paths": uri_paths})
9595
return resources
9696

97-
def list_templates(self, prefix: str | None = None) -> list[ResourceTemplate]:
98-
"""List all registered templates, optionally filtered by URI template prefix."""
97+
def list_templates(self, uri_paths: list[str] | None = None) -> list[ResourceTemplate]:
98+
"""List all registered templates, optionally filtered by URI paths."""
9999
templates = list(self._templates.values())
100-
if prefix:
101-
# Ensure prefix ends with / for proper path matching
102-
if not prefix.endswith("/"):
103-
prefix = prefix + "/"
104-
templates = [t for t in templates if t.matches_prefix(prefix)]
105-
logger.debug("Listing templates", extra={"count": len(templates), "prefix": prefix})
100+
if uri_paths:
101+
filtered: list[ResourceTemplate] = []
102+
for template in templates:
103+
for prefix in uri_paths:
104+
# Ensure prefix ends with / for proper path matching
105+
if not prefix.endswith("/"):
106+
prefix = prefix + "/"
107+
if template.matches_prefix(prefix):
108+
filtered.append(template)
109+
break
110+
templates = filtered
111+
logger.debug("Listing templates", extra={"count": len(templates), "uri_paths": uri_paths})
106112
return templates

src/mcp/server/fastmcp/server.py

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,11 @@ def _setup_handlers(self) -> None:
268268
self._mcp_server.list_resource_templates()(self.list_resource_templates)
269269

270270
async def list_tools(self, request: types.ListToolsRequest | None = None) -> list[MCPTool]:
271-
"""List all available tools, optionally filtered by prefix."""
272-
prefix = None
273-
if request and request.params:
274-
prefix = request.params.prefix
275-
tools = self._tool_manager.list_tools(prefix=prefix)
271+
"""List all available tools, optionally filtered by URI paths."""
272+
uri_paths = None
273+
if request and request.params and request.params.filters:
274+
uri_paths = request.params.filters.uri_paths
275+
tools = self._tool_manager.list_tools(uri_paths=uri_paths)
276276
return [
277277
MCPTool(
278278
name=info.name,
@@ -303,11 +303,11 @@ async def call_tool(self, name: str, arguments: dict[str, Any]) -> Sequence[Cont
303303
return await self._tool_manager.call_tool(name, arguments, context=context, convert_result=True)
304304

305305
async def list_resources(self, request: types.ListResourcesRequest | None = None) -> list[MCPResource]:
306-
"""List all available resources, optionally filtered by prefix."""
307-
prefix = None
308-
if request and request.params:
309-
prefix = request.params.prefix
310-
resources = self._resource_manager.list_resources(prefix=prefix)
306+
"""List all available resources, optionally filtered by URI paths."""
307+
uri_paths = None
308+
if request and request.params and request.params.filters:
309+
uri_paths = request.params.filters.uri_paths
310+
resources = self._resource_manager.list_resources(uri_paths=uri_paths)
311311
return [
312312
MCPResource(
313313
uri=resource.uri,
@@ -322,11 +322,11 @@ async def list_resources(self, request: types.ListResourcesRequest | None = None
322322
async def list_resource_templates(
323323
self, request: types.ListResourceTemplatesRequest | None = None
324324
) -> list[MCPResourceTemplate]:
325-
"""List all available resource templates, optionally filtered by prefix."""
326-
prefix = None
327-
if request and request.params:
328-
prefix = request.params.prefix
329-
templates = self._resource_manager.list_templates(prefix=prefix)
325+
"""List all available resource templates, optionally filtered by URI paths."""
326+
uri_paths = None
327+
if request and request.params and request.params.filters:
328+
uri_paths = request.params.filters.uri_paths
329+
templates = self._resource_manager.list_templates(uri_paths=uri_paths)
330330
return [
331331
MCPResourceTemplate(
332332
uriTemplate=template.uri_template,
@@ -970,11 +970,11 @@ def streamable_http_app(self) -> Starlette:
970970
)
971971

972972
async def list_prompts(self, request: types.ListPromptsRequest | None = None) -> list[MCPPrompt]:
973-
"""List all available prompts, optionally filtered by prefix."""
974-
prefix = None
975-
if request and request.params:
976-
prefix = request.params.prefix
977-
prompts = self._prompt_manager.list_prompts(prefix=prefix)
973+
"""List all available prompts, optionally filtered by URI paths."""
974+
uri_paths = None
975+
if request and request.params and request.params.filters:
976+
uri_paths = request.params.filters.uri_paths
977+
prompts = self._prompt_manager.list_prompts(uri_paths=uri_paths)
978978
return [
979979
MCPPrompt(
980980
name=prompt.name,

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from mcp.server.fastmcp.exceptions import ToolError
77
from mcp.server.fastmcp.tools.base import Tool
8-
from mcp.server.fastmcp.uri_utils import filter_by_prefix, normalize_to_tool_uri
8+
from mcp.server.fastmcp.uri_utils import filter_by_uri_paths, normalize_to_tool_uri
99
from mcp.server.fastmcp.utilities.logging import get_logger
1010
from mcp.shared.context import LifespanContextT, RequestT
1111
from mcp.types import ToolAnnotations
@@ -44,11 +44,11 @@ def get_tool(self, name: str) -> Tool | None:
4444
uri = self._normalize_to_uri(name)
4545
return self._tools.get(uri)
4646

47-
def list_tools(self, prefix: str | None = None) -> list[Tool]:
48-
"""List all registered tools, optionally filtered by URI prefix."""
47+
def list_tools(self, uri_paths: list[str] | None = None) -> list[Tool]:
48+
"""List all registered tools, optionally filtered by URI paths."""
4949
tools = list(self._tools.values())
50-
tools = filter_by_prefix(tools, prefix, lambda t: t.uri)
51-
logger.debug("Listing tools", extra={"count": len(tools), "prefix": prefix})
50+
tools = filter_by_uri_paths(tools, uri_paths, lambda t: t.uri)
51+
logger.debug("Listing tools", extra={"count": len(tools), "uri_paths": uri_paths})
5252
return tools
5353

5454
def add_tool(

src/mcp/server/fastmcp/uri_utils.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,35 @@ def normalize_to_prompt_uri(name_or_uri: str) -> str:
3535
return normalize_to_uri(name_or_uri, PROMPT_SCHEME)
3636

3737

38-
def filter_by_prefix(items: list[T], prefix: str | None, uri_getter: Callable[[T], AnyUrl | str]) -> list[T]:
39-
"""Filter items by URI prefix.
38+
def filter_by_uri_paths(
39+
items: list[T], uri_paths: list[str] | None, uri_getter: Callable[[T], AnyUrl | str]
40+
) -> list[T]:
41+
"""Filter items by multiple URI path prefixes.
4042
4143
Args:
4244
items: List of items to filter
43-
prefix: Optional prefix to filter by. If None, returns all items.
45+
uri_paths: Optional list of URI path prefixes to filter by. If None or empty, returns all items.
4446
uri_getter: Function to extract URI from an item
4547
4648
Returns:
47-
Filtered list of items
49+
Filtered list of items matching any of the provided prefixes
4850
"""
49-
if not prefix:
51+
if not uri_paths:
5052
return items
5153

52-
# Filter items where the URI starts with the prefix
54+
# Filter items where the URI matches any of the prefixes
5355
filtered: list[T] = []
5456
for item in items:
5557
uri = str(uri_getter(item))
56-
if uri.startswith(prefix):
57-
# If prefix ends with a separator, we already have a proper boundary
58-
if prefix.endswith(("/", "?", "#")):
59-
filtered.append(item)
60-
# Otherwise check if it's an exact match or if the next character is a separator
61-
elif len(uri) == len(prefix) or uri[len(prefix)] in ("/", "?", "#"):
62-
filtered.append(item)
58+
for prefix in uri_paths:
59+
if uri.startswith(prefix):
60+
# If prefix ends with a separator, we already have a proper boundary
61+
if prefix.endswith(("/", "?", "#")):
62+
filtered.append(item)
63+
break
64+
# Otherwise check if it's an exact match or if the next character is a separator
65+
elif len(uri) == len(prefix) or uri[len(prefix)] in ("/", "?", "#"):
66+
filtered.append(item)
67+
break
6368

6469
return filtered

src/mcp/types.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,16 @@ class Meta(BaseModel):
6060
meta: Meta | None = Field(alias="_meta", default=None)
6161

6262

63+
class ListFilters(BaseModel):
64+
"""Filters for list operations."""
65+
66+
uri_paths: list[str] | None = None
67+
"""Optional list of absolute URI path prefixes to filter results."""
68+
69+
6370
class ListRequestParams(RequestParams):
64-
prefix: str | None = None
65-
"""Optional prefix to filter results by URI."""
71+
filters: ListFilters | None = None
72+
"""Optional filters to apply to the list results."""
6673

6774
cursor: Cursor | None = None
6875
"""

tests/server/fastmcp/prompts/test_manager.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,38 +104,45 @@ def question_age() -> str:
104104
all_prompts = manager.list_prompts()
105105
assert len(all_prompts) == 4
106106

107-
# Test prefix filtering - greeting prompts
108-
greeting_prompts = manager.list_prompts(prefix=f"{PROMPT_SCHEME}/greeting/")
107+
# Test uri_paths filtering - greeting prompts
108+
greeting_prompts = manager.list_prompts(uri_paths=[f"{PROMPT_SCHEME}/greeting/"])
109109
assert len(greeting_prompts) == 2
110110
assert all(str(p.uri).startswith(f"{PROMPT_SCHEME}/greeting/") for p in greeting_prompts)
111111
assert hello_prompt in greeting_prompts
112112
assert goodbye_prompt in greeting_prompts
113113

114-
# Test prefix filtering - question prompts
115-
question_prompts = manager.list_prompts(prefix=f"{PROMPT_SCHEME}/question/")
114+
# Test uri_paths filtering - question prompts
115+
question_prompts = manager.list_prompts(uri_paths=[f"{PROMPT_SCHEME}/question/"])
116116
assert len(question_prompts) == 2
117117
assert all(str(p.uri).startswith(f"{PROMPT_SCHEME}/question/") for p in question_prompts)
118118
assert name_prompt in question_prompts
119119
assert age_prompt in question_prompts
120120

121121
# Test exact URI match
122-
hello_prompts = manager.list_prompts(prefix=f"{PROMPT_SCHEME}/greeting/hello")
122+
hello_prompts = manager.list_prompts(uri_paths=[f"{PROMPT_SCHEME}/greeting/hello"])
123123
assert len(hello_prompts) == 1
124124
assert hello_prompts[0] == hello_prompt
125125

126126
# Test partial prefix doesn't match
127-
no_partial = manager.list_prompts(prefix=f"{PROMPT_SCHEME}/greeting/h")
127+
no_partial = manager.list_prompts(uri_paths=[f"{PROMPT_SCHEME}/greeting/h"])
128128
assert len(no_partial) == 0 # Won't match because next char is 'e' not a separator
129129

130130
# Test no matches
131-
no_matches = manager.list_prompts(prefix=f"{PROMPT_SCHEME}/nonexistent")
131+
no_matches = manager.list_prompts(uri_paths=[f"{PROMPT_SCHEME}/nonexistent"])
132132
assert len(no_matches) == 0
133133

134134
# Test with trailing slash
135-
greeting_prompts_slash = manager.list_prompts(prefix=f"{PROMPT_SCHEME}/greeting/")
135+
greeting_prompts_slash = manager.list_prompts(uri_paths=[f"{PROMPT_SCHEME}/greeting/"])
136136
assert len(greeting_prompts_slash) == 2
137137
assert greeting_prompts_slash == greeting_prompts
138138

139+
# Test multiple uri_paths
140+
greeting_and_question = manager.list_prompts(
141+
uri_paths=[f"{PROMPT_SCHEME}/greeting/", f"{PROMPT_SCHEME}/question/"]
142+
)
143+
assert len(greeting_and_question) == 4
144+
assert all(p in greeting_and_question for p in all_prompts)
145+
139146
@pytest.mark.anyio
140147
async def test_render_prompt(self):
141148
"""Test rendering a prompt."""

0 commit comments

Comments
 (0)