mcp2cli 2.2.4__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp2cli
3
- Version: 2.2.4
3
+ Version: 2.4.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>
@@ -225,6 +225,14 @@ mcp2cli --spec ./spec.json --pretty list-pets
225
225
  # Raw response body (no JSON parsing)
226
226
  mcp2cli --spec ./spec.json --raw get-data
227
227
 
228
+ # Filter JSON with jq (preferred over Python for JSON processing)
229
+ mcp2cli --spec ./spec.json list-pets --jq '.[].name'
230
+ mcp2cli --spec ./spec.json list-pets --jq '[.[] | select(.status == "available")] | length'
231
+
232
+ # Truncate large responses to first N records
233
+ mcp2cli --spec ./spec.json list-records --head 5
234
+ mcp2cli --spec ./spec.json list-records --head 3 --jq '.' # preview then filter
235
+
228
236
  # Pipe-friendly (compact JSON when not a TTY)
229
237
  mcp2cli --spec ./spec.json list-pets | jq '.[] | .name'
230
238
 
@@ -282,6 +290,8 @@ Options:
282
290
  --pretty Pretty-print JSON output
283
291
  --raw Print raw response body
284
292
  --toon Encode output as TOON (token-efficient for LLMs)
293
+ --jq EXPR Filter JSON output through jq expression
294
+ --head N Limit output to first N records (arrays)
285
295
  --version Show version
286
296
 
287
297
  Bake mode:
@@ -206,6 +206,14 @@ mcp2cli --spec ./spec.json --pretty list-pets
206
206
  # Raw response body (no JSON parsing)
207
207
  mcp2cli --spec ./spec.json --raw get-data
208
208
 
209
+ # Filter JSON with jq (preferred over Python for JSON processing)
210
+ mcp2cli --spec ./spec.json list-pets --jq '.[].name'
211
+ mcp2cli --spec ./spec.json list-pets --jq '[.[] | select(.status == "available")] | length'
212
+
213
+ # Truncate large responses to first N records
214
+ mcp2cli --spec ./spec.json list-records --head 5
215
+ mcp2cli --spec ./spec.json list-records --head 3 --jq '.' # preview then filter
216
+
209
217
  # Pipe-friendly (compact JSON when not a TTY)
210
218
  mcp2cli --spec ./spec.json list-pets | jq '.[] | .name'
211
219
 
@@ -263,6 +271,8 @@ Options:
263
271
  --pretty Pretty-print JSON output
264
272
  --raw Print raw response body
265
273
  --toon Encode output as TOON (token-efficient for LLMs)
274
+ --jq EXPR Filter JSON output through jq expression
275
+ --head N Limit output to first N records (arrays)
266
276
  --version Show version
267
277
 
268
278
  Bake mode:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp2cli"
3
- version = "2.2.4"
3
+ version = "2.4.0"
4
4
  description = "Turn any MCP server or OpenAPI spec into a CLI"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -2,12 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "2.2.1"
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
@@ -246,7 +248,48 @@ def _toon_encode(json_str: str) -> str | None:
246
248
  return None
247
249
 
248
250
 
249
- def output_result(data, *, pretty: bool = False, raw: bool = False, toon: bool = False):
251
+ def _run_jq(json_str: str, expr: str) -> str:
252
+ """Pipe JSON through jq with the given expression. Exits on failure."""
253
+ if not shutil.which("jq"):
254
+ print(
255
+ "Error: --jq requires jq to be installed. "
256
+ "See https://jqlang.github.io/jq/",
257
+ file=sys.stderr,
258
+ )
259
+ sys.exit(1)
260
+ try:
261
+ result = subprocess.run(
262
+ ["jq", expr],
263
+ input=json_str,
264
+ capture_output=True,
265
+ text=True,
266
+ timeout=30,
267
+ )
268
+ if result.returncode != 0:
269
+ print(f"jq error: {result.stderr.strip()}", file=sys.stderr)
270
+ sys.exit(1)
271
+ return result.stdout
272
+ except subprocess.TimeoutExpired:
273
+ print("Error: jq timed out", file=sys.stderr)
274
+ sys.exit(1)
275
+
276
+
277
+ def _apply_head(data, n: int):
278
+ """Truncate data to first N elements (array) or return as-is (dict/scalar)."""
279
+ if isinstance(data, list):
280
+ return data[:n]
281
+ return data
282
+
283
+
284
+ def output_result(
285
+ data,
286
+ *,
287
+ pretty: bool = False,
288
+ raw: bool = False,
289
+ toon: bool = False,
290
+ jq_expr: str | None = None,
291
+ head: int | None = None,
292
+ ):
250
293
  if raw:
