Skip to content

Commit c9f5df7

Browse files
committed
Merge branch 'main' into auth-metadata
Signed-off-by: Xin Fu <[email protected]>
2 parents 59673e6 + 6e418e6 commit c9f5df7

File tree

8 files changed

+805
-694
lines changed

8 files changed

+805
-694
lines changed

.github/workflows/publish-docs-manually.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
uses: astral-sh/setup-uv@v3
2020
with:
2121
enable-cache: true
22+
version: 0.7.2
2223

2324
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
2425
- uses: actions/cache@v4

.github/workflows/publish-pypi.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ jobs:
1616
uses: astral-sh/setup-uv@v3
1717
with:
1818
enable-cache: true
19+
version: 0.7.2
1920

2021
- name: Set up Python 3.12
2122
run: uv python install 3.12
@@ -67,6 +68,7 @@ jobs:
6768
uses: astral-sh/setup-uv@v3
6869
with:
6970
enable-cache: true
71+
version: 0.7.2
7072

7173
- run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
7274
- uses: actions/cache@v4

.github/workflows/shared.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jobs:
1313
uses: astral-sh/setup-uv@v3
1414
with:
1515
enable-cache: true
16+
version: 0.7.2
1617

1718
- name: Install the project
1819
run: uv sync --frozen --all-extras --dev --python 3.12
@@ -29,6 +30,7 @@ jobs:
2930
uses: astral-sh/setup-uv@v3
3031
with:
3132
enable-cache: true
33+
version: 0.7.2
3234

3335
- name: Install the project
3436
run: uv sync --frozen --all-extras --dev --python 3.12
@@ -50,6 +52,7 @@ jobs:
5052
uses: astral-sh/setup-uv@v3
5153
with:
5254
enable-cache: true
55+
version: 0.7.2
5356

5457
- name: Install the project
5558
run: uv sync --frozen --all-extras --dev --python ${{ matrix.python-version }}

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ from dataclasses import dataclass
160160

161161
from fake_database import Database # Replace with your actual DB type
162162

163-
from mcp.server.fastmcp import Context, FastMCP
163+
from mcp.server.fastmcp import FastMCP
164164

165165
# Create a named server
166166
mcp = FastMCP("My App")
@@ -192,9 +192,10 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
192192

193193
# Access type-safe lifespan context in tools
194194
@mcp.tool()
195-
def query_db(ctx: Context) -> str:
195+
def query_db() -> str:
196196
"""Tool that uses initialized resources"""
197-
db = ctx.request_context.lifespan_context.db
197+
ctx = mcp.get_context()
198+
db = ctx.request_context.lifespan_context["db"]
198199
return db.query()
199200
```
200201

@@ -631,7 +632,7 @@ server = Server("example-server", lifespan=server_lifespan)
631632
# Access lifespan context in handlers
632633
@server.call_tool()
633634
async def query_db(name: str, arguments: dict) -> list:
634-
ctx = server.get_context()
635+
ctx = server.request_context
635636
db = ctx.lifespan_context["db"]
636637
return await db.query(arguments["query"])
637638
```

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ mcp = "mcp.cli:app [cli]"
4444
[tool.uv]
4545
resolution = "lowest-direct"
4646
default-groups = ["dev", "docs"]
47+
required-version = ">=0.7.2"
4748

