mcp2cli 3.8.0__tar.gz → 3.8.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-3.8.0 → mcp2cli-3.8.1}/PKG-INFO +3 -3
- {mcp2cli-3.8.0 → mcp2cli-3.8.1}/README.md +2 -2
- {mcp2cli-3.8.0 → mcp2cli-3.8.1}/pyproject.toml +1 -1
- {mcp2cli-3.8.0 → mcp2cli-3.8.1}/pyproject.toml.orig +1 -1
- {mcp2cli-3.8.0 → mcp2cli-3.8.1}/src/mcp2cli/__init__.py +58 -13
- {mcp2cli-3.8.0 → mcp2cli-3.8.1}/src/mcp2cli/__main__.py +0 -0
- {mcp2cli-3.8.0 → mcp2cli-3.8.1}/src/mcp2cli/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp2cli
|
|
3
|
-
Version: 3.8.
|
|
3
|
+
Version: 3.8.1
|
|
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>
|
|
@@ -217,7 +217,7 @@ mcp2cli --graphql https://api.example.com/graphql users --limit 10
|
|
|
217
217
|
mcp2cli --graphql https://api.example.com/graphql create-user --name "Alice" --email "alice@example.com"
|
|
218
218
|
|
|
219
219
|
# Override auto-generated selection set fields
|
|
220
|
-
mcp2cli --graphql https://api.example.com/graphql
|
|
220
|
+
mcp2cli --graphql https://api.example.com/graphql --fields "id name email" users
|
|
221
221
|
|
|
222
222
|
# With auth
|
|
223
223
|
mcp2cli --graphql https://api.example.com/graphql --auth-header "Authorization:Bearer tok_..." users
|
|
@@ -322,7 +322,7 @@ mcp2cli --spec ./spec.json --pretty list-pets
|
|
|
322
322
|
mcp2cli --spec ./spec.json --raw get-data
|
|
323
323
|
|
|
324
324
|
# Truncate large responses to first N records
|
|
325
|
-
mcp2cli --spec ./spec.json
|
|
325
|
+
mcp2cli --spec ./spec.json --head 5 list-records
|
|
326
326
|
|
|
327
327
|
# Pipe-friendly (compact JSON when not a TTY)
|
|
328
328
|
mcp2cli --spec ./spec.json list-pets | jq '.[] | .name'
|
|
@@ -198,7 +198,7 @@ mcp2cli --graphql https://api.example.com/graphql users --limit 10
|
|
|
198
198
|
mcp2cli --graphql https://api.example.com/graphql create-user --name "Alice" --email "alice@example.com"
|
|
199
199
|
|
|
200
200
|
# Override auto-generated selection set fields
|
|
201
|
-
mcp2cli --graphql https://api.example.com/graphql
|
|
201
|
+
mcp2cli --graphql https://api.example.com/graphql --fields "id name email" users
|
|
202
202
|
|
|
203
203
|
# With auth
|
|
204
204
|
mcp2cli --graphql https://api.example.com/graphql --auth-header "Authorization:Bearer tok_..." users
|
|
@@ -303,7 +303,7 @@ mcp2cli --spec ./spec.json --pretty list-pets
|
|
|
303
303
|
mcp2cli --spec ./spec.json --raw get-data
|
|
304
304
|
|
|
305
305
|
# Truncate large responses to first N records
|
|
306
|
-
mcp2cli --spec ./spec.json
|
|
306
|
+
mcp2cli --spec ./spec.json --head 5 list-records
|
|
307
307
|
|
|
308
308
|
# Pipe-friendly (compact JSON when not a TTY)
|
|
309
309
|
mcp2cli --spec ./spec.json list-pets | jq '.[] | .name'
|
|
@@ -24,7 +24,7 @@ import tempfile
|
|
|
24
24
|
import threading
|
|
25
25
|
import time
|
|
26
26
|
import webbrowser
|
|
27
|
-
from contextlib import asynccontextmanager
|
|
27
|
+
from contextlib import asynccontextmanager, suppress
|
|
28
28
|
from dataclasses import dataclass, field
|
|
29
29
|
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
30
30
|
from pathlib import Path
|
|
@@ -58,7 +58,7 @@ ARGPARSE_HELP_PERCENT_RE = re.compile(r"(?<!%)%(?![%\(])")
|
|
|
58
58
|
class ParamDef:
|
|
59
59
|
name: str # kebab-case CLI flag
|
|
60
60
|
original_name: str # original name for API/tool call
|
|
61
|
-
python_type: type | None # None means boolean (
|
|
61
|
+
python_type: type | None # None means boolean (--flag / --no-flag)
|
|
62
62
|
required: bool = False
|
|
63
63
|
description: str = ""
|
|
64
64
|
choices: list | None = None
|
|
@@ -1833,7 +1833,11 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
|
|
|
1833
1833
|
location=param.get("in", "query"),
|
|
1834
1834
|
schema=schema,
|
|
1835
1835
|
style=param.get("style"),
|
|
1836
|
-
explode=param.get(
|
|
1836
|
+
explode=param.get(
|
|
1837
|
+
"explode",
|
|
1838
|
+
param.get("in", "query") == "query"
|
|
1839
|
+
and param.get("style", "form") == "form",
|
|
1840
|
+
),
|
|
1837
1841
|
)
|
|
1838
1842
|
params.append(p)
|
|
1839
1843
|
|
|
@@ -2103,7 +2107,7 @@ def graphql_type_to_python(
|
|
|
2103
2107
|
"ID": str,
|
|
2104
2108
|
"Int": int,
|
|
2105
2109
|
"Float": float,
|
|
2106
|
-
"Boolean": None, #
|
|
2110
|
+
"Boolean": None, # boolean flag
|
|
2107
2111
|
}
|
|
2108
2112
|
py_type = scalar_map.get(type_name, str)
|
|
2109
2113
|
return py_type, is_non_null, None
|
|
@@ -3043,13 +3047,18 @@ def build_argparse(
|
|
|
3043
3047
|
"are merged in (a conflicting value is an error)",
|
|
3044
3048
|
)
|
|
3045
3049
|
|
|
3050
|
+
cli_names = {p.cli_name or p.name for p in cmd.params}
|
|
3046
3051
|
for p in cmd.params:
|
|
3047
3052
|
flag = f"--{p.cli_name or p.name}"
|
|
3048
3053
|
kwargs: dict = {}
|
|
3049
3054
|
if p.python_type is not None:
|
|
3050
3055
|
kwargs["type"] = p.python_type
|
|
3051
|
-
|
|
3056
|
+
elif f"no-{p.cli_name or p.name}" in cli_names:
|
|
3057
|
+
# Another parameter already owns --no-<name>; keep the plain switch.
|
|
3052
3058
|
kwargs["action"] = "store_true"
|
|
3059
|
+
else:
|
|
3060
|
+
# --<name> sends true, --no-<name> sends false, omitted sends nothing.
|
|
3061
|
+
kwargs["action"] = argparse.BooleanOptionalAction
|
|
3053
3062
|
# Body/tool_input params are never argparse-required (--stdin bypasses them)
|
|
3054
3063
|
if (
|
|
3055
3064
|
p.required
|
|
@@ -3283,6 +3292,30 @@ def _collect_openapi_params(
|
|
|
3283
3292
|
return path, query_params, extra_headers, body, files
|
|
3284
3293
|
|
|
3285
3294
|
|
|
3295
|
+
def _serialize_openapi_query_params(cmd: CommandDef, query_params: dict) -> str:
|
|
3296
|
+
"""Encode query array atoms before joining their OpenAPI delimiters."""
|
|
3297
|
+
parameters = {p.original_name: p for p in cmd.params if p.location == "query"}
|
|
3298
|
+
parts = []
|
|
3299
|
+
for name, value in query_params.items():
|
|
3300
|
+
param = parameters[name]
|
|
3301
|
+
separator = {"form": ",", "spaceDelimited": "%20", "pipeDelimited": "%7C"}.get(
|
|
3302
|
+
param.style or "form"
|
|
3303
|
+
)
|
|
3304
|
+
if isinstance(value, list) and not param.explode and separator is not None:
|
|
3305
|
+
if not value:
|
|
3306
|
+
continue
|
|
3307
|
+
atoms = [
|
|
3308
|
+
quote(str(item).lower() if isinstance(item, bool) else str(item), safe="")
|
|
3309
|
+
for item in value
|
|
3310
|
+
if item is not None
|
|
3311
|
+
]
|
|
3312
|
+
if atoms:
|
|
3313
|
+
parts.append(f"{quote(name, safe='')}={separator.join(atoms)}")
|
|
3314
|
+
else:
|
|
3315
|
+
parts.append(str(httpx.QueryParams({name: value})))
|
|
3316
|
+
return "&".join(part for part in parts if part)
|
|
3317
|
+
|
|
3318
|
+
|
|
3286
3319
|
def execute_openapi(
|
|
3287
3320
|
args: argparse.Namespace,
|
|
3288
3321
|
cmd: CommandDef,
|
|
@@ -3297,6 +3330,14 @@ def execute_openapi(
|
|
|
3297
3330
|
):
|
|
3298
3331
|
path, query_params, extra_headers, body, files = _collect_openapi_params(cmd, args)
|
|
3299
3332
|
url = base_url.rstrip("/") + path
|
|
3333
|
+
if query_params:
|
|
3334
|
+
# Append to, rather than replace, any query string the spec path or
|
|
3335
|
+
# base URL already carries (e.g. `/search?type=user`).
|
|
3336
|
+
parsed = httpx.URL(url)
|
|
3337
|
+
query = _serialize_openapi_query_params(cmd, query_params).encode("ascii")
|
|
3338
|
+
if parsed.query:
|
|
3339
|
+
query = parsed.query + b"&" + query
|
|
3340
|
+
url = parsed.copy_with(query=query)
|
|
3300
3341
|
|
|
3301
3342
|
is_multipart = files is not None or cmd.content_type == "multipart/form-data"
|
|
3302
3343
|
headers = _build_http_headers(auth_headers, multipart=is_multipart)
|
|
@@ -3309,7 +3350,6 @@ def execute_openapi(
|
|
|
3309
3350
|
(cmd.method or "get").upper(),
|
|
3310
3351
|
url,
|
|
3311
3352
|
headers=headers,
|
|
3312
|
-
params=query_params or None,
|
|
3313
3353
|
data=body,
|
|
3314
3354
|
files=files,
|
|
3315
3355
|
)
|
|
@@ -3318,7 +3358,6 @@ def execute_openapi(
|
|
|
3318
3358
|
(cmd.method or "get").upper(),
|
|
3319
3359
|
url,
|
|
3320
3360
|
headers=headers,
|
|
3321
|
-
params=query_params or None,
|
|
3322
3361
|
data=body,
|
|
3323
3362
|
)
|
|
3324
3363
|
else:
|
|
@@ -3326,7 +3365,6 @@ def execute_openapi(
|
|
|
3326
3365
|
(cmd.method or "get").upper(),
|
|
3327
3366
|
url,
|
|
3328
3367
|
headers=headers,
|
|
3329
|
-
params=query_params or None,
|
|
3330
3368
|
json=body,
|
|
3331
3369
|
)
|
|
3332
3370
|
_handle_http_error(resp)
|
|
@@ -4045,7 +4083,7 @@ def session_start(
|
|
|
4045
4083
|
_session_sock_path(name).unlink(missing_ok=True)
|
|
4046
4084
|
|
|
4047
4085
|
# Spawn daemon
|
|
4048
|
-
|
|
4086
|
+
daemon_config = json.dumps(
|
|
4049
4087
|
{
|
|
4050
4088
|
"name": name,
|
|
4051
4089
|
"source": source,
|
|
@@ -4067,17 +4105,24 @@ def session_start(
|
|
|
4067
4105
|
[
|
|
4068
4106
|
sys.executable,
|
|
4069
4107
|
"-c",
|
|
4070
|
-
|
|
4108
|
+
(
|
|
4109
|
+
"import sys; config = sys.stdin.read(); "
|
|
4110
|
+
"import mcp2cli; mcp2cli._run_session_daemon(config)"
|
|
4111
|
+
),
|
|
4071
4112
|
],
|
|
4072
4113
|
start_new_session=True,
|
|
4073
4114
|
stdout=log_file,
|
|
4074
4115
|
stderr=log_file,
|
|
4075
4116
|
stdin=subprocess.PIPE,
|
|
4076
4117
|
)
|
|
4077
|
-
#
|
|
4078
|
-
#
|
|
4118
|
+
# Keep resolved headers, environment values and source URLs out of argv.
|
|
4119
|
+
# Read before importing mcp2cli so a large config cannot block our write
|
|
4120
|
+
# during slow imports, before the startup timeout below takes effect.
|
|
4121
|
+
# Closing the pipe completes the daemon's read and releases our descriptor.
|
|
4079
4122
|
if proc.stdin is not None:
|
|
4080
|
-
|
|
4123
|
+
# An early exit can break either write or close; report it below.
|
|
4124
|
+
with suppress(BrokenPipeError), proc.stdin:
|
|
4125
|
+
proc.stdin.write(daemon_config.encode("utf-8"))
|
|
4081
4126
|
|
|
4082
4127
|
# Wait for socket to appear
|
|
4083
4128
|
sock_path = _session_sock_path(name)
|
|
File without changes
|
|
File without changes
|