mcp2cli 2.3.0__tar.gz → 2.4.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.3.0 → mcp2cli-2.4.1}/PKG-INFO +1 -1
- {mcp2cli-2.3.0 → mcp2cli-2.4.1}/pyproject.toml +1 -1
- {mcp2cli-2.3.0 → mcp2cli-2.4.1}/src/mcp2cli/__init__.py +101 -28
- {mcp2cli-2.3.0 → mcp2cli-2.4.1}/README.md +0 -0
- {mcp2cli-2.3.0 → mcp2cli-2.4.1}/src/mcp2cli/__main__.py +0 -0
- {mcp2cli-2.3.0 → mcp2cli-2.4.1}/src/mcp2cli/py.typed +0 -0
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
__version__ = "2.
|
|
5
|
+
__version__ = "2.4.0"
|
|
6
6
|
|
|
7
7
|
import argparse
|
|
8
8
|
import copy
|
|
9
9
|
import hashlib
|
|
10
10
|
import json
|
|
11
|
+
import mimetypes
|
|
11
12
|
import os
|
|
12
13
|
import fnmatch
|
|
13
14
|
import re
|
|
@@ -36,6 +37,7 @@ CONFIG_DIR = Path(
|
|
|
36
37
|
os.environ.get("MCP2CLI_CONFIG_DIR", Path.home() / ".config" / "mcp2cli")
|
|
37
38
|
)
|
|
38
39
|
BAKED_FILE = CONFIG_DIR / "baked.json"
|
|
40
|
+
ARGPARSE_HELP_PERCENT_RE = re.compile(r"(?<!%)%(?![%\(])")
|
|
39
41
|
|
|
40
42
|
|
|
41
43
|
# ---------------------------------------------------------------------------
|
|
@@ -64,6 +66,7 @@ class CommandDef:
|
|
|
64
66
|
# OpenAPI
|
|
65
67
|
method: str | None = None
|
|
66
68
|
path: str | None = None
|
|
69
|
+
content_type: str | None = None # None = json, "multipart/form-data", etc.
|
|
67
70
|
# MCP
|
|
68
71
|
tool_name: str | None = None
|
|
69
72
|
# GraphQL
|
|
@@ -108,6 +111,11 @@ def resolve_secret(value: str) -> str:
|
|
|
108
111
|
return value
|
|
109
112
|
|
|
110
113
|
|
|
114
|
+
def escape_argparse_help(help_text: str) -> str:
|
|
115
|
+
"""Escape literal percent signs in help text for argparse."""
|
|
116
|
+
return ARGPARSE_HELP_PERCENT_RE.sub("%%", help_text)
|
|
117
|
+
|
|
118
|
+
|
|
111
119
|
def _parse_kv_list(
|
|
112
120
|
items: list[str],
|
|
113
121
|
delimiter: str,
|
|
@@ -322,10 +330,11 @@ def output_result(
|
|
|
322
330
|
print(json.dumps(data))
|
|
323
331
|
|
|
324
332
|
|
|
325
|
-
def _build_http_headers(auth_headers: list[tuple[str, str]]) -> dict[str, str]:
|
|
333
|
+
def _build_http_headers(auth_headers: list[tuple[str, str]], multipart: bool = False) -> dict[str, str]:
|
|
326
334
|
"""Build HTTP headers dict from auth_headers with a Content-Type default."""
|
|
327
335
|
headers = dict(auth_headers)
|
|
328
|
-
|
|
336
|
+
if not multipart:
|
|
337
|
+
headers.setdefault("Content-Type", "application/json")
|
|
329
338
|
return headers
|
|
330
339
|
|
|
331
340
|
|
|
@@ -662,19 +671,43 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
|
|
|
662
671
|
)
|
|
663
672
|
params.append(p)
|
|
664
673
|
|
|
665
|
-
# Request body
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
674
|
+
# Request body — negotiate content type
|
|
675
|
+
rb_content = details.get("requestBody", {}).get("content", {})
|
|
676
|
+
multipart_schema = rb_content.get("multipart/form-data", {}).get("schema", {})
|
|
677
|
+
json_schema = rb_content.get("application/json", {}).get("schema", {})
|
|
678
|
+
|
|
679
|
+
mp_props = multipart_schema.get("properties", {})
|
|
680
|
+
has_binary = any(
|
|
681
|
+
p.get("format") == "binary" for p in mp_props.values()
|
|
671
682
|
)
|
|
683
|
+
|
|
684
|
+
if has_binary:
|
|
685
|
+
rb_schema = multipart_schema
|
|
686
|
+
cmd_content_type = "multipart/form-data"
|
|
687
|
+
elif json_schema:
|
|
688
|
+
rb_schema = json_schema
|
|
689
|
+
cmd_content_type = None
|
|
690
|
+
elif mp_props:
|
|
691
|
+
rb_schema = multipart_schema
|
|
692
|
+
cmd_content_type = "multipart/form-data"
|
|
693
|
+
else:
|
|
694
|
+
rb_schema = {}
|
|
695
|
+
cmd_content_type = None
|
|
696
|
+
|
|
672
697
|
required_fields = set(rb_schema.get("required", []))
|
|
673
698
|
properties = rb_schema.get("properties", {})
|
|
674
699
|
has_body = bool(properties)
|
|
675
700
|
|
|
676
701
|
for prop_name, prop_schema in properties.items():
|
|
677
|
-
|
|
702
|
+
is_binary = (
|
|
703
|
+
cmd_content_type == "multipart/form-data"
|
|
704
|
+
and prop_schema.get("format") == "binary"
|
|
705
|
+
)
|
|
706
|
+
if is_binary:
|
|
707
|
+
loc, py_type, suffix = "file", str, " (file path)"
|
|
708
|
+
else:
|
|
709
|
+
py_type, suffix = schema_type_to_python(prop_schema)
|
|
710
|
+
loc = "body"
|
|
678
711
|
p = ParamDef(
|
|
679
712
|
name=to_kebab(prop_name),
|
|
680
713
|
original_name=prop_name,
|
|
@@ -682,7 +715,7 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
|
|
|
682
715
|
required=prop_name in required_fields,
|
|
683
716
|
description=(prop_schema.get("description") or prop_name) + suffix,
|
|
684
717
|
choices=prop_schema.get("enum"),
|
|
685
|
-
location=
|
|
718
|
+
location=loc,
|
|
686
719
|
)
|
|
687
720
|
params.append(p)
|
|
688
721
|
|
|
@@ -694,6 +727,7 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
|
|
|
694
727
|
has_body=has_body,
|
|
695
728
|
method=method,
|
|
696
729
|
path=path,
|
|
730
|
+
content_type=cmd_content_type,
|
|
697
731
|
)
|
|
698
732
|
)
|
|
699
733
|
|
|
@@ -1577,7 +1611,10 @@ def build_argparse(
|
|
|
1577
1611
|
subparsers = parser.add_subparsers(dest="_command")
|
|
1578
1612
|
|
|
1579
1613
|
for cmd in commands:
|
|
1580
|
-
sub = subparsers.add_parser(
|
|
1614
|
+
sub = subparsers.add_parser(
|
|
1615
|
+
cmd.name,
|
|
1616
|
+
help=escape_argparse_help(cmd.description),
|
|
1617
|
+
)
|
|
1581
1618
|
sub.set_defaults(_cmd=cmd)
|
|
1582
1619
|
|
|
1583
1620
|
if cmd.has_body:
|
|
@@ -1608,7 +1645,7 @@ def build_argparse(
|
|
|
1608
1645
|
kwargs["required"] = True
|
|
1609
1646
|
else:
|
|
1610
1647
|
kwargs.setdefault("default", None)
|
|
1611
|
-
kwargs["help"] = p.description
|
|
1648
|
+
kwargs["help"] = escape_argparse_help(p.description)
|
|
1612
1649
|
if p.choices:
|
|
1613
1650
|
kwargs["choices"] = p.choices
|
|
1614
1651
|
sub.add_argument(flag, **kwargs)
|
|
@@ -1662,16 +1699,17 @@ def _filter_commands(commands: list[CommandDef], pattern: str) -> list[CommandDe
|
|
|
1662
1699
|
def _collect_openapi_params(
|
|
1663
1700
|
cmd: CommandDef,
|
|
1664
1701
|
args: argparse.Namespace,
|
|
1665
|
-
) -> tuple[str, dict[str, str], dict[str, str], dict | None]:
|
|
1702
|
+
) -> tuple[str, dict[str, str], dict[str, str], dict | None, dict | None]:
|
|
1666
1703
|
"""Collect OpenAPI params from parsed args, separated by location.
|
|
1667
1704
|
|
|
1668
|
-
Returns (path, query_params, extra_headers, body_or_none)
|
|
1669
|
-
has ``{param}`` placeholders substituted with actual values.
|
|
1705
|
+
Returns (path, query_params, extra_headers, body_or_none, files_or_none)
|
|
1706
|
+
where *path* has ``{param}`` placeholders substituted with actual values.
|
|
1670
1707
|
"""
|
|
1671
1708
|
path = cmd.path or ""
|
|
1672
1709
|
query_params: dict[str, str] = {}
|
|
1673
1710
|
extra_headers: dict[str, str] = {}
|
|
1674
1711
|
body: dict | None = None
|
|
1712
|
+
files: dict | None = None
|
|
1675
1713
|
|
|
1676
1714
|
for p in cmd.params:
|
|
1677
1715
|
if p.location == "path":
|
|
@@ -1701,6 +1739,17 @@ def _collect_openapi_params(
|
|
|
1701
1739
|
continue
|
|
1702
1740
|
if p.location == "path":
|
|
1703
1741
|
continue
|
|
1742
|
+
if p.location == "file":
|
|
1743
|
+
if val is not None:
|
|
1744
|
+
fp = Path(val)
|
|
1745
|
+
if not fp.is_file():
|
|
1746
|
+
print(f"Error: file not found: {val}", file=sys.stderr)
|
|
1747
|
+
sys.exit(1)
|
|
1748
|
+
mime = mimetypes.guess_type(val)[0] or "application/octet-stream"
|
|
1749
|
+
if files is None:
|
|
1750
|
+
files = {}
|
|
1751
|
+
files[p.original_name] = (fp.name, open(fp, "rb"), mime)
|
|
1752
|
+
continue
|
|
1704
1753
|
if val is not None:
|
|
1705
1754
|
body[p.original_name] = val
|
|
1706
1755
|
if not body:
|
|
@@ -1712,7 +1761,7 @@ def _collect_openapi_params(
|
|
|
1712
1761
|
if val is not None:
|
|
1713
1762
|
query_params[p.original_name] = val
|
|
1714
1763
|
|
|
1715
|
-
return path, query_params, extra_headers, body
|
|
1764
|
+
return path, query_params, extra_headers, body, files
|
|
1716
1765
|
|
|
1717
1766
|
|
|
1718
1767
|
def execute_openapi(
|
|
@@ -1727,21 +1776,45 @@ def execute_openapi(
|
|
|
1727
1776
|
jq_expr: str | None = None,
|
|
1728
1777
|
head: int | None = None,
|
|
1729
1778
|
):
|
|
1730
|
-
path, query_params, extra_headers, body = _collect_openapi_params(cmd, args)
|
|
1779
|
+
path, query_params, extra_headers, body, files = _collect_openapi_params(cmd, args)
|
|
1731
1780
|
url = base_url.rstrip("/") + path
|
|
1732
1781
|
|
|
1733
|
-
|
|
1782
|
+
is_multipart = files is not None or cmd.content_type == "multipart/form-data"
|
|
1783
|
+
headers = _build_http_headers(auth_headers, multipart=is_multipart)
|
|
1734
1784
|
headers.update(extra_headers)
|
|
1735
1785
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1786
|
+
try:
|
|
1787
|
+
with httpx.Client(timeout=60, auth=oauth_provider) as client:
|
|
1788
|
+
if files is not None:
|
|
1789
|
+
resp = client.request(
|
|
1790
|
+
(cmd.method or "get").upper(),
|
|
1791
|
+
url,
|
|
1792
|
+
headers=headers,
|
|
1793
|
+
params=query_params or None,
|
|
1794
|
+
data=body,
|
|
1795
|
+
files=files,
|
|
1796
|
+
)
|
|
1797
|
+
elif cmd.content_type == "multipart/form-data":
|
|
1798
|
+
resp = client.request(
|
|
1799
|
+
(cmd.method or "get").upper(),
|
|
1800
|
+
url,
|
|
1801
|
+
headers=headers,
|
|
1802
|
+
params=query_params or None,
|
|
1803
|
+
data=body,
|
|
1804
|
+
)
|
|
1805
|
+
else:
|
|
1806
|
+
resp = client.request(
|
|
1807
|
+
(cmd.method or "get").upper(),
|
|
1808
|
+
url,
|
|
1809
|
+
headers=headers,
|
|
1810
|
+
params=query_params or None,
|
|
1811
|
+
json=body,
|
|
1812
|
+
)
|
|
1813
|
+
_handle_http_error(resp)
|
|
1814
|
+
finally:
|
|
1815
|
+
if files:
|
|
1816
|
+
for _, file_tuple in files.items():
|
|
1817
|
+
file_tuple[1].close()
|
|
1745
1818
|
|
|
1746
1819
|
if raw:
|
|
1747
1820
|
sys.stdout.buffer.write(resp.content)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|