Skip to content

Commit 3482857

Browse files
Merge branch 'main' into windows-process
2 parents a1641fd + 2210c1b commit 3482857

Some content is hidden

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

47 files changed

+4565
-156
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,5 @@ cython_debug/
166166

167167
# vscode
168168
.vscode/
169+
.windsurfrules
169170
**/CLAUDE.local.md

CLAUDE.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ This document contains critical information about working with this codebase. Fo
1919
- Line length: 88 chars maximum
2020

2121
3. Testing Requirements
22-
- Framework: `uv run pytest`
22+
- Framework: `uv run --frozen pytest`
2323
- Async testing: use anyio, not asyncio
2424
- Coverage: test edge cases and errors
2525
- New features require tests
@@ -54,9 +54,9 @@ This document contains critical information about working with this codebase. Fo
5454
## Code Formatting
5555

5656
1. Ruff
57-
- Format: `uv run ruff format .`
58-
- Check: `uv run ruff check .`
59-
- Fix: `uv run ruff check . --fix`
57+
- Format: `uv run --frozen ruff format .`
58+
- Check: `uv run --frozen ruff check .`
59+
- Fix: `uv run --frozen ruff check . --fix`
6060
- Critical issues:
6161
- Line length (88 chars)
6262
- Import sorting (I001)
@@ -67,7 +67,7 @@ This document contains critical information about working with this codebase. Fo
6767
- Imports: split into multiple lines
6868

6969
2. Type Checking
70-
- Tool: `uv run pyright`
70+
- Tool: `uv run --frozen pyright`
7171
- Requirements:
7272
- Explicit None checks for Optional
7373
- Type narrowing for strings
@@ -104,6 +104,10 @@ This document contains critical information about working with this codebase. Fo
104104
- Add None checks
105105
- Narrow string types
106106
- Match existing patterns
107+
- Pytest:
108+
- If the tests aren't finding the anyio pytest mark, try adding PYTEST_DISABLE_PLUGIN_AUTOLOAD=""
109+
to the start of the pytest run command eg:
110+
`PYTEST_DISABLE_PLUGIN_AUTOLOAD="" uv run --frozen pytest`
107111

108112
3. Best Practices
109113
- Check git status before commits

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,33 @@ async def long_task(files: list[str], ctx: Context) -> str:
309309
return "Processing complete"
310310
```
311311

312+
### Authentication
313+
314+
Authentication can be used by servers that want to expose tools accessing protected resources.
315+
316+
`mcp.server.auth` implements an OAuth 2.0 server interface, which servers can use by
317+
providing an implementation of the `OAuthServerProvider` protocol.
318+
319+
```
320+
mcp = FastMCP("My App",
321+
auth_provider=MyOAuthServerProvider(),
322+
auth=AuthSettings(
323+
issuer_url="https://myapp.com",
324+
revocation_options=RevocationOptions(
325+
enabled=True,
326+
),
327+
client_registration_options=ClientRegistrationOptions(
328+
enabled=True,
329+
valid_scopes=["myscope", "myotherscope"],
330+
default_scopes=["myscope"],
331+
),
332+
required_scopes=["myscope"],
333+
),
334+
)
335+
```
336+
337+
See [OAuthServerProvider](mcp/server/auth/provider.py) for more details.
338+
312339
## Running Your Server
313340

314341
### Development Mode

examples/clients/simple-chatbot/mcp_simple_chatbot/main.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,10 @@ async def list_tools(self) -> list[Any]:
122122

123123
for item in tools_response:
124124
if isinstance(item, tuple) and item[0] == "tools":
125-
for tool in item[1]:
126-
tools.append(Tool(tool.name, tool.description, tool.inputSchema))
125+
tools.extend(
126+
Tool(tool.name, tool.description, tool.inputSchema)
127+
for tool in item[1]
128+
)
127129

128130
return tools
129131

@@ -282,10 +284,9 @@ def __init__(self, servers: list[Server], llm_client: LLMClient) -> None:
282284

283285
async def cleanup_servers(self) -> None:
284286
"""Clean up all servers properly."""
285-
cleanup_tasks = []
286-
for server in self.servers:
287-
cleanup_tasks.append(asyncio.create_task(server.cleanup()))
288-
287+
cleanup_tasks = [
288+
asyncio.create_task(server.cleanup()) for server in self.servers
289+
]
289290
if cleanup_tasks:
290291
try:
291292
await asyncio.gather(*cleanup_tasks, return_exceptions=True)
@@ -322,8 +323,7 @@ async def process_llm_response(self, llm_response: str) -> str:
322323
total = result["total"]
323324
percentage = (progress / total) * 100
324325
logging.info(
325-
f"Progress: {progress}/{total} "
326-
f"({percentage:.1f}%)"
326+
f"Progress: {progress}/{total} ({percentage:.1f}%)"
327327
)
328328

329329
return f"Tool execution result: {result}"

pyproject.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ dependencies = [
2727
"httpx-sse>=0.4",
2828
"pydantic>=2.7.2,<3.0.0",
2929
"starlette>=0.27",
30+
"python-multipart>=0.0.9",
3031
"sse-starlette>=1.6.1",
3132
"pydantic-settings>=2.5.2",
32-
"uvicorn>=0.23.1",
3333
"psutil>=7.0.0",
34+
"uvicorn>=0.23.1; sys_platform != 'emscripten'",
3435
]
3536

3637
[project.optional-dependencies]
@@ -54,6 +55,7 @@ dev = [
5455
"pytest-flakefinder>=1.1.0",
5556
"pytest-xdist>=3.6.1",
5657
"pytest-examples>=0.0.14",
58+
"pytest-pretty>=1.2.0",
5759
]
5860
docs = [
5961
"mkdocs>=1.6.1",
@@ -90,8 +92,8 @@ venv = ".venv"
9092
strict = ["src/mcp/**/*.py"]
9193

9294
[tool.ruff.lint]
93-
select = ["E", "F", "I", "UP"]
94-
ignore = []
95+
select = ["C4", "E", "F", "I", "PERF", "UP"]
96+
ignore = ["PERF203"]
9597

9698
[tool.ruff]
9799
line-length = 88

src/mcp/client/session.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,10 @@ async def unsubscribe_resource(self, uri: AnyUrl) -> types.EmptyResult:
254254
)
255255

256256
async def call_tool(
257-
self, name: str, arguments: dict[str, Any] | None = None
257+
self,
258+
name: str,
259+
arguments: dict[str, Any] | None = None,
260+
read_timeout_seconds: timedelta | None = None,
258261
) -> types.CallToolResult:
259262
"""Send a tools/call request."""
260263
return await self.send_request(
@@ -265,6 +268,7 @@ async def call_tool(
265268
)
266269
),
267270
types.CallToolResult,
271+
request_read_timeout_seconds=read_timeout_seconds,
268272
)
269273

270274
async def list_prompts(self) -> types.ListPromptsResult:

src/mcp/server/auth/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
MCP OAuth server authorization components.
3+
"""

src/mcp/server/auth/errors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from pydantic import ValidationError
2+
3+
4+
def stringify_pydantic_error(validation_error: ValidationError) -> str:
5+
return "\n".join(
6+
f"{'.'.join(str(loc) for loc in e['loc'])}: {e['msg']}"
7+
for e in validation_error.errors()
8+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Request handlers for MCP authorization endpoints.
3+
"""

0 commit comments

Comments
 (0)