mcp2cli 2.8.0__tar.gz → 3.0.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.8.0
3
+ Version: 3.0.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>
@@ -216,6 +216,26 @@ Filtering options:
216
216
 
217
217
  Configs are stored in `~/.config/mcp2cli/baked.json`. Override with `MCP2CLI_CONFIG_DIR`.
218
218
 
219
+ ### Usage-aware tool ranking
220
+
221
+ mcp2cli tracks tool invocations locally and uses that data to rank `--list` output, reducing token costs for LLM agents working with large servers.
222
+
223
+ ```bash
224
+ # Default --list: ~1,400 tokens for 96 tools
225
+ mcp2cli @myapi --list
226
+
227
+ # Top 10 most-used tools, names only: ~20 tokens
228
+ mcp2cli @myapi --list --top 10 --compact
229
+
230
+ # Sort by most recently used
231
+ mcp2cli @myapi --list --sort recent
232
+
233
+ # Alphabetical sort
234
+ mcp2cli @myapi --list --sort alpha
235
+ ```
236
+
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
+
219
239
  ### Output control
220
240
 
221
241
  ```bash
@@ -225,13 +245,8 @@ mcp2cli --spec ./spec.json --pretty list-pets
225
245
  # Raw response body (no JSON parsing)
226
246
  mcp2cli --spec ./spec.json --raw get-data
227
247
 
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
248
  # Truncate large responses to first N records
233
249
  mcp2cli --spec ./spec.json list-records --head 5
234
- mcp2cli --spec ./spec.json list-records --head 3 --jq '.' # preview then filter
235
250
 
236
251
  # Pipe-friendly (compact JSON when not a TTY)
237
252
  mcp2cli --spec ./spec.json list-pets | jq '.[] | .name'
@@ -286,11 +301,14 @@ Options:
286
301
  --refresh Bypass cache
287
302
  --list List available subcommands
288
303
  --search PATTERN Search tools by name or description (implies --list)
304
+ --sort MODE Sort --list output: usage|recent|alpha|default
305
+ --top N Show only the top N tools in --list output
306
+ --compact Space-separated tool names only, no descriptions
307
+ --verbose Show full tool descriptions (unwrapped)
289
308
  --fields FIELDS Override GraphQL selection set (e.g. "id name email")
290
309
  --pretty Pretty-print JSON output
291
310
  --raw Print raw response body
292
311
  --toon Encode output as TOON (token-efficient for LLMs)
293
- --jq EXPR Filter JSON output through jq expression
294
312
  --head N Limit output to first N records (arrays)
295
313
  --version Show version
296
314
 
@@ -197,6 +197,26 @@ Filtering options:
197
197
 
198
198
  Configs are stored in `~/.config/mcp2cli/baked.json`. Override with `MCP2CLI_CONFIG_DIR`.
199
199
 
200
+ ### Usage-aware tool ranking
201
+
202
+ mcp2cli tracks tool invocations locally and uses that data to rank `--list` output, reducing token costs for LLM agents working with large servers.
203
+
204
+ ```bash
205
+ # Default --list: ~1,400 tokens for 96 tools
206
+ mcp2cli @myapi --list
207
+
208
+ # Top 10 most-used tools, names only: ~20 tokens
209
+ mcp2cli @myapi --list --top 10 --compact
210
+
211
+ # Sort by most recently used
212
+ mcp2cli @myapi --list --sort recent
213
+
214
+ # Alphabetical sort
215
+ mcp2cli @myapi --list --sort alpha
216
+ ```
217
+
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
+
200
220
  ### Output control
201
221
 
202
222
  ```bash
@@ -206,13 +226,8 @@ mcp2cli --spec ./spec.json --pretty list-pets
206
226
  # Raw response body (no JSON parsing)
207
227
  mcp2cli --spec ./spec.json --raw get-data
208
228
 
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
229
  # Truncate large responses to first N records
214
230
  mcp2cli --spec ./spec.json list-records --head 5
215
- mcp2cli --spec ./spec.json list-records --head 3 --jq '.' # preview then filter
216
231
 
217
232
  # Pipe-friendly (compact JSON when not a TTY)
218
233
  mcp2cli --spec ./spec.json list-pets | jq '.[] | .name'
@@ -267,11 +282,14 @@ Options:
267
282
  --refresh Bypass cache
268
283
  --list List available subcommands
269
284
  --search PATTERN Search tools by name or description (implies --list)
