Skip to content

Commit 777b670

Browse files
author
Tapan Chugh
committed
feat: Implement consistent prefix-based filtering for all list operations
- Rename PaginatedRequestParams to ListRequestParams and add prefix field - Rename PaginatedRequest to ListRequest for semantic clarity - Rename PaginatedResult to ListResult for consistency - Remove redundant ListResourcesRequestParams and ListResourceTemplatesRequestParams - Add prefix filtering to tools and prompts list operations - Ensure all prefixes are treated as path prefixes ending with / - Update lowlevel server handlers to accept full request objects - Update FastMCP server to extract prefix from request params This unifies the list operation API across resources, templates, tools, and prompts. All list methods now support optional prefix filtering using URI path prefixes (e.g., 'tool://category/' to match only tools under that path).
1 parent a9635c3 commit 777b670

File tree

7 files changed

+63
-52
lines changed

7 files changed

+63
-52
lines changed

src/mcp/client/session.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ async def list_prompts(self, cursor: str | None = None) -> types.ListPromptsResu
348348
types.ClientRequest(
349349
types.ListPromptsRequest(
350350
method="prompts/list",
351-
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
351+
params=types.ListRequestParams(cursor=cursor) if cursor is not None else None,
352352
)
353353
),
354354
types.ListPromptsResult,
@@ -397,7 +397,7 @@ async def list_tools(self, cursor: str | None = None) -> types.ListToolsResult:
397397
types.ClientRequest(
398398
types.ListToolsRequest(
399399
method="tools/list",
400-
params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
400+
params=types.ListRequestParams(cursor=cursor) if cursor is not None else None,
401401
)
402402
),
403403
types.ListToolsResult,

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ def get_prompt(self, name: str) -> Prompt | None:
3535
uri = self._normalize_to_uri(name)
3636
return self._prompts.get(uri)
3737

38-
def list_prompts(self) -> list[Prompt]:
39-
"""List all registered prompts."""
40-
return list(self._prompts.values())
38+
def list_prompts(self, prefix: str | None = None) -> list[Prompt]:
39+
"""List all registered prompts, optionally filtered by URI prefix."""
40+
prompts = list(self._prompts.values())
41+
if prefix:
42+
# Ensure prefix ends with / for proper path matching
43+
if not prefix.endswith("/"):
44+
prefix = prefix + "/"
45+
prompts = [p for p in prompts if str(p.uri).startswith(prefix)]
46+
logger.debug("Listing prompts", extra={"count": len(prompts), "prefix": prefix})
47+
return prompts

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ def list_resources(self, prefix: str | None = None) -> list[Resource]:
9090
"""List all registered resources, optionally filtered by URI prefix."""
9191
resources = list(self._resources.values())
9292
if prefix:
93+
# Ensure prefix ends with / for proper path matching
94+
if not prefix.endswith("/"):
95+
prefix = prefix + "/"
9396
resources = [r for r in resources if str(r.uri).startswith(prefix)]
9497
logger.debug("Listing resources", extra={"count": len(resources), "prefix": prefix})
9598
return resources
@@ -98,6 +101,9 @@ def list_templates(self, prefix: str | None = None) -> list[ResourceTemplate]:
98101
"""List all registered templates, optionally filtered by URI template prefix."""
99102
templates = list(self._templates.values())
100103
if prefix:
104+
# Ensure prefix ends with / for proper path matching
105+
if not prefix.endswith("/"):
106+
prefix = prefix + "/"
101107
templates = [t for t in templates if t.matches_prefix(prefix)]
102108
logger.debug("Listing templates", extra={"count": len(templates), "prefix": prefix})
103109
return templates

src/mcp/server/fastmcp/server.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,12 @@ def _setup_handlers(self) -> None:
267267
self._mcp_server.get_prompt()(self.get_prompt)
268268
self._mcp_server.list_resource_templates()(self.list_resource_templates)
269269

