mcp2cli 2.2.4__tar.gz → 2.3.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.3.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.3.0"
4
4
  description = "Turn any MCP server or OpenAPI spec into a CLI"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "2.2.1"
5
+ __version__ = "2.3.0"
6
6
 
7
7
  import argparse
8
8
  import copy
@@ -246,7 +246,48 @@ def _toon_encode(json_str: str) -> str | None:
246
246
  return None
247
247
 
248
248
 
249
- def output_result(data, *, pretty: bool = False, raw: bool = False, toon: bool = False):
249
+ def _run_jq(json_str: str, expr: str) -> str:
250
+ """Pipe JSON through jq with the given expression. Exits on failure."""
251
+ if not shutil.which("jq"):
252
+ print(
253
+ "Error: --jq requires jq to be installed. "
254
+ "See https://jqlang.github.io/jq/",
255
+ file=sys.stderr,
256
+ )
257
+ sys.exit(1)
258
+ try:
259
+ result = subprocess.run(
260
+ ["jq", expr],
261
+ input=json_str,
262
+ capture_output=True,
263
+ text=True,
264
+ timeout=30,
265
+ )
266
+ if result.returncode != 0:
267
+ print(f"jq error: {result.stderr.strip()}", file=sys.stderr)
268
+ sys.exit(1)
269
+ return result.stdout
270
+ except subprocess.TimeoutExpired:
271
+ print("Error: jq timed out", file=sys.stderr)
272
+ sys.exit(1)
273
+
274
+
275
+ def _apply_head(data, n: int):
276
+ """Truncate data to first N elements (array) or return as-is (dict/scalar)."""
277
+ if isinstance(data, list):
278
+ return data[:n]
279
+ return data
280
+
281
+
282
+ def output_result(
283
+ data,
284
+ *,
285
+ pretty: bool = False,
286
+ raw: bool = False,
287
+ toon: bool = False,
288
+ jq_expr: str | None = None,
289
+ head: int | None = None,
290
+ ):
250
291
  if raw:
251
292
  if isinstance(data, str):
252
293
  print(data)