285
+ --sort MODE Sort --list output: usage|recent|alpha|default
286
+ --top N Show only the top N tools in --list output
287
+ --compact Space-separated tool names only, no descriptions
288
+ --verbose Show full tool descriptions (unwrapped)
270
289
  --fields FIELDS Override GraphQL selection set (e.g. "id name email")
271
290
  --pretty Pretty-print JSON output
272
291
  --raw Print raw response body
273
292
  --toon Encode output as TOON (token-efficient for LLMs)
274
- --jq EXPR Filter JSON output through jq expression
275
293
  --head N Limit output to first N records (arrays)
276
294
  --version Show version
277
295
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mcp2cli"
3
- version = "2.8.0"
3
+ version = "3.0.0"
4
4
  description = "Turn any MCP server or OpenAPI spec into a CLI"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -219,6 +219,16 @@ def coerce_value(value, schema: dict):
219
219
  return int(value)
220
220
  if t == "number":
221
221
  return float(value)
222
+ # Schema-less fallback: try to parse JSON objects/arrays from strings
223
+ if t is None and isinstance(value, str):
224
+ stripped = value.strip()
225
+ if stripped and stripped[0] in ('{', '['):
226
+ try:
227
+ parsed = json.loads(stripped)
228
+ if isinstance(parsed, (dict, list)):
229
+ return parsed
230
+ except (json.JSONDecodeError, TypeError):
231
+ pass
222
232
  return value
223
233
 
224
234
 
@@ -257,31 +267,6 @@ def _toon_encode(json_str: str) -> str | None:
257
267
  return None
258
268
 
259
269
 
260
- def _run_jq(json_str: str, expr: str) -> str:
261
- """Pipe JSON through jq with the given expression. Exits on failure."""
262
- if not shutil.which("jq"):
263
- print(
264
- "Error: --jq requires jq to be installed. "
265
- "See https://jqlang.github.io/jq/",
266
- file=sys.stderr,
267
- )
268
- sys.exit(1)
269
- try:
270
- result = subprocess.run(
271
- ["jq", expr],
272
- input=json_str,
273
- capture_output=True,
274
- text=True,
275
- timeout=30,
276
- )
277
- if result.returncode != 0:
278
- print(f"jq error: {result.stderr.strip()}", file=sys.stderr)
279
- sys.exit(1)
280
- return result.stdout
281
- except subprocess.TimeoutExpired:
282
- print("Error: jq timed out", file=sys.stderr)
283
- sys.exit(1)
284
-
285
270
 
286
271
  def _apply_head(data, n: int):
287
272
  """Truncate data to first N elements (array) or return as-is (dict/scalar)."""
@@ -296,7 +281,6 @@ def output_result(
296
281
  pretty: bool = False,
297
282
  raw: bool = False,
298
283
  toon: bool = False,
299
- jq_expr: str | None = None,
300
284
  head: int | None = None,
301
285
  ):
302
286
  if raw:
@@ -313,9 +297,6 @@ def output_result(
313
297
  return
314
298
  if head is not None:
315
299
  data = _apply_head(data, head)
316
- if jq_expr:
317
- print(_run_jq(json.dumps(data), jq_expr), end="")
318
- return
319
300
  if toon:
320
301
  encoded = _toon_encode(json.dumps(data))
321
302
  if encoded is not None:
@@ -860,6 +841,7 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
860
841
  description=(param.get("description") or param["name"]) + suffix,
861
842
  choices=schema.get("enum"),
862
843
  location=param.get("in", "query"),
844
+ schema=schema,
863
845
  )
864
846
  params.append(p)
865
847
 
@@ -908,6 +890,7 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
908
890
  description=(prop_schema.get("description") or prop_name) + suffix,
909
891
  choices=prop_schema.get("enum"),
910
892
  location=loc,
893
+ schema=prop_schema,
911
894
  )
912
895
  params.append(p)
913
896
 
@@ -1417,7 +1400,6 @@ def execute_graphql(
1417
1400
  toon: bool = False,
1418
1401
  fields_override: str | None = None,
1419
1402
  oauth_provider: "httpx.Auth | None" = None,
1420
- jq_expr: str | None = None,
1421
1403
  head: int | None = None,
1422
1404
  ):
1423
1405
  """Build and execute a GraphQL query/mutation."""
