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
12 changes: 12 additions & 0 deletions src/mcp/server/fastmcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/mcp/server/lowlevel/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
Expand Down
2 changes: 2 additions & 0 deletions src/mcp/server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions src/mcp/server/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down
9 changes: 9 additions & 0 deletions src/mcp/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
34 changes: 34 additions & 0 deletions tests/server/fastmcp/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
ContentBlock,
EmbeddedResource,
ImageContent,
InitializeResult,
TextContent,
TextResourceContents,
)
Expand All @@ -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."""
Expand Down
41 changes: 41 additions & 0 deletions tests/server/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down