Skip to content

Commit 6ed58df

Browse files
author
Tapan Chugh
committed
fix: Update prompt manager to support prefix filtering and URI-based storage
- Add prefix parameter to PromptManager.list_prompts() with path prefix behavior - Add _normalize_to_uri() method to convert names to prompt:// URIs - Update add_prompt() to use URIs as keys for consistent storage - Update get_prompt() to accept both names and URIs - Update FastMCP server to pass request object to lowlevel handlers - Fix test expectation for new path prefix behavior in list_templates This completes the implementation of consistent prefix-based filtering across all list operations (resources, templates, tools, and prompts).
1 parent 777b670 commit 6ed58df

File tree

4 files changed

+33
-19
lines changed

4 files changed

+33
-19
lines changed

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

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,43 @@ def __init__(self, warn_on_duplicate_prompts: bool = True):
1515
self._prompts: dict[str, Prompt] = {}
1616
self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
1717

18-
def get_prompt(self, name: str) -> Prompt | None:
19-
"""Get prompt by name."""
20-
return self._prompts.get(name)
18+
def _normalize_to_uri(self, name_or_uri: str) -> str:
19+
"""Convert name to URI if needed."""
20+
if name_or_uri.startswith("prompt://"):
21+
return name_or_uri
22+
return f"prompt://{name_or_uri}"
2123

22-
def list_prompts(self) -> list[Prompt]:
23-
"""List all registered prompts."""
24-
return list(self._prompts.values())
24+
def get_prompt(self, name: str) -> Prompt | None:
25+
"""Get prompt by name or URI."""
26+
uri = self._normalize_to_uri(name)
27+
return self._prompts.get(uri)
28+
29+
def list_prompts(self, prefix: str | None = None) -> list[Prompt]:
30+
"""List all registered prompts, optionally filtered by URI prefix."""
31+
prompts = list(self._prompts.values())
32+
if prefix:
33+
# Ensure prefix ends with / for proper path matching
34+
if not prefix.endswith("/"):
35+
prefix = prefix + "/"
36+
prompts = [p for p in prompts if str(p.uri).startswith(prefix)]
37+
logger.debug("Listing prompts", extra={"count": len(prompts), "prefix": prefix})
38+
return prompts
2539

2640
def add_prompt(
2741
self,
2842
prompt: Prompt,
2943
) -> Prompt:
3044
"""Add a prompt to the manager."""
31-
45+
logger.debug(f"Adding prompt: {prompt.name} with URI: {prompt.uri}")
46+
3247
# Check for duplicates
33-
existing = self._prompts.get(prompt.name)
48+
existing = self._prompts.get(prompt.uri)
3449
if existing:
3550
if self.warn_on_duplicate_prompts:
36-
logger.warning(f"Prompt already exists: {prompt.name}")
51+
logger.warning(f"Prompt already exists: {prompt.uri}")
3752
return existing
3853

39-
self._prompts[prompt.name] = prompt
54+
self._prompts[prompt.uri] = prompt
4055
return prompt
4156

4257
async def render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> list[Message]:

src/mcp/server/fastmcp/server.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,10 @@ 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, request: types.ListToolsRequest) -> list[MCPTool]:
270+
async def list_tools(self, request: types.ListToolsRequest | None = None) -> list[MCPTool]:
271271
"""List all available tools, optionally filtered by prefix."""
272272
prefix = None
273-
if request.params:
273+
if request and request.params:
274274
prefix = request.params.prefix
275275
tools = self._tool_manager.list_tools(prefix=prefix)
276276
return [
@@ -969,10 +969,10 @@ def streamable_http_app(self) -> Starlette:
969969
lifespan=lambda app: self.session_manager.run(),
970970
)
971971

972-
async def list_prompts(self, request: types.ListPromptsRequest) -> list[MCPPrompt]:
972+
async def list_prompts(self, request: types.ListPromptsRequest | None = None) -> list[MCPPrompt]:
973973
"""List all available prompts, optionally filtered by prefix."""
974974
prefix = None
975-
if request.params:
975+
if request and request.params:
976976
prefix = request.params.prefix
977977
prompts = self._prompt_manager.list_prompts(prefix=prefix)
978978
return [

src/mcp/server/lowlevel/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ async def _get_cached_tool_definition(self, tool_name: str) -> types.Tool | None
415415
if tool_name not in self._tool_cache:
416416
if types.ListToolsRequest in self.request_handlers:
417417
logger.debug("Tool cache miss for %s, refreshing cache", tool_name)
418-
await self.request_handlers[types.ListToolsRequest](None)
418+
await self.request_handlers[types.ListToolsRequest](types.ListToolsRequest(method="tools/list"))
419419

420420
tool = self._tool_cache.get(tool_name)
421421
if tool is None:

tests/server/fastmcp/resources/test_resource_manager.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,11 +219,10 @@ def product_func(product_id: str) -> str:
219219
assert len(user_123_templates) == 1
220220
assert template2 in user_123_templates # users/{user_id}/posts/{post_id} matches
221221

222-
# Without trailing slash, both match
222+
# Without trailing slash, it gets added automatically so only posts template matches
223223
user_123_no_slash = manager.list_templates(prefix="http://api.com/users/123")
224-
assert len(user_123_no_slash) == 2
225-
assert template1 in user_123_no_slash
226-
assert template2 in user_123_no_slash
224+
assert len(user_123_no_slash) == 1
225+
assert template2 in user_123_no_slash # Only posts template has path after users/123/
227226

228227
# Test product prefix
229228
product_templates = manager.list_templates(prefix="http://api.com/products/")

0 commit comments

Comments
 (0)