diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 865b8e7e72..e19882823d 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -143,9 +143,12 @@ class FastMCP(Generic[LifespanResultT]): def __init__( # noqa: PLR0913 self, name: str | None = None, + title: str | None = None, + description: str | None = None, instructions: str | None = None, website_url: str | None = None, icons: list[Icon] | None = None, + version: str | None = None, auth_server_provider: (OAuthAuthorizationServerProvider[Any, Any, Any] | None) = None, token_verifier: TokenVerifier | None = None, event_store: EventStore | None = None, @@ -191,9 +194,12 @@ def __init__( # noqa: PLR0913 self._mcp_server = MCPServer( name=name or "FastMCP", + title=title, + description=description, instructions=instructions, website_url=website_url, icons=icons, + version=version, # 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 @@ -231,6 +237,14 @@ def __init__( # noqa: PLR0913 def name(self) -> str: return self._mcp_server.name + @property + def title(self) -> str | None: + return self._mcp_server.title + + @property + def description(self) -> str | None: + return self._mcp_server.description + @property def instructions(self) -> str | None: return self._mcp_server.instructions @@ -243,6 +257,10 @@ def website_url(self) -> str | None: def icons(self) -> list[Icon] | None: return self._mcp_server.icons + @property + def version(self) -> str | None: + return self._mcp_server.version + @property def session_manager(self) -> StreamableHTTPSessionManager: """Get the StreamableHTTP session manager. diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 49d289fb75..2dfb29dd63 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -135,6 +135,8 @@ def __init__( self, name: str, version: str | None = None, + title: str | None = None, + description: str | None = None, instructions: str | None = None, website_url: str | None = None, icons: list[types.Icon] | None = None, @@ -145,6 +147,8 @@ def __init__( ): self.name = name self.version = version + self.title = title + self.description = description self.instructions = instructions self.website_url = website_url self.icons = icons @@ -176,6 +180,8 @@ def pkg_version(package: str) -> str: return InitializationOptions( server_name=self.name, server_version=self.version if self.version else pkg_version("mcp"), + title=self.title, + description=self.description, capabilities=self.get_capabilities( notification_options or NotificationOptions(), experimental_capabilities or {}, diff --git a/src/mcp/server/models.py b/src/mcp/server/models.py index ddf716cb95..eb972e33a5 100644 --- a/src/mcp/server/models.py +++ b/src/mcp/server/models.py @@ -14,6 +14,8 @@ class InitializationOptions(BaseModel): server_name: str server_version: str + title: str | None = None + description: str | None = None capabilities: ServerCapabilities instructions: str | None = None website_url: str | None = None diff --git a/src/mcp/server/session.py b/src/mcp/server/session.py index a1bfadc9fc..8fd407d1b9 100644 --- a/src/mcp/server/session.py +++ b/src/mcp/server/session.py @@ -155,6 +155,8 @@ async def _received_request(self, responder: RequestResponder[types.ClientReques capabilities=self._init_options.capabilities, serverInfo=types.Implementation( name=self._init_options.server_name, + title=self._init_options.title, + description=self._init_options.description, version=self._init_options.server_version, websiteUrl=self._init_options.website_url, icons=self._init_options.icons, diff --git a/src/mcp/types.py b/src/mcp/types.py index 8713227404..54be29c621 100644 --- a/src/mcp/types.py +++ b/src/mcp/types.py @@ -233,6 +233,12 @@ class Implementation(BaseMetadata): version: str + title: str | None = None + """An optional human-readable title for this implementation.""" + + description: str | None = None + """An optional human-readable description of what this implementation does.""" + websiteUrl: str | None = None """An optional URL of the website for this implementation.""" diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index fdbb04694c..b732cfeaf0 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -21,6 +21,7 @@ BlobResourceContents, ContentBlock, EmbeddedResource, + Icon, ImageContent, TextContent, TextResourceContents, @@ -33,9 +34,23 @@ class TestServer: @pytest.mark.anyio async def test_create_server(self): - mcp = FastMCP(instructions="Server instructions") + mcp = FastMCP( + title="FastMCP Server", + description="Server description", + instructions="Server instructions", + website_url="https://example.com/mcp_server", + version="1.0", + icons=[Icon(src="https://example.com/icon.png", mimeType="image/png", sizes=["48x48", "96x96"])], + ) assert mcp.name == "FastMCP" + assert mcp.title == "FastMCP Server" + assert mcp.description == "Server description" assert mcp.instructions == "Server instructions" + assert mcp.website_url == "https://example.com/mcp_server" + assert mcp.version == "1.0" + assert isinstance(mcp.icons, list) + assert len(mcp.icons) == 1 + assert mcp.icons[0].src == "https://example.com/icon.png" @pytest.mark.anyio async def test_normalize_path(self): diff --git a/tests/server/fastmcp/test_title.py b/tests/server/fastmcp/test_title.py index 7cac570123..774f8dd63c 100644 --- a/tests/server/fastmcp/test_title.py +++ b/tests/server/fastmcp/test_title.py @@ -10,6 +10,29 @@ from mcp.types import Prompt, Resource, ResourceTemplate, Tool, ToolAnnotations +@pytest.mark.anyio +async def test_server_name_title_description_version(): + """Test that server title and description are set and retrievable correctly.""" + mcp = FastMCP( + name="TestServer", + title="Test Server Title", + description="This is a test server description.", + version="1.0", + ) + + assert mcp.title == "Test Server Title" + assert mcp.description == "This is a test server description." + assert mcp.version == "1.0" + + # Start server and connect client + async with create_connected_server_and_client_session(mcp._mcp_server) as client: + init_result = await client.initialize() + assert init_result.serverInfo.name == "TestServer" + assert init_result.serverInfo.title == "Test Server Title" + assert init_result.serverInfo.description == "This is a test server description." + assert init_result.serverInfo.version == "1.0" + + @pytest.mark.anyio async def test_tool_title_precedence(): """Test that tool title precedence works correctly: title > annotations.title > name."""