Skip to content

Commit dcb9e35

Browse files
Merge branch 'main' into brandon/remove-tools
2 parents 0812b94 + c0f1657 commit dcb9e35

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

+2307
-414
lines changed

.gitattribute

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Generated
2+
uv.lock linguist-generated=true

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ coverage.xml
5252
*.py,cover
5353
.hypothesis/
5454
.pytest_cache/
55+
.ruff_cache/
5556
cover/
5657

5758
# Translations
@@ -168,3 +169,6 @@ cython_debug/
168169
.vscode/
169170
.windsurfrules
170171
**/CLAUDE.local.md
172+
173+
# claude code
174+
.claude/

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,22 @@ repos:
2525
hooks:
2626
- id: ruff-format
2727
name: Ruff Format
28-
entry: uv run ruff
28+
entry: uv run --frozen ruff
2929
args: [format]
3030
language: system
3131
types: [python]
3232
pass_filenames: false
3333
- id: ruff
3434
name: Ruff
35-
entry: uv run ruff
35+
entry: uv run --frozen ruff
3636
args: ["check", "--fix", "--exit-non-zero-on-fix"]
3737
types: [python]
3838
language: system
3939
pass_filenames: false
4040
exclude: ^README\.md$
4141
- id: pyright
4242
name: pyright
43-
entry: uv run pyright
43+
entry: uv run --frozen pyright
4444
language: system
4545
types: [python]
4646
pass_filenames: false
@@ -52,7 +52,7 @@ repos:
5252
pass_filenames: false
5353
- id: readme-snippets
5454
name: Check README snippets are up to date
55-
entry: uv run scripts/update_readme_snippets.py --check
55+
entry: uv run --frozen python scripts/update_readme_snippets.py --check
5656
language: system
5757
files: ^(README\.md|examples/.*\.py|scripts/update_readme_snippets\.py)$
5858
pass_filenames: false

