Skip to content

Commit c6247a6

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

File tree

7 files changed

+121
-0
lines changed

7 files changed

+121
-0
lines changed

src/mcp/server/fastmcp/server.py

Lines changed: 12 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
@@ -249,6 +253,14 @@ def name(self) -> str:
249253
def instructions(self) -> str | None:
250254
return self._mcp_server.instructions
251255

256+
@property
257+
def title(self) -> str | None:
258+
return self._mcp_server.title
259+
260+
@property
261+
def description(self) -> str | None:
262+
return self._mcp_server.description
263+
252264
@property
253265
def website_url(self) -> str | None:
254266
return self._mcp_server.website_url

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.title == "Test FastMCP Server Title"
52+
assert mcp.description == "A test server that demonstrates title and description support."
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: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,62 @@ async def run_server():
8383
assert received_initialized
8484

8585

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

0 commit comments

Comments
 (0)