251
294
  if isinstance(data, str):
252
295
  print(data)
@@ -259,6 +302,11 @@ def output_result(data, *, pretty: bool = False, raw: bool = False, toon: bool =
259
302
  except (json.JSONDecodeError, TypeError):
260
303
  print(data)
261
304
  return
305
+ if head is not None:
306
+ data = _apply_head(data, head)
307
+ if jq_expr:
308
+ print(_run_jq(json.dumps(data), jq_expr), end="")
309
+ return
262
310
  if toon:
263
311
  encoded = _toon_encode(json.dumps(data))
264
312
  if encoded is not None:
@@ -276,10 +324,11 @@ def output_result(data, *, pretty: bool = False, raw: bool = False, toon: bool =
276
324
  print(json.dumps(data))
277
325
 
278
326
 
279
- 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]:
280
328
  """Build HTTP headers dict from auth_headers with a Content-Type default."""
281
329
  headers = dict(auth_headers)
282
- headers.setdefault("Content-Type", "application/json")
330
+ if not multipart:
331
+ headers.setdefault("Content-Type", "application/json")
283
332
  return headers
284
333
 
285
334
 
@@ -616,19 +665,43 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
616
665
  )
617
666
  params.append(p)
618
667
 
619
- # Request body
620
- rb_schema = (
621
- details.get("requestBody", {})
622
- .get("content", {})
623
- .get("application/json", {})
624
- .get("schema", {})
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()
625
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
+
626
691
  required_fields = set(rb_schema.get("required", []))
627
692
  properties = rb_schema.get("properties", {})
628
693
  has_body = bool(properties)
629
694
 
630
695
  for prop_name, prop_schema in properties.items():
631
- py_type, suffix = schema_type_to_python(prop_schema)
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"
632
705
  p = ParamDef(
633
706
  name=to_kebab(prop_name),
634
707
  original_name=prop_name,
@@ -636,7 +709,7 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
636
709
  required=prop_name in required_fields,
637
710
  description=(prop_schema.get("description") or prop_name) + suffix,
638
711
  choices=prop_schema.get("enum"),
639
- location="body",
712
+ location=loc,
640
713
  )
641
714
  params.append(p)
642
715
 
@@ -648,6 +721,7 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
648
721
  has_body=has_body,
649
722
  method=method,
650
723
  path=path,
724
+ content_type=cmd_content_type,
651
725
  )
652
726
  )
653
727
 
@@ -1103,6 +1177,8 @@ def execute_graphql(
1103
1177
  toon: bool = False,
1104
1178
  fields_override: str | None = None,
1105
1179
  oauth_provider: "httpx.Auth | None" = None,
1180
+ jq_expr: str | None = None,
1181
+ head: int | None = None,
1106
1182
  ):
1107
1183
  """Build and execute a GraphQL query/mutation."""
