Skip to content

Commit 26117da

Browse files
Merge branch 'main' into fix/session-cleanup
2 parents 7534b37 + 6b69f63 commit 26117da

File tree

42 files changed

+994
-457
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+994
-457
lines changed

.github/workflows/shared.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
continue-on-error: true
3636
strategy:
3737
matrix:
38-
python-version: ["3.10", "3.11", "3.12", "3.13"]
38+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
3939
dep-resolution:
4040
- name: lowest-direct
4141
install-flags: "--upgrade --resolution lowest-direct"

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/clients/conformance-auth-client/mcp_conformance_auth_client/__init__.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
import logging
3030
import os
3131
import sys
32-
from urllib.parse import ParseResult, parse_qs, urlparse
32+
from typing import Any, cast
33+
from urllib.parse import parse_qs, urlparse
3334

3435
import httpx
3536
from mcp import ClientSession
@@ -39,12 +40,12 @@
3940
PrivateKeyJWTOAuthProvider,
4041
SignedJWTParameters,
4142
)
42-
from mcp.client.streamable_http import streamablehttp_client
43+
from mcp.client.streamable_http import streamable_http_client
4344
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
4445
from pydantic import AnyUrl
4546

4647

47-
def get_conformance_context() -> dict:
48+
def get_conformance_context() -> dict[str, Any]:
4849
"""Load conformance test context from MCP_CONFORMANCE_CONTEXT environment variable."""
4950
context_json = os.environ.get("MCP_CONFORMANCE_CONTEXT")
5051
if not context_json:
@@ -116,9 +117,9 @@ async def handle_redirect(self, authorization_url: str) -> None:
116117

117118
# Check for redirect response
118119
if response.status_code in (301, 302, 303, 307, 308):
119-
location = response.headers.get("location")
120+
location = cast(str, response.headers.get("location"))
120121
if location:
121-
redirect_url: ParseResult = urlparse(location)
122+
redirect_url = urlparse(location)
122123
query_params: dict[str, list[str]] = parse_qs(redirect_url.query)
123124

124125
if "code" in query_params:
@@ -259,12 +260,8 @@ async def run_client_credentials_basic_client(server_url: str) -> None:
259260
async def _run_session(server_url: str, oauth_auth: OAuthClientProvider) -> None:
260261
"""Common session logic for all OAuth flows."""
261262
# Connect using streamable HTTP transport with OAuth
262-
async with streamablehttp_client(
263-
url=server_url,
264-
auth=oauth_auth,
265-
timeout=30.0,
266-
sse_read_timeout=60.0,
267-
) as (read_stream, write_stream, _):
263+
client = httpx.AsyncClient(auth=oauth_auth, timeout=30.0)
264+
async with streamable_http_client(url=server_url, http_client=client) as (read_stream, write_stream, _):
268265
async with ClientSession(read_stream, write_stream) as session:
269266
# Initialize the session
270267
await session.initialize()

examples/clients/simple-auth-client/README.md

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,63 +12,87 @@ A demonstration of how to use the MCP Python SDK with OAuth authentication over
1212

1313
```bash
1414
cd examples/clients/simple-auth-client
15-
uv sync --reinstall
15+
uv sync --reinstall
1616
```
1717

1818
## Usage
1919

2020
### 1. Start an MCP server with OAuth support
2121

22+
The simple-auth server example provides three server configurations. See [examples/servers/simple-auth/README.md](../../servers/simple-auth/README.md) for full details.
23+
24+
#### Option A: New Architecture (Recommended)
25+
26+
Separate Authorization Server and Resource Server:
27+
28+
```bash
29+
# Terminal 1: Start Authorization Server on port 9000
30+
cd examples/servers/simple-auth
31+
uv run mcp-simple-auth-as --port=9000
32+
33+
# Terminal 2: Start Resource Server on port 8001
34+
cd examples/servers/simple-auth
35+
uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http
36+
```
37+
38+
#### Option B: Legacy Server (Backwards Compatibility)
39+
2240
```bash
23-
# Example with mcp-simple-auth
24-
cd path/to/mcp-simple-auth
25-
uv run mcp-simple-auth --transport streamable-http --port 3001
41+
# Single server that acts as both AS and RS (port 8000)
42+
cd examples/servers/simple-auth
43+
uv run mcp-simple-auth-legacy --port=8000 --transport=streamable-http
2644
```
2745

2846
### 2. Run the client
2947

3048
```bash
31-
uv run mcp-simple-auth-client
49+
# Connect to Resource Server (new architecture, default port 8001)
50+
MCP_SERVER_PORT=8001 uv run mcp-simple-auth-client
3251

33-
# Or with custom server URL
34-
MCP_SERVER_PORT=3001 uv run mcp-simple-auth-client
52+
# Connect to Legacy Server (port 8000)
53+
uv run mcp-simple-auth-client
3554

3655
# Use SSE transport
37-
MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client
56+
MCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client
3857
```
3958

4059
### 3. Complete OAuth flow
4160

4261
The client will open your browser for authentication. After completing OAuth, you can use commands:
4362

4463
- `list` - List available tools
45-
- `call <tool_name> [args]` - Call a tool with optional JSON arguments
64+
- `call <tool_name> [args]` - Call a tool with optional JSON arguments
4665
- `quit` - Exit
4766

4867
## Example
4968

