Skip to content

Commit 4f23ce6

Browse files
DX-122122: normalize MCP URL as cache key; add redirect-path/port to cli commands
- Add _normalize_url() that prepends https:// when scheme is absent and strips trailing slashes, so the cache key is canonical regardless of how the caller spells the URL. - Apply normalization in every code path that reads or writes the auth cache: _resolve_token, _handle_token_expired, check_auth, cache_update, cache_show. - Add --redirect-port and --redirect-path options to `cli list-tools` and `cli call-tool` so OAuth PKCE flows can specify a custom redirect (e.g. --redirect-path /Callback required by Dremio connectors). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent a9bba6b commit 4f23ce6

1 file changed

Lines changed: 31 additions & 2 deletions

File tree

tests/stremable_http_cli.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def _resolve_token(
170170
Returns ``(token, AuthCache)``. The cache is empty when the caller supplied
171171
an explicit token.
172172
"""
173+
url = _normalize_url(url)
173174
if explicit_token is not None:
174175
return explicit_token, AuthCache()
175176

@@ -211,6 +212,7 @@ def _handle_token_expired(url: str, cache: AuthCache) -> str | None:
211212
On success writes the new token to cache and returns it.
212213
Returns ``None`` if the cache lacks a refresh token or the refresh fails.
213214
"""
215+
url = _normalize_url(url)
214216
if not (cache.refresh_token and cache.client_id):
215217
return None
216218
try:
@@ -230,7 +232,7 @@ def _handle_token_expired(url: str, cache: AuthCache) -> str | None:
230232
return None
231233

232234

233-
def with_auth(fn: "Callable[..., Awaitable[Any]]") -> "Callable[..., Awaitable[Any]]":
235+
def with_auth(fn: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
234236
"""Decorator: resolve bearer token (explicit → cache → OAuth) and retry once on 401.
235237
236238
The wrapped function's ``token`` kwarg is replaced with the resolved token
@@ -290,6 +292,17 @@ async def wrapper(*args: Any, **kwargs: Any) -> Any:
290292
)
291293

292294

295+
def _normalize_url(url: str) -> str:
296+
"""Canonicalize *url* so it is always usable as an unambiguous cache key.
297+
298+
* Prepends ``https://`` when no scheme is present.
299+
* Strips a trailing ``/`` to avoid ``/mcp`` vs ``/mcp/`` mismatches.
300+
"""
301+
if url and not url.startswith(("http://", "https://")):
302+
url = f"https://{url}"
303+
return url.rstrip("/")
304+
305+
293306
def get_oauth_config(url: str) -> OAuthMetadata:
294307
u = urlparse(url)
295308
u = u._replace(path="/.well-known/oauth-authorization-server")
@@ -345,6 +358,7 @@ def check_auth(
345358
str, Option(help="Path for OAuth redirect (e.g. /Callback)")
346359
] = "/",
347360
) -> OAuth2Redirect:
361+
url = _normalize_url(url or "http://127.0.0.1:8000/mcp")
348362
md = get_oauth_config(url)
349363
oauth = get_oauth2_tokens(
350364
client_id,
@@ -353,7 +367,7 @@ def check_auth(
353367
redirect_port,
354368
redirect_path,
355369
)
356-
if url and oauth.access_token:
370+
if oauth.access_token:
357371
cache_update(
358372
url=url,
359373
token=oauth.access_token,
@@ -546,6 +560,7 @@ def cache_update(
546560
--refresh-token rrt_... \\
547561
--client-id my-client-id
548562
"""
563+
url = _normalize_url(url)
549564
_write_auth_cache(
550565
url,
551566
AuthCache(token=token, refresh_token=refresh_token, client_id=client_id),
@@ -560,6 +575,8 @@ def cache_show(
560575
] = None,
561576
):
562577
"""Display the current auth cache contents (tokens are redacted)."""
578+
if url:
579+
url = _normalize_url(url)
563580
store = _read_auth_store()
564581
if not store.root:
565582
pp("[dim]Auth cache is empty.[/dim]")
@@ -632,6 +649,12 @@ async def list_tools(
632649
client_id: Annotated[
633650
Optional[str], Option(help="OAuth client ID (used when no --token or cached token)")
634651
] = None,
652+
redirect_port: Annotated[
653+
int, Option("--redirect-port", help="Local port for OAuth redirect listener")
654+
] = 8976,
655+
redirect_path: Annotated[
656+
str, Option("--redirect-path", help="Path for OAuth redirect (e.g. /Callback)")
657+
] = "/",
635658
):
636659
async with mcp_client_session(url, token) as session:
637660
result = await session.list_tools()
@@ -659,6 +682,12 @@ async def call_tool(
659682
client_id: Annotated[
660683
Optional[str], Option(help="OAuth client ID (used when no --token or cached token)")
661684
] = None,
685+
redirect_port: Annotated[
686+
int, Option("--redirect-port", help="Local port for OAuth redirect listener")
687+
] = 8976,
688+
redirect_path: Annotated[
689+
str, Option("--redirect-path", help="Path for OAuth redirect (e.g. /Callback)")
690+
] = "/",
662691
args: Annotated[
663692
Optional[str], Option(help="The arguments to pass to the tool as a JSON")
664693
] = None,

0 commit comments

Comments
 (0)