Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/mcp/server/lowlevel/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ def __init__(
[ServerRequestContext[LifespanResultT], types.RequestParams | None],
Awaitable[types.EmptyResult],
] = _ping_handler,
capability_filter: Callable[[types.ServerCapabilities], types.ServerCapabilities] | None = None,
# Notification handlers
on_roots_list_changed: Callable[
[ServerRequestContext[LifespanResultT], types.NotificationParams | None],
Expand All @@ -198,6 +199,7 @@ def __init__(
str, Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[None]]
] = {}
self._experimental_handlers: ExperimentalHandlers[LifespanResultT] | None = None
self._capability_filter = capability_filter
self._session_manager: StreamableHTTPSessionManager | None = None
logger.debug("Initializing server %r", name)

Expand Down Expand Up @@ -325,6 +327,8 @@ def get_capabilities(
)
if self._experimental_handlers:
self._experimental_handlers.update_capabilities(capabilities)
if self._capability_filter is not None:
capabilities = self._capability_filter(capabilities)
return capabilities

@property
Expand Down
12 changes: 12 additions & 0 deletions src/mcp/server/mcpserver/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
PaginatedRequestParams,
ReadResourceRequestParams,
ReadResourceResult,
ServerCapabilities,
TextContent,
TextResourceContents,
ToolAnnotations,
Expand Down Expand Up @@ -167,6 +168,16 @@ def __init__(
resources=resources, warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources
)
self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)

def _filter_capabilities(caps: ServerCapabilities) -> ServerCapabilities:
if not self._tool_manager.list_tools():
caps.tools = None
if not (self._resource_manager.list_resources() or self._resource_manager.list_templates()):
caps.resources = None
if not self._prompt_manager.list_prompts():
caps.prompts = None
return caps

self._lowlevel_server = Server(
name=name or "mcp-server",
title=title,
Expand All @@ -182,6 +193,7 @@ def __init__(
on_list_resource_templates=self._handle_list_resource_templates,
on_list_prompts=self._handle_list_prompts,
on_get_prompt=self._handle_get_prompt,
capability_filter=_filter_capabilities,
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an MCPServer and Server.
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
Expand Down
99 changes: 99 additions & 0 deletions tests/server/mcpserver/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1516,3 +1516,102 @@ async def test_report_progress_passes_related_request_id():
message="halfway",
related_request_id="req-abc-123",
)


# ---------------------------------------------------------------------------
# Capability filtering: MCPServer only advertises capabilities for primitives
# that are actually registered, per the MCP schema spec.
# ---------------------------------------------------------------------------


def _get_caps(mcp: MCPServer) -> Any:
"""Return the ServerCapabilities advertised by this MCPServer at run time."""
return mcp._lowlevel_server.create_initialization_options().capabilities # type: ignore[reportPrivateUsage]


def test_capabilities_empty_server():
mcp = MCPServer("test")
caps = _get_caps(mcp)
assert caps.tools is None
assert caps.resources is None
assert caps.prompts is None


def test_capabilities_tool_only():
mcp = MCPServer("test")

@mcp.tool()
def echo(text: str) -> str:
return text

assert echo("hi") == "hi"
caps = _get_caps(mcp)
assert caps.tools is not None
assert caps.resources is None
assert caps.prompts is None


def test_capabilities_resource_only():
mcp = MCPServer("test")

@mcp.resource("resource://data")
def get_data() -> str:
return "hello"

assert get_data() == "hello"
caps = _get_caps(mcp)
assert caps.tools is None
assert caps.resources is not None
assert caps.prompts is None


def test_capabilities_resource_template_only():
mcp = MCPServer("test")

@mcp.resource("resource://{city}/weather")
def get_weather(city: str) -> str:
return f"weather for {city}"

assert get_weather("london") == "weather for london"
caps = _get_caps(mcp)
assert caps.tools is None
assert caps.resources is not None
assert caps.prompts is None


def test_capabilities_prompt_only():
mcp = MCPServer("test")

@mcp.prompt()
def greet(name: str) -> str:
return f"Hello, {name}!"

assert greet("world") == "Hello, world!"
caps = _get_caps(mcp)
assert caps.tools is None
assert caps.resources is None
assert caps.prompts is not None


def test_capabilities_all_registered():
mcp = MCPServer("test")

@mcp.tool()
def echo(text: str) -> str:
return text

@mcp.resource("resource://data")
def get_data() -> str:
return "hello"

@mcp.prompt()
def greet(name: str) -> str:
return f"Hello, {name}!"

assert echo("x") == "x"
assert get_data() == "hello"
assert greet("y") == "Hello, y!"
caps = _get_caps(mcp)
assert caps.tools is not None
assert caps.resources is not None
assert caps.prompts is not None
Loading