Skip to content

Commit 87e42cf

Browse files
Merge branch 'main' into feat/sampling-resources
2 parents 7cf0cc2 + 9dad266 commit 87e42cf

File tree

9 files changed

+807
-699
lines changed

9 files changed

+807
-699
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/client/session_group.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,6 @@ async def __aexit__(
154154
for exit_stack in self._session_exit_stacks.values():
155155
tg.start_soon(exit_stack.aclose)
156156

157-
158157
@property
159158
def sessions(self) -> list[mcp.ClientSession]:
160159
"""Returns the list of sessions being managed."""

src/mcp/server/auth/routes.py

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -147,31 +147,15 @@ def create_auth_routes(
147147
return routes
148148

149149

150-
def modify_url_path(url: AnyHttpUrl, path_mapper: Callable[[str], str]) -> AnyHttpUrl:
151-
return AnyHttpUrl.build(
152-
scheme=url.scheme,
153-
username=url.username,
154-
password=url.password,
155-
host=url.host,
156-
port=url.port,
157-
path=path_mapper(url.path or ""),
158-
query=url.query,
159-
fragment=url.fragment,
160-
)
161-
162-
163150
def build_metadata(
164151
issuer_url: AnyHttpUrl,
165152
service_documentation_url: AnyHttpUrl | None,
166153
client_registration_options: ClientRegistrationOptions,
167154
revocation_options: RevocationOptions,
168155
) -> OAuthMetadata:
169-
authorization_url = modify_url_path(
170-
issuer_url, lambda path: path.rstrip("/") + AUTHORIZATION_PATH.lstrip("/")
171-
)
172-
token_url = modify_url_path(
173-
issuer_url, lambda path: path.rstrip("/") + TOKEN_PATH.lstrip("/")
174-
)
156+
authorization_url = AnyHttpUrl(str(issuer_url).rstrip("/") + AUTHORIZATION_PATH)
157+
token_url = AnyHttpUrl(str(issuer_url).rstrip("/") + TOKEN_PATH)
158+
175159
# Create metadata
176160
metadata = OAuthMetadata(
177161
issuer=issuer_url,
@@ -193,14 +177,14 @@ def build_metadata(
193177

194178
# Add registration endpoint if supported
195179
if client_registration_options.enabled:
196-
metadata.registration_endpoint = modify_url_path(
197-
issuer_url, lambda path: path.rstrip("/") + REGISTRATION_PATH.lstrip("/")
180+
metadata.registration_endpoint = AnyHttpUrl(
181+
str(issuer_url).rstrip("/") + REGISTRATION_PATH
198182
)
199183

200184
# Add revocation endpoint if supported
201185
if revocation_options.enabled:
202-
metadata.revocation_endpoint = modify_url_path(
203-
issuer_url, lambda path: path.rstrip("/") + REVOCATION_PATH.lstrip("/")
186+
metadata.revocation_endpoint = AnyHttpUrl(
187+
str(issuer_url).rstrip("/") + REVOCATION_PATH
204188
)
205189
metadata.revocation_endpoint_auth_methods_supported = ["client_secret_post"]
206190

tests/client/test_auth.py

Lines changed: 80 additions & 2 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,
@@ -356,7 +359,8 @@ def test_has_valid_token_valid(self, oauth_provider, oauth_token):
356359

357360
assert oauth_provider._has_valid_token()
358361

359-
def test_has_valid_token_expired(self, oauth_provider, oauth_token):
362+
@pytest.mark.anyio
363+
async def test_has_valid_token_expired(self, oauth_provider, oauth_token):
360364
"""Test token validation with expired token."""
361365
oauth_provider._current_tokens = oauth_token
362366
oauth_provider._token_expiry_time = time.time() - 3600 # Past expiry
@@ -807,7 +811,8 @@ def test_scope_priority_no_client_metadata_scope(
807811
# No scope should be set since client metadata doesn't have explicit scope
808812
assert "scope" not in auth_params
809813

810-
def test_scope_priority_no_scope(self, oauth_provider, oauth_client_info):
814+
@pytest.mark.anyio
815+
async def test_scope_priority_no_scope(self, oauth_provider, oauth_client_info):
811816
"""Test that no scope parameter is set when no scopes specified."""
812817
oauth_provider.client_metadata.scope = None
813818
oauth_provider._client_info = oauth_client_info
@@ -905,3 +910,76 @@ async def test_token_exchange_error_basic(self, oauth_provider, oauth_client_inf
905910
await oauth_provider._exchange_code_for_token(
906911
"invalid_auth_code", oauth_client_info
907912
)
913+
914+
915+
@pytest.mark.parametrize(
916+
(
917+
"issuer_url",
918+
"service_documentation_url",
919+
"authorization_endpoint",
920+
"token_endpoint",
921+
"registration_endpoint",
922+
"revocation_endpoint",
923+
),
924+
(
925+
pytest.param(
926+
"https://auth.example.com",
927+
"https://auth.example.com/docs",
928+
"https://auth.example.com/authorize",
929+
"https://auth.example.com/token",
930+
"https://auth.example.com/register",
931+
"https://auth.example.com/revoke",
932+
id="simple-url",
933+
),
934+
pytest.param(
935+
"https://auth.example.com/",
936+
"https://auth.example.com/docs",
937+
"https://auth.example.com/authorize",
938+
"https://auth.example.com/token",
939+
"https://auth.example.com/register",
940+
"https://auth.example.com/revoke",
941+
id="with-trailing-slash",
942+
),
943+
pytest.param(
944+
"https://auth.example.com/v1/mcp",
945+
"https://auth.example.com/v1/mcp/docs",
946+
"https://auth.example.com/v1/mcp/authorize",
947+
"https://auth.example.com/v1/mcp/token",
948+
"https://auth.example.com/v1/mcp/register",
949+
"https://auth.example.com/v1/mcp/revoke",
950+
id="with-path-param",
951+
),
952+
),
953+
)
954+
def test_build_metadata(
955+
issuer_url: str,
956+
service_documentation_url: str,
957+
authorization_endpoint: str,
958+
token_endpoint: str,
959+
registration_endpoint: str,
960+
revocation_endpoint: str,
961+
):
962+
metadata = build_metadata(
963+
issuer_url=AnyHttpUrl(issuer_url),
964+
service_documentation_url=AnyHttpUrl(service_documentation_url),
965+
client_registration_options=ClientRegistrationOptions(
966+
enabled=True, valid_scopes=["read", "write", "admin"]
967+
),
968+
revocation_options=RevocationOptions(enabled=True),
969+
)
970+
971+
assert metadata == snapshot(
972+
OAuthMetadata(
973+
issuer=AnyHttpUrl(issuer_url),
974+
authorization_endpoint=AnyHttpUrl(authorization_endpoint),
975+
token_endpoint=AnyHttpUrl(token_endpoint),
976+
registration_endpoint=AnyHttpUrl(registration_endpoint),
977+
scopes_supported=["read", "write", "admin"],
978+
grant_types_supported=["authorization_code", "refresh_token"],
979+
token_endpoint_auth_methods_supported=["client_secret_post"],
980+
service_documentation=AnyHttpUrl(service_documentation_url),
981+
revocation_endpoint=AnyHttpUrl(revocation_endpoint),
982+
revocation_endpoint_auth_methods_supported=["client_secret_post"],
983+
code_challenge_methods_supported=["S256"],
984+
)
985+
)

0 commit comments

Comments
 (0)