Skip to content

Commit e596736

Browse files
XInke YIXInke YI
authored andcommitted
feat: support setting title and description for server
1 parent 8ac0cab commit e596736

File tree

7 files changed

+112
-0
lines changed

7 files changed

+112
-0
lines changed

src/mcp/server/fastmcp/server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ def __init__( # noqa: PLR0913
150150
instructions: str | None = None,
151151
website_url: str | None = None,
152152
icons: list[Icon] | None = None,
153+
title: str | None = None,
154+
description: str | None = None,
153155
auth_server_provider: (OAuthAuthorizationServerProvider[Any, Any, Any] | None) = None,
154156
token_verifier: TokenVerifier | None = None,
155157
event_store: EventStore | None = None,
@@ -207,6 +209,8 @@ def __init__( # noqa: PLR0913
207209
instructions=instructions,
208210
website_url=website_url,
209211
icons=icons,
212+
title=title,
213+
description=description,
210214
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an FastMCP and Server.
211215
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
212216
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore

src/mcp/server/lowlevel/server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ def __init__(
142142
instructions: str | None = None,
143143
website_url: str | None = None,
144144
icons: list[types.Icon] | None = None,
145+
title: str | None = None,
146+
description: str | None = None,
145147
lifespan: Callable[
146148
[Server[LifespanResultT, RequestT]],
147149
AbstractAsyncContextManager[LifespanResultT],
@@ -152,6 +154,8 @@ def __init__(
152154
self.instructions = instructions
153155
self.website_url = website_url
154156
self.icons = icons
157+
self.title = title
158+
self.description = description
155159
self.lifespan = lifespan
156160
self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = {
157161
types.PingRequest: _ping_handler,
@@ -186,6 +190,8 @@ def pkg_version(package: str) -> str:
186190
experimental_capabilities or {},
187191
),
188192
instructions=self.instructions,
193+
title=self.title,
194+
description=self.description,
189195
website_url=self.website_url,
190196
icons=self.icons,
191197
)

src/mcp/server/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,7 @@ class InitializationOptions(BaseModel):
1616
server_version: str
1717
capabilities: ServerCapabilities
1818
instructions: str | None = None
19+
title: str | None = None
20+
description: str | None = None
1921
website_url: str | None = None
2022
icons: list[Icon] | None = None

src/mcp/server/session.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ async def _received_request(self, responder: RequestResponder[types.ClientReques
177177
serverInfo=types.Implementation(
178178
name=self._init_options.server_name,
179179
version=self._init_options.server_version,
180+
title=self._init_options.title,
181+
description=self._init_options.description,
180182
websiteUrl=self._init_options.website_url,
181183
icons=self._init_options.icons,
182184
),

src/mcp/types.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,15 @@ class Implementation(BaseMetadata):
265265

266266
version: str
267267

268+
description: str | None = None
269+
"""
270+
An optional human-readable description of what this implementation does.
271+
272+
This can be used by clients or servers to provide context about their purpose
273+
and capabilities. For example, a server might describe the types of resources
274+
or tools it provides, while a client might describe its intended use case.
275+
"""
276+
268277
websiteUrl: str | None = None
269278
"""An optional URL of the website for this implementation."""
270279

tests/server/fastmcp/test_server.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ContentBlock,
2424
EmbeddedResource,
2525
ImageContent,
26+
InitializeResult,
2627
TextContent,
2728
TextResourceContents,
2829
)
@@ -38,6 +39,39 @@ async def test_create_server(self):
3839
assert mcp.name == "FastMCP"
3940
assert mcp.instructions == "Server instructions"
4041

42+
@pytest.mark.anyio
43+
async def test_server_with_title_and_description(self):
44+
"""Test that FastMCP server title and description are passed through to serverInfo."""
45+
mcp = FastMCP(
46+
name="test-fastmcp-server",
47+
title="Test FastMCP Server Title",
48+
description="A test server that demonstrates title and description support.",
49+
)
50+
51+
assert mcp.name == "test-fastmcp-server"
52+
assert mcp.instructions == "Test FastMCP Server Title"
53+
54+
async with client_session(mcp._mcp_server) as client_session_instance:
55+
result = await client_session_instance.initialize()
56+
57+
assert isinstance(result, InitializeResult)
58+
assert result.serverInfo.name == "test-fastmcp-server"
59+
assert result.serverInfo.title == "Test FastMCP Server Title"
60+
assert result.serverInfo.description == "A test server that demonstrates title and description support."
61+
62+
@pytest.mark.anyio
63+
async def test_server_without_title_and_description(self):
64+
"""Test that FastMCP server works correctly when title and description are not provided."""
65+
mcp = FastMCP(name="test-fastmcp-server")
66+
67+
async with client_session(mcp._mcp_server) as client_session_instance:
68+
result = await client_session_instance.initialize()
69+
70+
assert isinstance(result, InitializeResult)
71+
assert result.serverInfo.name == "test-fastmcp-server"
72+
assert result.serverInfo.title is None
73+
assert result.serverInfo.description is None
74+
4175
@pytest.mark.anyio
4276
async def test_normalize_path(self):
4377
"""Test path normalization for mount paths."""

tests/server/test_session.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,61 @@ async def run_server():
8282

8383
assert received_initialized
8484

85+
@pytest.mark.anyio
86+
async def test_server_session_initialize_with_title_and_description():
87+
"""Test that server_title and server_description are passed through to serverInfo."""
88+
server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)
89+
client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)
90+
91+
async def message_handler(
92+
message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
93+
) -> None:
94+
if isinstance(message, Exception):
95+
raise message
96+
97+
async def run_server():
98+
async with ServerSession(
99+
client_to_server_receive,
100+
server_to_client_send,
101+
InitializationOptions(
102+
server_name="test-server",
103+
server_version="1.0.0",
104+
title="Test Server Title",
105+
description="A description of what this server does.",
106+
capabilities=ServerCapabilities(),
107+
),
108+
) as server_session:
109+
async for message in server_session.incoming_messages: # pragma: no branch
110+
if isinstance(message, Exception): # pragma: no cover
111+
raise message
112+
113+
if isinstance(message, ClientNotification) and isinstance(
114+
message.root, InitializedNotification
115+
): # pragma: no branch
116+
return
117+
118+
result: types.InitializeResult | None = None
119+
try:
120+
async with (
121+
ClientSession(
122+
server_to_client_receive,
123+
client_to_server_send,
124+
message_handler=message_handler,
125+
) as client_session,
126+
anyio.create_task_group() as tg,
127+
):
128+
tg.start_soon(run_server)
129+
130+
result = await client_session.initialize()
131+
except anyio.ClosedResourceError: # pragma: no cover
132+
pass
133+
134+
assert result is not None
135+
assert result.serverInfo.name == "test-server"
136+
assert result.serverInfo.title == "Test Server Title"
137+
assert result.serverInfo.version == "1.0.0"
138+
assert result.serverInfo.description == "A description of what this server does."
139+
85140

86141
@pytest.mark.anyio
87142
async def test_server_capabilities():

0 commit comments

Comments
 (0)