5069
```markdown
51-
🔐 Simple MCP Auth Client
52-
Connecting to: http://localhost:3001
70+
🚀 Simple MCP Auth Client
71+
Connecting to: http://localhost:8001/mcp
72+
Transport type: streamable-http
5373

54-
Please visit the following URL to authorize the application:
55-
http://localhost:3001/authorize?response_type=code&client_id=...
74+
🔗 Attempting to connect to http://localhost:8001/mcp...
75+
📡 Opening StreamableHTTP transport connection with auth...
76+
Opening browser for authorization: http://localhost:9000/authorize?...
5677

57-
✅ Connected to MCP server at http://localhost:3001
78+
✅ Connected to MCP server at http://localhost:8001/mcp
5879

5980
mcp> list
6081
📋 Available tools:
61-
1. echo - Echo back the input text
82+
1. get_time
83+
Description: Get the current server time.
6284

63-
mcp> call echo {"text": "Hello, world!"}
64-
🔧 Tool 'echo' result:
65-
Hello, world!
85+
mcp> call get_time
86+
🔧 Tool 'get_time' result:
87+
{"current_time": "2024-01-15T10:30:00", "timezone": "UTC", ...}
6688

6789
mcp> quit
68-
👋 Goodbye!
6990
```
7091

7192
## Configuration
7293

73-
- `MCP_SERVER_PORT` - Server URL (default: 8000)
74-
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable-http` (default) or `sse`
94+
| Environment Variable | Description | Default |
95+
|---------------------|-------------|---------|
96+
| `MCP_SERVER_PORT` | Port number of the MCP server | `8000` |
97+
| `MCP_TRANSPORT_TYPE` | Transport type: `streamable-http` or `sse` | `streamable-http` |
98+
| `MCP_CLIENT_METADATA_URL` | Optional URL for client metadata (CIMD) | None |

examples/clients/simple-auth-client/mcp_simple_auth_client/main.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,26 @@
66
77
"""
88

9+
from __future__ import annotations as _annotations
10+
911
import asyncio
1012
import os
13+
import socketserver
1114
import threading
1215
import time
1316
import webbrowser
1417
from http.server import BaseHTTPRequestHandler, HTTPServer
15-
from typing import Any
18+
from typing import Any, Callable
1619
from urllib.parse import parse_qs, urlparse
1720

1821
import httpx
22+
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
1923
from mcp.client.auth import OAuthClientProvider, TokenStorage
2024
from mcp.client.session import ClientSession
2125
from mcp.client.sse import sse_client
2226
from mcp.client.streamable_http import streamable_http_client
2327
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
28+
from mcp.shared.message import SessionMessage
2429

2530

2631
class InMemoryTokenStorage(TokenStorage):
@@ -46,7 +51,13 @@ async def set_client_info(self, client_info: OAuthClientInformationFull) -> None
4651
class CallbackHandler(BaseHTTPRequestHandler):
4752
"""Simple HTTP handler to capture OAuth callback."""
4853

49-
def __init__(self, request, client_address, server, callback_data):
54+
def __init__(
55+
self,
56+
request: Any,
57+
client_address: tuple[str, int],
58+
server: socketserver.BaseServer,
59+
callback_data: dict[str, Any],
60+
):
5061
"""Initialize with callback data storage."""
5162
self.callback_data = callback_data
5263
super().__init__(request, client_address, server)
@@ -91,15 +102,14 @@ def do_GET(self):
91102
self.send_response(404)
92103
self.end_headers()
93104

94-
def log_message(self, format, *args):
105+
def log_message(self, format: str, *args: Any):
95106
"""Suppress default logging."""
96-
pass
97107

98108

99109
class CallbackServer:
100110
"""Simple server to handle OAuth callbacks."""
101111

102-
def __init__(self, port=3000):
112+
def __init__(self, port: int = 3000):
103113
self.port = port
104114
self.server = None
105115
self.thread = None
@@ -110,7 +120,12 @@ def _create_handler_with_data(self):
110120
callback_data = self.callback_data
111121

112122
class DataCallbackHandler(CallbackHandler):
113-
def __init__(self, request, client_address, server):
123+
def __init__(
124+
self,
125+
request: BaseHTTPRequestHandler,
126+
client_address: tuple[str, int],
127+
server: socketserver.BaseServer,
128+
):
114129
super().__init__(request, client_address, server, callback_data)
115130

116131
return DataCallbackHandler
@@ -131,7 +146,7 @@ def stop(self):
131146
if self.thread:
132147
self.thread.join(timeout=1)
133148

134-
def wait_for_callback(self, timeout=300):
149+
def wait_for_callback(self, timeout: int = 300):
135150
"""Wait for OAuth callback with timeout."""
136151
start_time = time.time()
137152
while time.time() - start_time < timeout:
@@ -225,7 +240,12 @@ async def _default_redirect_handler(authorization_url: str) -> None:
225240

226241
traceback.print_exc()
227242

228-
async def _run_session(self, read_stream, write_stream, get_session_id):
243+
async def _run_session(
244+
self,
245+
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
246+
write_stream: MemoryObjectSendStream[SessionMessage],
247+
get_session_id: Callable[[], str | None] | None = None,
248+
):
229249
"""Run the MCP session with the given streams."""
230250
print("🤝 Initializing MCP session...")
231251
async with ClientSession(read_stream, write_stream) as session:
@@ -314,7 +334,7 @@ async def interactive_loop(self):
314334
continue
315335

316336
# Parse arguments (simple JSON-like format)
317-
arguments = {}
337+
arguments: dict[str, Any] = {}
318338
if len(parts) > 2:
319339
import json
320340

0 commit comments

Comments
 (0)