270-
async def list_tools(self) -> list[MCPTool]:
271-
"""List all available tools."""
272-
tools = self._tool_manager.list_tools()
270+
async def list_tools(self, request: types.ListToolsRequest) -> list[MCPTool]:
271+
"""List all available tools, optionally filtered by prefix."""
272+
prefix = None
273+
if request.params:
274+
prefix = request.params.prefix
275+
tools = self._tool_manager.list_tools(prefix=prefix)
273276
return [
274277
MCPTool(
275278
name=info.name,
@@ -966,9 +969,12 @@ def streamable_http_app(self) -> Starlette:
966969
lifespan=lambda app: self.session_manager.run(),
967970
)
968971

969-
async def list_prompts(self) -> list[MCPPrompt]:
970-
"""List all available prompts."""
971-
prompts = self._prompt_manager.list_prompts()
972+
async def list_prompts(self, request: types.ListPromptsRequest) -> list[MCPPrompt]:
973+
"""List all available prompts, optionally filtered by prefix."""
974+
prefix = None
975+
if request.params:
976+
prefix = request.params.prefix
977+
prompts = self._prompt_manager.list_prompts(prefix=prefix)
972978
return [
973979
MCPPrompt(
974980
name=prompt.name,

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,16 @@ def get_tool(self, name: str) -> Tool | None:
4545
uri = self._normalize_to_uri(name)
4646
return self._tools.get(uri)
4747

48-
def list_tools(self) -> list[Tool]:
49-
"""List all registered tools."""
50-
return list(self._tools.values())
48+
def list_tools(self, prefix: str | None = None) -> list[Tool]:
49+
"""List all registered tools, optionally filtered by URI prefix."""
50+
tools = list(self._tools.values())
51+
if prefix:
52+
# Ensure prefix ends with / for proper path matching
53+
if not prefix.endswith("/"):
54+
prefix = prefix + "/"
55+
tools = [t for t in tools if str(t.uri).startswith(prefix)]
56+
logger.debug("Listing tools", extra={"count": len(tools), "prefix": prefix})
57+
return tools
5158

5259
def add_tool(
5360
self,

src/mcp/server/lowlevel/server.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,11 +231,11 @@ def request_context(
231231
return request_ctx.get()
232232

233233
def list_prompts(self):
234-
def decorator(func: Callable[[], Awaitable[list[types.Prompt]]]):
234+
def decorator(func: Callable[[types.ListPromptsRequest], Awaitable[list[types.Prompt]]]):
235235
logger.debug("Registering handler for PromptListRequest")
236236

237-
async def handler(_: Any):
238-
prompts = await func()
237+
async def handler(request: types.ListPromptsRequest):
238+
prompts = await func(request)
239239
return types.ServerResult(types.ListPromptsResult(prompts=prompts))
240240

241241
self.request_handlers[types.ListPromptsRequest] = handler
@@ -382,11 +382,11 @@ async def handler(req: types.UnsubscribeRequest):
382382
return decorator
383383

384384
def list_tools(self):
385-
def decorator(func: Callable[[], Awaitable[list[types.Tool]]]):
385+
def decorator(func: Callable[[types.ListToolsRequest], Awaitable[list[types.Tool]]]):
386386
logger.debug("Registering handler for ListToolsRequest")
387387

388-
async def handler(_: Any):
389-
tools = await func()
388+
async def handler(request: types.ListToolsRequest):
389+
tools = await func(request)
390390
# Refresh the tool cache
391391
self._tool_cache.clear()
392392
for tool in tools:

src/mcp/types.py

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -55,28 +55,17 @@ class Meta(BaseModel):
5555
meta: Meta | None = Field(alias="_meta", default=None)
5656

5757

58-
class PaginatedRequestParams(RequestParams):
58+
class ListRequestParams(RequestParams):
59+
prefix: str | None = None
60+
"""Optional prefix to filter results by URI."""
61+
5962
cursor: Cursor | None = None
6063
"""
6164
An opaque token representing the current pagination position.
6265
If provided, the server should return results starting after this cursor.
6366
"""
6467

6568

66-
class ListResourcesRequestParams(PaginatedRequestParams):
67-
"""Parameters for listing resources with optional prefix filtering."""
68-
69-
prefix: str | None = None
70-
"""Optional prefix to filter resources by URI."""
71-
72-
73-
class ListResourceTemplatesRequestParams(PaginatedRequestParams):
74-
"""Parameters for listing resource templates with optional prefix filtering."""
75-
76-
prefix: str | None = None
77-
"""Optional prefix to filter resource templates by URI template."""
78-
79-
8069
class NotificationParams(BaseModel):
8170
class Meta(BaseModel):
8271
model_config = ConfigDict(extra="allow")
@@ -101,11 +90,11 @@ class Request(BaseModel, Generic[RequestParamsT, MethodT]):
10190
model_config = ConfigDict(extra="allow")
10291

10392

104-
class PaginatedRequest(Request[PaginatedRequestParams | None, MethodT], Generic[MethodT]):
105-
"""Base class for paginated requests,
106-
matching the schema's PaginatedRequest interface."""
93+
class ListRequest(Request[ListRequestParams | None, MethodT], Generic[MethodT]):
94+
"""Base class for list requests,
95+
matching the schema's ListRequest interface."""
10796

108-
params: PaginatedRequestParams | None = None
97+
params: ListRequestParams | None = None
10998

11099

111100
class Notification(BaseModel, Generic[NotificationParamsT, MethodT]):
@@ -127,7 +116,7 @@ class Result(BaseModel):
127116
model_config = ConfigDict(extra="allow")
128117

129118

130-
class PaginatedResult(Result):
119+
class ListResult(Result):
131120
nextCursor: Cursor | None = None
132121
"""
133122
An opaque token representing the pagination position after the last returned result.
@@ -408,11 +397,10 @@ class ProgressNotification(Notification[ProgressNotificationParams, Literal["not
408397
params: ProgressNotificationParams
409398

410399

411-
class ListResourcesRequest(Request[ListResourcesRequestParams | None, Literal["resources/list"]]):
400+
class ListResourcesRequest(ListRequest[Literal["resources/list"]]):
412401
"""Sent from the client to request a list of resources the server has."""
413402

414403
method: Literal["resources/list"]
415-
params: ListResourcesRequestParams | None = None
416404

417405

418406
class Annotations(BaseModel):
@@ -478,22 +466,19 @@ class ResourceTemplate(BaseMetadata):
478466
model_config = ConfigDict(extra="allow")
479467

480468

481-
class ListResourcesResult(PaginatedResult):
469+
class ListResourcesResult(ListResult):
482470
"""The server's response to a resources/list request from the client."""
483471

484472
resources: list[Resource]
485473

486474

487-
class ListResourceTemplatesRequest(
488-
Request[ListResourceTemplatesRequestParams | None, Literal["resources/templates/list"]]
489-
):
475+
class ListResourceTemplatesRequest(ListRequest[Literal["resources/templates/list"]]):
490476
"""Sent from the client to request a list of resource templates the server has."""
491477

492478
method: Literal["resources/templates/list"]
493-
params: ListResourceTemplatesRequestParams | None = None
494479

495480

496-
class ListResourceTemplatesResult(PaginatedResult):
481+
class ListResourceTemplatesResult(ListResult):
497482
"""The server's response to a resources/templates/list request from the client."""
498483

499484
resourceTemplates: list[ResourceTemplate]
@@ -629,7 +614,7 @@ class ResourceUpdatedNotification(
629614
params: ResourceUpdatedNotificationParams
630615

631616

632-
class ListPromptsRequest(PaginatedRequest[Literal["prompts/list"]]):
617+
class ListPromptsRequest(ListRequest[Literal["prompts/list"]]):
633618
"""Sent from the client to request a list of prompts and prompt templates."""
634619

635620
method: Literal["prompts/list"]
@@ -664,7 +649,7 @@ class Prompt(BaseMetadata):
664649
model_config = ConfigDict(extra="allow")
665650

666651

667-
class ListPromptsResult(PaginatedResult):
652+
class ListPromptsResult(ListResult):
668653
"""The server's response to a prompts/list request from the client."""
669654

670655
prompts: list[Prompt]
@@ -814,7 +799,7 @@ class PromptListChangedNotification(
814799
params: NotificationParams | None = None
815800

816801

817-
class ListToolsRequest(PaginatedRequest[Literal["tools/list"]]):
802+
class ListToolsRequest(ListRequest[Literal["tools/list"]]):
818803
"""Sent from the client to request a list of tools the server has."""
819804

820805
method: Literal["tools/list"]
@@ -892,7 +877,7 @@ class Tool(BaseMetadata):
892877
model_config = ConfigDict(extra="allow")
893878

894879

895-
class ListToolsResult(PaginatedResult):
880+
class ListToolsResult(ListResult):
896881
"""The server's response to a tools/list request from the client."""
897882

898883
tools: list[Tool]

0 commit comments

Comments
 (0)