4849
[dependency-groups]
4950
dev = [
@@ -55,6 +56,7 @@ dev = [
5556
"pytest-xdist>=3.6.1",
5657
"pytest-examples>=0.0.14",
5758
"pytest-pretty>=1.2.0",
59+
"inline-snapshot>=0.23.0",
5860
]
5961
docs = [
6062
"mkdocs>=1.6.1",
@@ -63,7 +65,6 @@ docs = [
6365
"mkdocstrings-python>=1.12.2",
6466
]
6567

66-
6768
[build-system]
6869
requires = ["hatchling", "uv-dynamic-versioning"]
6970
build-backend = "hatchling.build"

src/mcp/server/auth/routes.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -166,31 +166,19 @@ def create_auth_routes(
166166
return routes
167167

168168

169-
def modify_url_path(url: AnyHttpUrl, path_mapper: Callable[[str], str]) -> AnyHttpUrl:
170-
return AnyHttpUrl.build(
171-
scheme=url.scheme,
172-
username=url.username,
173-
password=url.password,
174-
host=url.host,
175-
port=url.port,
176-
path=path_mapper(url.path or ""),
177-
query=url.query,
178-
fragment=url.fragment,
179-
)
180-
181-
182169
def build_metadata(
183170
issuer_url: AnyHttpUrl,
184171
service_documentation_url: AnyHttpUrl | None,
185172
client_registration_options: ClientRegistrationOptions,
186173
revocation_options: RevocationOptions,
187174
) -> OAuthMetadata:
188-
authorization_url = modify_url_path(
189-
issuer_url, lambda path: path.rstrip("/") + AUTHORIZATION_PATH.lstrip("/")
175+
authorization_url = AnyHttpUrl(
176+
str(issuer_url).rstrip("/") + AUTHORIZATION_PATH
190177
)
191-
token_url = modify_url_path(
192-
issuer_url, lambda path: path.rstrip("/") + TOKEN_PATH.lstrip("/")
178+
token_url = AnyHttpUrl(
179+
str(issuer_url).rstrip("/") + TOKEN_PATH
193180
)
181+
194182
# Create metadata
195183
metadata = OAuthMetadata(
196184
issuer=issuer_url,
@@ -212,14 +200,14 @@ def build_metadata(
212200

213201
# Add registration endpoint if supported
214202
if client_registration_options.enabled:
215-
metadata.registration_endpoint = modify_url_path(
216-
issuer_url, lambda path: path.rstrip("/") + REGISTRATION_PATH.lstrip("/")
203+
metadata.registration_endpoint = AnyHttpUrl(
204+
str(issuer_url).rstrip("/") + REGISTRATION_PATH
217205
)
218206

219207
# Add revocation endpoint if supported
220208
if revocation_options.enabled:
221-
metadata.revocation_endpoint = modify_url_path(
222-
issuer_url, lambda path: path.rstrip("/") + REVOCATION_PATH.lstrip("/")
209+
metadata.revocation_endpoint = AnyHttpUrl(
210+
str(issuer_url).rstrip("/") + REVOCATION_PATH
223211
)
224212
metadata.revocation_endpoint_auth_methods_supported = ["client_secret_post"]
225213

tests/client/test_auth.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010

1111
import httpx
1212
import pytest
13+
from inline_snapshot import snapshot
1314
from pydantic import AnyHttpUrl
1415

1516
from mcp.client.auth import OAuthClientProvider
17+
from mcp.server.auth.routes import build_metadata
18+
from mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions
1619
from mcp.shared.auth import (
1720
OAuthClientInformationFull,
1821
OAuthClientMetadata,
@@ -905,3 +908,76 @@ async def test_token_exchange_error_basic(self, oauth_provider, oauth_client_inf
905908
await oauth_provider._exchange_code_for_token(
906909
"invalid_auth_code", oauth_client_info
907910
)
911+
912+
913+
@pytest.mark.parametrize(
914+
(
915+
"issuer_url",
916+
"service_documentation_url",
917+
"authorization_endpoint",
918+
"token_endpoint",
919+
"registration_endpoint",
920+
"revocation_endpoint",
921+
),
922+
(
923+
pytest.param(
924+
"https://auth.example.com",
925+
"https://auth.example.com/docs",
926+
"https://auth.example.com/authorize",
927+
"https://auth.example.com/token",
928+
"https://auth.example.com/register",
929+
"https://auth.example.com/revoke",
930+
id="simple-url",
931+
),
932+
pytest.param(
933+
"https://auth.example.com/",
934+
"https://auth.example.com/docs",
935+
"https://auth.example.com/authorize",
936+
"https://auth.example.com/token",
937+
"https://auth.example.com/register",
938+
"https://auth.example.com/revoke",
939+
id="with-trailing-slash",
940+
),
941+
pytest.param(
942+
"https://auth.example.com/v1/mcp",
943+
"https://auth.example.com/v1/mcp/docs",
944+
"https://auth.example.com/v1/mcp/authorize",
945+
"https://auth.example.com/v1/mcp/token",
946+
"https://auth.example.com/v1/mcp/register",
947+
"https://auth.example.com/v1/mcp/revoke",
948+
id="with-path-param",
949+
),
950+
),
951+
)
952+
def test_build_metadata(
953+
issuer_url: str,
954+
service_documentation_url: str,
955+
authorization_endpoint: str,
956+
token_endpoint: str,
957+
registration_endpoint: str,
958+
revocation_endpoint: str,
959+
):
960+
metadata = build_metadata(
961+
issuer_url=AnyHttpUrl(issuer_url),
962+
service_documentation_url=AnyHttpUrl(service_documentation_url),
963+
client_registration_options=ClientRegistrationOptions(
964+
enabled=True, valid_scopes=["read", "write", "admin"]
965+
),
966+
revocation_options=RevocationOptions(enabled=True),
967+
)
968+
969+
assert metadata == snapshot(
970+
OAuthMetadata(
971+
issuer=AnyHttpUrl(issuer_url),
972+
authorization_endpoint=AnyHttpUrl(authorization_endpoint),
973+
token_endpoint=AnyHttpUrl(token_endpoint),
974+
registration_endpoint=AnyHttpUrl(registration_endpoint),
975+
scopes_supported=["read", "write", "admin"],
976+
grant_types_supported=["authorization_code", "refresh_token"],
977+
token_endpoint_auth_methods_supported=["client_secret_post"],
978+
service_documentation=AnyHttpUrl(service_documentation_url),
979+
revocation_endpoint=AnyHttpUrl(revocation_endpoint),
980+
revocation_endpoint_auth_methods_supported=["client_secret_post"],
981+
code_challenge_methods_supported=["S256"],
982+
)
983+
)

0 commit comments

Comments
 (0)