mcp2cli 2.3.0__tar.gz → 2.4.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.3.0 → mcp2cli-2.4.0}/PKG-INFO +1 -1
- {mcp2cli-2.3.0 → mcp2cli-2.4.0}/pyproject.toml +1 -1
- {mcp2cli-2.3.0 → mcp2cli-2.4.0}/src/mcp2cli/__init__.py +90 -26
- {mcp2cli-2.3.0 → mcp2cli-2.4.0}/README.md +0 -0
- {mcp2cli-2.3.0 → mcp2cli-2.4.0}/src/mcp2cli/__main__.py +0 -0
- {mcp2cli-2.3.0 → mcp2cli-2.4.0}/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
|
|
@@ -64,6 +65,7 @@ class CommandDef:
|
|
|
64
65
|
# OpenAPI
|
|
65
66
|
method: str | None = None
|
|
66
67
|
path: str | None = None
|
|
68
|
+
content_type: str | None = None # None = json, "multipart/form-data", etc.
|
|
67
69
|
# MCP
|
|
68
70
|
tool_name: str | None = None
|
|
69
71
|
# GraphQL
|
|
@@ -322,10 +324,11 @@ def output_result(
|
|
|
322
324
|
print(json.dumps(data))
|
|
323
325
|
|
|
324
326
|
|
|
325
|
-
def _build_http_headers(auth_headers: list[tuple[str, str]]) -> dict[str, str]:
|
|
327
|
+
def _build_http_headers(auth_headers: list[tuple[str, str]], multipart: bool = False) -> dict[str, str]:
|
|
326
328
|
"""Build HTTP headers dict from auth_headers with a Content-Type default."""
|
|
327
329
|
headers = dict(auth_headers)
|
|
328
|
-
|
|
330
|
+
if not multipart:
|
|
331
|
+
headers.setdefault("Content-Type", "application/json")
|
|
329
332
|
return headers
|
|
330
333
|
|
|
331
334
|
|
|
@@ -662,19 +665,43 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
|
|
|
662
665
|
)
|
|
663
666
|
params.append(p)
|
|
664
667
|
|
|
665
|
-
# Request body
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
668
|
+
# Request body — negotiate content type
|
|
669
|
+
rb_content = details.get("requestBody", {}).get("content", {})
|
|
670
|
+
multipart_schema = rb_content.get("multipart/form-data", {}).get("schema", {})
|
|
671
|
+
json_schema = rb_content.get("application/json", {}).get("schema", {})
|
|
672
|
+
|
|
673
|
+
mp_props = multipart_schema.get("properties", {})
|
|
674
|
+
has_binary = any(
|
|
675
|
+
p.get("format") == "binary" for p in mp_props.values()
|
|
671
676
|
)
|
|
677
|
+
|
|
678
|
+
if has_binary:
|
|
679
|
+
rb_schema = multipart_schema
|
|
680
|
+
cmd_content_type = "multipart/form-data"
|
|
681
|
+
elif json_schema:
|
|
682
|
+
rb_schema = json_schema
|
|
683
|
+
cmd_content_type = None
|
|
684
|
+
elif mp_props:
|
|
685
|
+
rb_schema = multipart_schema
|
|
686
|
+
cmd_content_type = "multipart/form-data"
|
|
687
|
+
else:
|
|
688
|
+
rb_schema = {}
|
|
689
|
+
cmd_content_type = None
|
|
690
|
+
|
|
672
691
|
required_fields = set(rb_schema.get("required", []))
|
|
673
692
|
properties = rb_schema.get("properties", {})
|
|
674
693
|
has_body = bool(properties)
|
|
675
694
|
|
|
676
695
|
for prop_name, prop_schema in properties.items():
|
|
677
|
-
|
|
696
|
+
is_binary = (
|
|
697
|
+
cmd_content_type == "multipart/form-data"
|
|
698
|
+
and prop_schema.get("format") == "binary"
|
|
699
|
+
)
|
|
700
|
+
if is_binary:
|
|
701
|
+
loc, py_type, suffix = "file", str, " (file path)"
|
|
702
|
+
else:
|
|
703
|
+
py_type, suffix = schema_type_to_python(prop_schema)
|
|
704
|
+
loc = "body"
|
|
678
705
|
p = ParamDef(
|
|
679
706
|
name=to_kebab(prop_name),
|
|
680
707
|
original_name=prop_name,
|
|
@@ -682,7 +709,7 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
|
|
|
682
709
|
required=prop_name in required_fields,
|
|
683
710
|
description=(prop_schema.get("description") or prop_name) + suffix,
|
|
684
711
|
choices=prop_schema.get("enum"),
|
|
685
|
-
location=
|
|
712
|
+
location=loc,
|
|
686
713
|
)
|
|
687
714
|
params.append(p)
|
|
688
715
|
|
|
@@ -694,6 +721,7 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
|
|
|
694
721
|
has_body=has_body,
|
|
695
722
|
method=method,
|
|
696
723
|
path=path,
|
|
724
|
+
content_type=cmd_content_type,
|
|
697
725
|
)
|
|
698
726
|
)
|
|
699
727
|
|
|
@@ -1662,16 +1690,17 @@ def _filter_commands(commands: list[CommandDef], pattern: str) -> list[CommandDe
|
|
|
1662
1690
|
def _collect_openapi_params(
|
|
1663
1691
|
cmd: CommandDef,
|
|
1664
1692
|
args: argparse.Namespace,
|
|
1665
|
-
) -> tuple[str, dict[str, str], dict[str, str], dict | None]:
|
|
1693
|
+
) -> tuple[str, dict[str, str], dict[str, str], dict | None, dict | None]:
|
|
1666
1694
|
"""Collect OpenAPI params from parsed args, separated by location.
|
|
1667
1695
|
|
|
1668
|
-
Returns (path, query_params, extra_headers, body_or_none)
|
|
1669
|
-
has ``{param}`` placeholders substituted with actual values.
|
|
1696
|
+
Returns (path, query_params, extra_headers, body_or_none, files_or_none)
|
|
1697
|
+
where *path* has ``{param}`` placeholders substituted with actual values.
|
|
1670
1698
|
"""
|
|
1671
1699
|
path = cmd.path or ""
|
|
1672
1700
|
query_params: dict[str, str] = {}
|
|
1673
1701
|
extra_headers: dict[str, str] = {}
|
|
1674
1702
|
body: dict | None = None
|
|
1703
|
+
files: dict | None = None
|
|
1675
1704
|
|
|
1676
1705
|
for p in cmd.params:
|
|
1677
1706
|
if p.location == "path":
|
|
@@ -1701,6 +1730,17 @@ def _collect_openapi_params(
|
|
|
1701
1730
|
continue
|
|
1702
1731
|
if p.location == "path":
|
|
1703
1732
|
continue
|
|
1733
|
+
if p.location == "file":
|
|
1734
|
+
if val is not None:
|
|
1735
|
+
fp = Path(val)
|
|
1736
|
+
if not fp.is_file():
|
|
1737
|
+
print(f"Error: file not found: {val}", file=sys.stderr)
|
|
1738
|
+
sys.exit(1)
|
|
1739
|
+
mime = mimetypes.guess_type(val)[0] or "application/octet-stream"
|
|
1740
|
+
if files is None:
|
|
1741
|
+
files = {}
|
|
1742
|
+
files[p.original_name] = (fp.name, open(fp, "rb"), mime)
|
|
1743
|
+
continue
|
|
1704
1744
|
if val is not None:
|
|
1705
1745
|
body[p.original_name] = val
|
|
1706
1746
|
if not body:
|
|
@@ -1712,7 +1752,7 @@ def _collect_openapi_params(
|
|
|
1712
1752
|
if val is not None:
|
|
1713
1753
|
query_params[p.original_name] = val
|
|
1714
1754
|
|
|
1715
|
-
return path, query_params, extra_headers, body
|
|
1755
|
+
return path, query_params, extra_headers, body, files
|
|
1716
1756
|
|
|
1717
1757
|
|
|
1718
1758
|
def execute_openapi(
|
|
@@ -1727,21 +1767,45 @@ def execute_openapi(
|
|
|
1727
1767
|
jq_expr: str | None = None,
|
|
1728
1768
|
head: int | None = None,
|
|
1729
1769
|
):
|
|
1730
|
-
path, query_params, extra_headers, body = _collect_openapi_params(cmd, args)
|
|
1770
|
+
path, query_params, extra_headers, body, files = _collect_openapi_params(cmd, args)
|
|
1731
1771
|
url = base_url.rstrip("/") + path
|
|
1732
1772
|
|
|
1733
|
-
|
|
1773
|
+
is_multipart = files is not None or cmd.content_type == "multipart/form-data"
|
|
1774
|
+
headers = _build_http_headers(auth_headers, multipart=is_multipart)
|
|
1734
1775
|
headers.update(extra_headers)
|
|
1735
1776
|
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1777
|
+
try:
|
|
1778
|
+
with httpx.Client(timeout=60, auth=oauth_provider) as client:
|
|
1779
|
+
if files is not None:
|
|
1780
|
+
resp = client.request(
|
|
1781
|
+
(cmd.method or "get").upper(),
|
|
1782
|
+
url,
|
|
1783
|
+
headers=headers,
|
|
1784
|
+
params=query_params or None,
|
|
1785
|
+
data=body,
|
|
1786
|
+
files=files,
|
|
1787
|
+
)
|
|
1788
|
+
elif cmd.content_type == "multipart/form-data":
|
|
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
|
+
)
|
|
1796
|
+
else:
|
|
1797
|
+
resp = client.request(
|
|
1798
|
+
(cmd.method or "get").upper(),
|
|
1799
|
+
url,
|
|
1800
|
+
headers=headers,
|
|
1801
|
+
params=query_params or None,
|
|
1802
|
+
json=body,
|
|
1803
|
+
)
|
|
1804
|
+
_handle_http_error(resp)
|
|
1805
|
+
finally:
|
|
1806
|
+
if files:
|
|
1807
|
+
for _, file_tuple in files.items():
|
|
1808
|
+
file_tuple[1].close()
|
|
1745
1809
|
|
|
1746
1810
|
if raw:
|
|
1747
1811
|
sys.stdout.buffer.write(resp.content)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|