Skip to content

Commit add5f08

Browse files
authored
Merge branch 'main' into main
2 parents c4900ea + 99c4f3c commit add5f08

File tree

14 files changed

+683
-294
lines changed

14 files changed

+683
-294
lines changed

.github/ISSUE_TEMPLATE/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
blank_issues_enabled: true
1+
blank_issues_enabled: false

CONTRIBUTING.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ Thank you for your interest in contributing to the MCP Python SDK! This document
1414
uv sync --frozen --all-extras --dev
1515
```
1616

17+
6. Set up pre-commit hooks:
18+
19+
```bash
20+
uv tool install pre-commit --with pre-commit-uv --force-reinstall
21+
```
22+
1723
## Development Workflow
1824

1925
1. Choose the correct branch for your changes:
@@ -50,7 +56,13 @@ uv run ruff format .
5056
uv run scripts/update_readme_snippets.py
5157
```
5258

53-
8. Submit a pull request to the same branch you branched from
59+
8. (Optional) Run pre-commit hooks on all files:
60+
61+
```bash
62+
pre-commit run --all-files
63+
```
64+
65+
9. Submit a pull request to the same branch you branched from
5466

5567
## Code Style
5668

README.md

Lines changed: 178 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
- [Advanced Usage](#advanced-usage)
4545
- [Low-Level Server](#low-level-server)
4646
- [Writing MCP Clients](#writing-mcp-clients)
47+
- [Parsing Tool Results](#parsing-tool-results)
4748
- [MCP Primitives](#mcp-primitives)
4849
- [Server Capabilities](#server-capabilities)
4950
- [Documentation](#documentation)
@@ -744,30 +745,59 @@ Authentication can be used by servers that want to expose tools accessing protec
744745

745746
MCP servers can use authentication by providing an implementation of the `TokenVerifier` protocol:
746747

748+
<!-- snippet-source examples/snippets/servers/oauth_server.py -->
747749
```python
748-
from mcp import FastMCP
749-
from mcp.server.auth.provider import TokenVerifier, TokenInfo
750+
"""
751+
Run from the repository root:
752+
uv run examples/snippets/servers/oauth_server.py
753+
"""
754+
755+
from pydantic import AnyHttpUrl
756+
757+
from mcp.server.auth.provider import AccessToken, TokenVerifier
750758
from mcp.server.auth.settings import AuthSettings
759+
from mcp.server.fastmcp import FastMCP
760+
751761

762+
class SimpleTokenVerifier(TokenVerifier):
763+
"""Simple token verifier for demonstration."""
752764

753-
class MyTokenVerifier(TokenVerifier):
754-
# Implement token validation logic (typically via token introspection)
755-
async def verify_token(self, token: str) -> TokenInfo:
756-
# Verify with your authorization server
757-
...
765+
async def verify_token(self, token: str) -> AccessToken | None:
766+
pass # This is where you would implement actual token validation
758767

759768

769+
# Create FastMCP instance as a Resource Server
760770
mcp = FastMCP(
761-
"My App",
762-
token_verifier=MyTokenVerifier(),
771+
"Weather Service",
772+
# Token verifier for authentication
773+
token_verifier=SimpleTokenVerifier(),
774+
# Auth settings for RFC 9728 Protected Resource Metadata
763775
auth=AuthSettings(
764-
issuer_url="https://auth.example.com",
765-
resource_server_url="http://localhost:3001",
766-
required_scopes=["mcp:read", "mcp:write"],
776+
issuer_url=AnyHttpUrl("https://auth.example.com"), # Authorization Server URL
777+
resource_server_url=AnyHttpUrl("http://localhost:3001"), # This server's URL
778+
required_scopes=["user"],
767779
),
768780
)
781+
782+
783+
@mcp.tool()
784+
async def get_weather(city: str = "London") -> dict[str, str]:
785+
"""Get weather data for a city"""
786+
return {
787+
"city": city,
788+
"temperature": "22",
789+
"condition": "Partly cloudy",
790+
"humidity": "65%",
791+
}
792+
793+
794+
if __name__ == "__main__":
795+
mcp.run(transport="streamable-http")
769796
```
770797

798+
_Full example: [examples/snippets/servers/oauth_server.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/oauth_server.py)_
799+
<!-- /snippet-source -->
800+
771801
For a complete example with separate Authorization Server and Resource Server implementations, see [`examples/servers/simple-auth/`](examples/servers/simple-auth/).
772802

773803
**Architecture:**
@@ -1556,46 +1586,76 @@ This ensures your client UI shows the most user-friendly names that servers prov
15561586

15571587
The SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers:
15581588

1589+
<!-- snippet-source examples/snippets/clients/oauth_client.py -->
15591590
```python
1560-
from mcp.client.auth import (
1561-
OAuthClientProvider,
1562-
TokenExchangeProvider,
1563-
TokenStorage,
1564-
)
1565-
from mcp.client.session import ClientSession
1591+
"""
1592+
Before running, specify running MCP RS server URL.
1593+
To spin up RS server locally, see
1594+
examples/servers/simple-auth/README.md
1595+
1596+
cd to the `examples/snippets` directory and run:
1597+
uv run oauth-client
1598+
"""
1599+
1600+
import asyncio
1601+
from urllib.parse import parse_qs, urlparse
1602+
1603+
from pydantic import AnyUrl
1604+
1605+
from mcp import ClientSession
1606+
from mcp.client.auth import OAuthClientProvider, TokenExchangeProvider, TokenStorage
15661607
from mcp.client.streamable_http import streamablehttp_client
15671608
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
15681609

15691610

1570-
class CustomTokenStorage(TokenStorage):
1571-
"""Simple in-memory token storage implementation."""
1611+
class InMemoryTokenStorage(TokenStorage):
1612+
"""Demo In-memory token storage implementation."""
1613+
1614+
def __init__(self):
1615+
self.tokens: OAuthToken | None = None
1616+
self.client_info: OAuthClientInformationFull | None = None
15721617

15731618
async def get_tokens(self) -> OAuthToken | None:
1574-
pass
1619+
"""Get stored tokens."""
1620+
return self.tokens
15751621

15761622
async def set_tokens(self, tokens: OAuthToken) -> None:
1577-
pass
1623+
"""Store tokens."""
1624+
self.tokens = tokens
15781625

15791626
async def get_client_info(self) -> OAuthClientInformationFull | None:
1580-
pass
1627+
"""Get stored client information."""
1628+
return self.client_info
15811629

15821630
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
1583-
pass
1631+
"""Store client information."""
1632+
self.client_info = client_info
1633+
1634+
1635+
async def handle_redirect(auth_url: str) -> None:
1636+
print(f"Visit: {auth_url}")
1637+
1638+
1639+
async def handle_callback() -> tuple[str, str | None]:
1640+
callback_url = input("Paste callback URL: ")
1641+
params = parse_qs(urlparse(callback_url).query)
1642+
return params["code"][0], params.get("state", [None])[0]
15841643

15851644

15861645
async def main():
1587-
# Set up OAuth authentication
1646+
"""Run the OAuth client example."""
15881647
oauth_auth = OAuthClientProvider(
1589-
server_url="https://api.example.com",
1648+
server_url="http://localhost:8001",
15901649
client_metadata=OAuthClientMetadata(
1591-
client_name="My Client",
1592-
redirect_uris=["http://localhost:3000/callback"],
1650+
client_name="Example MCP Client",
1651+
redirect_uris=[AnyUrl("http://localhost:3000/callback")],
15931652
grant_types=["authorization_code", "refresh_token"],
15941653
response_types=["code"],
1654+
scope="user",
15951655
),
1596-
storage=CustomTokenStorage(),
1597-
redirect_handler=lambda url: print(f"Visit: {url}"),
1598-
callback_handler=lambda: ("auth_code", None),
1656+
storage=InMemoryTokenStorage(),
1657+
redirect_handler=handle_redirect,
1658+
callback_handler=handle_callback,
15991659
)
16001660

16011661
# For machine-to-machine scenarios, use ClientCredentialsProvider
@@ -1617,16 +1677,99 @@ async def main():
16171677
)
16181678

16191679
# Use with streamable HTTP client
1620-
async with streamablehttp_client(
1621-
"https://api.example.com/mcp", auth=oauth_auth
1622-
) as (read, write, _):
1680+
async with streamablehttp_client("http://localhost:8001/mcp", auth=oauth_auth) as (read, write, _):
16231681
async with ClientSession(read, write) as session:
16241682
await session.initialize()
1625-
# Authenticated session ready
1683+
1684+
tools = await session.list_tools()
1685+
print(f"Available tools: {[tool.name for tool in tools.tools]}")
1686+
1687+
resources = await session.list_resources()
1688+
print(f"Available resources: {[r.uri for r in resources.resources]}")
1689+
1690+
1691+
def run():
1692+
asyncio.run(main())
1693+
1694+
1695+
if __name__ == "__main__":
1696+
run()
16261697
```
16271698

1699+
_Full example: [examples/snippets/clients/oauth_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/oauth_client.py)_
1700+
<!-- /snippet-source -->
1701+
16281702
For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/).
16291703

1704+
### Parsing Tool Results
1705+
1706+
When calling tools through MCP, the `CallToolResult` object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs.
1707+
1708+
```python
1709+
"""examples/snippets/clients/parsing_tool_results.py"""
1710+
1711+
import asyncio
1712+
1713+
from mcp import ClientSession, StdioServerParameters, types
1714+
from mcp.client.stdio import stdio_client
1715+
1716+
1717+
async def parse_tool_results():
1718+
"""Demonstrates how to parse different types of content in CallToolResult."""
1719+
server_params = StdioServerParameters(
1720+
command="python", args=["path/to/mcp_server.py"]
1721+
)
1722+
1723+
async with stdio_client(server_params) as (read, write):
1724+
async with ClientSession(read, write) as session:
1725+
await session.initialize()
1726+
1727+
# Example 1: Parsing text content
1728+
result = await session.call_tool("get_data", {"format": "text"})
1729+
for content in result.content:
1730+
if isinstance(content, types.TextContent):
1731+
print(f"Text: {content.text}")
1732+
1733+
# Example 2: Parsing structured content from JSON tools
1734+
result = await session.call_tool("get_user", {"id": "123"})
1735+
if hasattr(result, "structuredContent") and result.structuredContent:
1736+
# Access structured data directly
1737+
user_data = result.structuredContent
1738+
print(f"User: {user_data.get('name')}, Age: {user_data.get('age')}")
1739+
1740+
# Example 3: Parsing embedded resources
1741+
result = await session.call_tool("read_config", {})
1742+
for content in result.content:
1743+
if isinstance(content, types.EmbeddedResource):
1744+
resource = content.resource
1745+
if isinstance(resource, types.TextResourceContents):
1746+
print(f"Config from {resource.uri}: {resource.text}")
1747+
elif isinstance(resource, types.BlobResourceContents):
1748+
print(f"Binary data from {resource.uri}")
1749+
1750+
# Example 4: Parsing image content
1751+
result = await session.call_tool("generate_chart", {"data": [1, 2, 3]})
1752+
for content in result.content:
1753+
if isinstance(content, types.ImageContent):
1754+
print(f"Image ({content.mimeType}): {len(content.data)} bytes")
1755+
1756+
# Example 5: Handling errors
1757+
result = await session.call_tool("failing_tool", {})
1758+
if result.isError:
1759+
print("Tool execution failed!")
1760+
for content in result.content:
1761+
if isinstance(content, types.TextContent):
1762+
print(f"Error: {content.text}")
1763+
1764+
1765+
async def main():
1766+
await parse_tool_results()
1767+
1768+
1769+
if __name__ == "__main__":
1770+
asyncio.run(main())
1771+
```
1772+
16301773
### MCP Primitives
16311774

16321775
The MCP protocol defines three core primitives that servers can implement:
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
Before running, specify running MCP RS server URL.
3+
To spin up RS server locally, see
4+
examples/servers/simple-auth/README.md
5+
6+
cd to the `examples/snippets` directory and run:
7+
uv run oauth-client
8+
"""
9+
10+
import asyncio
11+
from urllib.parse import parse_qs, urlparse
12+
13+
from pydantic import AnyUrl
14+
15+
from mcp import ClientSession
16+
from mcp.client.auth import OAuthClientProvider, TokenStorage
17+
from mcp.client.streamable_http import streamablehttp_client
18+
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
19+
20+
21+
class InMemoryTokenStorage(TokenStorage):
22+
"""Demo In-memory token storage implementation."""
23+
24+
def __init__(self):
25+
self.tokens: OAuthToken | None = None
26+
self.client_info: OAuthClientInformationFull | None = None
27+
28+
async def get_tokens(self) -> OAuthToken | None:
29+
"""Get stored tokens."""
30+
return self.tokens
31+
32+
async def set_tokens(self, tokens: OAuthToken) -> None:
33+
"""Store tokens."""
34+
self.tokens = tokens
35+
36+
async def get_client_info(self) -> OAuthClientInformationFull | None:
37+
"""Get stored client information."""
38+
return self.client_info
39+
40+
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
41+
"""Store client information."""
42+
self.client_info = client_info
43+
44+
45+
async def handle_redirect(auth_url: str) -> None:
46+
print(f"Visit: {auth_url}")
47+
48+
49+
async def handle_callback() -> tuple[str, str | None]:
50+
callback_url = input("Paste callback URL: ")
51+
params = parse_qs(urlparse(callback_url).query)
52+
return params["code"][0], params.get("state", [None])[0]
53+
54+
55+
async def main():
56+
"""Run the OAuth client example."""
57+
oauth_auth = OAuthClientProvider(
58+
server_url="http://localhost:8001",
59+
client_metadata=OAuthClientMetadata(
60+
client_name="Example MCP Client",
61+
redirect_uris=[AnyUrl("http://localhost:3000/callback")],
62+
grant_types=["authorization_code", "refresh_token"],
63+
response_types=["code"],
64+
scope="user",
65+
),
66+
storage=InMemoryTokenStorage(),
67+
redirect_handler=handle_redirect,
68+
callback_handler=handle_callback,
69+
)
70+
71+
async with streamablehttp_client("http://localhost:8001/mcp", auth=oauth_auth) as (read, write, _):
72+
async with ClientSession(read, write) as session:
73+
await session.initialize()
74+
75+
tools = await session.list_tools()
76+
print(f"Available tools: {[tool.name for tool in tools.tools]}")
77+
78+
resources = await session.list_resources()
79+
print(f"Available resources: {[r.uri for r in resources.resources]}")
80+
81+
82+
def run():
83+
asyncio.run(main())
84+
85+
86+
if __name__ == "__main__":
87+
run()

0 commit comments

Comments
 (0)