diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 51e93b677..93484d34b 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -173,6 +173,8 @@ def __init__( # noqa: PLR0913 lifespan: (Callable[[FastMCP[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None) = None, auth: AuthSettings | None = None, transport_security: TransportSecuritySettings | None = None, + title: str | None = None, + description: str | None = None, ): # Auto-enable DNS rebinding protection for localhost (IPv4 and IPv6) if transport_security is None and host in ("127.0.0.1", "localhost", "::1"): @@ -207,6 +209,8 @@ def __init__( # noqa: PLR0913 instructions=instructions, website_url=website_url, icons=icons, + title=title, + description=description, # TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an FastMCP 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 @@ -249,6 +253,14 @@ def name(self) -> str: def instructions(self) -> str | None: return self._mcp_server.instructions + @property + def title(self) -> str | None: + return self._mcp_server.title + + @property + def description(self) -> str | None: + return self._mcp_server.description + @property def website_url(self) -> str | None: return self._mcp_server.website_url diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 3fc2d497d..363c920b4 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -146,12 +146,16 @@ def __init__( [Server[LifespanResultT, RequestT]], AbstractAsyncContextManager[LifespanResultT], ] = lifespan, + title: str | None = None, + description: str | None = None, ): self.name = name self.version = version self.instructions = instructions self.website_url = website_url self.icons = icons + self.title = title + self.description = description self.lifespan = lifespan self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = { types.PingRequest: _ping_handler, @@ -186,6 +190,8 @@ def pkg_version(package: str) -> str: experimental_capabilities or {}, ), instructions=self.instructions, + title=self.title, + description=self.description, website_url=self.website_url, icons=self.icons, ) diff --git a/src/mcp/server/models.py b/src/mcp/server/models.py index ddf716cb9..fca64e3c9 100644 --- a/src/mcp/server/models.py +++ b/src/mcp/server/models.py @@ -16,5 +16,7 @@ class InitializationOptions(BaseModel): server_version: str capabilities: ServerCapabilities instructions: str | None = None + title: str | None = None + description: str | None = None website_url: str | None = None icons: list[Icon] | None = None diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index 8f0baa3e9..bea9c2020 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -177,6 +177,8 @@ async def _received_request(self, responder: RequestResponder[types.ClientReques serverInfo=types.Implementation( name=self._init_options.server_name, version=self._init_options.server_version, + title=self._init_options.title, + description=self._init_options.description, websiteUrl=self._init_options.website_url, icons=self._init_options.icons, ), diff --git a/src/mcp/types.py b/src/mcp/types.py index 654c00660..186c58378 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -273,6 +273,15 @@ class Implementation(BaseMetadata): model_config = ConfigDict(extra="allow") + description: str | None = None + """ + An optional human-readable description of what this implementation does. + + This can be used by clients or servers to provide context about their purpose + and capabilities. For example, a server might describe the types of resources + or tools it provides, while a client might describe its intended use case. + """ + class RootsCapability(BaseModel): """Capability for root operations.""" diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index 3935f3bd1..726ea9726 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -23,6 +23,7 @@ ContentBlock, EmbeddedResource, ImageContent, + InitializeResult, TextContent, TextResourceContents, ) @@ -38,6 +39,39 @@ async def test_create_server(self): assert mcp.name == "FastMCP" assert mcp.instructions == "Server instructions" + @pytest.mark.anyio + async def test_server_with_title_and_description(self): + """Test that FastMCP server title and description are passed through to serverInfo.""" + mcp = FastMCP( + name="test-fastmcp-server", + title="Test FastMCP Server Title", + description="A test server that demonstrates title and description support.", + ) + + assert mcp.title == "Test FastMCP Server Title" + assert mcp.description == "A test server that demonstrates title and description support." + + async with client_session(mcp._mcp_server) as client_session_instance: + result = await client_session_instance.initialize() + + assert isinstance(result, InitializeResult) + assert result.serverInfo.name == "test-fastmcp-server" + assert result.serverInfo.title == "Test FastMCP Server Title" + assert result.serverInfo.description == "A test server that demonstrates title and description support." + + @pytest.mark.anyio + async def test_server_without_title_and_description(self): + """Test that FastMCP server works correctly when title and description are not provided.""" + mcp = FastMCP(name="test-fastmcp-server") + + async with client_session(mcp._mcp_server) as client_session_instance: + result = await client_session_instance.initialize() + + assert isinstance(result, InitializeResult) + assert result.serverInfo.name == "test-fastmcp-server" + assert result.serverInfo.title is None + assert result.serverInfo.description is None + @pytest.mark.anyio async def test_normalize_path(self): """Test path normalization for mount paths.""" diff --git a/tests/server/test_session.py b/tests/server/test_session.py index 34f9c6e28..0330fb636 100644 --- a/tests/server/test_session.py +++ b/tests/server/test_session.py @@ -83,6 +83,47 @@ async def run_server(): assert received_initialized +@pytest.mark.anyio +async def test_server_session_initialize_with_title_and_description(): + """Test that server_title and server_description are passed through to serverInfo.""" + server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1) + client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1) + + async def run_server(): + async with ServerSession( + client_to_server_receive, + server_to_client_send, + InitializationOptions( + server_name="test-server", + server_version="1.0.0", + title="Test Server Title", + description="A description of what this server does.", + capabilities=ServerCapabilities(), + ), + ) as _: + # Just run the server without handling incoming messages + # The server will process messages internally + await anyio.sleep(0.1) # Give time for initialization to complete + + result: types.InitializeResult | None = None + async with ( + ClientSession( + server_to_client_receive, + client_to_server_send, + ) as client_session, + anyio.create_task_group() as tg, + ): + tg.start_soon(run_server) + + result = await client_session.initialize() + + assert result is not None + assert result.serverInfo.name == "test-server" + assert result.serverInfo.title == "Test Server Title" + assert result.serverInfo.version == "1.0.0" + assert result.serverInfo.description == "A description of what this server does." + + @pytest.mark.anyio async def test_server_capabilities(): server = Server("test")