Skip to content

Commit bd255ae

Browse files
committed
Filter MCPServer capabilities by registered primitives
1 parent 3d7b311 commit bd255ae

File tree

3 files changed

+63
-0
lines changed

3 files changed

+63
-0
lines changed

src/mcp/server/lowlevel/server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ def __init__(
169169
Awaitable[types.EmptyResult],
170170
]
171171
| None = None,
172+
capability_filter: Callable[[types.ServerCapabilities], types.ServerCapabilities] | None = None,
172173
on_ping: Callable[
173174
[ServerRequestContext[LifespanResultT], types.RequestParams | None],
174175
Awaitable[types.EmptyResult],
@@ -192,6 +193,7 @@ def __init__(
192193
self.instructions = instructions
193194
self.website_url = website_url
194195
self.icons = icons
196+
self._capability_filter = capability_filter
195197
self.lifespan = lifespan
196198
self._request_handlers: dict[str, Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[Any]]] = {}
197199
self._notification_handlers: dict[
@@ -325,6 +327,8 @@ def get_capabilities(
325327
)
326328
if self._experimental_handlers:
327329
self._experimental_handlers.update_capabilities(capabilities)
330+
if self._capability_filter:
331+
capabilities = self._capability_filter(capabilities)
328332
return capabilities
329333

330334
@property

src/mcp/server/mcpserver/server.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ def __init__(
182182
on_list_resource_templates=self._handle_list_resource_templates,
183183
on_list_prompts=self._handle_list_prompts,
184184
on_get_prompt=self._handle_get_prompt,
185+
capability_filter=self._filter_capabilities,
185186
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an MCPServer and Server.
186187
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
187188
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
@@ -206,6 +207,16 @@ def __init__(
206207
# Configure logging
207208
configure_logging(self.settings.log_level)
208209

210+
def _filter_capabilities(self, capabilities: Any) -> Any:
211+
"""Hide MCPServer capabilities for primitives that have not been registered."""
212+
if not self._tool_manager.list_tools():
213+
capabilities.tools = None
214+
if not self._prompt_manager.list_prompts():
215+
capabilities.prompts = None
216+
if not self._resource_manager.list_resources() and not self._resource_manager.list_templates():
217+
capabilities.resources = None
218+
return capabilities
219+
209220
@property
210221
def name(self) -> str:
211222
return self._lowlevel_server.name

tests/server/mcpserver/test_server.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,54 @@ def test_dependencies(self):
7474
mcp_no_deps = MCPServer("test")
7575
assert mcp_no_deps.dependencies == []
7676

77+
async def test_init_without_primitives_omits_prompt_resource_and_tool_capabilities(self):
78+
mcp = MCPServer("empty")
79+
80+
async with Client(mcp) as client:
81+
capabilities = client.initialize_result.capabilities
82+
assert capabilities.tools is None
83+
assert capabilities.resources is None
84+
assert capabilities.prompts is None
85+
86+
async def test_init_with_tool_only_announces_only_tool_capability(self):
87+
mcp = MCPServer("tool-only")
88+
89+
@mcp.tool()
90+
def echo(text: str) -> str:
91+
return text
92+
93+
async with Client(mcp) as client:
94+
capabilities = client.initialize_result.capabilities
95+
assert capabilities.tools is not None
96+
assert capabilities.resources is None
97+
assert capabilities.prompts is None
98+
99+
async def test_init_with_prompt_only_announces_only_prompt_capability(self):
100+
mcp = MCPServer("prompt-only")
101+
102+
@mcp.prompt()
103+
def greet(name: str) -> str:
104+
return f"Hello {name}"
105+
106+
async with Client(mcp) as client:
107+
capabilities = client.initialize_result.capabilities
108+
assert capabilities.prompts is not None
109+
assert capabilities.tools is None
110+
assert capabilities.resources is None
111+
112+
async def test_init_with_resource_template_announces_resource_capability(self):
113+
mcp = MCPServer("template-only")
114+
115+
@mcp.resource("resource://{city}/weather")
116+
def weather(city: str) -> str:
117+
return f"Weather for {city}"
118+
119+
async with Client(mcp) as client:
120+
capabilities = client.initialize_result.capabilities
121+
assert capabilities.resources is not None
122+
assert capabilities.tools is None
123+
assert capabilities.prompts is None
124+
77125
async def test_sse_app_returns_starlette_app(self):
78126
"""Test that sse_app returns a Starlette application with correct routes."""
79127
mcp = MCPServer("test")

0 commit comments

Comments
 (0)