mcp2cli 2.5.0__tar.gz → 2.6.1__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.5.0 → mcp2cli-2.6.1}/PKG-INFO +1 -1
- {mcp2cli-2.5.0 → mcp2cli-2.6.1}/pyproject.toml +1 -1
- {mcp2cli-2.5.0 → mcp2cli-2.6.1}/src/mcp2cli/__init__.py +113 -14
- {mcp2cli-2.5.0 → mcp2cli-2.6.1}/README.md +0 -0
- {mcp2cli-2.5.0 → mcp2cli-2.6.1}/src/mcp2cli/__main__.py +0 -0
- {mcp2cli-2.5.0 → mcp2cli-2.6.1}/src/mcp2cli/py.typed +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "2.
|
|
5
|
+
__version__ = "2.6.0"
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
8
8
|
import copy
|
|
@@ -352,9 +352,25 @@ def _handle_http_error(resp) -> None:
|
|
|
352
352
|
# ---------------------------------------------------------------------------
|
|
353
353
|
|
|
354
354
|
|
|
355
|
-
def cache_key_for(
|
|
356
|
-
|
|
355
|
+
def cache_key_for(config: dict) -> str:
|
|
356
|
+
"""
|
|
357
|
+
Generate cache key from tool configuration dict.
|
|
358
|
+
|
|
359
|
+
Hashes all config fields that affect MCP server behavior to ensure
|
|
360
|
+
unique caching for tools with the same URL but different configurations.
|
|
361
|
+
"""
|
|
362
|
+
# Exclude fields that don't affect which tools are returned
|
|
363
|
+
cache_config = {
|
|
364
|
+
k: v for k, v in config.items()
|
|
365
|
+
if k not in ('cache_ttl', 'description', 'include', 'exclude', 'methods')
|
|
366
|
+
}
|
|
367
|
+
# Ensure auth_headers are sorted for stable hashing
|
|
368
|
+
if 'auth_headers' in cache_config and cache_config['auth_headers']:
|
|
369
|
+
cache_config['auth_headers'] = sorted(cache_config['auth_headers'])
|
|
357
370
|
|
|
371
|
+
return hashlib.sha256(
|
|
372
|
+
json.dumps(cache_config, sort_keys=True).encode()
|
|
373
|
+
).hexdigest()[:16]
|
|
358
374
|
|
|
359
375
|
def load_cached(key: str, ttl: int) -> dict | None:
|
|
360
376
|
path = CACHE_DIR / f"{key}.json"
|
|
@@ -465,23 +481,37 @@ def build_oauth_provider(
|
|
|
465
481
|
*,
|
|
466
482
|
client_id: str | None = None,
|
|
467
483
|
client_secret: str | None = None,
|
|
484
|
+
client_name: str = "mcp2cli",
|
|
468
485
|
scope: str | None = None,
|
|
469
486
|
redirect_uri: str | None = None,
|
|
487
|
+
flow: str = "auto",
|
|
470
488
|
) -> "httpx.Auth":
|
|
471
489
|
"""Build an OAuth provider for HTTP connections.
|
|
472
490
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
491
|
+
The ``flow`` parameter controls which grant type is used:
|
|
492
|
+
|
|
493
|
+
- ``"auto"`` (default): client_id + client_secret → client credentials;
|
|
494
|
+
client_id only → authorization code + PKCE (public client);
|
|
495
|
+
neither → authorization code + PKCE with dynamic client registration.
|
|
496
|
+
- ``"authorization_code"``: always use authorization code + PKCE.
|
|
497
|
+
When a client_secret is also provided the token-endpoint request
|
|
498
|
+
authenticates as a confidential client (``client_secret_post``).
|
|
499
|
+
This is required by servers like Slack that issue confidential OAuth
|
|
500
|
+
clients but only support the authorization-code grant.
|
|
501
|
+
- ``"client_credentials"``: always use client credentials (requires both
|
|
502
|
+
client_id and client_secret).
|
|
478
503
|
|
|
479
504
|
redirect_uri controls the full callback URL (scheme, host, port, path).
|
|
480
505
|
When None, defaults to http://127.0.0.1:<random-free-port>/callback.
|
|
481
506
|
"""
|
|
482
507
|
storage = FileTokenStorage(server_url)
|
|
483
508
|
|
|
484
|
-
|
|
509
|
+
use_client_credentials = (
|
|
510
|
+
flow == "client_credentials"
|
|
511
|
+
or (flow == "auto" and client_id and client_secret)
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
if use_client_credentials:
|
|
485
515
|
from mcp.client.auth.extensions.client_credentials import (
|
|
486
516
|
ClientCredentialsOAuthProvider,
|
|
487
517
|
)
|
|
@@ -530,6 +560,7 @@ def build_oauth_provider(
|
|
|
530
560
|
redirect_uri = f"http://127.0.0.1:{port}/callback"
|
|
531
561
|
|
|
532
562
|
client_metadata = OAuthClientMetadata(
|
|
563
|
+
client_name=client_name,
|
|
533
564
|
redirect_uris=[redirect_uri],
|
|
534
565
|
grant_types=["authorization_code", "refresh_token"],
|
|
535
566
|
response_types=["code"],
|
|
@@ -540,10 +571,18 @@ def build_oauth_provider(
|
|
|
540
571
|
# Pre-seed storage with the caller-supplied client_id so the OAuth
|
|
541
572
|
# provider skips dynamic client registration entirely. The write is
|
|
542
573
|
# synchronous (plain file I/O) so no async context is needed here.
|
|
574
|
+
#
|
|
575
|
+
# When a client_secret is provided (confidential client, e.g. Slack),
|
|
576
|
+
# include it and use client_secret_post so the token-endpoint request
|
|
577
|
+
# sends the secret in the POST body.
|
|
578
|
+
if client_secret:
|
|
579
|
+
auth_method = "client_secret_post"
|
|
580
|
+
else:
|
|
581
|
+
auth_method = "none"
|
|
543
582
|
pre_client_info = OAuthClientInformationFull(
|
|
544
583
|
client_id=client_id,
|
|
545
|
-
client_secret=
|
|
546
|
-
token_endpoint_auth_method=
|
|
584
|
+
client_secret=client_secret,
|
|
585
|
+
token_endpoint_auth_method=auth_method,
|
|
547
586
|
redirect_uris=[redirect_uri],
|
|
548
587
|
grant_types=["authorization_code", "refresh_token"],
|
|
549
588
|
response_types=["code"],
|
|
@@ -642,7 +681,10 @@ def load_openapi_spec(
|
|
|
642
681
|
is_url = source.startswith("http://") or source.startswith("https://")
|
|
643
682
|
|
|
644
683
|
if is_url:
|
|
645
|
-
key = cache_key or cache_key_for(
|
|
684
|
+
key = cache_key or cache_key_for({
|
|
685
|
+
'source': source,
|
|
686
|
+
'auth_headers': auth_headers,
|
|
687
|
+
})
|
|
646
688
|
if not refresh:
|
|
647
689
|
cached = load_cached(key, ttl)
|
|
648
690
|
if cached is not None:
|
|
@@ -1024,7 +1066,10 @@ def load_graphql_schema(
|
|
|
1024
1066
|
oauth_provider: "httpx.Auth | None" = None,
|
|
1025
1067
|
) -> dict:
|
|
1026
1068
|
"""POST introspection query to a GraphQL endpoint, with caching."""
|
|
1027
|
-
key = cache_key or cache_key_for(
|
|
1069
|
+
key = cache_key or cache_key_for({
|
|
1070
|
+
'source': f"graphql:{url}",
|
|
1071
|
+
'auth_headers': auth_headers,
|
|
1072
|
+
})
|
|
1028
1073
|
if not refresh:
|
|
1029
1074
|
cached = load_cached(key, ttl)
|
|
1030
1075
|
if cached is not None:
|
|
@@ -1438,10 +1483,14 @@ def _baked_to_argv(config: dict) -> list[str]:
|
|
|
1438
1483
|
argv += ["--oauth-client-id", config["oauth_client_id"]]
|
|
1439
1484
|
if config.get("oauth_client_secret"):
|
|
1440
1485
|
argv += ["--oauth-client-secret", config["oauth_client_secret"]]
|
|
1486
|
+
if config.get("oauth_client_name") and config["oauth_client_name"] != "mcp2cli":
|
|
1487
|
+
argv += ["--oauth-client-name", config["oauth_client_name"]]
|
|
1441
1488
|
if config.get("oauth_scope"):
|
|
1442
1489
|
argv += ["--oauth-scope", config["oauth_scope"]]
|
|
1443
1490
|
if config.get("oauth_redirect_uri"):
|
|
1444
1491
|
argv += ["--oauth-redirect-uri", config["oauth_redirect_uri"]]
|
|
1492
|
+
if config.get("oauth_flow") and config["oauth_flow"] != "auto":
|
|
1493
|
+
argv += ["--oauth-flow", config["oauth_flow"]]
|
|
1445
1494
|
return argv
|
|
1446
1495
|
|
|
1447
1496
|
|
|
@@ -1488,8 +1537,20 @@ def _bake_create(argv: list[str]) -> None:
|
|
|
1488
1537
|
p.add_argument("--oauth", action="store_true")
|
|
1489
1538
|
p.add_argument("--oauth-client-id", default=None)
|
|
1490
1539
|
p.add_argument("--oauth-client-secret", default=None)
|
|
1540
|
+
p.add_argument("--oauth-client-name", default="mcp2cli")
|
|
1491
1541
|
p.add_argument("--oauth-scope", default=None)
|
|
1492
1542
|
p.add_argument("--oauth-redirect-uri", default=None, metavar="URI")
|
|
1543
|
+
p.add_argument(
|
|
1544
|
+
"--oauth-flow",
|
|
1545
|
+
choices=["auto", "authorization_code", "client_credentials"],
|
|
1546
|
+
default="auto",
|
|
1547
|
+
help=(
|
|
1548
|
+
"OAuth flow to use. 'auto' (default) picks client_credentials when both "
|
|
1549
|
+
"client-id and client-secret are provided, otherwise authorization_code. "
|
|
1550
|
+
"Use 'authorization_code' to force the auth code + PKCE flow even with a "
|
|
1551
|
+
"client secret (required for confidential-client servers like Slack)."
|
|
1552
|
+
),
|
|
1553
|
+
)
|
|
1493
1554
|
p.add_argument("--include", default="", help="Comma-separated include globs")
|
|
1494
1555
|
p.add_argument("--exclude", default="", help="Comma-separated exclude globs")
|
|
1495
1556
|
p.add_argument("--methods", default="", help="Comma-separated HTTP methods")
|
|
@@ -1542,8 +1603,10 @@ def _bake_create(argv: list[str]) -> None:
|
|
|
1542
1603
|
"oauth": args.oauth,
|
|
1543
1604
|
"oauth_client_id": args.oauth_client_id,
|
|
1544
1605
|
"oauth_client_secret": args.oauth_client_secret,
|
|
1606
|
+
"oauth_client_name": args.oauth_client_name,
|
|
1545
1607
|
"oauth_scope": args.oauth_scope,
|
|
1546
1608
|
"oauth_redirect_uri": args.oauth_redirect_uri,
|
|
1609
|
+
"oauth_flow": args.oauth_flow,
|
|
1547
1610
|
"include": [x.strip() for x in args.include.split(",") if x.strip()],
|
|
1548
1611
|
"exclude": [x.strip() for x in args.exclude.split(",") if x.strip()],
|
|
1549
1612
|
"methods": [x.strip().upper() for x in args.methods.split(",") if x.strip()],
|
|
@@ -2775,7 +2838,16 @@ def handle_mcp(
|
|
|
2775
2838
|
head: int | None = None,
|
|
2776
2839
|
verbose: bool = False,
|
|
2777
2840
|
):
|
|
2778
|
-
|
|
2841
|
+
# Build a config dict for cache key generation (future-proof)
|
|
2842
|
+
config_for_cache = {
|
|
2843
|
+
'source': source,
|
|
2844
|
+
'auth_headers': auth_headers,
|
|
2845
|
+
'transport': transport,
|
|
2846
|
+
'env_vars': env_vars,
|
|
2847
|
+
'is_stdio': is_stdio,
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
key = cache_key_override or cache_key_for(config_for_cache)
|
|
2779
2851
|
|
|
2780
2852
|
# Resource/prompt operations skip the tool flow entirely
|
|
2781
2853
|
if resource_action or prompt_action:
|
|
@@ -3095,6 +3167,12 @@ def _build_main_parser() -> argparse.ArgumentParser:
|
|
|
3095
3167
|
default=None,
|
|
3096
3168
|
help="OAuth client secret — supports env:VAR and file:/path prefixes",
|
|
3097
3169
|
)
|
|
3170
|
+
pre.add_argument(
|
|
3171
|
+
"--oauth-client-name",
|
|
3172
|
+
default="mcp2cli",
|
|
3173
|
+
help="Client name sent during OAuth Dynamic Client Registration (default: mcp2cli). "
|
|
3174
|
+
"Some servers require a specific client name for DCR to succeed.",
|
|
3175
|
+
)
|
|
3098
3176
|
pre.add_argument(
|
|
3099
3177
|
"--oauth-scope",
|
|
3100
3178
|
default=None,
|
|
@@ -3107,6 +3185,17 @@ def _build_main_parser() -> argparse.ArgumentParser:
|
|
|
3107
3185
|
help="Full redirect URI for the OAuth callback (e.g. http://localhost:3334/oauth/callback). "
|
|
3108
3186
|
"Overrides the default http://127.0.0.1:<random-port>/callback.",
|
|
3109
3187
|
)
|
|
3188
|
+
pre.add_argument(
|
|
3189
|
+
"--oauth-flow",
|
|
3190
|
+
choices=["auto", "authorization_code", "client_credentials"],
|
|
3191
|
+
default="auto",
|
|
3192
|
+
help=(
|
|
3193
|
+
"OAuth flow to use. 'auto' (default) picks client_credentials when both "
|
|
3194
|
+
"client-id and client-secret are provided, otherwise authorization_code. "
|
|
3195
|
+
"Use 'authorization_code' to force the auth code + PKCE flow even with a "
|
|
3196
|
+
"client secret (required for confidential-client servers like Slack)."
|
|
3197
|
+
),
|
|
3198
|
+
)
|
|
3110
3199
|
# Resource flags
|
|
3111
3200
|
pre.add_argument(
|
|
3112
3201
|
"--list-resources", action="store_true", help="List available resources"
|
|
@@ -3220,12 +3309,22 @@ def _setup_oauth(pre_args):
|
|
|
3220
3309
|
if pre_args.oauth_client_secret
|
|
3221
3310
|
else None
|
|
3222
3311
|
)
|
|
3312
|
+
flow = getattr(pre_args, "oauth_flow", "auto")
|
|
3313
|
+
if flow == "client_credentials" and not (client_id and client_secret):
|
|
3314
|
+
print(
|
|
3315
|
+
"Error: --oauth-flow=client_credentials requires both "
|
|
3316
|
+
"--oauth-client-id and --oauth-client-secret",
|
|
3317
|
+
file=sys.stderr,
|
|
3318
|
+
)
|
|
3319
|
+
sys.exit(1)
|
|
3223
3320
|
return build_oauth_provider(
|
|
3224
3321
|
server_url,
|
|
3225
3322
|
client_id=client_id,
|
|
3226
3323
|
client_secret=client_secret,
|
|
3324
|
+
client_name=getattr(pre_args, "oauth_client_name", "mcp2cli"),
|
|
3227
3325
|
scope=pre_args.oauth_scope,
|
|
3228
3326
|
redirect_uri=pre_args.oauth_redirect_uri,
|
|
3327
|
+
flow=flow,
|
|
3229
3328
|
)
|
|
3230
3329
|
|
|
3231
3330
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|