1108
1184
  document, variables, field_name = _build_graphql_document(
@@ -1126,13 +1202,13 @@ def execute_graphql(
1126
1202
  print(f"GraphQL error: {msgs}", file=sys.stderr)
1127
1203
  sys.exit(1)
1128
1204
  # Partial errors — include them in output
1129
- output_result(result, pretty=pretty, raw=raw, toon=toon)
1205
+ output_result(result, pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1130
1206
  return
1131
1207
 
1132
1208
  data = result.get("data", {})
1133
1209
  # Extract the specific field's data
1134
1210
  field_data = data.get(field_name, data)
1135
- output_result(field_data, pretty=pretty, raw=raw, toon=toon)
1211
+ output_result(field_data, pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1136
1212
 
1137
1213
 
1138
1214
  def handle_graphql(
@@ -1148,6 +1224,8 @@ def handle_graphql(
1148
1224
  toon: bool = False,
1149
1225
  fields_override: str | None = None,
1150
1226
  oauth_provider: "httpx.Auth | None" = None,
1227
+ jq_expr: str | None = None,
1228
+ head: int | None = None,
1151
1229
  ):
1152
1230
  """Top-level handler for --graphql mode."""
1153
1231
  schema = load_graphql_schema(url, auth_headers, cache_key, ttl, refresh, oauth_provider=oauth_provider)
@@ -1175,6 +1253,7 @@ def handle_graphql(
1175
1253
  execute_graphql(
1176
1254
  args, cmd, url, schema, auth_headers, pretty, raw, toon=toon,
1177
1255
  fields_override=fields_override, oauth_provider=oauth_provider,
1256
+ jq_expr=jq_expr, head=head,
1178
1257
  )
1179
1258
 
1180
1259
 
@@ -1611,16 +1690,17 @@ def _filter_commands(commands: list[CommandDef], pattern: str) -> list[CommandDe
1611
1690
  def _collect_openapi_params(
1612
1691
  cmd: CommandDef,
1613
1692
  args: argparse.Namespace,
1614
- ) -> tuple[str, dict[str, str], dict[str, str], dict | None]:
1693
+ ) -> tuple[str, dict[str, str], dict[str, str], dict | None, dict | None]:
1615
1694
  """Collect OpenAPI params from parsed args, separated by location.
1616
1695
 
1617
- Returns (path, query_params, extra_headers, body_or_none) where *path*
1618
- 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.
1619
1698
  """
1620
1699
  path = cmd.path or ""
1621
1700
  query_params: dict[str, str] = {}
1622
1701
  extra_headers: dict[str, str] = {}
1623
1702
  body: dict | None = None
1703
+ files: dict | None = None
1624
1704
 
1625
1705
  for p in cmd.params:
1626
1706
  if p.location == "path":
@@ -1650,6 +1730,17 @@ def _collect_openapi_params(
1650
1730
  continue
1651
1731
  if p.location == "path":
1652
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
1653
1744
  if val is not None:
1654
1745
  body[p.original_name] = val
1655
1746
  if not body:
@@ -1661,7 +1752,7 @@ def _collect_openapi_params(
1661
1752
  if val is not None:
1662
1753
  query_params[p.original_name] = val
1663
1754
 
1664
- return path, query_params, extra_headers, body
1755
+ return path, query_params, extra_headers, body, files
1665
1756
 
1666
1757
 
1667
1758
  def execute_openapi(
@@ -1673,22 +1764,48 @@ def execute_openapi(
1673
1764
  raw: bool,
1674
1765
  toon: bool = False,
1675
1766
  oauth_provider: "httpx.Auth | None" = None,
1767
+ jq_expr: str | None = None,
1768
+ head: int | None = None,
1676
1769
  ):
1677
- path, query_params, extra_headers, body = _collect_openapi_params(cmd, args)
1770
+ path, query_params, extra_headers, body, files = _collect_openapi_params(cmd, args)
1678
1771
  url = base_url.rstrip("/") + path
1679
1772
 
1680
- headers = _build_http_headers(auth_headers)
1773
+ is_multipart = files is not None or cmd.content_type == "multipart/form-data"
1774
+ headers = _build_http_headers(auth_headers, multipart=is_multipart)
1681
1775
  headers.update(extra_headers)
1682
1776
 
1683
- with httpx.Client(timeout=60, auth=oauth_provider) as client:
1684
- resp = client.request(
1685
- (cmd.method or "get").upper(),
1686
- url,
1687
- headers=headers,
1688
- params=query_params or None,
1689
- json=body,
1690
- )
1691
- _handle_http_error(resp)
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()
1692
1809
 
1693
1810
  if raw:
1694
1811
  sys.stdout.buffer.write(resp.content)
@@ -1700,7 +1817,7 @@ def execute_openapi(
1700
1817
  print(resp.text)
1701
1818
  return
1702
1819
 
1703
- output_result(data, pretty=pretty, toon=toon)
1820
+ output_result(data, pretty=pretty, toon=toon, jq_expr=jq_expr, head=head)
1704
1821
 
1705
1822
 
1706
1823
  # ---------------------------------------------------------------------------
@@ -1728,6 +1845,8 @@ def run_mcp_http(
1728
1845
  prompt_name: str | None = None,
1729
1846
  prompt_arguments: dict | None = None,
1730
1847
  search_pattern: str | None = None,
1848
+ jq_expr: str | None = None,
1849
+ head: int | None = None,
1731
1850
  ):
1732
1851
  extra = dict(
1733
1852
  resource_action=resource_action,
@@ -1736,6 +1855,8 @@ def run_mcp_http(
1736
1855
  prompt_name=prompt_name,
1737
1856
  prompt_arguments=prompt_arguments,
1738
1857
  search_pattern=search_pattern,
1858
+ jq_expr=jq_expr,
1859
+ head=head,
1739
1860
  )
1740
1861
 
1741
1862
  async def _run():
@@ -1819,6 +1940,8 @@ def run_mcp_stdio(
1819
1940
  prompt_name: str | None = None,
1820
1941
  prompt_arguments: dict | None = None,
1821
1942
  search_pattern: str | None = None,
1943
+ jq_expr: str | None = None,
1944
+ head: int | None = None,
1822
1945
  ):
1823
1946
  extra = dict(
1824
1947
  resource_action=resource_action,
@@ -1827,6 +1950,8 @@ def run_mcp_stdio(
1827
1950
  prompt_name=prompt_name,
1828
1951
  prompt_arguments=prompt_arguments,
1829
1952
  search_pattern=search_pattern,
1953
+ jq_expr=jq_expr,
1954
+ head=head,
1830
1955
  )
1831
1956
 
1832
1957
  import anyio
@@ -1876,18 +2001,22 @@ async def _mcp_session(
1876
2001
  prompt_name: str | None = None,
1877
2002
  prompt_arguments: dict | None = None,
1878
2003
  search_pattern: str | None = None,
2004
+ jq_expr: str | None = None,
2005
+ head: int | None = None,
1879
2006
  ):
1880
2007
  # Handle resource operations
1881
2008
  if resource_action:
1882
2009
  await _handle_resources(
1883
- session, resource_action, resource_uri, pretty, raw, toon
2010
+ session, resource_action, resource_uri, pretty, raw, toon,
2011
+ jq_expr=jq_expr, head=head,
1884
2012
  )
1885
2013
  return
1886
2014
 
1887
2015
  # Handle prompt operations
1888
2016
  if prompt_action:
1889
2017
  await _handle_prompts(
1890
- session, prompt_action, prompt_name, prompt_arguments, pretty, raw, toon
2018
+ session, prompt_action, prompt_name, prompt_arguments, pretty, raw, toon,
2019
+ jq_expr=jq_expr, head=head,
1891
2020
  )
1892
2021
  return
1893
2022
 
@@ -1923,7 +2052,7 @@ async def _mcp_session(
1923
2052
  result = await session.call_tool(tool_name, arguments or {})
1924
2053
 
1925
2054
  text = _extract_content_parts(result.content)
1926
- output_result(text, pretty=pretty, raw=raw, toon=toon)
2055
+ output_result(text, pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1927
2056
 
1928
2057
 
1929
2058
  # ---------------------------------------------------------------------------
@@ -1932,8 +2061,10 @@ async def _mcp_session(
1932
2061
 
1933
2062
 
1934
2063
  async def _handle_resources(
1935
- session, action: str, uri: str | None, pretty: bool, raw: bool, toon: bool
2064
+ session, action: str, uri: str | None, pretty: bool, raw: bool, toon: bool,
2065
+ jq_expr: str | None = None, head: int | None = None,
1936
2066
  ):
2067
+ _out = dict(pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1937
2068
  if action == "list":
1938
2069
  result = await session.list_resources()
1939
2070
  data = [
@@ -1945,7 +2076,7 @@ async def _handle_resources(
1945
2076
  }
1946
2077
  for r in result.resources
1947
2078
  ]
1948
- output_result(data, pretty=pretty, raw=raw, toon=toon)
2079
+ output_result(data, **_out)
1949
2080
  elif action == "templates":
1950
2081
  result = await session.list_resource_templates()
1951
2082
  data = [
@@ -1957,7 +2088,7 @@ async def _handle_resources(
1957
2088
  }
1958
2089
  for t in result.resourceTemplates
1959
2090
  ]
1960
- output_result(data, pretty=pretty, raw=raw, toon=toon)
2091
+ output_result(data, **_out)
1961
2092
  elif action == "read":
1962
2093
  from pydantic import AnyUrl
1963
2094
 
@@ -1969,7 +2100,7 @@ async def _handle_resources(
1969
2100
  elif hasattr(content, "blob"):
1970
2101
  parts.append(content.blob)
1971
2102
  text = "\n".join(parts) if parts else ""
1972
- output_result(text, pretty=pretty, raw=raw, toon=toon)
2103
+ output_result(text, **_out)
1973
2104
 
1974
2105
 
1975
2106
  # ---------------------------------------------------------------------------
@@ -1985,7 +2116,10 @@ async def _handle_prompts(
1985
2116
  pretty: bool,
1986
2117
  raw: bool,
1987
2118
  toon: bool,
2119
+ jq_expr: str | None = None,
2120
+ head: int | None = None,
1988
2121
  ):
2122
+ _out = dict(pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1989
2123
  if action == "list":
1990
2124
  result = await session.list_prompts()
1991
2125
  data = [
@@ -2003,7 +2137,7 @@ async def _handle_prompts(
2003
2137
  }
2004
2138
  for p in result.prompts
2005
2139
  ]
2006
- output_result(data, pretty=pretty, raw=raw, toon=toon)
2140
+ output_result(data, **_out)
2007
2141
  elif action == "get":
2008
2142
  result = await session.get_prompt(name, arguments or {})
2009
2143
  messages = []
@@ -2016,7 +2150,7 @@ async def _handle_prompts(
2016
2150
  {"role": msg.role, "content": json.dumps(content.model_dump())}
2017
2151
  )
2018
2152
  data = {"description": result.description or "", "messages": messages}
2019
- output_result(data, pretty=pretty, raw=raw, toon=toon)
2153
+ output_result(data, **_out)
2020
2154
 
2021
2155
 
2022
2156
  # ---------------------------------------------------------------------------
@@ -2521,6 +2655,8 @@ def handle_mcp(
2521
2655
  prompt_arguments: dict | None = None,
2522
2656
  search_pattern: str | None = None,
2523
2657
  bake_config: BakeConfig | None = None,
2658
+ jq_expr: str | None = None,
2659
+ head: int | None = None,
2524
2660
  ):
2525
2661
  key = cache_key_override or cache_key_for(source)
2526
2662
 
@@ -2532,6 +2668,8 @@ def handle_mcp(
2532
2668
  prompt_action=prompt_action,
2533
2669
  prompt_name=prompt_name,
2534
2670
  prompt_arguments=prompt_arguments,
2671
+ jq_expr=jq_expr,
2672
+ head=head,
2535
2673
  )
2536
2674
  _dispatch_mcp_call(
2537
2675
  source, is_stdio, auth_headers, env_vars,
@@ -2560,6 +2698,7 @@ def handle_mcp(
2560
2698
  None, None, True, pretty, raw, key, ttl, refresh,
2561
2699
  toon=toon, transport=transport, oauth_provider=oauth_provider,
2562
2700
  search_pattern=search_pattern,
2701
+ jq_expr=jq_expr, head=head,
2563
2702
  )
2564
2703
  return
2565
2704
 
@@ -2604,6 +2743,7 @@ def handle_mcp(
2604
2743
  source, is_stdio, auth_headers, env_vars,
2605
2744
  cmd.tool_name, arguments, False, pretty, raw, key, ttl, refresh,
2606
2745
  toon=toon, transport=transport, oauth_provider=oauth_provider,
2746
+ jq_expr=jq_expr, head=head,
2607
2747
  )
2608
2748
 
2609
2749
 
@@ -2786,6 +2926,19 @@ def _build_main_parser() -> argparse.ArgumentParser:
2786
2926
  "of large result sets. Requires @toon-format/cli (npm install -g @toon-format/cli)."
2787
2927
  ),
2788
2928
  )
2929
+ pre.add_argument(
2930
+ "--jq",
2931
+ default=None,
2932
+ metavar="EXPR",
2933
+ help="Filter JSON output through jq (e.g. '.[] | .name'). Requires jq installed.",
2934
+ )
2935
+ pre.add_argument(
2936
+ "--head",
2937
+ type=int,
2938
+ default=None,
2939
+ metavar="N",
2940
+ help="Limit output to first N records (arrays) or N lines (text)",
2941
+ )
2789
2942
  pre.add_argument(
2790
2943
  "--fields",
2791
2944
  default=None,
@@ -3005,13 +3158,15 @@ def _handle_session_operations(
3005
3158
  if pre_args.list_resources:
3006
3159
  result = _session_request(sess_name, "list_resources")
3007
3160
  output_result(
3008
- result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
3161
+ result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3162
+ jq_expr=pre_args.jq, head=pre_args.head,
3009
3163
  )
3010
3164
  return True
3011
3165
  if pre_args.list_resource_templates:
3012
3166
  result = _session_request(sess_name, "list_resource_templates")
3013
3167
  output_result(
3014
- result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
3168
+ result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3169
+ jq_expr=pre_args.jq, head=pre_args.head,
3015
3170
  )
3016
3171
  return True
3017
3172
  if pre_args.read_resource:
@@ -3019,13 +3174,15 @@ def _handle_session_operations(
3019
3174
  sess_name, "read_resource", {"uri": pre_args.read_resource}
3020
3175
  )
3021
3176
  output_result(
3022
- result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
3177
+ result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3178
+ jq_expr=pre_args.jq, head=pre_args.head,
3023
3179
  )
3024
3180
  return True
3025
3181
  if pre_args.list_prompts:
3026
3182
  result = _session_request(sess_name, "list_prompts")
3027
3183
  output_result(
3028
- result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
3184
+ result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3185
+ jq_expr=pre_args.jq, head=pre_args.head,
3029
3186
  )
3030
3187
  return True
3031
3188
  if pre_args.get_prompt:
@@ -3040,7 +3197,8 @@ def _handle_session_operations(
3040
3197
  {"name": pre_args.get_prompt, "arguments": p_args},
3041
3198
  )
3042
3199
  output_result(
3043
- result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
3200
+ result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3201
+ jq_expr=pre_args.jq, head=pre_args.head,
3044
3202
  )
3045
3203
  return True
3046
3204
  if pre_args.list_commands:
@@ -3202,6 +3360,7 @@ def _handle_openapi_mode(
3202
3360
  args, cmd, base_url, auth_headers,
3203
3361
  pre_args.pretty, pre_args.raw, toon=pre_args.toon,
3204
3362
  oauth_provider=oauth_provider,
3363
+ jq_expr=pre_args.jq, head=pre_args.head,
3205
3364
  )
3206
3365
 
3207
3366
 
@@ -3215,6 +3374,11 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
3215
3374
  pre_args, leftover = pre.parse_known_args(global_argv)
3216
3375
  remaining = leftover + tool_argv
3217
3376
 
3377
+ # Validate mutually exclusive output flags
3378
+ if pre_args.jq and pre_args.toon:
3379
+ print("Error: --jq and --toon are mutually exclusive.", file=sys.stderr)
3380
+ sys.exit(1)
3381
+
3218
3382
  # --search implies --list
3219
3383
  search_pattern = pre_args.search_pattern
3220
3384
  if search_pattern:
@@ -3253,6 +3417,8 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
3253
3417
  toon=pre_args.toon,
3254
3418
  fields_override=pre_args.fields,
3255
3419
  oauth_provider=oauth_provider,
3420
+ jq_expr=pre_args.jq,
3421
+ head=pre_args.head,
3256
3422
  )
3257
3423
  return
3258
3424
 
@@ -3282,6 +3448,8 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
3282
3448
  prompt_arguments=prompt_arguments,
3283
3449
  search_pattern=search_pattern,
3284
3450
  bake_config=bake_config,
3451
+ jq_expr=pre_args.jq,
3452
+ head=pre_args.head,
3285
3453
  )
3286
3454
  return
3287
3455
 
File without changes
File without changes