mcp2cli 3.0.3__tar.gz → 3.2.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-3.0.3 → mcp2cli-3.2.0}/PKG-INFO +34 -1
- {mcp2cli-3.0.3 → mcp2cli-3.2.0}/README.md +33 -0
- {mcp2cli-3.0.3 → mcp2cli-3.2.0}/pyproject.toml +1 -1
- {mcp2cli-3.0.3 → mcp2cli-3.2.0}/src/mcp2cli/__init__.py +288 -46
- {mcp2cli-3.0.3 → mcp2cli-3.2.0}/src/mcp2cli/__main__.py +0 -0
- {mcp2cli-3.0.3 → mcp2cli-3.2.0}/src/mcp2cli/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp2cli
|
|
3
|
-
Version: 3.0
|
|
3
|
+
Version: 3.2.0
|
|
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>
|
|
@@ -236,6 +236,36 @@ mcp2cli @myapi --list --sort alpha
|
|
|
236
236
|
|
|
237
237
|
When usage data exists for a source, `--list` defaults to sorting by call frequency. Otherwise insertion order is preserved. Usage data is stored in `~/.cache/mcp2cli/usage.json`.
|
|
238
238
|
|
|
239
|
+
### JSON output
|
|
240
|
+
|
|
241
|
+
`--json` forces **valid JSON on stdout for every command**, in every mode. It is the
|
|
242
|
+
machine-readable counterpart to the human-formatted default output, designed for LLM
|
|
243
|
+
agents and scripts that need to parse results reliably.
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# --list emits a JSON array of command objects (name, description, parameters, ...)
|
|
247
|
+
mcp2cli --mcp https://mcp.example.com/sse --list --json
|
|
248
|
+
mcp2cli --spec ./openapi.json --list --json
|
|
249
|
+
mcp2cli --graphql https://api.example.com/graphql --list --json
|
|
250
|
+
|
|
251
|
+
# --list --json --compact emits a JSON array of names only
|
|
252
|
+
mcp2cli --mcp https://mcp.example.com/sse --list --json --compact
|
|
253
|
+
|
|
254
|
+
# MCP tool calls emit the FULL CallToolResult envelope — including
|
|
255
|
+
# structuredContent and isError, not just the flattened text. This surfaces the
|
|
256
|
+
# machine-readable result that modern MCP tools put in structuredContent.
|
|
257
|
+
mcp2cli --mcp https://mcp.example.com/sse --json search --query "test"
|
|
258
|
+
# { "content": [...], "structuredContent": {...}, "isError": false }
|
|
259
|
+
|
|
260
|
+
# OpenAPI / GraphQL calls emit the response as JSON (non-JSON bodies become a JSON string)
|
|
261
|
+
mcp2cli --spec ./openapi.json --json list-pets
|
|
262
|
+
mcp2cli --graphql https://api.example.com/graphql --json users
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`--json` takes precedence over `--raw` and `--toon` (both of which can produce
|
|
266
|
+
non-JSON), so it always wins — that is what makes it a reliable "force JSON" switch.
|
|
267
|
+
Indentation follows the usual rule: pretty on a TTY or with `--pretty`, compact when piped.
|
|
268
|
+
|
|
239
269
|
### Output control
|
|
240
270
|
|
|
241
271
|
```bash
|
|
@@ -308,6 +338,9 @@ Options:
|
|
|
308
338
|
--fields FIELDS Override GraphQL selection set (e.g. "id name email")
|
|
309
339
|
--pretty Pretty-print JSON output
|
|
310
340
|
--raw Print raw response body
|
|
341
|
+
--json Force valid JSON output for every command (--list and tool
|
|
342
|
+
calls). MCP calls emit the full result envelope including
|
|
343
|
+
structuredContent. Takes precedence over --raw and --toon.
|
|
311
344
|
--toon Encode output as TOON (token-efficient for LLMs)
|
|
312
345
|
--head N Limit output to first N records (arrays)
|
|
313
346
|
--version Show version
|
|
@@ -217,6 +217,36 @@ mcp2cli @myapi --list --sort alpha
|
|
|
217
217
|
|
|
218
218
|
When usage data exists for a source, `--list` defaults to sorting by call frequency. Otherwise insertion order is preserved. Usage data is stored in `~/.cache/mcp2cli/usage.json`.
|
|
219
219
|
|
|
220
|
+
### JSON output
|
|
221
|
+
|
|
222
|
+
`--json` forces **valid JSON on stdout for every command**, in every mode. It is the
|
|
223
|
+
machine-readable counterpart to the human-formatted default output, designed for LLM
|
|
224
|
+
agents and scripts that need to parse results reliably.
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
# --list emits a JSON array of command objects (name, description, parameters, ...)
|
|
228
|
+
mcp2cli --mcp https://mcp.example.com/sse --list --json
|
|
229
|
+
mcp2cli --spec ./openapi.json --list --json
|
|
230
|
+
mcp2cli --graphql https://api.example.com/graphql --list --json
|
|
231
|
+
|
|
232
|
+
# --list --json --compact emits a JSON array of names only
|
|
233
|
+
mcp2cli --mcp https://mcp.example.com/sse --list --json --compact
|
|
234
|
+
|
|
235
|
+
# MCP tool calls emit the FULL CallToolResult envelope — including
|
|
236
|
+
# structuredContent and isError, not just the flattened text. This surfaces the
|
|
237
|
+
# machine-readable result that modern MCP tools put in structuredContent.
|
|
238
|
+
mcp2cli --mcp https://mcp.example.com/sse --json search --query "test"
|
|
239
|
+
# { "content": [...], "structuredContent": {...}, "isError": false }
|
|
240
|
+
|
|
241
|
+
# OpenAPI / GraphQL calls emit the response as JSON (non-JSON bodies become a JSON string)
|
|
242
|
+
mcp2cli --spec ./openapi.json --json list-pets
|
|
243
|
+
mcp2cli --graphql https://api.example.com/graphql --json users
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
`--json` takes precedence over `--raw` and `--toon` (both of which can produce
|
|
247
|
+
non-JSON), so it always wins — that is what makes it a reliable "force JSON" switch.
|
|
248
|
+
Indentation follows the usual rule: pretty on a TTY or with `--pretty`, compact when piped.
|
|
249
|
+
|
|
220
250
|
### Output control
|
|
221
251
|
|
|
222
252
|
```bash
|
|
@@ -289,6 +319,9 @@ Options:
|
|
|
289
319
|
--fields FIELDS Override GraphQL selection set (e.g. "id name email")
|
|
290
320
|
--pretty Pretty-print JSON output
|
|
291
321
|
--raw Print raw response body
|
|
322
|
+
--json Force valid JSON output for every command (--list and tool
|
|
323
|
+
calls). MCP calls emit the full result envelope including
|
|
324
|
+
structuredContent. Takes precedence over --raw and --toon.
|
|
292
325
|
--toon Encode output as TOON (token-efficient for LLMs)
|
|
293
326
|
--head N Limit output to first N records (arrays)
|
|
294
327
|
--version Show version
|
|
@@ -277,6 +277,14 @@ def _apply_head(data, n: int):
|
|
|
277
277
|
return data
|
|
278
278
|
|
|
279
279
|
|
|
280
|
+
def _emit_json(data, pretty: bool = False) -> None:
|
|
281
|
+
"""Print *data* as JSON. Indented when *pretty* or stdout is a TTY, else compact."""
|
|
282
|
+
if pretty or sys.stdout.isatty():
|
|
283
|
+
print(json.dumps(data, indent=2))
|
|
284
|
+
else:
|
|
285
|
+
print(json.dumps(data))
|
|
286
|
+
|
|
287
|
+
|
|
280
288
|
def output_result(
|
|
281
289
|
data,
|
|
282
290
|
*,
|
|
@@ -284,7 +292,22 @@ def output_result(
|
|
|
284
292
|
raw: bool = False,
|
|
285
293
|
toon: bool = False,
|
|
286
294
|
head: int | None = None,
|
|
295
|
+
json_output: bool = False,
|
|
287
296
|
):
|
|
297
|
+
# --json forces valid JSON for every command, overriding --raw and --toon
|
|
298
|
+
# (both of which can produce non-JSON). Strings that contain JSON are
|
|
299
|
+
# unwrapped; everything else is emitted as a JSON value (e.g. a string
|
|
300
|
+
# literal for plain prose), guaranteeing parseable stdout.
|
|
301
|
+
if json_output:
|
|
302
|
+
if isinstance(data, str):
|
|
303
|
+
try:
|
|
304
|
+
data = json.loads(data)
|
|
305
|
+
except (json.JSONDecodeError, TypeError, ValueError):
|
|
306
|
+
pass
|
|
307
|
+
if head is not None:
|
|
308
|
+
data = _apply_head(data, head)
|
|
309
|
+
_emit_json(data, pretty)
|
|
310
|
+
return
|
|
288
311
|
if raw:
|
|
289
312
|
if isinstance(data, str):
|
|
290
313
|
print(data)
|
|
@@ -310,10 +333,53 @@ def output_result(
|
|
|
310
333
|
file=sys.stderr,
|
|
311
334
|
)
|
|
312
335
|
# Fall through to normal output
|
|
313
|
-
|
|
314
|
-
|
|
336
|
+
_emit_json(data, pretty)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _python_type_name(t: type | None) -> str:
|
|
340
|
+
"""Human/JSON-friendly type label for a ParamDef.python_type (None = boolean flag)."""
|
|
341
|
+
if t is None:
|
|
342
|
+
return "boolean"
|
|
343
|
+
return getattr(t, "__name__", str(t))
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _param_to_dict(p: "ParamDef") -> dict:
|
|
347
|
+
d = {
|
|
348
|
+
"name": p.name,
|
|
349
|
+
"type": _python_type_name(p.python_type),
|
|
350
|
+
"required": p.required,
|
|
351
|
+
"description": p.description,
|
|
352
|
+
"location": p.location,
|
|
353
|
+
}
|
|
354
|
+
if p.choices:
|
|
355
|
+
d["choices"] = p.choices
|
|
356
|
+
return d
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def command_to_dict(cmd: "CommandDef") -> dict:
|
|
360
|
+
"""Serialize a CommandDef to a JSON-friendly dict for `--list --json`."""
|
|
361
|
+
d: dict = {"name": cmd.name, "description": cmd.description or ""}
|
|
362
|
+
if cmd.method:
|
|
363
|
+
d["method"] = cmd.method.upper()
|
|
364
|
+
if cmd.path:
|
|
365
|
+
d["path"] = cmd.path
|
|
366
|
+
if cmd.tool_name:
|
|
367
|
+
d["toolName"] = cmd.tool_name
|
|
368
|
+
if cmd.graphql_operation_type:
|
|
369
|
+
d["operationType"] = cmd.graphql_operation_type
|
|
370
|
+
d["parameters"] = [_param_to_dict(p) for p in cmd.params]
|
|
371
|
+
return d
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def print_commands_json(
|
|
375
|
+
commands: "list[CommandDef]", compact: bool = False, pretty: bool = False
|
|
376
|
+
) -> None:
|
|
377
|
+
"""Emit a command list as JSON (array of objects, or names when *compact*)."""
|
|
378
|
+
if compact:
|
|
379
|
+
payload = [cmd.name for cmd in commands]
|
|
315
380
|
else:
|
|
316
|
-
|
|
381
|
+
payload = [command_to_dict(cmd) for cmd in commands]
|
|
382
|
+
_emit_json(payload, pretty)
|
|
317
383
|
|
|
318
384
|
|
|
319
385
|
def _build_http_headers(auth_headers: list[tuple[str, str]], multipart: bool = False) -> dict[str, str]:
|
|
@@ -469,13 +535,21 @@ OAUTH_DIR = CACHE_DIR / "oauth"
|
|
|
469
535
|
|
|
470
536
|
|
|
471
537
|
class FileTokenStorage:
|
|
472
|
-
"""File-based token storage for OAuth tokens and client info.
|
|
538
|
+
"""File-based token storage for OAuth tokens and client info.
|
|
539
|
+
|
|
540
|
+
Persists an absolute ``expires_at`` timestamp alongside ``tokens.json``
|
|
541
|
+
(in ``tokens_meta.json``) so that on a fresh process the SDK can tell an
|
|
542
|
+
access token has expired and trigger a refresh, rather than blindly
|
|
543
|
+
re-sending the stale Bearer and falling through to a full re-auth that
|
|
544
|
+
reuses a possibly-forgotten DCR client_id (issue #50).
|
|
545
|
+
"""
|
|
473
546
|
|
|
474
547
|
def __init__(self, server_url: str):
|
|
475
548
|
key = hashlib.sha256(server_url.encode()).hexdigest()[:16]
|
|
476
549
|
self._dir = OAUTH_DIR / key
|
|
477
550
|
self._dir.mkdir(parents=True, exist_ok=True)
|
|
478
551
|
self._tokens_path = self._dir / "tokens.json"
|
|
552
|
+
self._tokens_meta_path = self._dir / "tokens_meta.json"
|
|
479
553
|
self._client_path = self._dir / "client.json"
|
|
480
554
|
|
|
481
555
|
async def get_tokens(self):
|
|
@@ -491,6 +565,50 @@ class FileTokenStorage:
|
|
|
491
565
|
|
|
492
566
|
async def set_tokens(self, tokens) -> None:
|
|
493
567
|
self._tokens_path.write_text(tokens.model_dump_json())
|
|
568
|
+
# Persist an absolute expiry timestamp so we can detect on a
|
|
569
|
+
# later process start that the access token has expired.
|
|
570
|
+
if tokens.expires_in is not None:
|
|
571
|
+
try:
|
|
572
|
+
meta = {"expires_at": time.time() + float(tokens.expires_in)}
|
|
573
|
+
self._tokens_meta_path.write_text(json.dumps(meta))
|
|
574
|
+
except Exception:
|
|
575
|
+
pass
|
|
576
|
+
else:
|
|
577
|
+
# No expiry info; drop any stale meta sidecar.
|
|
578
|
+
try:
|
|
579
|
+
self._tokens_meta_path.unlink()
|
|
580
|
+
except FileNotFoundError:
|
|
581
|
+
pass
|
|
582
|
+
|
|
583
|
+
def get_expires_at(self) -> float | None:
|
|
584
|
+
"""Return the absolute expiry timestamp persisted alongside tokens.
|
|
585
|
+
|
|
586
|
+
Returns None when no sidecar exists (older caches or tokens without
|
|
587
|
+
an ``expires_in`` value).
|
|
588
|
+
"""
|
|
589
|
+
if not self._tokens_meta_path.exists():
|
|
590
|
+
return None
|
|
591
|
+
try:
|
|
592
|
+
data = json.loads(self._tokens_meta_path.read_text())
|
|
593
|
+
value = data.get("expires_at")
|
|
594
|
+
return float(value) if value is not None else None
|
|
595
|
+
except Exception:
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
def clear_tokens(self) -> None:
|
|
599
|
+
"""Remove persisted tokens and their expiry sidecar."""
|
|
600
|
+
for path in (self._tokens_path, self._tokens_meta_path):
|
|
601
|
+
try:
|
|
602
|
+
path.unlink()
|
|
603
|
+
except FileNotFoundError:
|
|
604
|
+
pass
|
|
605
|
+
|
|
606
|
+
def clear_client_info(self) -> None:
|
|
607
|
+
"""Remove persisted DCR client info so a fresh registration runs."""
|
|
608
|
+
try:
|
|
609
|
+
self._client_path.unlink()
|
|
610
|
+
except FileNotFoundError:
|
|
611
|
+
pass
|
|
494
612
|
|
|
495
613
|
async def get_client_info(self):
|
|
496
614
|
from mcp.shared.auth import OAuthClientInformationFull
|
|
@@ -601,6 +719,53 @@ def build_oauth_provider(
|
|
|
601
719
|
from mcp.client.auth.oauth2 import OAuthClientProvider
|
|
602
720
|
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata
|
|
603
721
|
|
|
722
|
+
class _RobustOAuthClientProvider(OAuthClientProvider):
|
|
723
|
+
"""OAuthClientProvider with cross-process expiry + stale-DCR recovery.
|
|
724
|
+
|
|
725
|
+
Fixes for issue #50 — Atlassian "Internal Server Error" on next-day
|
|
726
|
+
OAuth calls. The upstream SDK has two gaps when state is reloaded
|
|
727
|
+
from disk in a new process:
|
|
728
|
+
|
|
729
|
+
1. ``_initialize`` reloads ``current_tokens`` from storage but never
|
|
730
|
+
recomputes ``token_expiry_time`` from the bare ``OAuthToken``
|
|
731
|
+
dump (which only carries ``expires_in``). An expired access
|
|
732
|
+
token therefore looks valid, is sent, and only the 401 fallback
|
|
733
|
+
runs — skipping the refresh-token grant entirely.
|
|
734
|
+
|
|
735
|
+
2. The 401 fallback re-uses the cached DCR ``client_id`` because
|
|
736
|
+
``client_info`` was reloaded from disk; if the auth server has
|
|
737
|
+
since forgotten that registration, ``/authorize`` returns an
|
|
738
|
+
opaque 500 page rather than a clean ``invalid_client`` redirect
|
|
739
|
+
and the CLI hangs on the callback.
|
|
740
|
+
|
|
741
|
+
We patch both by restoring ``token_expiry_time`` from a sidecar
|
|
742
|
+
we persist in :class:`FileTokenStorage`, and by wiping the cached
|
|
743
|
+
``client_info`` from disk and memory whenever a refresh fails so
|
|
744
|
+
the subsequent re-auth performs fresh Dynamic Client Registration.
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
async def _initialize(self) -> None:
|
|
748
|
+
await super()._initialize()
|
|
749
|
+
storage = self.context.storage
|
|
750
|
+
if isinstance(storage, FileTokenStorage) and self.context.current_tokens:
|
|
751
|
+
expires_at = storage.get_expires_at()
|
|
752
|
+
if expires_at is not None:
|
|
753
|
+
self.context.token_expiry_time = expires_at
|
|
754
|
+
|
|
755
|
+
async def _handle_refresh_response(self, response) -> bool:
|
|
756
|
+
ok = await super()._handle_refresh_response(response)
|
|
757
|
+
if not ok:
|
|
758
|
+
# Refresh failed. The cached DCR client_id may have been
|
|
759
|
+
# forgotten by the auth server too; clear it so the
|
|
760
|
+
# subsequent 401 fallback performs a fresh registration
|
|
761
|
+
# instead of /authorize?client_id=<stale> → opaque 500.
|
|
762
|
+
self.context.client_info = None
|
|
763
|
+
storage = self.context.storage
|
|
764
|
+
if isinstance(storage, FileTokenStorage):
|
|
765
|
+
storage.clear_client_info()
|
|
766
|
+
storage.clear_tokens()
|
|
767
|
+
return ok
|
|
768
|
+
|
|
604
769
|
_LOOPBACK_HOSTS = {"localhost", "127.0.0.1", "::1"}
|
|
605
770
|
|
|
606
771
|
if redirect_uri is not None:
|
|
@@ -700,7 +865,7 @@ def build_oauth_provider(
|
|
|
700
865
|
raise RuntimeError("No authorization code received")
|
|
701
866
|
return (_CallbackHandler.auth_code, _CallbackHandler.state)
|
|
702
867
|
|
|
703
|
-
return
|
|
868
|
+
return _RobustOAuthClientProvider(
|
|
704
869
|
server_url=server_url,
|
|
705
870
|
client_metadata=client_metadata,
|
|
706
871
|
storage=storage,
|
|
@@ -1308,10 +1473,16 @@ def list_graphql_commands(
|
|
|
1308
1473
|
source_hash: str = "",
|
|
1309
1474
|
sort_mode: str | None = None,
|
|
1310
1475
|
top: int | None = None,
|
|
1476
|
+
json_output: bool = False,
|
|
1477
|
+
pretty: bool = False,
|
|
1311
1478
|
):
|
|
1312
1479
|
"""Group commands by operation type and print."""
|
|
1313
1480
|
commands = _apply_list_options(commands, source_hash, sort_mode, top)
|
|
1314
1481
|
|
|
1482
|
+
if json_output:
|
|
1483
|
+
print_commands_json(commands, compact=compact, pretty=pretty)
|
|
1484
|
+
return
|
|
1485
|
+
|
|
1315
1486
|
if compact:
|
|
1316
1487
|
print(" ".join(cmd.name for cmd in commands))
|
|
1317
1488
|
return
|
|
@@ -1403,6 +1574,7 @@ def execute_graphql(
|
|
|
1403
1574
|
fields_override: str | None = None,
|
|
1404
1575
|
oauth_provider: "httpx.Auth | None" = None,
|
|
1405
1576
|
head: int | None = None,
|
|
1577
|
+
json_output: bool = False,
|
|
1406
1578
|
):
|
|
1407
1579
|
"""Build and execute a GraphQL query/mutation."""
|
|
1408
1580
|
document, variables, field_name = _build_graphql_document(
|
|
@@ -1426,13 +1598,13 @@ def execute_graphql(
|
|
|
1426
1598
|
print(f"GraphQL error: {msgs}", file=sys.stderr)
|
|
1427
1599
|
sys.exit(1)
|
|
1428
1600
|
# Partial errors — include them in output
|
|
1429
|
-
output_result(result, pretty=pretty, raw=raw, toon=toon, head=head)
|
|
1601
|
+
output_result(result, pretty=pretty, raw=raw, toon=toon, head=head, json_output=json_output)
|
|
1430
1602
|
return
|
|
1431
1603
|
|
|
1432
1604
|
data = result.get("data", {})
|
|
1433
1605
|
# Extract the specific field's data
|
|
1434
1606
|
field_data = data.get(field_name, data)
|
|
1435
|
-
output_result(field_data, pretty=pretty, raw=raw, toon=toon, head=head)
|
|
1607
|
+
output_result(field_data, pretty=pretty, raw=raw, toon=toon, head=head, json_output=json_output)
|
|
1436
1608
|
|
|
1437
1609
|
|
|
1438
1610
|
def handle_graphql(
|
|
@@ -1453,6 +1625,7 @@ def handle_graphql(
|
|
|
1453
1625
|
sort_mode: str | None = None,
|
|
1454
1626
|
top: int | None = None,
|
|
1455
1627
|
compact: bool = False,
|
|
1628
|
+
json_output: bool = False,
|
|
1456
1629
|
):
|
|
1457
1630
|
"""Top-level handler for --graphql mode."""
|
|
1458
1631
|
src_hash = _source_hash_for(url)
|
|
@@ -1462,6 +1635,7 @@ def handle_graphql(
|
|
|
1462
1635
|
list_kwargs = dict(
|
|
1463
1636
|
verbose=verbose, compact=compact,
|
|
1464
1637
|
source_hash=src_hash, sort_mode=sort_mode, top=top,
|
|
1638
|
+
json_output=json_output, pretty=pretty,
|
|
1465
1639
|
)
|
|
1466
1640
|
|
|
1467
1641
|
if list_mode:
|
|
@@ -1469,10 +1643,10 @@ def handle_graphql(
|
|
|
1469
1643
|
return
|
|
1470
1644
|
|
|
1471
1645
|
if not remaining:
|
|
1472
|
-
if not compact:
|
|
1646
|
+
if not compact and not json_output:
|
|
1473
1647
|
print("Available operations:")
|
|
1474
1648
|
list_graphql_commands(commands, **list_kwargs)
|
|
1475
|
-
if not compact:
|
|
1649
|
+
if not compact and not json_output:
|
|
1476
1650
|
print("\nUse --list for the same output, or provide a subcommand.")
|
|
1477
1651
|
return
|
|
1478
1652
|
|
|
@@ -1488,6 +1662,7 @@ def handle_graphql(
|
|
|
1488
1662
|
execute_graphql(
|
|
1489
1663
|
args, cmd, url, schema, auth_headers, pretty, raw, toon=toon,
|
|
1490
1664
|
fields_override=fields_override, oauth_provider=oauth_provider,
|
|
1665
|
+
json_output=json_output,
|
|
1491
1666
|
)
|
|
1492
1667
|
|
|
1493
1668
|
# Record usage after successful execution
|
|
@@ -1941,9 +2116,15 @@ def list_openapi_commands(
|
|
|
1941
2116
|
source_hash: str = "",
|
|
1942
2117
|
sort_mode: str | None = None,
|
|
1943
2118
|
top: int | None = None,
|
|
2119
|
+
json_output: bool = False,
|
|
2120
|
+
pretty: bool = False,
|
|
1944
2121
|
):
|
|
1945
2122
|
commands = _apply_list_options(commands, source_hash, sort_mode, top)
|
|
1946
2123
|
|
|
2124
|
+
if json_output:
|
|
2125
|
+
print_commands_json(commands, compact=compact, pretty=pretty)
|
|
2126
|
+
return
|
|
2127
|
+
|
|
1947
2128
|
if compact:
|
|
1948
2129
|
print(" ".join(cmd.name for cmd in commands))
|
|
1949
2130
|
return
|
|
@@ -1974,9 +2155,15 @@ def list_mcp_commands(
|
|
|
1974
2155
|
source_hash: str = "",
|
|
1975
2156
|
sort_mode: str | None = None,
|
|
1976
2157
|
top: int | None = None,
|
|
2158
|
+
json_output: bool = False,
|
|
2159
|
+
pretty: bool = False,
|
|
1977
2160
|
):
|
|
1978
2161
|
commands = _apply_list_options(commands, source_hash, sort_mode, top)
|
|
1979
2162
|
|
|
2163
|
+
if json_output:
|
|
2164
|
+
print_commands_json(commands, compact=compact, pretty=pretty)
|
|
2165
|
+
return
|
|
2166
|
+
|
|
1980
2167
|
if compact:
|
|
1981
2168
|
print(" ".join(cmd.name for cmd in commands))
|
|
1982
2169
|
return
|
|
@@ -2087,6 +2274,7 @@ def execute_openapi(
|
|
|
2087
2274
|
toon: bool = False,
|
|
2088
2275
|
oauth_provider: "httpx.Auth | None" = None,
|
|
2089
2276
|
head: int | None = None,
|
|
2277
|
+
json_output: bool = False,
|
|
2090
2278
|
):
|
|
2091
2279
|
path, query_params, extra_headers, body, files = _collect_openapi_params(cmd, args)
|
|
2092
2280
|
url = base_url.rstrip("/") + path
|
|
@@ -2128,6 +2316,14 @@ def execute_openapi(
|
|
|
2128
2316
|
for _, file_tuple in files.items():
|
|
2129
2317
|
file_tuple[1].close()
|
|
2130
2318
|
|
|
2319
|
+
if json_output:
|
|
2320
|
+
try:
|
|
2321
|
+
data = resp.json()
|
|
2322
|
+
except Exception:
|
|
2323
|
+
data = resp.text
|
|
2324
|
+
output_result(data, pretty=pretty, head=head, json_output=True)
|
|
2325
|
+
return
|
|
2326
|
+
|
|
2131
2327
|
if raw:
|
|
2132
2328
|
sys.stdout.buffer.write(resp.content)
|
|
2133
2329
|
return
|
|
@@ -2172,6 +2368,7 @@ def run_mcp_http(
|
|
|
2172
2368
|
top: int | None = None,
|
|
2173
2369
|
compact: bool = False,
|
|
2174
2370
|
source_hash: str = "",
|
|
2371
|
+
json_output: bool = False,
|
|
2175
2372
|
):
|
|
2176
2373
|
extra = dict(
|
|
2177
2374
|
resource_action=resource_action,
|
|
@@ -2186,6 +2383,7 @@ def run_mcp_http(
|
|
|
2186
2383
|
top=top,
|
|
2187
2384
|
compact=compact,
|
|
2188
2385
|
source_hash=source_hash,
|
|
2386
|
+
json_output=json_output,
|
|
2189
2387
|
)
|
|
2190
2388
|
|
|
2191
2389
|
async def _run():
|
|
@@ -2275,6 +2473,7 @@ def run_mcp_stdio(
|
|
|
2275
2473
|
top: int | None = None,
|
|
2276
2474
|
compact: bool = False,
|
|
2277
2475
|
source_hash: str = "",
|
|
2476
|
+
json_output: bool = False,
|
|
2278
2477
|
):
|
|
2279
2478
|
extra = dict(
|
|
2280
2479
|
resource_action=resource_action,
|
|
@@ -2289,6 +2488,7 @@ def run_mcp_stdio(
|
|
|
2289
2488
|
top=top,
|
|
2290
2489
|
compact=compact,
|
|
2291
2490
|
source_hash=source_hash,
|
|
2491
|
+
json_output=json_output,
|
|
2292
2492
|
)
|
|
2293
2493
|
|
|
2294
2494
|
import anyio
|
|
@@ -2344,11 +2544,13 @@ async def _mcp_session(
|
|
|
2344
2544
|
top: int | None = None,
|
|
2345
2545
|
compact: bool = False,
|
|
2346
2546
|
source_hash: str = "",
|
|
2547
|
+
json_output: bool = False,
|
|
2347
2548
|
):
|
|
2348
2549
|
# Handle resource operations
|
|
2349
2550
|
if resource_action:
|
|
2350
2551
|
await _handle_resources(
|
|
2351
2552
|
session, resource_action, resource_uri, pretty, raw, toon, head=head,
|
|
2553
|
+
json_output=json_output,
|
|
2352
2554
|
)
|
|
2353
2555
|
return
|
|
2354
2556
|
|
|
@@ -2356,13 +2558,14 @@ async def _mcp_session(
|
|
|
2356
2558
|
if prompt_action:
|
|
2357
2559
|
await _handle_prompts(
|
|
2358
2560
|
session, prompt_action, prompt_name, prompt_arguments,
|
|
2359
|
-
pretty, raw, toon, head=head,
|
|
2561
|
+
pretty, raw, toon, head=head, json_output=json_output,
|
|
2360
2562
|
)
|
|
2361
2563
|
return
|
|
2362
2564
|
|
|
2363
2565
|
list_kwargs = dict(
|
|
2364
2566
|
verbose=verbose, compact=compact,
|
|
2365
2567
|
source_hash=source_hash, sort_mode=sort_mode, top=top,
|
|
2568
|
+
json_output=json_output, pretty=pretty,
|
|
2366
2569
|
)
|
|
2367
2570
|
|
|
2368
2571
|
if list_mode:
|
|
@@ -2379,12 +2582,15 @@ async def _mcp_session(
|
|
|
2379
2582
|
if search_pattern:
|
|
2380
2583
|
commands = _filter_commands(commands, search_pattern)
|
|
2381
2584
|
if not commands:
|
|
2382
|
-
|
|
2585
|
+
if json_output:
|
|
2586
|
+
list_mcp_commands(commands, **list_kwargs)
|
|
2587
|
+
else:
|
|
2588
|
+
print(f"\nNo tools matching '{search_pattern}'.")
|
|
2383
2589
|
return
|
|
2384
|
-
if not compact:
|
|
2590
|
+
if not compact and not json_output:
|
|
2385
2591
|
print(f"\nTools matching '{search_pattern}':")
|
|
2386
2592
|
else:
|
|
2387
|
-
if not compact:
|
|
2593
|
+
if not compact and not json_output:
|
|
2388
2594
|
print("\nAvailable tools:")
|
|
2389
2595
|
list_mcp_commands(commands, **list_kwargs)
|
|
2390
2596
|
return
|
|
@@ -2398,6 +2604,12 @@ async def _mcp_session(
|
|
|
2398
2604
|
|
|
2399
2605
|
result = await session.call_tool(tool_name, arguments or {})
|
|
2400
2606
|
|
|
2607
|
+
if json_output:
|
|
2608
|
+
# Emit the full MCP CallToolResult envelope (content, structuredContent,
|
|
2609
|
+
# isError) using the SDK's own serializer — 100% MCP-compatible.
|
|
2610
|
+
output_result(result.model_dump(mode="json"), pretty=pretty, head=head, json_output=True)
|
|
2611
|
+
return
|
|
2612
|
+
|
|
2401
2613
|
text = _extract_content_parts(result.content)
|
|
2402
2614
|
output_result(text, pretty=pretty, raw=raw, toon=toon, head=head)
|
|
2403
2615
|
|
|
@@ -2415,8 +2627,9 @@ async def _handle_resources(
|
|
|
2415
2627
|
raw: bool,
|
|
2416
2628
|
toon: bool,
|
|
2417
2629
|
head: int | None = None,
|
|
2630
|
+
json_output: bool = False,
|
|
2418
2631
|
):
|
|
2419
|
-
_out = dict(pretty=pretty, raw=raw, toon=toon, head=head)
|
|
2632
|
+
_out = dict(pretty=pretty, raw=raw, toon=toon, head=head, json_output=json_output)
|
|
2420
2633
|
if action == "list":
|
|
2421
2634
|
result = await session.list_resources()
|
|
2422
2635
|
data = [
|
|
@@ -2469,8 +2682,9 @@ async def _handle_prompts(
|
|
|
2469
2682
|
raw: bool,
|
|
2470
2683
|
toon: bool,
|
|
2471
2684
|
head: int | None = None,
|
|
2685
|
+
json_output: bool = False,
|
|
2472
2686
|
):
|
|
2473
|
-
_out = dict(pretty=pretty, raw=raw, toon=toon, head=head)
|
|
2687
|
+
_out = dict(pretty=pretty, raw=raw, toon=toon, head=head, json_output=json_output)
|
|
2474
2688
|
if action == "list":
|
|
2475
2689
|
result = await session.list_prompts()
|
|
2476
2690
|
data = [
|
|
@@ -3011,6 +3225,7 @@ def handle_mcp(
|
|
|
3011
3225
|
sort_mode: str | None = None,
|
|
3012
3226
|
top: int | None = None,
|
|
3013
3227
|
compact: bool = False,
|
|
3228
|
+
json_output: bool = False,
|
|
3014
3229
|
):
|
|
3015
3230
|
# Build a config dict for cache key generation (future-proof)
|
|
3016
3231
|
config_for_cache = {
|
|
@@ -3033,6 +3248,7 @@ def handle_mcp(
|
|
|
3033
3248
|
prompt_name=prompt_name,
|
|
3034
3249
|
prompt_arguments=prompt_arguments,
|
|
3035
3250
|
head=head,
|
|
3251
|
+
json_output=json_output,
|
|
3036
3252
|
)
|
|
3037
3253
|
_dispatch_mcp_call(
|
|
3038
3254
|
source, is_stdio, auth_headers, env_vars,
|
|
@@ -3045,6 +3261,7 @@ def handle_mcp(
|
|
|
3045
3261
|
list_kwargs = dict(
|
|
3046
3262
|
verbose=verbose, compact=compact,
|
|
3047
3263
|
source_hash=src_hash, sort_mode=sort_mode, top=top,
|
|
3264
|
+
json_output=json_output, pretty=pretty,
|
|
3048
3265
|
)
|
|
3049
3266
|
|
|
3050
3267
|
if list_mode:
|
|
@@ -3058,7 +3275,7 @@ def handle_mcp(
|
|
|
3058
3275
|
commands = filter_commands(
|
|
3059
3276
|
commands, bake_config.include, bake_config.exclude, bake_config.methods,
|
|
3060
3277
|
)
|
|
3061
|
-
if not compact:
|
|
3278
|
+
if not compact and not json_output:
|
|
3062
3279
|
print("\nAvailable tools:")
|
|
3063
3280
|
list_mcp_commands(commands, **list_kwargs)
|
|
3064
3281
|
return
|
|
@@ -3070,6 +3287,7 @@ def handle_mcp(
|
|
|
3070
3287
|
verbose=verbose,
|
|
3071
3288
|
sort_mode=sort_mode, top=top, compact=compact,
|
|
3072
3289
|
source_hash=src_hash,
|
|
3290
|
+
json_output=json_output,
|
|
3073
3291
|
)
|
|
3074
3292
|
return
|
|
3075
3293
|
|
|
@@ -3086,10 +3304,10 @@ def handle_mcp(
|
|
|
3086
3304
|
)
|
|
3087
3305
|
|
|
3088
3306
|
if not remaining:
|
|
3089
|
-
if not compact:
|
|
3307
|
+
if not compact and not json_output:
|
|
3090
3308
|
print("Available tools:")
|
|
3091
3309
|
list_mcp_commands(commands, **list_kwargs)
|
|
3092
|
-
if not compact:
|
|
3310
|
+
if not compact and not json_output:
|
|
3093
3311
|
print("\nUse --list for the same output, or provide a subcommand.")
|
|
3094
3312
|
return
|
|
3095
3313
|
|
|
@@ -3116,6 +3334,7 @@ def handle_mcp(
|
|
|
3116
3334
|
source, is_stdio, auth_headers, env_vars,
|
|
3117
3335
|
cmd.tool_name, arguments, False, pretty, raw, key, ttl, refresh,
|
|
3118
3336
|
toon=toon, transport=transport, oauth_provider=oauth_provider,
|
|
3337
|
+
json_output=json_output,
|
|
3119
3338
|
)
|
|
3120
3339
|
|
|
3121
3340
|
# Record usage after successful execution
|
|
@@ -3333,6 +3552,16 @@ def _build_main_parser() -> argparse.ArgumentParser:
|
|
|
3333
3552
|
)
|
|
3334
3553
|
pre.add_argument("--pretty", action="store_true", help="Pretty-print JSON output")
|
|
3335
3554
|
pre.add_argument("--raw", action="store_true", help="Print raw response body")
|
|
3555
|
+
pre.add_argument(
|
|
3556
|
+
"--json",
|
|
3557
|
+
action="store_true",
|
|
3558
|
+
dest="json_output",
|
|
3559
|
+
help=(
|
|
3560
|
+
"Force valid JSON output for every command. --list emits a JSON array of "
|
|
3561
|
+
"commands; MCP tool calls emit the full result envelope including "
|
|
3562
|
+
"structuredContent and isError. Takes precedence over --raw and --toon."
|
|
3563
|
+
),
|
|
3564
|
+
)
|
|
3336
3565
|
pre.add_argument(
|
|
3337
3566
|
"--toon",
|
|
3338
3567
|
action="store_true",
|
|
@@ -3594,32 +3823,28 @@ def _handle_session_operations(
|
|
|
3594
3823
|
|
|
3595
3824
|
# --- Session client mode ---
|
|
3596
3825
|
sess_name = pre_args.session
|
|
3826
|
+
_sess_out = dict(
|
|
3827
|
+
pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
|
|
3828
|
+
json_output=pre_args.json_output,
|
|
3829
|
+
)
|
|
3597
3830
|
|
|
3598
3831
|
if pre_args.list_resources:
|
|
3599
3832
|
result = _session_request(sess_name, "list_resources")
|
|
3600
|
-
output_result(
|
|
3601
|
-
result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
|
|
3602
|
-
)
|
|
3833
|
+
output_result(result, **_sess_out)
|
|
3603
3834
|
return True
|
|
3604
3835
|
if pre_args.list_resource_templates:
|
|
3605
3836
|
result = _session_request(sess_name, "list_resource_templates")
|
|
3606
|
-
output_result(
|
|
3607
|
-
result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
|
|
3608
|
-
)
|
|
3837
|
+
output_result(result, **_sess_out)
|
|
3609
3838
|
return True
|
|
3610
3839
|
if pre_args.read_resource:
|
|
3611
3840
|
result = _session_request(
|
|
3612
3841
|
sess_name, "read_resource", {"uri": pre_args.read_resource}
|
|
3613
3842
|
)
|
|
3614
|
-
output_result(
|
|
3615
|
-
result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
|
|
3616
|
-
)
|
|
3843
|
+
output_result(result, **_sess_out)
|
|
3617
3844
|
return True
|
|
3618
3845
|
if pre_args.list_prompts:
|
|
3619
3846
|
result = _session_request(sess_name, "list_prompts")
|
|
3620
|
-
output_result(
|
|
3621
|
-
result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
|
|
3622
|
-
)
|
|
3847
|
+
output_result(result, **_sess_out)
|
|
3623
3848
|
return True
|
|
3624
3849
|
if pre_args.get_prompt:
|
|
3625
3850
|
p_args = {}
|
|
@@ -3632,9 +3857,7 @@ def _handle_session_operations(
|
|
|
3632
3857
|
"get_prompt",
|
|
3633
3858
|
{"name": pre_args.get_prompt, "arguments": p_args},
|
|
3634
3859
|
)
|
|
3635
|
-
output_result(
|
|
3636
|
-
result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
|
|
3637
|
-
)
|
|
3860
|
+
output_result(result, **_sess_out)
|
|
3638
3861
|
return True
|
|
3639
3862
|
if pre_args.list_commands:
|
|
3640
3863
|
result = _session_request(sess_name, "list_tools")
|
|
@@ -3642,21 +3865,36 @@ def _handle_session_operations(
|
|
|
3642
3865
|
if search_pattern:
|
|
3643
3866
|
commands = _filter_commands(commands, search_pattern)
|
|
3644
3867
|
if not commands:
|
|
3645
|
-
|
|
3868
|
+
if pre_args.json_output:
|
|
3869
|
+
list_mcp_commands(
|
|
3870
|
+
commands, verbose=pre_args.verbose,
|
|
3871
|
+
json_output=True, pretty=pre_args.pretty,
|
|
3872
|
+
)
|
|
3873
|
+
else:
|
|
3874
|
+
print(f"\nNo tools matching '{search_pattern}'.")
|
|
3646
3875
|
return True
|
|
3647
|
-
|
|
3648
|
-
|
|
3876
|
+
if not pre_args.json_output:
|
|
3877
|
+
print(f"\nTools matching '{search_pattern}':")
|
|
3878
|
+
elif not pre_args.json_output:
|
|
3649
3879
|
print("\nAvailable tools:")
|
|
3650
|
-
list_mcp_commands(
|
|
3880
|
+
list_mcp_commands(
|
|
3881
|
+
commands, verbose=pre_args.verbose,
|
|
3882
|
+
json_output=pre_args.json_output, pretty=pre_args.pretty,
|
|
3883
|
+
)
|
|
3651
3884
|
return True
|
|
3652
3885
|
|
|
3653
3886
|
# Tool call via session
|
|
3654
3887
|
if not remaining:
|
|
3655
3888
|
result = _session_request(sess_name, "list_tools")
|
|
3656
3889
|
commands = extract_mcp_commands(result)
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3890
|
+
if not pre_args.json_output:
|
|
3891
|
+
print("Available tools:")
|
|
3892
|
+
list_mcp_commands(
|
|
3893
|
+
commands, verbose=pre_args.verbose,
|
|
3894
|
+
json_output=pre_args.json_output, pretty=pre_args.pretty,
|
|
3895
|
+
)
|
|
3896
|
+
if not pre_args.json_output:
|
|
3897
|
+
print("\nUse --list for the same output, or provide a subcommand.")
|
|
3660
3898
|
return True
|
|
3661
3899
|
|
|
3662
3900
|
tools = _session_request(sess_name, "list_tools")
|
|
@@ -3682,9 +3920,7 @@ def _handle_session_operations(
|
|
|
3682
3920
|
result = _session_request(
|
|
3683
3921
|
sess_name, "call_tool", {"name": cmd.tool_name, "arguments": arguments}
|
|
3684
3922
|
)
|
|
3685
|
-
output_result(
|
|
3686
|
-
result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
|
|
3687
|
-
)
|
|
3923
|
+
output_result(result, **_sess_out)
|
|
3688
3924
|
return True
|
|
3689
3925
|
|
|
3690
3926
|
|
|
@@ -3749,15 +3985,19 @@ def _handle_openapi_mode(
|
|
|
3749
3985
|
list_kwargs = dict(
|
|
3750
3986
|
verbose=pre_args.verbose, compact=pre_args.compact,
|
|
3751
3987
|
source_hash=src_hash, sort_mode=pre_args.sort_mode, top=pre_args.top,
|
|
3988
|
+
json_output=pre_args.json_output, pretty=pre_args.pretty,
|
|
3752
3989
|
)
|
|
3753
3990
|
|
|
3754
3991
|
if pre_args.list_commands:
|
|
3755
3992
|
if search_pattern:
|
|
3756
3993
|
commands = _filter_commands(commands, search_pattern)
|
|
3757
3994
|
if not commands:
|
|
3758
|
-
|
|
3995
|
+
if not pre_args.json_output:
|
|
3996
|
+
print(f"\nNo tools matching '{search_pattern}'.")
|
|
3997
|
+
else:
|
|
3998
|
+
list_openapi_commands(commands, **list_kwargs)
|
|
3759
3999
|
return
|
|
3760
|
-
if not pre_args.compact:
|
|
4000
|
+
if not pre_args.compact and not pre_args.json_output:
|
|
3761
4001
|
print(f"\nTools matching '{search_pattern}':")
|
|
3762
4002
|
list_openapi_commands(commands, **list_kwargs)
|
|
3763
4003
|
return
|
|
@@ -3801,7 +4041,7 @@ def _handle_openapi_mode(
|
|
|
3801
4041
|
execute_openapi(
|
|
3802
4042
|
args, cmd, base_url, auth_headers,
|
|
3803
4043
|
pre_args.pretty, pre_args.raw, toon=pre_args.toon,
|
|
3804
|
-
oauth_provider=oauth_provider,
|
|
4044
|
+
oauth_provider=oauth_provider, json_output=pre_args.json_output,
|
|
3805
4045
|
)
|
|
3806
4046
|
|
|
3807
4047
|
# Record usage after successful execution
|
|
@@ -3861,6 +4101,7 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
|
|
|
3861
4101
|
sort_mode=pre_args.sort_mode,
|
|
3862
4102
|
top=pre_args.top,
|
|
3863
4103
|
compact=pre_args.compact,
|
|
4104
|
+
json_output=pre_args.json_output,
|
|
3864
4105
|
)
|
|
3865
4106
|
return
|
|
3866
4107
|
|
|
@@ -3895,6 +4136,7 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
|
|
|
3895
4136
|
sort_mode=pre_args.sort_mode,
|
|
3896
4137
|
top=pre_args.top,
|
|
3897
4138
|
compact=pre_args.compact,
|
|
4139
|
+
json_output=pre_args.json_output,
|
|
3898
4140
|
)
|
|
3899
4141
|
return
|
|
3900
4142
|
|
|
File without changes
|
|
File without changes
|