README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
- [Advanced Usage](#advanced-usage)
5858
- [Low-Level Server](#low-level-server)
5959
- [Structured Output Support](#structured-output-support)
60+
- [Pagination (Advanced)](#pagination-advanced)
6061
- [Writing MCP Clients](#writing-mcp-clients)
6162
- [Client Display Utilities](#client-display-utilities)
6263
- [OAuth Authentication for Clients](#oauth-authentication-for-clients)
@@ -1737,6 +1738,116 @@ Tools can return data in three ways:
17371738

17381739
When an `outputSchema` is defined, the server automatically validates the structured output against the schema. This ensures type safety and helps catch errors early.
17391740

1741+
### Pagination (Advanced)
1742+
1743+
For servers that need to handle large datasets, the low-level server provides paginated versions of list operations. This is an optional optimization - most servers won't need pagination unless they're dealing with hundreds or thousands of items.
1744+
1745+
#### Server-side Implementation
1746+
1747+
<!-- snippet-source examples/snippets/servers/pagination_example.py -->
1748+
```python
1749+
"""
1750+
Example of implementing pagination with MCP server decorators.
1751+
"""
1752+
1753+
from pydantic import AnyUrl
1754+
1755+
import mcp.types as types
1756+
from mcp.server.lowlevel import Server
1757+
1758+
# Initialize the server
1759+
server = Server("paginated-server")
1760+
1761+
# Sample data to paginate
1762+
ITEMS = [f"Item {i}" for i in range(1, 101)] # 100 items
1763+
1764+
1765+
@server.list_resources()
1766+
async def list_resources_paginated(request: types.ListResourcesRequest) -> types.ListResourcesResult:
1767+
"""List resources with pagination support."""
1768+
page_size = 10
1769+
1770+
# Extract cursor from request params
1771+
cursor = request.params.cursor if request.params is not None else None
1772+
1773+
# Parse cursor to get offset
1774+
start = 0 if cursor is None else int(cursor)
1775+
end = start + page_size
1776+
1777+
# Get page of resources
1778+
page_items = [
1779+
types.Resource(uri=AnyUrl(f"resource://items/{item}"), name=item, description=f"Description for {item}")
1780+
for item in ITEMS[start:end]
1781+
]
1782+
1783+
# Determine next cursor
1784+
next_cursor = str(end) if end < len(ITEMS) else None
1785+
1786+
return types.ListResourcesResult(resources=page_items, nextCursor=next_cursor)
1787+
```
1788+
1789+
_Full example: [examples/snippets/servers/pagination_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/pagination_example.py)_
1790+
<!-- /snippet-source -->
1791+
1792+
#### Client-side Consumption
1793+
1794+
<!-- snippet-source examples/snippets/clients/pagination_client.py -->
1795+
```python
1796+
"""
1797+
Example of consuming paginated MCP endpoints from a client.
1798+
"""
1799+
1800+
import asyncio
1801+
1802+
from mcp.client.session import ClientSession
1803+
from mcp.client.stdio import StdioServerParameters, stdio_client
1804+
from mcp.types import Resource
1805+
1806+
1807+
async def list_all_resources() -> None:
1808+
"""Fetch all resources using pagination."""
1809+
async with stdio_client(StdioServerParameters(command="uv", args=["run", "mcp-simple-pagination"])) as (
1810+
read,
1811+
write,
1812+
):
1813+
async with ClientSession(read, write) as session:
1814+
await session.initialize()
1815+
1816+
all_resources: list[Resource] = []
1817+
cursor = None
1818+
1819+
while True:
1820+
# Fetch a page of resources
1821+
result = await session.list_resources(cursor=cursor)
1822+
all_resources.extend(result.resources)
1823+
1824+
print(f"Fetched {len(result.resources)} resources")
1825+
1826+
# Check if there are more pages
1827+
if result.nextCursor:
1828+
cursor = result.nextCursor
1829+
else:
1830+
break
1831+
1832+
print(f"Total resources: {len(all_resources)}")
1833+
1834+
1835+
if __name__ == "__main__":
1836+
asyncio.run(list_all_resources())
1837+
```
1838+
1839+
_Full example: [examples/snippets/clients/pagination_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/pagination_client.py)_
1840+
<!-- /snippet-source -->
1841+
1842+
#### Key Points
1843+
1844+
- **Cursors are opaque strings** - the server defines the format (numeric offsets, timestamps, etc.)
1845+
- **Return `nextCursor=None`** when there are no more pages
1846+
- **Backward compatible** - clients that don't support pagination will still work (they'll just get the first page)
1847+
- **Flexible page sizes** - Each endpoint can define its own page size based on data characteristics
1848+
1849+
See the [simple-pagination example](examples/servers/simple-pagination) for a complete implementation.
1850+
17401851
### Writing MCP Clients
17411852

17421853
The SDK provides a high-level client interface for connecting to MCP servers using various [transports](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports):

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ mcp> quit
7171
## Configuration
7272

7373
- `MCP_SERVER_PORT` - Server URL (default: 8000)
74-
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable_http` (default) or `sse`
74+
- `MCP_TRANSPORT_TYPE` - Transport type: `streamable-http` (default) or `sse`

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def get_state(self):
150150
class SimpleAuthClient:
151151
"""Simple MCP client with auth support."""
152152

153-
def __init__(self, server_url: str, transport_type: str = "streamable_http"):
153+
def __init__(self, server_url: str, transport_type: str = "streamable-http"):
154154
self.server_url = server_url
155155
self.transport_type = transport_type
156156
self.session: ClientSession | None = None
@@ -334,10 +334,10 @@ async def main():
334334
# Default server URL - can be overridden with environment variable
335335
# Most MCP streamable HTTP servers use /mcp as the endpoint
336336
server_url = os.getenv("MCP_SERVER_PORT", 8000)
337-
transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable_http")
337+
transport_type = os.getenv("MCP_TRANSPORT_TYPE", "streamable-http")
338338
server_url = (
339339
f"http://localhost:{server_url}/mcp"
340-
if transport_type == "streamable_http"
340+
if transport_type == "streamable-http"
341341
else f"http://localhost:{server_url}/sse"
342342
)
343343

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""
2+
FastMCP Echo Server that sends log messages and progress updates to the client
3+
"""
4+
5+
import asyncio
6+
7+
from mcp.server.fastmcp import Context, FastMCP
8+
9+
# Create server
10+
mcp = FastMCP("Echo Server with logging and progress updates")
11+
12+
13+
@mcp.tool()
14+
async def echo(text: str, ctx: Context) -> str:
15+
"""Echo the input text sending log messages and progress updates during processing."""
16+
await ctx.report_progress(progress=0, total=100)
17+
await ctx.info("Starting to process echo for input: " + text)
18+
19+
await asyncio.sleep(2)
20+
21+
await ctx.info("Halfway through processing echo for input: " + text)
22+
await ctx.report_progress(progress=50, total=100)
23+
24+
await asyncio.sleep(2)
25+
26+
await ctx.info("Finished processing echo for input: " + text)
27+
await ctx.report_progress(progress=100, total=100)
28+
29+
# Progress notifications are process asynchronously by the client.
30+
# A small delay here helps ensure the last notification is processed by the client.
31+
await asyncio.sleep(0.1)
32+
33+
return text

examples/servers/simple-auth/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ uv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --tra
4343
```bash
4444
cd examples/clients/simple-auth-client
4545
# Start client with streamable HTTP
46-
MCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=streamable_http uv run mcp-simple-auth-client
46+
MCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=streamable-http uv run mcp-simple-auth-client
4747
```
4848

4949
## How It Works
@@ -101,7 +101,7 @@ uv run mcp-simple-auth-legacy --port=8002
101101
```bash
102102
# Test with client (will automatically fall back to legacy discovery)
103103
cd examples/clients/simple-auth-client
104-
MCP_SERVER_PORT=8002 MCP_TRANSPORT_TYPE=streamable_http uv run mcp-simple-auth-client
104+
MCP_SERVER_PORT=8002 MCP_TRANSPORT_TYPE=streamable-http uv run mcp-simple-auth-client
105105
```
106106

107107
The client will:
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# MCP Simple Pagination
2+
3+
A simple MCP server demonstrating pagination for tools, resources, and prompts using cursor-based pagination.
4+
5+
## Usage
6+
7+
Start the server using either stdio (default) or SSE transport:
8+
9+
```bash
10+
# Using stdio transport (default)
11+
uv run mcp-simple-pagination
12+
13+
# Using SSE transport on custom port
14+
uv run mcp-simple-pagination --transport sse --port 8000
15+
```
16+
17+
The server exposes:
18+
19+
- 25 tools (paginated, 5 per page)
20+
- 30 resources (paginated, 10 per page)
21+
- 20 prompts (paginated, 7 per page)
22+
23+
Each paginated list returns a `nextCursor` when more pages are available. Use this cursor in subsequent requests to retrieve the next page.
24+
25+
## Example
26+
27+
Using the MCP client, you can retrieve paginated items like this using the STDIO transport:
28+
29+
```python
30+
import asyncio
31+
from mcp.client.session import ClientSession
32+
from mcp.client.stdio import StdioServerParameters, stdio_client
33+
34+
35+
async def main():
36+
async with stdio_client(
37+
StdioServerParameters(command="uv", args=["run", "mcp-simple-pagination"])
38+
) as (read, write):
39+
async with ClientSession(read, write) as session:
40+
await session.initialize()
41+
42+
# Get first page of tools
43+
tools_page1 = await session.list_tools()
44+
print(f"First page: {len(tools_page1.tools)} tools")
45+
print(f"Next cursor: {tools_page1.nextCursor}")
46+
47+
# Get second page using cursor
48+
if tools_page1.nextCursor:
49+
tools_page2 = await session.list_tools(cursor=tools_page1.nextCursor)
50+
print(f"Second page: {len(tools_page2.tools)} tools")
51+
52+
# Similarly for resources
53+
resources_page1 = await session.list_resources()
54+
print(f"First page: {len(resources_page1.resources)} resources")
55+
56+
# And for prompts
57+
prompts_page1 = await session.list_prompts()
58+
print(f"First page: {len(prompts_page1.prompts)} prompts")
59+
60+
61+
asyncio.run(main())
62+
```
63+
64+
## Pagination Details
65+
66+
The server uses simple numeric indices as cursors for demonstration purposes. In production scenarios, you might use:
67+
68+
- Database offsets or row IDs
69+
- Timestamps for time-based pagination
70+
- Opaque tokens encoding pagination state
71+
72+
The pagination implementation demonstrates:
73+
74+
- Handling `None` cursor for the first page
75+
- Returning `nextCursor` when more data exists
76+
- Gracefully handling invalid cursors
77+
- Different page sizes for different resource types

examples/servers/simple-pagination/mcp_simple_pagination/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)