Skip to content

Commit fa681a8

Browse files
authored
Merge branch 'main' into FastMCP-and-structured-output
2 parents c676189 + 3863f20 commit fa681a8

File tree

24 files changed

+451
-71
lines changed

24 files changed

+451
-71
lines changed

CONTRIBUTING.md

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,43 @@
22

33
Thank you for your interest in contributing to the MCP Python SDK! This document provides guidelines and instructions for contributing.
44

5+
## Before You Start
6+
7+
We welcome contributions! These guidelines exist to save everyone time, yours included. Following them means your work is more likely to be accepted.
8+
9+
**All pull requests require a corresponding issue.** Unless your change is trivial (typo, docs tweak, broken link), create an issue first. Every merged feature becomes ongoing maintenance, so we need to agree something is worth doing before reviewing code. PRs without a linked issue will be closed.
10+
11+
Having an issue doesn't guarantee acceptance. Wait for maintainer feedback or a `ready for work` label before starting. PRs for issues without buy-in may also be closed.
12+
13+
Use issues to validate your idea before investing time in code. PRs are for execution, not exploration.
14+
15+
### The SDK is Opinionated
16+
17+
Not every contribution will be accepted, even with a working implementation. We prioritize maintainability and consistency over adding capabilities. This is at maintainers' discretion.
18+
19+
### What Needs Discussion
20+
21+
These always require an issue first:
22+
23+
- New public APIs or decorators
24+
- Architectural changes or refactoring
25+
- Changes that touch multiple modules
26+
- Features that might require spec changes (these need a [SEP](https://github.com/modelcontextprotocol/modelcontextprotocol) first)
27+
28+
Bug fixes for clear, reproducible issues are welcome—but still create an issue to track the fix.
29+
30+
### Finding Issues to Work On
31+
32+
| Label | For | Description |
33+
|-------|-----|-------------|
34+
| [`good first issue`](https://github.com/modelcontextprotocol/python-sdk/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) | Newcomers | Can tackle without deep codebase knowledge |
35+
| [`help wanted`](https://github.com/modelcontextprotocol/python-sdk/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) | Experienced contributors | Maintainers probably won't get to this |
36+
| [`ready for work`](https://github.com/modelcontextprotocol/python-sdk/issues?q=is%3Aopen+is%3Aissue+label%3A%22ready+for+work%22) | Maintainers | Triaged and ready for a maintainer to pick up |
37+
38+
Issues labeled `needs confirmation` or `needs maintainer action` are **not** ready for work—wait for maintainer input first.
39+
40+
Before starting, comment on the issue so we can assign it to you. This prevents duplicate effort.
41+
542
## Development Setup
643

744
1. Make sure you have Python 3.10+ installed
@@ -76,13 +113,29 @@ pre-commit run --all-files
76113
- Add type hints to all functions
77114
- Include docstrings for public APIs
78115

79-
## Pull Request Process
116+
## Pull Requests
117+
118+
By the time you open a PR, the "what" and "why" should already be settled in an issue. This keeps reviews focused on implementation.
119+
120+
### Scope
121+
122+
Small PRs get reviewed fast. Large PRs sit in the queue.
123+
124+
A few dozen lines can be reviewed in minutes. Hundreds of lines across many files takes real effort and things slip through. If your change is big, break it into smaller PRs or get alignment from a maintainer first.
125+
126+
### What Gets Rejected
127+
128+
- **No prior discussion**: Features or significant changes without an approved issue
129+
- **Scope creep**: Changes that go beyond what was discussed
130+
- **Misalignment**: Even well-implemented features may be rejected if they don't fit the SDK's direction
131+
- **Overengineering**: Unnecessary complexity for simple problems
132+
133+
### Checklist
80134

81135
1. Update documentation as needed
82136
2. Add tests for new functionality
83137
3. Ensure CI passes
84-
4. Maintainers will review your code
85-
5. Address review feedback
138+
4. Address review feedback
86139

87140
## Code of Conduct
88141

examples/servers/simple-auth/mcp_simple_auth/py.typed

Whitespace-only changes.

mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ plugins:
126126
group_by_category: false
127127
# 3 because docs are in pages with an H2 just above them
128128
heading_level: 3
129-
import:
129+
inventories:
130130
- url: https://docs.python.org/3/objects.inv
131131
- url: https://docs.pydantic.dev/latest/objects.inv
132132
- url: https://typing-extensions.readthedocs.io/en/latest/objects.inv

src/mcp/__init__.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,7 @@
6262
ToolUseContent,
6363
UnsubscribeRequest,
6464
)
65-
from .types import (
66-
Role as SamplingRole,
67-
)
65+
from .types import Role as SamplingRole
6866

6967
__all__ = [
7068
"CallToolRequest",

src/mcp/client/session.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ def get_server_capabilities(self) -> types.ServerCapabilities | None:
206206
def experimental(self) -> ExperimentalClientFeatures:
207207
"""Experimental APIs for tasks and other features.
208208
209-
WARNING: These APIs are experimental and may change without notice.
209+
!!! warning
210+
These APIs are experimental and may change without notice.
210211
211212
Example:
212213
status = await session.experimental.get_task(task_id)

src/mcp/server/auth/handlers/register.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,11 @@ async def handle(self, request: Request) -> Response:
7373
),
7474
status_code=400,
7575
)
76-
if not {"authorization_code", "refresh_token"}.issubset(set(client_metadata.grant_types)):
76+
if "authorization_code" not in client_metadata.grant_types:
7777
return PydanticJSONResponse(
7878
content=RegistrationErrorResponse(
7979
error="invalid_client_metadata",
80-
error_description="grant_types must be authorization_code and refresh_token",
80+
error_description="grant_types must include 'authorization_code'",
8181
),
8282
status_code=400,
8383
)

src/mcp/server/fastmcp/server.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,12 @@ class FastMCP(Generic[LifespanResultT]):
147147
def __init__( # noqa: PLR0913
148148
self,
149149
name: str | None = None,
150+
title: str | None = None,
151+
description: str | None = None,
150152
instructions: str | None = None,
151153
website_url: str | None = None,
152154
icons: list[Icon] | None = None,
155+
version: str | None = None,
153156
auth_server_provider: (OAuthAuthorizationServerProvider[Any, Any, Any] | None) = None,
154157
token_verifier: TokenVerifier | None = None,
155158
event_store: EventStore | None = None,
@@ -204,9 +207,12 @@ def __init__( # noqa: PLR0913
204207

205208
self._mcp_server = MCPServer(
206209
name=name or "FastMCP",
210+
title=title,
211+
description=description,
207212
instructions=instructions,
208213
website_url=website_url,
209214
icons=icons,
215+
version=version,
210216
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an FastMCP and Server.
211217
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
212218
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
@@ -245,6 +251,14 @@ def __init__( # noqa: PLR0913
245251
def name(self) -> str:
246252
return self._mcp_server.name
247253

254+
@property
255+
def title(self) -> str | None:
256+
return self._mcp_server.title
257+
258+
@property
259+
def description(self) -> str | None:
260+
return self._mcp_server.description
261+
248262
@property
249263
def instructions(self) -> str | None:
250264
return self._mcp_server.instructions
@@ -257,6 +271,10 @@ def website_url(self) -> str | None:
257271
def icons(self) -> list[Icon] | None:
258272
return self._mcp_server.icons
259273

274+
@property
275+
def version(self) -> str | None:
276+
return self._mcp_server.version
277+
260278
@property
261279
def session_manager(self) -> StreamableHTTPSessionManager:
262280
"""Get the StreamableHTTP session manager.

src/mcp/server/lowlevel/server.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,8 @@ def __init__(
139139
self,
140140
name: str,
141141
version: str | None = None,
142+
title: str | None = None,
143+
description: str | None = None,
142144
instructions: str | None = None,
143145
website_url: str | None = None,
144146
icons: list[types.Icon] | None = None,
@@ -149,6 +151,8 @@ def __init__(
149151
):
150152
self.name = name
151153
self.version = version
154+
self.title = title
155+
self.description = description
152156
self.instructions = instructions
153157
self.website_url = website_url
154158
self.icons = icons
@@ -181,6 +185,8 @@ def pkg_version(package: str) -> str:
181185
return InitializationOptions(
182186
server_name=self.name,
183187
server_version=self.version if self.version else pkg_version("mcp"),
188+
title=self.title,
189+
description=self.description,
184190
capabilities=self.get_capabilities(
185191
notification_options or NotificationOptions(),
186192
experimental_capabilities or {},
@@ -499,7 +505,7 @@ def call_tool(self, *, validate_input: bool = True):
499505

500506
def decorator(
501507
func: Callable[
502-
...,
508+
[str, dict[str, Any]],
503509
Awaitable[
504510
UnstructuredContent
505511
| StructuredContent

src/mcp/server/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
class InitializationOptions(BaseModel):
1515
server_name: str
1616
server_version: str
17+
title: str | None = None
18+
description: str | None = None
1719
capabilities: ServerCapabilities
1820
instructions: str | None = None
1921
website_url: str | None = None

src/mcp/server/session.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ async def handle_list_prompts(ctx: RequestContext) -> list[types.Prompt]:
4949
from mcp.server.experimental.session_features import ExperimentalServerSessionFeatures
5050
from mcp.server.models import InitializationOptions
5151
from mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages
52+
from mcp.shared.exceptions import StatelessModeNotSupported
5253
from mcp.shared.experimental.tasks.capabilities import check_tasks_capability
5354
from mcp.shared.experimental.tasks.helpers import RELATED_TASK_METADATA_KEY
5455
from mcp.shared.message import ServerMessageMetadata, SessionMessage
@@ -93,6 +94,7 @@ def __init__(
9394
stateless: bool = False,
9495
) -> None:
9596
super().__init__(read_stream, write_stream, types.ClientRequest, types.ClientNotification)
97+
self._stateless = stateless
9698
self._initialization_state = (
9799
InitializationState.Initialized if stateless else InitializationState.NotInitialized
98100
)
@@ -176,6 +178,8 @@ async def _received_request(self, responder: RequestResponder[types.ClientReques
176178
capabilities=self._init_options.capabilities,
177179
serverInfo=types.Implementation(
178180
name=self._init_options.server_name,
181+
title=self._init_options.title,
182+
description=self._init_options.description,
179183
version=self._init_options.server_version,
180184
websiteUrl=self._init_options.website_url,
181185
icons=self._init_options.icons,
@@ -311,7 +315,10 @@ async def create_message(
311315
Raises:
312316
McpError: If tools are provided but client doesn't support them.
313317
ValueError: If tool_use or tool_result message structure is invalid.
318+
StatelessModeNotSupported: If called in stateless HTTP mode.
314319
"""
320+
if self._stateless:
321+
raise StatelessModeNotSupported(method="sampling")
315322
client_caps = self._client_params.capabilities if self._client_params else None
316323
validate_sampling_tools(client_caps, tools, tool_choice)
317324
validate_tool_use_result_messages(messages)
@@ -349,6 +356,8 @@ async def create_message(
349356

350357
async def list_roots(self) -> types.ListRootsResult:
351358
"""Send a roots/list request."""
359+
if self._stateless:
360+
raise StatelessModeNotSupported(method="list_roots")
352361
return await self.send_request(
353362
types.ServerRequest(types.ListRootsRequest()),
354363
types.ListRootsResult,
@@ -391,7 +400,12 @@ async def elicit_form(
391400
392401
Returns:
393402
The client's response with form data
403+
404+
Raises:
405+
StatelessModeNotSupported: If called in stateless HTTP mode.
394406
"""
407+
if self._stateless:
408+
raise StatelessModeNotSupported(method="elicitation")
395409
return await self.send_request(
396410
types.ServerRequest(
397411
types.ElicitRequest(
@@ -425,7 +439,12 @@ async def elicit_url(
425439
426440
Returns:
427441
The client's response indicating acceptance, decline, or cancellation
442+
443+
Raises:
444+
StatelessModeNotSupported: If called in stateless HTTP mode.
428445
"""
446+
if self._stateless:
447+
raise StatelessModeNotSupported(method="elicitation")
429448
return await self.send_request(
430449
types.ServerRequest(
431450
types.ElicitRequest(

0 commit comments

Comments
 (0)