Skip to content

Commit 07eb9da

Browse files
authored
Merge branch 'main' into server-init-implementation-update
2 parents d5f6b44 + f397783 commit 07eb9da

File tree

21 files changed

+378
-70
lines changed

21 files changed

+378
-70
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/lowlevel/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ def call_tool(self, *, validate_input: bool = True):
505505

506506
def decorator(
507507
func: Callable[
508-
...,
508+
[str, dict[str, Any]],
509509
Awaitable[
510510
UnstructuredContent
511511
| StructuredContent

src/mcp/server/session.py

Lines changed: 17 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
)
@@ -313,7 +315,10 @@ async def create_message(
313315
Raises:
314316
McpError: If tools are provided but client doesn't support them.
315317
ValueError: If tool_use or tool_result message structure is invalid.
318+
StatelessModeNotSupported: If called in stateless HTTP mode.
316319
"""
320+
if self._stateless:
321+
raise StatelessModeNotSupported(method="sampling")
317322
client_caps = self._client_params.capabilities if self._client_params else None
318323
validate_sampling_tools(client_caps, tools, tool_choice)
319324
validate_tool_use_result_messages(messages)
@@ -351,6 +356,8 @@ async def create_message(
351356

352357
async def list_roots(self) -> types.ListRootsResult:
353358
"""Send a roots/list request."""
359+
if self._stateless:
360+
raise StatelessModeNotSupported(method="list_roots")
354361
return await self.send_request(
355362
types.ServerRequest(types.ListRootsRequest()),
356363
types.ListRootsResult,
@@ -393,7 +400,12 @@ async def elicit_form(
393400
394401
Returns:
395402
The client's response with form data
403+
404+
Raises:
405+
StatelessModeNotSupported: If called in stateless HTTP mode.
396406
"""
407+
if self._stateless:
408+
raise StatelessModeNotSupported(method="elicitation")
397409
return await self.send_request(
398410
types.ServerRequest(
399411
types.ElicitRequest(
@@ -427,7 +439,12 @@ async def elicit_url(
427439
428440
Returns:
429441
The client's response indicating acceptance, decline, or cancellation
442+
443+
Raises:
444+
StatelessModeNotSupported: If called in stateless HTTP mode.
430445
"""
446+
if self._stateless:
447+
raise StatelessModeNotSupported(method="elicitation")
431448
return await self.send_request(
432449
types.ServerRequest(
433450
types.ElicitRequest(

src/mcp/server/streamable_http_manager.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
StreamableHTTPServerTransport,
2323
)
2424
from mcp.server.transport_security import TransportSecuritySettings
25+
from mcp.types import INVALID_REQUEST, ErrorData, JSONRPCError
2526

2627
logger = logging.getLogger(__name__)
2728

@@ -276,10 +277,21 @@ async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORE
276277

277278
# Handle the HTTP request and return the response
278279
await http_transport.handle_request(scope, receive, send)
279-
else: # pragma: no cover
280-
# Invalid session ID
280+
else:
281+
# Unknown or expired session ID - return 404 per MCP spec
282+
# TODO: Align error code once spec clarifies
283+
# See: https://github.com/modelcontextprotocol/python-sdk/issues/1821
284+
error_response = JSONRPCError(
285+
jsonrpc="2.0",
286+
id="server-error",
287+
error=ErrorData(
288+
code=INVALID_REQUEST,
289+
message="Session not found",
290+
),
291+
)
281292
response = Response(
282-
"Bad Request: No valid session ID provided",
283-
status_code=HTTPStatus.BAD_REQUEST,
293+
content=error_response.model_dump_json(by_alias=True, exclude_none=True),
294+
status_code=HTTPStatus.NOT_FOUND,
295+
media_type="application/json",
284296
)
285297
await response(scope, receive, send)

src/mcp/shared/exceptions.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,24 @@ def __init__(self, error: ErrorData):
1818
self.error = error
1919

2020

21+
class StatelessModeNotSupported(RuntimeError):
22+
"""
23+
Raised when attempting to use a method that is not supported in stateless mode.
24+
25+
Server-to-client requests (sampling, elicitation, list_roots) are not
26+
supported in stateless HTTP mode because there is no persistent connection
27+
for bidirectional communication.
28+
"""
29+
30+
def __init__(self, method: str):
31+
super().__init__(
32+
f"Cannot use {method} in stateless HTTP mode. "
33+
"Stateless mode does not support server-to-client requests. "
34+
"Use stateful mode (stateless_http=False) to enable this feature."
35+
)
36+
self.method = method
37+
38+
2139
class UrlElicitationRequiredError(McpError):
2240
"""
2341
Specialized error for when a tool requires URL mode elicitation(s) before proceeding.

0 commit comments

Comments
 (0)