Skip to content

Commit d1bbbac

Browse files
authored
Merge branch 'main' into main
2 parents 545d047 + d1ac8d6 commit d1bbbac

File tree

84 files changed

+1144
-958
lines changed

Some content is hidden

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

84 files changed

+1144
-958
lines changed

.github/workflows/shared.yml

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ on:
66
permissions:
77
contents: read
88

9+
env:
10+
COLUMNS: 150
11+
912
jobs:
1013
pre-commit:
1114
runs-on: ubuntu-latest
@@ -33,6 +36,7 @@ jobs:
3336
strategy:
3437
matrix:
3538
python-version: ["3.10", "3.11", "3.12", "3.13"]
39+
dep-resolution: ["lowest-direct", "highest"]
3640
os: [ubuntu-latest, windows-latest]
3741

3842
steps:
@@ -45,18 +49,11 @@ jobs:
4549
version: 0.7.2
4650

4751
- name: Install the project
48-
run: uv sync --frozen --all-extras --python ${{ matrix.python-version }}
52+
run: uv sync --frozen --all-extras --python ${{ matrix.python-version }} --resolution ${{ matrix.dep-resolution }}
4953

5054
- name: Run pytest
5155
run: uv run --frozen --no-sync pytest
5256

53-
# This must run last as it modifies the environment!
54-
- name: Run pytest with lowest versions
55-
run: |
56-
uv sync --all-extras --upgrade
57-
uv run --no-sync pytest
58-
env:
59-
UV_RESOLUTION: lowest-direct
6057
readme-snippets:
6158
runs-on: ubuntu-latest
6259
steps:

README.md

