Skip to content

Commit f67c3ee

Browse files
authored
Merge branch 'modelcontextprotocol:main' into patch-1
2 parents 1395607 + 775f879 commit f67c3ee

37 files changed

+2065
-154
lines changed

.git-blame-ignore-revs

Whitespace-only changes.

.github/workflows/check-lock.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Check uv.lock
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "pyproject.toml"
7+
- "uv.lock"
8+
push:
9+
paths:
10+
- "pyproject.toml"
11+
- "uv.lock"
12+
13+
jobs:
14+
check-lock:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Install uv
20+
run: |
21+
curl -LsSf https://astral.sh/uv/install.sh | sh
22+
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
23+
24+
- name: Check uv.lock is up to date
25+
run: uv lock --check

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.DS_Store
2+
scratch/
23

34
# Byte-compiled / optimized / DLL files
45
__pycache__/
@@ -162,3 +163,6 @@ cython_debug/
162163
# and can be added to the global gitignore or merged into this file. For a more nuclear
163164
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
164165
#.idea/
166+
167+
# vscode
168+
.vscode/

.pre-commit-config.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,12 @@ repos:
1313
- id: ruff-format
1414
- id: ruff
1515
args: [--fix, --exit-non-zero-on-fix]
16+
17+
- repo: local
18+
hooks:
19+
- id: uv-lock-check
20+
name: Check uv.lock is up to date
21+
entry: uv lock --check
22+
language: system
23+
files: ^(pyproject\.toml|uv\.lock)$
24+
pass_filenames: false

README.md

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,13 +128,41 @@ The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you bui
128128
The FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:
129129

130130
```python
131+
# Add lifespan support for startup/shutdown with strong typing
132+
from dataclasses import dataclass
133+
from typing import AsyncIterator
131134
from mcp.server.fastmcp import FastMCP
132135

133136
# Create a named server
134137
mcp = FastMCP("My App")
135138

136139
# Specify dependencies for deployment and development
137140
mcp = FastMCP("My App", dependencies=["pandas", "numpy"])
141+
142+
@dataclass
143+
class AppContext:
144+
db: Database # Replace with your actual DB type
145+
146+
@asynccontextmanager
147+
async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
148+
"""Manage application lifecycle with type-safe context"""
149+
try:
150+
# Initialize on startup
151+
await db.connect()
152+
yield AppContext(db=db)
153+
finally:
154+
# Cleanup on shutdown
155+
await db.disconnect()
156+
157+
# Pass lifespan to server
158+
mcp = FastMCP("My App", lifespan=app_lifespan)
159+
160+
# Access type-safe lifespan context in tools
161+
@mcp.tool()
162+
def query_db(ctx: Context) -> str:
163+
"""Tool that uses initialized resources"""
164+
db = ctx.request_context.lifespan_context["db"]
165+
return db.query()
138166
```
139167

140168
### Resources
@@ -334,7 +362,38 @@ def query_data(sql: str) -> str:
334362

335363
### Low-Level Server
336364

337-
For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server:
365+
For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server, including lifecycle management through the lifespan API:
366+
367+
```python
368+
from contextlib import asynccontextmanager
369+
from typing import AsyncIterator
370+
371+
@asynccontextmanager
372+
async def server_lifespan(server: Server) -> AsyncIterator[dict]:
373+
"""Manage server startup and shutdown lifecycle."""
374+
try:
375+
# Initialize resources on startup
376+
await db.connect()
377+
yield {"db": db}
378+
finally:
379+
# Clean up on shutdown
380+
await db.disconnect()
381+
382+
# Pass lifespan to server
383+
server = Server("example-server", lifespan=server_lifespan)
384+
385+
# Access lifespan context in handlers
386+
@server.call_tool()
387+
async def query_db(name: str, arguments: dict) -> list:
388+
ctx = server.request_context
389+
db = ctx.lifespan_context["db"]
390+
return await db.query(arguments["query"])
391+
```
392+
393+
The lifespan API provides:
394+
- A way to initialize resources when the server starts and clean them up when it stops
395+
- Access to initialized resources through the request context in handlers
396+
- Type-safe context passing between lifespan and request handlers
338397

339398
```python
340399
from mcp.server.lowlevel import Server, NotificationOptions
@@ -417,9 +476,21 @@ server_params = StdioServerParameters(
417476
env=None # Optional environment variables
418477
)
419478

479+
# Optional: create a sampling callback
480+
async def handle_sampling_message(message: types.CreateMessageRequestParams) -> types.CreateMessageResult:
481+
return types.CreateMessageResult(
482+
role="assistant",
483+
content=types.TextContent(
484+
type="text",
485+
text="Hello, world! from model",
486+
),
487+
model="gpt-3.5-turbo",
488+
stopReason="endTurn",
489+
)
490+
420491
async def run():
421492
async with stdio_client(server_params) as (read, write):
422-
async with ClientSession(read, write) as session:
493+
async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session:
423494
# Initialize the connection
424495
await session.initialize()
425496

0 commit comments

Comments
 (0)