mcp2cli 2.4.1__tar.gz → 2.4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mcp2cli-2.4.1 → mcp2cli-2.4.3}/PKG-INFO +1 -1
- {mcp2cli-2.4.1 → mcp2cli-2.4.3}/pyproject.toml +1 -1
- {mcp2cli-2.4.1 → mcp2cli-2.4.3}/src/mcp2cli/__init__.py +77 -12
- {mcp2cli-2.4.1 → mcp2cli-2.4.3}/README.md +0 -0
- {mcp2cli-2.4.1 → mcp2cli-2.4.3}/src/mcp2cli/__main__.py +0 -0
- {mcp2cli-2.4.1 → mcp2cli-2.4.3}/src/mcp2cli/py.typed +0 -0
|
@@ -466,11 +466,18 @@ def build_oauth_provider(
|
|
|
466
466
|
client_id: str | None = None,
|
|
467
467
|
client_secret: str | None = None,
|
|
468
468
|
scope: str | None = None,
|
|
469
|
+
redirect_uri: str | None = None,
|
|
469
470
|
) -> "httpx.Auth":
|
|
470
471
|
"""Build an OAuth provider for HTTP connections.
|
|
471
472
|
|
|
472
|
-
|
|
473
|
-
|
|
473
|
+
- client_id + client_secret → client credentials flow (machine-to-machine).
|
|
474
|
+
- client_id only → authorization code + PKCE, pre-configured client
|
|
475
|
+
(no dynamic client registration).
|
|
476
|
+
- neither → authorization code + PKCE with dynamic client
|
|
477
|
+
registration.
|
|
478
|
+
|
|
479
|
+
redirect_uri controls the full callback URL (scheme, host, port, path).
|
|
480
|
+
When None, defaults to http://127.0.0.1:<random-free-port>/callback.
|
|
474
481
|
"""
|
|
475
482
|
storage = FileTokenStorage(server_url)
|
|
476
483
|
|
|
@@ -488,10 +495,39 @@ def build_oauth_provider(
|
|
|
488
495
|
)
|
|
489
496
|
|
|
490
497
|
from mcp.client.auth.oauth2 import OAuthClientProvider
|
|
491
|
-
from mcp.shared.auth import OAuthClientMetadata
|
|
498
|
+
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata
|
|
499
|
+
|
|
500
|
+
_LOOPBACK_HOSTS = {"localhost", "127.0.0.1", "::1"}
|
|
492
501
|
|
|
493
|
-
|
|
494
|
-
|
|
502
|
+
if redirect_uri is not None:
|
|
503
|
+
parsed = urlparse(redirect_uri)
|
|
504
|
+
if parsed.scheme != "http":
|
|
505
|
+
print(
|
|
506
|
+
f"Error: --oauth-redirect-uri must use http://, got '{parsed.scheme}://'. "
|
|
507
|
+
"The local callback server is plain HTTP; use http://<host>:<port>/path.",
|
|
508
|
+
file=sys.stderr,
|
|
509
|
+
)
|
|
510
|
+
sys.exit(1)
|
|
511
|
+
if parsed.port is None:
|
|
512
|
+
print(
|
|
513
|
+
"Error: --oauth-redirect-uri must include an explicit port number "
|
|
514
|
+
"(e.g. http://localhost:3334/oauth/callback).",
|
|
515
|
+
file=sys.stderr,
|
|
516
|
+
)
|
|
517
|
+
sys.exit(1)
|
|
518
|
+
if (parsed.hostname or "") not in _LOOPBACK_HOSTS:
|
|
519
|
+
print(
|
|
520
|
+
f"Error: --oauth-redirect-uri host must be a loopback address "
|
|
521
|
+
f"(localhost, 127.0.0.1, or ::1), got '{parsed.hostname}'.",
|
|
522
|
+
file=sys.stderr,
|
|
523
|
+
)
|
|
524
|
+
sys.exit(1)
|
|
525
|
+
callback_host = parsed.hostname
|
|
526
|
+
port = parsed.port
|
|
527
|
+
else:
|
|
528
|
+
port = _find_free_port()
|
|
529
|
+
callback_host = "127.0.0.1"
|
|
530
|
+
redirect_uri = f"http://127.0.0.1:{port}/callback"
|
|
495
531
|
|
|
496
532
|
client_metadata = OAuthClientMetadata(
|
|
497
533
|
redirect_uris=[redirect_uri],
|
|
@@ -500,13 +536,36 @@ def build_oauth_provider(
|
|
|
500
536
|
scope=scope,
|
|
501
537
|
)
|
|
502
538
|
|
|
539
|
+
if client_id:
|
|
540
|
+
# Pre-seed storage with the caller-supplied client_id so the OAuth
|
|
541
|
+
# provider skips dynamic client registration entirely. The write is
|
|
542
|
+
# synchronous (plain file I/O) so no async context is needed here.
|
|
543
|
+
pre_client_info = OAuthClientInformationFull(
|
|
544
|
+
client_id=client_id,
|
|
545
|
+
client_secret=None,
|
|
546
|
+
token_endpoint_auth_method="none",
|
|
547
|
+
redirect_uris=[redirect_uri],
|
|
548
|
+
grant_types=["authorization_code", "refresh_token"],
|
|
549
|
+
response_types=["code"],
|
|
550
|
+
scope=scope,
|
|
551
|
+
)
|
|
552
|
+
storage._client_path.write_text(pre_client_info.model_dump_json())
|
|
553
|
+
|
|
503
554
|
# Reset callback handler state
|
|
504
555
|
_CallbackHandler.auth_code = None
|
|
505
556
|
_CallbackHandler.state = None
|
|
506
557
|
_CallbackHandler.error = None
|
|
507
558
|
_CallbackHandler.done = threading.Event()
|
|
508
559
|
|
|
509
|
-
|
|
560
|
+
if callback_host == "::1":
|
|
561
|
+
import socket as _socket
|
|
562
|
+
|
|
563
|
+
class _IPv6HTTPServer(HTTPServer):
|
|
564
|
+
address_family = _socket.AF_INET6
|
|
565
|
+
|
|
566
|
+
server = _IPv6HTTPServer((callback_host, port), _CallbackHandler)
|
|
567
|
+
else:
|
|
568
|
+
server = HTTPServer((callback_host, port), _CallbackHandler)
|
|
510
569
|
|
|
511
570
|
async def redirect_handler(auth_url: str) -> None:
|
|
512
571
|
print(f"Opening browser for authorization...", file=sys.stderr)
|
|
@@ -1353,6 +1412,8 @@ def _baked_to_argv(config: dict) -> list[str]:
|
|
|
1353
1412
|
argv += ["--oauth-client-secret", config["oauth_client_secret"]]
|
|
1354
1413
|
if config.get("oauth_scope"):
|
|
1355
1414
|
argv += ["--oauth-scope", config["oauth_scope"]]
|
|
1415
|
+
if config.get("oauth_redirect_uri"):
|
|
1416
|
+
argv += ["--oauth-redirect-uri", config["oauth_redirect_uri"]]
|
|
1356
1417
|
return argv
|
|
1357
1418
|
|
|
1358
1419
|
|
|
@@ -1400,6 +1461,7 @@ def _bake_create(argv: list[str]) -> None:
|
|
|
1400
1461
|
p.add_argument("--oauth-client-id", default=None)
|
|
1401
1462
|
p.add_argument("--oauth-client-secret", default=None)
|
|
1402
1463
|
p.add_argument("--oauth-scope", default=None)
|
|
1464
|
+
p.add_argument("--oauth-redirect-uri", default=None, metavar="URI")
|
|
1403
1465
|
p.add_argument("--include", default="", help="Comma-separated include globs")
|
|
1404
1466
|
p.add_argument("--exclude", default="", help="Comma-separated exclude globs")
|
|
1405
1467
|
p.add_argument("--methods", default="", help="Comma-separated HTTP methods")
|
|
@@ -1453,6 +1515,7 @@ def _bake_create(argv: list[str]) -> None:
|
|
|
1453
1515
|
"oauth_client_id": args.oauth_client_id,
|
|
1454
1516
|
"oauth_client_secret": args.oauth_client_secret,
|
|
1455
1517
|
"oauth_scope": args.oauth_scope,
|
|
1518
|
+
"oauth_redirect_uri": args.oauth_redirect_uri,
|
|
1456
1519
|
"include": [x.strip() for x in args.include.split(",") if x.strip()],
|
|
1457
1520
|
"exclude": [x.strip() for x in args.exclude.split(",") if x.strip()],
|
|
1458
1521
|
"methods": [x.strip().upper() for x in args.methods.split(",") if x.strip()],
|
|
@@ -2985,6 +3048,13 @@ def _build_main_parser() -> argparse.ArgumentParser:
|
|
|
2985
3048
|
default=None,
|
|
2986
3049
|
help="OAuth scope(s) to request",
|
|
2987
3050
|
)
|
|
3051
|
+
pre.add_argument(
|
|
3052
|
+
"--oauth-redirect-uri",
|
|
3053
|
+
default=None,
|
|
3054
|
+
metavar="URI",
|
|
3055
|
+
help="Full redirect URI for the OAuth callback (e.g. http://localhost:3334/oauth/callback). "
|
|
3056
|
+
"Overrides the default http://127.0.0.1:<random-port>/callback.",
|
|
3057
|
+
)
|
|
2988
3058
|
# Resource flags
|
|
2989
3059
|
pre.add_argument(
|
|
2990
3060
|
"--list-resources", action="store_true", help="List available resources"
|
|
@@ -3066,12 +3136,6 @@ def _setup_oauth(pre_args):
|
|
|
3066
3136
|
if not use_oauth:
|
|
3067
3137
|
return None
|
|
3068
3138
|
|
|
3069
|
-
if pre_args.oauth_client_id and not pre_args.oauth_client_secret:
|
|
3070
|
-
print(
|
|
3071
|
-
"Error: --oauth-client-secret is required with --oauth-client-id",
|
|
3072
|
-
file=sys.stderr,
|
|
3073
|
-
)
|
|
3074
|
-
sys.exit(1)
|
|
3075
3139
|
if pre_args.oauth_client_secret and not pre_args.oauth_client_id:
|
|
3076
3140
|
print(
|
|
3077
3141
|
"Error: --oauth-client-id is required with --oauth-client-secret",
|
|
@@ -3109,6 +3173,7 @@ def _setup_oauth(pre_args):
|
|
|
3109
3173
|
client_id=client_id,
|
|
3110
3174
|
client_secret=client_secret,
|
|
3111
3175
|
scope=pre_args.oauth_scope,
|
|
3176
|
+
redirect_uri=pre_args.oauth_redirect_uri,
|
|
3112
3177
|
)
|
|
3113
3178
|
|
|
3114
3179
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|