@@ -1442,13 +1424,13 @@ def execute_graphql(
1442
1424
  print(f"GraphQL error: {msgs}", file=sys.stderr)
1443
1425
  sys.exit(1)
1444
1426
  # Partial errors — include them in output
1445
- output_result(result, pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1427
+ output_result(result, pretty=pretty, raw=raw, toon=toon, head=head)
1446
1428
  return
1447
1429
 
1448
1430
  data = result.get("data", {})
1449
1431
  # Extract the specific field's data
1450
1432
  field_data = data.get(field_name, data)
1451
- output_result(field_data, pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
1433
+ output_result(field_data, pretty=pretty, raw=raw, toon=toon, head=head)
1452
1434
 
1453
1435
 
1454
1436
  def handle_graphql(
@@ -1464,7 +1446,6 @@ def handle_graphql(
1464
1446
  toon: bool = False,
1465
1447
  fields_override: str | None = None,
1466
1448
  oauth_provider: "httpx.Auth | None" = None,
1467
- jq_expr: str | None = None,
1468
1449
  head: int | None = None,
1469
1450
  verbose: bool = False,
1470
1451
  sort_mode: str | None = None,
@@ -1505,7 +1486,6 @@ def handle_graphql(
1505
1486
  execute_graphql(
1506
1487
  args, cmd, url, schema, auth_headers, pretty, raw, toon=toon,
1507
1488
  fields_override=fields_override, oauth_provider=oauth_provider,
1508
- jq_expr=jq_expr, head=head,
1509
1489
  )
1510
1490
 
1511
1491
  # Record usage after successful execution
@@ -2053,7 +2033,7 @@ def _collect_openapi_params(
2053
2033
  if val is None:
2054
2034
  continue
2055
2035
  if p.location == "query":
2056
- query_params[p.original_name] = val
2036
+ query_params[p.original_name] = coerce_value(val, p.schema)
2057
2037
  elif p.location == "header":
2058
2038
  extra_headers[p.original_name] = str(val)
2059
2039
  else:
@@ -2081,7 +2061,7 @@ def _collect_openapi_params(
2081
2061
  files[p.original_name] = (fp.name, open(fp, "rb"), mime)
2082
2062
  continue
2083
2063
  if val is not None:
2084
- body[p.original_name] = val
2064
+ body[p.original_name] = coerce_value(val, p.schema)
2085
2065
  if not body:
2086
2066
  body = None
2087
2067
  # Also collect query params for non-GET
@@ -2089,7 +2069,7 @@ def _collect_openapi_params(
2089
2069
  if p.location == "query":
2090
2070
  val = getattr(args, p.name.replace("-", "_"), None)
2091
2071
  if val is not None:
2092
- query_params[p.original_name] = val
2072
+ query_params[p.original_name] = coerce_value(val, p.schema)
2093
2073
 
2094
2074
  return path, query_params, extra_headers, body, files
2095
2075
 
@@ -2103,7 +2083,6 @@ def execute_openapi(
2103
2083
  raw: bool,
2104
2084
  toon: bool = False,
2105
2085
  oauth_provider: "httpx.Auth | None" = None,
2106
- jq_expr: str | None = None,
2107
2086
  head: int | None = None,
2108
2087
  ):
2109
2088
  path, query_params, extra_headers, body, files = _collect_openapi_params(cmd, args)
@@ -2156,7 +2135,7 @@ def execute_openapi(
2156
2135
  print(resp.text)
2157
2136
  return
2158
2137
 
2159
- output_result(data, pretty=pretty, toon=toon, jq_expr=jq_expr, head=head)
2138
+ output_result(data, pretty=pretty, toon=toon, head=head)
2160
2139
 
2161
2140
 
2162
2141
  # ---------------------------------------------------------------------------
@@ -2184,7 +2163,6 @@ def run_mcp_http(
2184
2163
  prompt_name: str | None = None,
2185
2164
  prompt_arguments: dict | None = None,
2186
2165
  search_pattern: str | None = None,
2187
- jq_expr: str | None = None,
2188
2166
  head: int | None = None,
2189
2167
  verbose: bool = False,
2190
2168
  sort_mode: str | None = None,
@@ -2199,7 +2177,6 @@ def run_mcp_http(
2199
2177
  prompt_name=prompt_name,
2200
2178
  prompt_arguments=prompt_arguments,
2201
2179
  search_pattern=search_pattern,
2202
- jq_expr=jq_expr,
2203
2180
  head=head,
2204
2181
  verbose=verbose,
2205
2182
  sort_mode=sort_mode,
@@ -2289,7 +2266,6 @@ def run_mcp_stdio(
2289
2266
  prompt_name: str | None = None,
2290
2267
  prompt_arguments: dict | None = None,
2291
2268
  search_pattern: str | None = None,
2292
- jq_expr: str | None = None,
2293
2269
  head: int | None = None,
2294
2270
  verbose: bool = False,
2295
2271
  sort_mode: str | None = None,
@@ -2304,7 +2280,6 @@ def run_mcp_stdio(
2304
2280
  prompt_name=prompt_name,
2305
2281
  prompt_arguments=prompt_arguments,
2306
2282
  search_pattern=search_pattern,
2307
- jq_expr=jq_expr,
2308
2283
  head=head,
2309
2284
  verbose=verbose,
2310
2285
  sort_mode=sort_mode,
@@ -2360,7 +2335,6 @@ async def _mcp_session(
2360
2335
  prompt_name: str | None = None,
2361
2336
  prompt_arguments: dict | None = None,
2362
2337
  search_pattern: str | None = None,
2363
- jq_expr: str | None = None,
2364
2338
  head: int | None = None,
2365
2339
  verbose: bool = False,
2366
2340
  sort_mode: str | None = None,
@@ -2372,7 +2346,6 @@ async def _mcp_session(
2372
2346
  if resource_action:
2373
2347
  await _handle_resources(
2374
2348
  session, resource_action, resource_uri, pretty, raw, toon,
2375
- jq_expr=jq_expr, head=head,
2376
2349
  )
2377
2350
  return
2378
2351
 
@@ -2380,7 +2353,6 @@ async def _mcp_session(
2380
2353
  if prompt_action:
2381
2354
  await _handle_prompts(
2382
2355
  session, prompt_action, prompt_name, prompt_arguments, pretty, raw, toon,
2383
- jq_expr=jq_expr, head=head,
2384
2356
  )
2385
2357
  return
2386
2358
 
@@ -2423,7 +2395,7 @@ async def _mcp_session(
2423
2395
  result = await session.call_tool(tool_name, arguments or {})
2424
2396
 
2425
2397
  text = _extract_content_parts(result.content)
2426
- output_result(text, pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
2398
+ output_result(text, pretty=pretty, raw=raw, toon=toon, head=head)
2427
2399
 
2428
2400
 
2429
2401
  # ---------------------------------------------------------------------------
@@ -2433,9 +2405,8 @@ async def _mcp_session(
2433
2405
 
2434
2406
  async def _handle_resources(
2435
2407
  session, action: str, uri: str | None, pretty: bool, raw: bool, toon: bool,
2436
- jq_expr: str | None = None, head: int | None = None,
2437
2408
  ):
2438
- _out = dict(pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
2409
+ _out = dict(pretty=pretty, raw=raw, toon=toon, head=head)
2439
2410
  if action == "list":
2440
2411
  result = await session.list_resources()
2441
2412
  data = [
@@ -2487,10 +2458,9 @@ async def _handle_prompts(
2487
2458
  pretty: bool,
2488
2459
  raw: bool,
2489
2460
  toon: bool,
2490
- jq_expr: str | None = None,
2491
2461
  head: int | None = None,
2492
2462
  ):
2493
- _out = dict(pretty=pretty, raw=raw, toon=toon, jq_expr=jq_expr, head=head)
2463
+ _out = dict(pretty=pretty, raw=raw, toon=toon, head=head)
2494
2464
  if action == "list":
2495
2465
  result = await session.list_prompts()
2496
2466
  data = [
@@ -3026,7 +2996,6 @@ def handle_mcp(
3026
2996
  prompt_arguments: dict | None = None,
3027
2997
  search_pattern: str | None = None,
3028
2998
  bake_config: BakeConfig | None = None,
3029
- jq_expr: str | None = None,
3030
2999
  head: int | None = None,
3031
3000
  verbose: bool = False,
3032
3001
  sort_mode: str | None = None,
@@ -3053,7 +3022,6 @@ def handle_mcp(
3053
3022
  prompt_action=prompt_action,
3054
3023
  prompt_name=prompt_name,
3055
3024
  prompt_arguments=prompt_arguments,
3056
- jq_expr=jq_expr,
3057
3025
  head=head,
3058
3026
  )
3059
3027
  _dispatch_mcp_call(
@@ -3089,7 +3057,6 @@ def handle_mcp(
3089
3057
  None, None, True, pretty, raw, key, ttl, refresh,
3090
3058
  toon=toon, transport=transport, oauth_provider=oauth_provider,
3091
3059
  search_pattern=search_pattern,
3092
- jq_expr=jq_expr, head=head,
3093
3060
  verbose=verbose,
3094
3061
  sort_mode=sort_mode, top=top, compact=compact,
3095
3062
  source_hash=src_hash,
@@ -3139,7 +3106,6 @@ def handle_mcp(
3139
3106
  source, is_stdio, auth_headers, env_vars,
3140
3107
  cmd.tool_name, arguments, False, pretty, raw, key, ttl, refresh,
3141
3108
  toon=toon, transport=transport, oauth_provider=oauth_provider,
3142
- jq_expr=jq_expr, head=head,
3143
3109
  )
3144
3110
 
3145
3111
  # Record usage after successful execution
@@ -3367,12 +3333,6 @@ def _build_main_parser() -> argparse.ArgumentParser:
3367
3333
  "of large result sets. Requires @toon-format/cli (npm install -g @toon-format/cli)."
3368
3334
  ),
3369
3335
  )
3370
- pre.add_argument(
3371
- "--jq",
3372
- default=None,
3373
- metavar="EXPR",
3374
- help="Filter JSON output through jq (e.g. '.[] | .name'). Requires jq installed.",
3375
- )
3376
3336
  pre.add_argument(
3377
3337
  "--head",
3378
3338
  type=int,
@@ -3629,14 +3589,12 @@ def _handle_session_operations(
3629
3589
  result = _session_request(sess_name, "list_resources")
3630
3590
  output_result(
3631
3591
  result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3632
- jq_expr=pre_args.jq, head=pre_args.head,
3633
3592
  )
3634
3593
  return True
3635
3594
  if pre_args.list_resource_templates:
3636
3595
  result = _session_request(sess_name, "list_resource_templates")
3637
3596
  output_result(
3638
3597
  result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3639
- jq_expr=pre_args.jq, head=pre_args.head,
3640
3598
  )
3641
3599
  return True
3642
3600
  if pre_args.read_resource:
@@ -3645,14 +3603,12 @@ def _handle_session_operations(
3645
3603
  )
3646
3604
  output_result(
3647
3605
  result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3648
- jq_expr=pre_args.jq, head=pre_args.head,
3649
3606
  )
3650
3607
  return True
3651
3608
  if pre_args.list_prompts:
3652
3609
  result = _session_request(sess_name, "list_prompts")
3653
3610
  output_result(
3654
3611
  result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3655
- jq_expr=pre_args.jq, head=pre_args.head,
3656
3612
  )
3657
3613
  return True
3658
3614
  if pre_args.get_prompt:
@@ -3668,7 +3624,6 @@ def _handle_session_operations(
3668
3624
  )
3669
3625
  output_result(
3670
3626
  result, pretty=pre_args.pretty, raw=pre_args.raw, toon=pre_args.toon,
3671
- jq_expr=pre_args.jq, head=pre_args.head,
3672
3627
  )
3673
3628
  return True
3674
3629
  if pre_args.list_commands:
@@ -3837,7 +3792,6 @@ def _handle_openapi_mode(
3837
3792
  args, cmd, base_url, auth_headers,
3838
3793
  pre_args.pretty, pre_args.raw, toon=pre_args.toon,
3839
3794
  oauth_provider=oauth_provider,
3840
- jq_expr=pre_args.jq, head=pre_args.head,
3841
3795
  )
3842
3796
 
3843
3797
  # Record usage after successful execution
@@ -3854,11 +3808,6 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
3854
3808
  pre_args, leftover = pre.parse_known_args(global_argv)
3855
3809
  remaining = leftover + tool_argv
3856
3810
 
3857
- # Validate mutually exclusive output flags
3858
- if pre_args.jq and pre_args.toon:
3859
- print("Error: --jq and --toon are mutually exclusive.", file=sys.stderr)
3860
- sys.exit(1)
3861
-
3862
3811
  # --search implies --list
3863
3812
  search_pattern = pre_args.search_pattern
3864
3813
  if search_pattern:
@@ -3897,7 +3846,6 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
3897
3846
  toon=pre_args.toon,
3898
3847
  fields_override=pre_args.fields,
3899
3848
  oauth_provider=oauth_provider,
3900
- jq_expr=pre_args.jq,
3901
3849
  head=pre_args.head,
3902
3850
  verbose=pre_args.verbose,
3903
3851
  sort_mode=pre_args.sort_mode,
@@ -3932,7 +3880,6 @@ def _main_impl(argv: list[str], bake_config: BakeConfig | None = None):
3932
3880
  prompt_arguments=prompt_arguments,
3933
3881
  search_pattern=search_pattern,
3934
3882
  bake_config=bake_config,
3935
- jq_expr=pre_args.jq,
3936
3883
  head=pre_args.head,
3937
3884
  verbose=pre_args.verbose,
3938
3885
  sort_mode=pre_args.sort_mode,
File without changes
File without changes