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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp2cli
3
- Version: 2.4.1
3
+ Version: 2.4.3
4
4
  Summary: Turn any MCP server or OpenAPI spec into a CLI
5
5
  Author: Stephan Fitzpatrick
6
6
  Author-email: Stephan Fitzpatrick <stephan@knowsuchagency.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp2cli"
3
- version = "2.4.1"
3
+ version = "2.4.3"
4
4
  description = "Turn any MCP server or OpenAPI spec into a CLI"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -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
- If client_id and client_secret are provided, uses client credentials flow.
473
- Otherwise, uses authorization code + PKCE with a local callback server.
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
- port = _find_free_port()
494
- redirect_uri = f"http://127.0.0.1:{port}/callback"
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
- server = HTTPServer(("127.0.0.1", port), _CallbackHandler)
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