Lines changed: 180 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,20 @@
3636
- [Sampling](#sampling)
3737
- [Logging and Notifications](#logging-and-notifications)
3838
- [Authentication](#authentication)
39+
- [FastMCP Properties](#fastmcp-properties)
40+
- [Session Properties](#session-properties-and-methods)
41+
- [Request Context Properties](#request-context-properties)
3942
- [Running Your Server](#running-your-server)
4043
- [Development Mode](#development-mode)
4144
- [Claude Desktop Integration](#claude-desktop-integration)
4245
- [Direct Execution](#direct-execution)
46+
- [Streamable HTTP Transport](#streamable-http-transport)
4347
- [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server)
4448
- [Advanced Usage](#advanced-usage)
4549
- [Low-Level Server](#low-level-server)
4650
- [Writing MCP Clients](#writing-mcp-clients)
51+
- [Client Display Utilities](#client-display-utilities)
52+
- [OAuth Authentication for Clients](#oauth-authentication-for-clients)
4753
- [Parsing Tool Results](#parsing-tool-results)
4854
- [MCP Primitives](#mcp-primitives)
4955
- [Server Capabilities](#server-capabilities)
@@ -191,6 +197,7 @@ from contextlib import asynccontextmanager
191197
from dataclasses import dataclass
192198

193199
from mcp.server.fastmcp import Context, FastMCP
200+
from mcp.server.session import ServerSession
194201

195202

196203
# Mock database class for example
@@ -236,7 +243,7 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
236243

237244
# Access type-safe lifespan context in tools
238245
@mcp.tool()
239-
def query_db(ctx: Context) -> str:
246+
def query_db(ctx: Context[ServerSession, AppContext]) -> str:
240247
"""Tool that uses initialized resources."""
241248
db = ctx.request_context.lifespan_context.db
242249
return db.query()
@@ -303,6 +310,36 @@ def get_weather(city: str, unit: str = "celsius") -> str:
303310
_Full example: [examples/snippets/servers/basic_tool.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_tool.py)_
304311
<!-- /snippet-source -->
305312

313+
Tools can optionally receive a Context object by including a parameter with the `Context` type annotation. This context is automatically injected by the FastMCP framework and provides access to MCP capabilities:
314+
315+
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
316+
```python
317+
from mcp.server.fastmcp import Context, FastMCP
318+
from mcp.server.session import ServerSession
319+
320+
mcp = FastMCP(name="Progress Example")
321+
322+
323+
@mcp.tool()
324+
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
325+
"""Execute a task with progress updates."""
326+
await ctx.info(f"Starting: {task_name}")
327+
328+
for i in range(steps):
329+
progress = (i + 1) / steps
330+
await ctx.report_progress(
331+
progress=progress,
332+
total=1.0,
333+
message=f"Step {i + 1}/{steps}",
334+
)
335+
await ctx.debug(f"Completed step {i + 1}")
336+
337+
return f"Task '{task_name}' completed"
338+
```
339+
340+
_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/tool_progress.py)_
341+
<!-- /snippet-source -->
342+
306343
#### Structured Output
307344

308345
Tools will return structured results by default, if their return type
@@ -410,7 +447,7 @@ def get_user(user_id: str) -> UserProfile:
410447

411448
# Classes WITHOUT type hints cannot be used for structured output
412449
class UntypedConfig:
413-
def __init__(self, setting1, setting2):
450+
def __init__(self, setting1, setting2): # type: ignore[reportMissingParameterType]
414451
self.setting1 = setting1
415452
self.setting2 = setting2
416453

@@ -496,17 +533,53 @@ _Full example: [examples/snippets/servers/images.py](https://github.com/modelcon
496533

497534
### Context
498535

499-
The Context object gives your tools and resources access to MCP capabilities:
536+
The Context object is automatically injected into tool and resource functions that request it via type hints. It provides access to MCP capabilities like logging, progress reporting, resource reading, user interaction, and request metadata.
537+
538+
#### Getting Context in Functions
539+
540+
To use context in a tool or resource function, add a parameter with the `Context` type annotation:
541+
542+
```python
543+
from mcp.server.fastmcp import Context, FastMCP
544+
545+
mcp = FastMCP(name="Context Example")
546+
547+
548+
@mcp.tool()
549+
async def my_tool(x: int, ctx: Context) -> str:
550+
"""Tool that uses context capabilities."""
551+
# The context parameter can have any name as long as it's type-annotated
552+
return await process_with_context(x, ctx)
553+
```
554+
555+
#### Context Properties and Methods
556+
557+
The Context object provides the following capabilities:
558+
559+
- `ctx.request_id` - Unique ID for the current request
560+
- `ctx.client_id` - Client ID if available
561+
- `ctx.fastmcp` - Access to the FastMCP server instance (see [FastMCP Properties](#fastmcp-properties))
562+
- `ctx.session` - Access to the underlying session for advanced communication (see [Session Properties and Methods](#session-properties-and-methods))
563+
- `ctx.request_context` - Access to request-specific data and lifespan resources (see [Request Context Properties](#request-context-properties))
564+
- `await ctx.debug(message)` - Send debug log message
565+
- `await ctx.info(message)` - Send info log message
566+
- `await ctx.warning(message)` - Send warning log message
567+
- `await ctx.error(message)` - Send error log message
568+
- `await ctx.log(level, message, logger_name=None)` - Send log with custom level
569+
- `await ctx.report_progress(progress, total=None, message=None)` - Report operation progress
570+
- `await ctx.read_resource(uri)` - Read a resource by URI
571+
- `await ctx.elicit(message, schema)` - Request additional information from user with validation
500572

501573
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
502574
```python
503575
from mcp.server.fastmcp import Context, FastMCP
576+
from mcp.server.session import ServerSession
504577

505578
mcp = FastMCP(name="Progress Example")
506579

507580

508581
@mcp.tool()
509-
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
582+
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
510583
"""Execute a task with progress updates."""
511584
await ctx.info(f"Starting: {task_name}")
512585

@@ -624,6 +697,7 @@ Request additional information from users. This example shows an Elicitation dur
624697
from pydantic import BaseModel, Field
625698

626699
from mcp.server.fastmcp import Context, FastMCP
700+
from mcp.server.session import ServerSession
627701

628702
mcp = FastMCP(name="Elicitation Example")
629703

@@ -639,12 +713,7 @@ class BookingPreferences(BaseModel):
639713

640714

641715
@mcp.tool()
642-
async def book_table(
643-
date: str,
644-
time: str,
645-
party_size: int,
646-
ctx: Context,
647-
) -> str:
716+
async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerSession, None]) -> str:
648717
"""Book a table with date availability check."""
649718
# Check if date is available
650719
if date == "2024-12-25":
@@ -680,13 +749,14 @@ Tools can interact with LLMs through sampling (generating text):
680749
<!-- snippet-source examples/snippets/servers/sampling.py -->
681750
```python
682751
from mcp.server.fastmcp import Context, FastMCP
752+
from mcp.server.session import ServerSession
683753
from mcp.types import SamplingMessage, TextContent
684754

685755
mcp = FastMCP(name="Sampling Example")
686756

687757

688758
@mcp.tool()
689-
async def generate_poem(topic: str, ctx: Context) -> str:
759+
async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str:
690760
"""Generate a poem using LLM sampling."""
691761
prompt = f"Write a short poem about {topic}"
692762

@@ -715,12 +785,13 @@ Tools can send logs and notifications through the context:
715785
<!-- snippet-source examples/snippets/servers/notifications.py -->
716786
```python
717787
from mcp.server.fastmcp import Context, FastMCP
788+
from mcp.server.session import ServerSession
718789

719790
mcp = FastMCP(name="Notifications Example")
720791

721792

722793
@mcp.tool()
723-
async def process_data(data: str, ctx: Context) -> str:
794+
async def process_data(data: str, ctx: Context[ServerSession, None]) -> str:
724795
"""Process data with logging."""
725796
# Different log levels
726797
await ctx.debug(f"Debug: Processing '{data}'")
@@ -808,6 +879,99 @@ For a complete example with separate Authorization Server and Resource Server im
808879

809880
See [TokenVerifier](src/mcp/server/auth/provider.py) for more details on implementing token validation.
810881

882+
### FastMCP Properties
883+
884+
The FastMCP server instance accessible via `ctx.fastmcp` provides access to server configuration and metadata:
885+
886+
- `ctx.fastmcp.name` - The server's name as defined during initialization
887+
- `ctx.fastmcp.instructions` - Server instructions/description provided to clients
888+
- `ctx.fastmcp.settings` - Complete server configuration object containing:
889+
- `debug` - Debug mode flag
890+
- `log_level` - Current logging level
891+
- `host` and `port` - Server network configuration
892+
- `mount_path`, `sse_path`, `streamable_http_path` - Transport paths
893+
- `stateless_http` - Whether the server operates in stateless mode
894+
- And other configuration options
895+
896+
```python
897+
@mcp.tool()
898+
def server_info(ctx: Context) -> dict:
899+
"""Get information about the current server."""
900+
return {
901+
"name": ctx.fastmcp.name,
902+
"instructions": ctx.fastmcp.instructions,
903+
"debug_mode": ctx.fastmcp.settings.debug,
904+
"log_level": ctx.fastmcp.settings.log_level,
905+
"host": ctx.fastmcp.settings.host,
906+
"port": ctx.fastmcp.settings.port,
907+
}
908+
```
909+
910+
### Session Properties and Methods
911+
912+
The session object accessible via `ctx.session` provides advanced control over client communication:
913+
914+
- `ctx.session.client_params` - Client initialization parameters and declared capabilities
915+
- `await ctx.session.send_log_message(level, data, logger)` - Send log messages with full control
916+
- `await ctx.session.create_message(messages, max_tokens)` - Request LLM sampling/completion
917+
- `await ctx.session.send_progress_notification(token, progress, total, message)` - Direct progress updates
918+
- `await ctx.session.send_resource_updated(uri)` - Notify clients that a specific resource changed
919+
- `await ctx.session.send_resource_list_changed()` - Notify clients that the resource list changed
920+
- `await ctx.session.send_tool_list_changed()` - Notify clients that the tool list changed
921+
- `await ctx.session.send_prompt_list_changed()` - Notify clients that the prompt list changed
922+
923+
```python
924+
@mcp.tool()
925+
async def notify_data_update(resource_uri: str, ctx: Context) -> str:
926+
"""Update data and notify clients of the change."""
927+
# Perform data update logic here
928+
929+
# Notify clients that this specific resource changed
930+
await ctx.session.send_resource_updated(AnyUrl(resource_uri))
931+
932+
# If this affects the overall resource list, notify about that too
933+
await ctx.session.send_resource_list_changed()
934+
935+
return f"Updated {resource_uri} and notified clients"
936+
```
937+
938+
### Request Context Properties
939+
940+
The request context accessible via `ctx.request_context` contains request-specific information and resources:
941+
942+
- `ctx.request_context.lifespan_context` - Access to resources initialized during server startup
943+
- Database connections, configuration objects, shared services
944+
- Type-safe access to resources defined in your server's lifespan function
945+
- `ctx.request_context.meta` - Request metadata from the client including:
946+
- `progressToken` - Token for progress notifications
947+
- Other client-provided metadata
948+
- `ctx.request_context.request` - The original MCP request object for advanced processing
949+
- `ctx.request_context.request_id` - Unique identifier for this request
950+
951+
```python
952+
# Example with typed lifespan context
953+
@dataclass
954+
class AppContext:
955+
db: Database
956+
config: AppConfig
957+
958+
@mcp.tool()
959+
def query_with_config(query: str, ctx: Context) -> str:
960+
"""Execute a query using shared database and configuration."""
961+
# Access typed lifespan context
962+
app_ctx: AppContext = ctx.request_context.lifespan_context
963+
964+
# Use shared resources
965+
connection = app_ctx.db
966+
settings = app_ctx.config
967+
968+
# Execute query with configuration
969+
result = connection.execute(query, timeout=settings.query_timeout)
970+
return str(result)
971+
```
972+
973+
_Full lifespan example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lifespan_example.py)_
974+
811975
## Running Your Server
812976

813977
### Development Mode
@@ -1081,6 +1245,7 @@ Run from the repository root:
10811245

10821246
from collections.abc import AsyncIterator
10831247
from contextlib import asynccontextmanager
1248+
from typing import Any
10841249

10851250
import mcp.server.stdio
10861251
import mcp.types as types
@@ -1109,7 +1274,7 @@ class Database:
11091274

11101275

11111276
@asynccontextmanager
1112-
async def server_lifespan(_server: Server) -> AsyncIterator[dict]:
1277+
async def server_lifespan(_server: Server) -> AsyncIterator[dict[str, Any]]:
11131278
"""Manage server startup and shutdown lifecycle."""
11141279
# Initialize resources on startup
11151280
db = await Database.connect()
@@ -1141,7 +1306,7 @@ async def handle_list_tools() -> list[types.Tool]:
11411306

11421307

11431308
@server.call_tool()
1144-
async def query_db(name: str, arguments: dict) -> list[types.TextContent]:
1309+
async def query_db(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
11451310
"""Handle database query tool call."""
11461311
if name != "query_db":
11471312
raise ValueError(f"Unknown tool: {name}")
@@ -1395,7 +1560,7 @@ server_params = StdioServerParameters(
13951560

13961561
# Optional: create a sampling callback
13971562
async def handle_sampling_message(
1398-
context: RequestContext, params: types.CreateMessageRequestParams
1563+
context: RequestContext[ClientSession, None], params: types.CreateMessageRequestParams
13991564
) -> types.CreateMessageResult:
14001565
print(f"Sampling request: {params.messages}")
14011566
return types.CreateMessageResult(

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,7 @@ async def _default_redirect_handler(authorization_url: str) -> None:
188188
# Create OAuth authentication handler using the new interface
189189
oauth_auth = OAuthClientProvider(
190190
server_url=self.server_url.replace("/mcp", ""),
191-
client_metadata=OAuthClientMetadata.model_validate(
192-
client_metadata_dict
193-
),
191+
client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict),
194192
storage=InMemoryTokenStorage(),
195193
redirect_handler=_default_redirect_handler,
196194
callback_handler=callback_handler,
@@ -322,9 +320,7 @@ async def interactive_loop(self):
322320
await self.call_tool(tool_name, arguments)
323321

324322
else:
325-
print(
326-
"❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'"
327-
)
323+
print("❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'")
328324

329325
except KeyboardInterrupt:
330326
print("\n\n👋 Goodbye!")

0 commit comments

Comments
 (0)