Skip to content

Commit 41184ba

Browse files
authored
README - replace code snippets with examples -- auth examples (#1164)
1 parent 813da6a commit 41184ba

File tree

4 files changed

+241
-31
lines changed

4 files changed

+241
-31
lines changed

README.md

Lines changed: 107 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -745,30 +745,59 @@ Authentication can be used by servers that want to expose tools accessing protec
745745

746746
MCP servers can use authentication by providing an implementation of the `TokenVerifier` protocol:
747747

748+
<!-- snippet-source examples/snippets/servers/oauth_server.py -->
748749
```python
749-
from mcp import FastMCP
750-
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
751758
from mcp.server.auth.settings import AuthSettings
759+
from mcp.server.fastmcp import FastMCP
760+
752761

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

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

760768

769+
# Create FastMCP instance as a Resource Server
761770
mcp = FastMCP(
762-
"My App",
763-
token_verifier=MyTokenVerifier(),
771+
"Weather Service",
772+
# Token verifier for authentication
773+
token_verifier=SimpleTokenVerifier(),
774+
# Auth settings for RFC 9728 Protected Resource Metadata
764775
auth=AuthSettings(
765-
issuer_url="https://auth.example.com",
766-
resource_server_url="http://localhost:3001",
767-
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"],
768779
),
769780
)
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")
770796
```
771797

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+
772801
For a complete example with separate Authorization Server and Resource Server implementations, see [`examples/servers/simple-auth/`](examples/servers/simple-auth/).
773802

774803
**Architecture:**
@@ -1557,53 +1586,100 @@ This ensures your client UI shows the most user-friendly names that servers prov
15571586

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

1589+
<!-- snippet-source examples/snippets/clients/oauth_client.py -->
15601590
```python
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
15611606
from mcp.client.auth import OAuthClientProvider, TokenStorage
1562-
from mcp.client.session import ClientSession
15631607
from mcp.client.streamable_http import streamablehttp_client
15641608
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
15651609

15661610

1567-
class CustomTokenStorage(TokenStorage):
1568-
"""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
15691617

15701618
async def get_tokens(self) -> OAuthToken | None:
1571-
pass
1619+
"""Get stored tokens."""
1620+
return self.tokens
15721621

15731622
async def set_tokens(self, tokens: OAuthToken) -> None:
1574-
pass
1623+
"""Store tokens."""
1624+
self.tokens = tokens
15751625

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

15791630
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
1580-
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]
15811643

15821644

15831645
async def main():
1584-
# Set up OAuth authentication
1646+
"""Run the OAuth client example."""
15851647
oauth_auth = OAuthClientProvider(
1586-
server_url="https://api.example.com",
1648+
server_url="http://localhost:8001",
15871649
client_metadata=OAuthClientMetadata(
1588-
client_name="My Client",
1589-
redirect_uris=["http://localhost:3000/callback"],
1650+
client_name="Example MCP Client",
1651+
redirect_uris=[AnyUrl("http://localhost:3000/callback")],
15901652
grant_types=["authorization_code", "refresh_token"],
15911653
response_types=["code"],
1654+
scope="user",
15921655
),
1593-
storage=CustomTokenStorage(),
1594-
redirect_handler=lambda url: print(f"Visit: {url}"),
1595-
callback_handler=lambda: ("auth_code", None),
1656+
storage=InMemoryTokenStorage(),
1657+
redirect_handler=handle_redirect,
1658+
callback_handler=handle_callback,
15961659
)
15971660

1598-
# Use with streamable HTTP client
1599-
async with streamablehttp_client(
1600-
"https://api.example.com/mcp", auth=oauth_auth
1601-
) as (read, write, _):
1661+
async with streamablehttp_client("http://localhost:8001/mcp", auth=oauth_auth) as (read, write, _):
16021662
async with ClientSession(read, write) as session:
16031663
await session.initialize()
1604-
# Authenticated session ready
1664+
1665+
tools = await session.list_tools()
1666+
print(f"Available tools: {[tool.name for tool in tools.tools]}")
1667+
1668+
resources = await session.list_resources()
1669+
print(f"Available resources: {[r.uri for r in resources.resources]}")
1670+
1671+
1672+
def run():
1673+
asyncio.run(main())
1674+
1675+
1676+
if __name__ == "__main__":
1677+
run()
16051678
```
16061679

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

16091685
### Parsing Tool Results
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()

examples/snippets/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ client = "clients.stdio_client:main"
2020
completion-client = "clients.completion_client:main"
2121
direct-execution-server = "servers.direct_execution:main"
2222
display-utilities-client = "clients.display_utilities:main"
23+
oauth-client = "clients.oauth_client:run"
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""
2+
Run from the repository root:
3+
uv run examples/snippets/servers/oauth_server.py
4+
"""
5+
6+
from pydantic import AnyHttpUrl
7+
8+
from mcp.server.auth.provider import AccessToken, TokenVerifier
9+
from mcp.server.auth.settings import AuthSettings
10+
from mcp.server.fastmcp import FastMCP
11+
12+
13+
class SimpleTokenVerifier(TokenVerifier):
14+
"""Simple token verifier for demonstration."""
15+
16+
async def verify_token(self, token: str) -> AccessToken | None:
17+
pass # This is where you would implement actual token validation
18+
19+
20+
# Create FastMCP instance as a Resource Server
21+
mcp = FastMCP(
22+
"Weather Service",
23+
# Token verifier for authentication
24+
token_verifier=SimpleTokenVerifier(),
25+
# Auth settings for RFC 9728 Protected Resource Metadata
26+
auth=AuthSettings(
27+
issuer_url=AnyHttpUrl("https://auth.example.com"), # Authorization Server URL
28+
resource_server_url=AnyHttpUrl("http://localhost:3001"), # This server's URL
29+
required_scopes=["user"],
30+
),
31+
)
32+
33+
34+
@mcp.tool()
35+
async def get_weather(city: str = "London") -> dict[str, str]:
36+
"""Get weather data for a city"""
37+
return {
38+
"city": city,
39+
"temperature": "22",
40+
"condition": "Partly cloudy",
41+
"humidity": "65%",
42+
}
43+
44+
45+
if __name__ == "__main__":
46+
mcp.run(transport="streamable-http")

0 commit comments

Comments
 (0)