@@ -259,6 +300,11 @@ def output_result(data, *, pretty: bool = False, raw: bool = False, toon: bool =
259
300
  except (json.JSONDecodeError, TypeError):
260
301
  print(data)
261
302
  return
303
+ if head is not None:
304
+ data = _apply_head(data, head)
305
+ if jq_expr:
306
+ print(_run_jq(json.dumps(data), jq_expr), end="")
307
+ return
262
308
  if toon:
263
309
  encoded = _toon_encode(json.dumps(data))
264
310
  if encoded is not None:
@@ -1103,6 +1149,8 @@ def execute_graphql(
1103
1149
  toon: bool = False,
1104
1150
  fields_override: str | None = None,
1105
1151
  oauth_provider: "httpx.Auth | None" = None,
1152
+ jq_expr: str | None = None,
1153
+ head: int | None = None,
1106
1154
  ):
1107
1155
  """Build and execute a GraphQL query/mutation."""
1108
1156
  document, variables, field_name = _build_graphql_document(
@@ -1126,13 +1174,13 @@ def execute_graphql(
1126
1174
  print(f"GraphQL error: {msgs}", file=sys.stderr)
1127
1175
  sys.exit(1)
1128
1176
  # Partial errors — include them in output
1129
- output_result(result, pretty=pretty, raw=raw, toon=toon)
1177
+ output_result(result, pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1130
1178
  return
1131
1179
 
1132
1180
  data = result.get("data", {})
1133
1181
  # Extract the specific field's data
1134
1182
  field_data = data.get(field_name, data)
1135
- output_result(field_data, pretty=pretty, raw=raw, toon=toon)
1183
+ output_result(field_data, pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1136
1184
 
1137
1185
 
1138
1186
  def handle_graphql(
@@ -1148,6 +1196,8 @@ def handle_graphql(
1148
1196
  toon: bool = False,
1149
1197
  fields_override: str | None = None,
1150
1198
  oauth_provider: "httpx.Auth | None" = None,
1199
+ jq_expr: str | None = None,
1200
+ head: int | None = None,
1151
1201
  ):
1152
1202
  """Top-level handler for --graphql mode."""
1153
1203
  schema = load_graphql_schema(url, auth_headers, cache_key, ttl, refresh, oauth_provider=oauth_provider)
@@ -1175,6 +1225,7 @@ def handle_graphql(
1175
1225
  execute_graphql(
1176
1226
  args, cmd, url, schema, auth_headers, pretty, raw, toon=toon,
1177
1227
  fields_override=fields_override, oauth_provider=oauth_provider,
1228
+ jq_expr=jq_expr, head=head,
1178
1229
  )
1179
1230
 
1180
1231
 
@@ -1673,6 +1724,8 @@ def execute_openapi(
1673
1724
  raw: bool,
1674
1725
  toon: bool = False,
1675
1726
  oauth_provider: "httpx.Auth | None" = None,
1727
+ jq_expr: str | None = None,
1728
+ head: int | None = None,
1676
1729
  ):
1677
1730
  path, query_params, extra_headers, body = _collect_openapi_params(cmd, args)
1678
1731
  url = base_url.rstrip("/") + path
@@ -1700,7 +1753,7 @@ def execute_openapi(
1700
1753
  print(resp.text)
1701
1754
  return
1702
1755
 
1703
- output_result(data, pretty=pretty, toon=toon)
1756
+ output_result(data, pretty=pretty, toon=toon, jq_expr=jq_expr, head=head)
1704
1757
 
1705
1758
 
1706
1759
  # ---------------------------------------------------------------------------
@@ -1728,6 +1781,8 @@ def run_mcp_http(
1728
1781
  prompt_name: str | None = None,
1729
1782
  prompt_arguments: dict | None = None,
1730
1783
  search_pattern: str | None = None,
1784
+ jq_expr: str | None = None,
1785
+ head: int | None = None,
1731
1786
  ):
1732
1787
  extra = dict(
1733
1788
  resource_action=resource_action,
@@ -1736,6 +1791,8 @@ def run_mcp_http(
1736
1791
  prompt_name=prompt_name,
1737
1792
  prompt_arguments=prompt_arguments,
1738
1793
  search_pattern=search_pattern,
1794
+ jq_expr=jq_expr,
1795
+ head=head,
1739
1796
  )
1740
1797
 
1741
1798
  async def _run():
@@ -1819,6 +1876,8 @@ def run_mcp_stdio(
1819
1876
  prompt_name: str | None = None,
1820
1877
  prompt_arguments: dict | None = None,
1821
1878
  search_pattern: str | None = None,
1879
+ jq_expr: str | None = None,
1880
+ head: int | None = None,
1822
1881
  ):
1823
1882
  extra = dict(
1824
1883
  resource_action=resource_action,
@@ -1827,6 +1886,8 @@ def run_mcp_stdio(
1827
1886
  prompt_name=prompt_name,
1828
1887
  prompt_arguments=prompt_arguments,
1829
1888
  search_pattern=search_pattern,
1889
+ jq_expr=jq_expr,
1890
+ head=head,
1830
1891
  )
1831
1892
 
1832
1893
  import anyio
@@ -1876,18 +1937,22 @@ async def _mcp_session(
1876
1937
  prompt_name: str | None = None,
1877
1938
  prompt_arguments: dict | None = None,
1878
1939
  search_pattern: str | None = None,
1940
+ jq_expr: str | None = None,
1941
+ head: int | None = None,
1879
1942
  ):
1880
1943
  # Handle resource operations
1881
1944
  if resource_action:
1882
1945
  await _handle_resources(
1883
- session, resource_action, resource_uri, pretty, raw, toon
1946
+ session, resource_action, resource_uri, pretty, raw, toon,
1947
+ jq_expr=jq_expr, head=head,
1884
1948
  )
1885
1949
  return
1886
1950
 
1887
1951
  # Handle prompt operations
1888
1952
  if prompt_action:
1889
1953
  await _handle_prompts(
1890
- session, prompt_action, prompt_name, prompt_arguments, pretty, raw, toon
1954
+ session, prompt_action, prompt_name, prompt_arguments, pretty, raw, toon,
1955
+ jq_expr=jq_expr, head=head,
1891
1956
  )
1892
1957
  return
1893
1958
 
@@ -1923,7 +1988,7 @@ async def _mcp_session(
1923
1988
  result = await session.call_tool(tool_name, arguments or {})
1924
1989
 
1925
1990
  text = _extract_content_parts(result.content)
1926
- output_result(text, pretty=pretty, raw=raw, toon=toon)
1991
+ output_result(text, pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1927
1992
 
1928
1993
 
1929
1994
  # ---------------------------------------------------------------------------
@@ -1932,8 +1997,10 @@ async def _mcp_session(
1932
1997
 
1933
1998
 
1934
1999
  async def _handle_resources(
1935
- session, action: str, uri: str | None, pretty: bool, raw: bool, toon: bool
2000
+ session, action: str, uri: str | None, pretty: bool, raw: bool, toon: bool,
2001
+ jq_expr: str | None = None, head: int | None = None,
1936
2002
  ):
2003
+ _out = dict(pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1937
2004
  if action == "list":
1938
2005
  result = await session.list_resources()
1939
2006
  data = [
@@ -1945,7 +2012,7 @@ async def _handle_resources(
1945
2012
  }
1946
2013
  for r in result.resources
1947
2014
  ]
1948
- output_result(data, pretty=pretty, raw=raw, toon=toon)
2015
+ output_result(data, **_out)
1949
2016
  elif action == "templates":
1950
2017
  result = await session.list_resource_templates()
1951
2018
  data = [
@@ -1957,7 +2024,7 @@ async def _handle_resources(
1957
2024
  }
1958
2025
  for t in result.resourceTemplates
1959
2026
  ]
1960
- output_result(data, pretty=pretty, raw=raw, toon=toon)
2027
+ output_result(data, **_out)
1961
2028
  elif action == "read":
1962
2029
  from pydantic import AnyUrl
1963
2030
 
@@ -1969,7 +2036,7 @@ async def _handle_resources(
1969
2036
  elif hasattr(content, "blob"):
1970
2037
  parts.append(content.blob)
1971
2038
  text = "\n".join(parts) if parts else ""
1972
- output_result(text, pretty=pretty, raw=raw, toon=toon)
2039
+ output_result(text, **_out)
1973
2040
 
1974
2041
 
1975
2042
  # ---------------------------------------------------------------------------
@@ -1985,7 +2052,10 @@ async def _handle_prompts(
1985
2052
  pretty: bool,
1986
2053
  raw: bool,
1987
2054
  toon: bool,
2055
+ jq_expr: str | None = None,
2056
+ head: int | None = None,
1988
2057
  ):
2058
+ _out = dict(pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1989
2059
  if action == "list":
1990
2060
  result = await session.list_prompts()
1991
2061
  data = [
@@ -2003,7 +2073,7 @@ async def _handle_prompts(
2003
2073
  }
2004
2074
  for p in result.prompts
2005
2075
  ]
2006
- output_result(data, pretty=pretty, raw=raw, toon=toon)
2076
+ output_result(data, **_out)
2007
2077
  elif action == "get":
2008
2078
  result = await session.get_prompt(name, arguments or {})
2009
2079
  messages = []
@@ -2016,7 +2086,7 @@ async def _handle_prompts(
2016
2086
  {"role": msg.role, "content": json.dumps(content.model_dump())}
2017
2087
  )
2018
2088
  data = {"description": result.description or "", "messages": messages}
2019
- output_result(data, pretty=pretty, raw=raw, toon=toon)
2089
+ output_result(data, **_out)
2020
2090
 
2021
2091
 
2022
2092
  # ---------------------------------------------------------------------------
@@ -2521,6 +2591,8 @@ def handle_mcp(
2521
2591
  prompt_arguments: dict | None = None,
2522
2592
  search_pattern: str | None = None,
2523
2593
  bake_config: BakeConfig | None = None,
2594
+ jq_expr: str | None = None,
2595
+ head: int | None = None,
2524
2596
  ):
2525
2597
  key = cache_key_override or cache_key_for(source)
2526
2598
 
@@ -2532,6 +2604,8 @@ def handle_mcp(
2532
2604
  prompt_action=prompt_action,
2533
2605
  prompt_name=prompt_name,
2534
2606
  prompt_arguments=prompt_arguments,
2607
+ jq_expr=jq_expr,
2608
+ head=head,
2535
2609
  )
2536
2610
  _dispatch_mcp_call(
2537
2611
  source, is_stdio, auth_headers, env_vars,
@@ -2560,6 +2634,7 @@ def handle_mcp(
2560
2634
  None, None, True, pretty, raw, key, ttl, refresh,
2561
2635
  toon=toon, transport=transport, oauth_provider=oauth_provider,
2562
2636
  search_pattern=search_pattern,
2637
+ jq_expr=jq_expr, head=head,
2563
2638
  )
2564
2639
  return
2565
2640
 
@@ -2604,6 +2679,7 @@ def handle_mcp(
2604
2679
  source, is_stdio, auth_headers, env_vars,
2605
2680
  cmd.tool_name, arguments, False, pretty, raw, key, ttl, refresh,
2606
2681
  toon=toon, transport=transport, oauth_provider=oauth_provider,
2682
+ jq_expr=jq_expr, head=head,
2607
2683
  )
2608
2684
 
2609
2685
 
@@ -2786,6 +2862,19 @@ def _build_main_parser() -> argparse.ArgumentParser:
2786
2862
  "of large result sets. Requires @toon-format/cli (npm install -g @toon-format/cli)."
2787
2863
  ),
2788
2864
  )
2865
+ pre.add_argument(
2866
+ "--jq",
2867
+ default=None,
2868
+ metavar="EXPR",
2869
+ help="Filter JSON output through jq (e.g. '.[] | .name'). Requires jq installed.",
2870
+ )
2871
+ pre.add_argument(
2872
+ "--head",
2873
+ type=int,
2874
+ default=None,
2875
+ metavar="N",
2876
+ help="Limit output to first N records (arrays) or N lines (text)",
2877
+ )
2789
2878
  pre.add_argument(
2790
2879
  "--fields",
2791
2880
  default=None,
@@ -3005,13 +3094,15 @@ def _handle_session_operations(
3005
3094
  if pre_args.list_resources:
3006
3095
  result = _session_request(sess_name, "list_resources")
3007
3096
  output_result(
3008
- result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
3097
+ result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3098
+ jq_expr=pre_args.jq, head=pre_args.head,
3009
3099
  )
3010
3100
  return True
3011
3101
  if pre_args.list_resource_templates:
3012
3102
  result = _session_request(sess_name, "list_resource_templates")
3013
3103
  output_result(
3014
- result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
3104
+ result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3105
+ jq_expr=pre_args.jq, head=pre_args.head,
3015
3106
  )
3016
3107
  return True
3017
3108
  if pre_args.read_resource:
@@ -3019,13 +3110,15 @@ def _handle_session_operations(
3019
3110
  sess_name, "read_resource", {"uri": pre_args.read_resource}
3020
3111
  )
3021
3112
  output_result(
3022
- result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
3113
+ result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3114
+ jq_expr=pre_args.jq, head=pre_args.head,
3023
3115
  )
3024
3116
  return True
3025
3117
  if pre_args.list_prompts:
3026
3118
  result = _session_request(sess_name, "list_prompts")
3027
3119
  output_result(
3028
- result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
3120
+ result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3121
+ jq_expr=pre_args.jq, head=pre_args.head,
3029
3122
  )
3030
3123
  return True
3031
3124
  if pre_args.get_prompt:
@@ -3040,7 +3133,8 @@ def _handle_session_operations(
3040
3133
  {"name": pre_args.get_prompt, "arguments": p_args},
3041
3134
  )
3042
3135
  output_result(
3043
- result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon
3136
+ result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3137
+ jq_expr=pre_args.jq, head=pre_args.head,
3044
3138
  )
3045
3139
  return True
3046
3140
  if pre_args.list_commands:
@@ -3202,6 +3296,7 @@ def _handle_openapi_mode(
3202
3296
  args, cmd, base_url, auth_headers,
3203
3297
  pre_args.pretty, pre_args.raw, toon=pre_args.toon,
3204
3298
  oauth_provider=oauth_provider,
3299
+ jq_expr=pre_args.jq, head=pre_args.head,
3205
3300
  )
3206
3301
 
3207
3302
 
@@ -3215,6 +3310,11 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
3215
3310
  pre_args, leftover = pre.parse_known_args(global_argv)
3216
3311
  remaining = leftover + tool_argv
3217
3312
 
3313
+ # Validate mutually exclusive output flags
3314
+ if pre_args.jq and pre_args.toon:
3315
+ print("Error: --jq and --toon are mutually exclusive.", file=sys.stderr)
3316
+ sys.exit(1)
3317
+
3218
3318
  # --search implies --list
3219
3319
  search_pattern = pre_args.search_pattern
3220
3320
  if search_pattern:
@@ -3253,6 +3353,8 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
3253
3353
  toon=pre_args.toon,
3254
3354
  fields_override=pre_args.fields,
3255
3355
  oauth_provider=oauth_provider,
3356
+ jq_expr=pre_args.jq,
3357
+ head=pre_args.head,
3256
3358
  )
3257
3359
  return
3258
3360
 
@@ -3282,6 +3384,8 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
3282
3384
  prompt_arguments=prompt_arguments,
3283
3385
  search_pattern=search_pattern,
3284
3386
  bake_config=bake_config,
3387
+ jq_expr=pre_args.jq,
3388
+ head=pre_args.head,
3285
3389
  )
3286
3390
  return
3287
3391
 
File without changes
File without changes