mcp2cli 2.5.0__tar.gz → 2.6.0__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.0}/PKG-INFO +1 -1
- {mcp2cli-2.5.0 → mcp2cli-2.6.0}/pyproject.toml +1 -1
- {mcp2cli-2.5.0 → mcp2cli-2.6.0}/src/mcp2cli/__init__.py +100 -14
- {mcp2cli-2.5.0 → mcp2cli-2.6.0}/README.md +0 -0
- {mcp2cli-2.5.0 → mcp2cli-2.6.0}/src/mcp2cli/__main__.py +0 -0
- {mcp2cli-2.5.0 → mcp2cli-2.6.0}/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"
|
|
@@ -467,21 +483,34 @@ def build_oauth_provider(
|
|
|
467
483
|
client_secret: str | None = None,
|
|
468
484
|
scope: str | None = None,
|
|
469
485
|
redirect_uri: str | None = None,
|
|
486
|
+
flow: str = "auto",
|
|
470
487
|
) -> "httpx.Auth":
|
|
471
488
|
"""Build an OAuth provider for HTTP connections.
|
|
472
489
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
490
|
+
The ``flow`` parameter controls which grant type is used:
|
|
491
|
+
|
|
492
|
+
- ``"auto"`` (default): client_id + client_secret → client credentials;
|
|
493
|
+
client_id only → authorization code + PKCE (public client);
|
|
494
|
+
neither → authorization code + PKCE with dynamic client registration.
|
|
495
|
+
- ``"authorization_code"``: always use authorization code + PKCE.
|
|
496
|
+
When a client_secret is also provided the token-endpoint request
|
|
497
|
+
authenticates as a confidential client (``client_secret_post``).
|
|
498
|
+
This is required by servers like Slack that issue confidential OAuth
|
|
499
|
+
clients but only support the authorization-code grant.
|
|
500
|
+
- ``"client_credentials"``: always use client credentials (requires both
|
|
501
|
+
client_id and client_secret).
|
|
478
502
|
|
|
479
503
|
redirect_uri controls the full callback URL (scheme, host, port, path).
|
|
480
504
|
When None, defaults to http://127.0.0.1:<random-free-port>/callback.
|
|
481
505
|
"""
|
|
482
506
|
storage = FileTokenStorage(server_url)
|
|
483
507
|
|
|
484
|
-
|
|
508
|
+
use_client_credentials = (
|
|
509
|
+
flow == "client_credentials"
|
|
510
|
+
or (flow == "auto" and client_id and client_secret)
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if use_client_credentials:
|
|
485
514
|
from mcp.client.auth.extensions.client_credentials import (
|
|
486
515
|
ClientCredentialsOAuthProvider,
|
|
487
516
|
)
|
|
@@ -540,10 +569,18 @@ def build_oauth_provider(
|
|
|
540
569
|
# Pre-seed storage with the caller-supplied client_id so the OAuth
|
|
541
570
|
# provider skips dynamic client registration entirely. The write is
|
|
542
571
|
# synchronous (plain file I/O) so no async context is needed here.
|
|
572
|
+
#
|
|
573
|
+
# When a client_secret is provided (confidential client, e.g. Slack),
|
|
574
|
+
# include it and use client_secret_post so the token-endpoint request
|
|
575
|
+
# sends the secret in the POST body.
|
|
576
|
+
if client_secret:
|
|
577
|
+
auth_method = "client_secret_post"
|
|
578
|
+
else:
|
|
579
|
+
auth_method = "none"
|
|
543
580
|
pre_client_info = OAuthClientInformationFull(
|
|
544
581
|
client_id=client_id,
|
|
545
|
-
client_secret=
|
|
546
|
-
token_endpoint_auth_method=
|
|
582
|
+
client_secret=client_secret,
|
|
583
|
+
token_endpoint_auth_method=auth_method,
|
|
547
584
|
redirect_uris=[redirect_uri],
|
|
548
585
|
grant_types=["authorization_code", "refresh_token"],
|
|
549
586
|
response_types=["code"],
|
|
@@ -642,7 +679,10 @@ def load_openapi_spec(
|
|
|
642
679
|
is_url = source.startswith("http://") or source.startswith("https://")
|
|
643
680
|
|
|
644
681
|
if is_url:
|
|
645
|
-
key = cache_key or cache_key_for(
|
|
682
|
+
key = cache_key or cache_key_for({
|
|
683
|
+
'source': source,
|
|
684
|
+
'auth_headers': auth_headers,
|
|
685
|
+
})
|
|
646
686
|
if not refresh:
|
|
647
687
|
cached = load_cached(key, ttl)
|
|
648
688
|
if cached is not None:
|
|
@@ -1024,7 +1064,10 @@ def load_graphql_schema(
|
|
|
1024
1064
|
oauth_provider: "httpx.Auth | None" = None,
|
|
1025
1065
|
) -> dict:
|
|
1026
1066
|
"""POST introspection query to a GraphQL endpoint, with caching."""
|
|
1027
|
-
key = cache_key or cache_key_for(
|
|
1067
|
+
key = cache_key or cache_key_for({
|
|
1068
|
+
'source': f"graphql:{url}",
|
|
1069
|
+
'auth_headers': auth_headers,
|
|
1070
|
+
})
|
|
1028
1071
|
if not refresh:
|
|
1029
1072
|
cached = load_cached(key, ttl)
|
|
1030
1073
|
if cached is not None:
|
|
@@ -1442,6 +1485,8 @@ def _baked_to_argv(config: dict) -> list[str]:
|
|
|
1442
1485
|
argv += ["--oauth-scope", config["oauth_scope"]]
|
|
1443
1486
|
if config.get("oauth_redirect_uri"):
|
|
1444
1487
|
argv += ["--oauth-redirect-uri", config["oauth_redirect_uri"]]
|
|
1488
|
+
if config.get("oauth_flow") and config["oauth_flow"] != "auto":
|
|
1489
|
+
argv += ["--oauth-flow", config["oauth_flow"]]
|
|
1445
1490
|
return argv
|
|
1446
1491
|
|
|
1447
1492
|
|
|
@@ -1490,6 +1535,17 @@ def _bake_create(argv: list[str]) -> None:
|
|
|
1490
1535
|
p.add_argument("--oauth-client-secret", default=None)
|
|
1491
1536
|
p.add_argument("--oauth-scope", default=None)
|
|
1492
1537
|
p.add_argument("--oauth-redirect-uri", default=None, metavar="URI")
|
|
1538
|
+
p.add_argument(
|
|
1539
|
+
"--oauth-flow",
|
|
1540
|
+
choices=["auto", "authorization_code", "client_credentials"],
|
|
1541
|
+
default="auto",
|
|
1542
|
+
help=(
|
|
1543
|
+
"OAuth flow to use. 'auto' (default) picks client_credentials when both "
|
|
1544
|
+
"client-id and client-secret are provided, otherwise authorization_code. "
|
|
1545
|
+
"Use 'authorization_code' to force the auth code + PKCE flow even with a "
|
|
1546
|
+
"client secret (required for confidential-client servers like Slack)."
|
|
1547
|
+
),
|
|
1548
|
+
)
|
|
1493
1549
|
p.add_argument("--include", default="", help="Comma-separated include globs")
|
|
1494
1550
|
p.add_argument("--exclude", default="", help="Comma-separated exclude globs")
|
|
1495
1551
|
p.add_argument("--methods", default="", help="Comma-separated HTTP methods")
|
|
@@ -1544,6 +1600,7 @@ def _bake_create(argv: list[str]) -> None:
|
|
|
1544
1600
|
"oauth_client_secret": args.oauth_client_secret,
|
|
1545
1601
|
"oauth_scope": args.oauth_scope,
|
|
1546
1602
|
"oauth_redirect_uri": args.oauth_redirect_uri,
|
|
1603
|
+
"oauth_flow": args.oauth_flow,
|
|
1547
1604
|
"include": [x.strip() for x in args.include.split(",") if x.strip()],
|
|
1548
1605
|
"exclude": [x.strip() for x in args.exclude.split(",") if x.strip()],
|
|
1549
1606
|
"methods": [x.strip().upper() for x in args.methods.split(",") if x.strip()],
|
|
@@ -2775,7 +2832,16 @@ def handle_mcp(
|
|
|
2775
2832
|
head: int | None = None,
|
|
2776
2833
|
verbose: bool = False,
|
|
2777
2834
|
):
|
|
2778
|
-
|
|
2835
|
+
# Build a config dict for cache key generation (future-proof)
|
|
2836
|
+
config_for_cache = {
|
|
2837
|
+
'source': source,
|
|
2838
|
+
'auth_headers': auth_headers,
|
|
2839
|
+
'transport': transport,
|
|
2840
|
+
'env_vars': env_vars,
|
|
2841
|
+
'is_stdio': is_stdio,
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
key = cache_key_override or cache_key_for(config_for_cache)
|
|
2779
2845
|
|
|
2780
2846
|
# Resource/prompt operations skip the tool flow entirely
|
|
2781
2847
|
if resource_action or prompt_action:
|
|
@@ -3107,6 +3173,17 @@ def _build_main_parser() -> argparse.ArgumentParser:
|
|
|
3107
3173
|
help="Full redirect URI for the OAuth callback (e.g. http://localhost:3334/oauth/callback). "
|
|
3108
3174
|
"Overrides the default http://127.0.0.1:<random-port>/callback.",
|
|
3109
3175
|
)
|
|
3176
|
+
pre.add_argument(
|
|
3177
|
+
"--oauth-flow",
|
|
3178
|
+
choices=["auto", "authorization_code", "client_credentials"],
|
|
3179
|
+
default="auto",
|
|
3180
|
+
help=(
|
|
3181
|
+
"OAuth flow to use. 'auto' (default) picks client_credentials when both "
|
|
3182
|
+
"client-id and client-secret are provided, otherwise authorization_code. "
|
|
3183
|
+
"Use 'authorization_code' to force the auth code + PKCE flow even with a "
|
|
3184
|
+
"client secret (required for confidential-client servers like Slack)."
|
|
3185
|
+
),
|
|
3186
|
+
)
|
|
3110
3187
|
# Resource flags
|
|
3111
3188
|
pre.add_argument(
|
|
3112
3189
|
"--list-resources", action="store_true", help="List available resources"
|
|
@@ -3220,12 +3297,21 @@ def _setup_oauth(pre_args):
|
|
|
3220
3297
|
if pre_args.oauth_client_secret
|
|
3221
3298
|
else None
|
|
3222
3299
|
)
|
|
3300
|
+
flow = getattr(pre_args, "oauth_flow", "auto")
|
|
3301
|
+
if flow == "client_credentials" and not (client_id and client_secret):
|
|
3302
|
+
print(
|
|
3303
|
+
"Error: --oauth-flow=client_credentials requires both "
|
|
3304
|
+
"--oauth-client-id and --oauth-client-secret",
|
|
3305
|
+
file=sys.stderr,
|
|
3306
|
+
)
|
|
3307
|
+
sys.exit(1)
|
|
3223
3308
|
return build_oauth_provider(
|
|
3224
3309
|
server_url,
|
|
3225
3310
|
client_id=client_id,
|
|
3226
3311
|
client_secret=client_secret,
|
|
3227
3312
|
scope=pre_args.oauth_scope,
|
|
3228
3313
|
redirect_uri=pre_args.oauth_redirect_uri,
|
|
3314
|
+
flow=flow,
|
|
3229
3315
|
)
|
|
3230
3316
|
|
|
3231
3317
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|