mcp-cli-skill 0.5.0__tar.gz → 0.5.2__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: mcp-cli-skill
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: Call any MCP server tool from the command line with shell composition support
5
5
  Project-URL: Homepage, https://github.com/wise-toddler/mcp-cli-skill
6
6
  Project-URL: Repository, https://github.com/wise-toddler/mcp-cli-skill
@@ -7,6 +7,7 @@ import subprocess
7
7
  import sys
8
8
  import tempfile
9
9
  import re
10
+ import shutil
10
11
  import urllib.request
11
12
  import urllib.error
12
13
 
@@ -106,7 +107,8 @@ def parse_args():
106
107
  print(" mcp-call --servers", file=sys.stderr)
107
108
  print(" mcp-call <server> --tools", file=sys.stderr)
108
109
  print(" mcp-call <server> --discover", file=sys.stderr)
109
- print(" mcp-call <server> <tool> --schema", file=sys.stderr)
110
+ print(" mcp-call <server> <tool> --help (formatted help)", file=sys.stderr)
111
+ print(" mcp-call <server> <tool> --schema (raw JSON schema)", file=sys.stderr)
110
112
  print(" mcp-call --add <name> <command> [args...] [--env KEY=VAL ...]", file=sys.stderr)
111
113
  print(" mcp-call --add-http <name> <url>", file=sys.stderr)
112
114
  print(" mcp-call --remove <name>", file=sys.stderr)
@@ -152,6 +154,8 @@ def parse_args():
152
154
  arg = args[i]
153
155
  if arg == "--schema":
154
156
  return server, "__schema__", {"_tool": tool}
157
+ elif arg in ("--help", "-h"):
158
+ return server, "__help__", {"_tool": tool}
155
159
  elif arg == "--input-json" and i + 1 < len(args):
156
160
  tool_args.update(json.loads(args[i + 1]))
157
161
  i += 2
@@ -419,15 +423,89 @@ def fetch_tools(config):
419
423
  proc.kill()
420
424
 
421
425
 
426
+ def _colors():
427
+ """Return ANSI color codes if stdout is a TTY, else empty strings."""
428
+ on = sys.stdout.isatty()
429
+ return {
430
+ "bold": "\033[1m" if on else "",
431
+ "dim": "\033[2m" if on else "",
432
+ "red": "\033[31m" if on else "",
433
+ "green": "\033[32m" if on else "",
434
+ "yellow": "\033[33m" if on else "",
435
+ "blue": "\033[34m" if on else "",
436
+ "magenta": "\033[35m" if on else "",
437
+ "cyan": "\033[36m" if on else "",
438
+ "reset": "\033[0m" if on else "",
439
+ }
440
+
441
+
422
442
  def _print_tools(tools):
423
- """Print tools in human-readable format."""
443
+ """Print tools in human-readable format with colors on a TTY."""
444
+ c = _colors()
445
+ print(f"{c['dim']}{len(tools)} tools ({c['yellow']}*{c['dim']} = required){c['reset']}\n")
424
446
  for tool in tools:
425
447
  schema = tool.get("inputSchema", {})
426
448
  props = schema.get("properties", {})
427
- flags = " ".join(f"--{k}" for k in props)
428
- print(f" {tool['name']:30s} {flags}")
429
- if tool.get("description"):
430
- print(f" {tool['description']}")
449
+ required = set(schema.get("required", []))
450
+ # required flags marked with *, sorted required-first
451
+ flags = []
452
+ for k in sorted(props, key=lambda x: x not in required):
453
+ mark = f"{c['yellow']}*{c['reset']}" if k in required else ""
454
+ col = c["yellow"] if k in required else c["dim"]
455
+ flags.append(f"{col}--{k}{c['reset']}{mark}")
456
+ print(f" {c['cyan']}{c['bold']}{tool['name']}{c['reset']}")
457
+ # only the summary line — skip verbose "Args:" docstring section
458
+ desc = (tool.get("description") or "").strip().split("\n")[0]
459
+ if desc:
460
+ print(f" {c['dim']}{desc}{c['reset']}")
461
+ if flags:
462
+ print(f" {' '.join(flags)}")
463
+ print()
464
+
465
+
466
+ def _print_tool_help(server_name, tool):
467
+ """Print formatted help for a single tool — usage, params, example."""
468
+ c = _colors()
469
+ schema = tool.get("inputSchema", {})
470
+ props = schema.get("properties", {})
471
+ required = set(schema.get("required", []))
472
+ # header
473
+ print(f"\n{c['bold']}{c['cyan']}{tool['name']}{c['reset']} {c['dim']}({server_name}){c['reset']}")
474
+ desc = (tool.get("description") or "").strip()
475
+ if desc:
476
+ print()
477
+ for line in desc.split("\n"):
478
+ print(f" {c['dim']}{line}{c['reset']}")
479
+ # usage
480
+ print(f"\n{c['bold']}Usage:{c['reset']}")
481
+ req_part = " ".join(f"{c['yellow']}--{k}=<{c['reset']}{c['dim']}{props.get(k, {}).get('type', 'value')}{c['reset']}{c['yellow']}>{c['reset']}" for k in sorted(required))
482
+ print(f" mcp-call {server_name} {tool['name']} {req_part}".rstrip())
483
+ # required args
484
+ if required:
485
+ print(f"\n{c['bold']}Required:{c['reset']}")
486
+ for k in sorted(required):
487
+ _print_arg(k, props.get(k, {}), c, required=True)
488
+ # optional args
489
+ optional = [k for k in props if k not in required]
490
+ if optional:
491
+ print(f"\n{c['bold']}Optional:{c['reset']}")
492
+ for k in sorted(optional):
493
+ _print_arg(k, props.get(k, {}), c, required=False)
494
+ print()
495
+
496
+
497
+ def _print_arg(name, prop, c, required):
498
+ """Print one argument's signature + description."""
499
+ t = prop.get("type", "any")
500
+ enum = prop.get("enum")
501
+ type_str = f"{'|'.join(map(str, enum))}" if enum else t
502
+ flag_col = c["yellow"] if required else c["reset"]
503
+ mark = f"{c['yellow']}*{c['reset']}" if required else ""
504
+ print(f" {flag_col}--{name}{c['reset']}{mark} {c['dim']}<{type_str}>{c['reset']}")
505
+ desc = prop.get("description", "").strip()
506
+ if desc:
507
+ for line in desc.split("\n"):
508
+ print(f" {c['dim']}{line}{c['reset']}")
431
509
 
432
510
 
433
511
  # --- Server management ---
@@ -437,14 +515,60 @@ def is_http(config):
437
515
  return config.get("type") == "http" or "url" in config
438
516
 
439
517
 
518
+ def _config_key(cfg):
519
+ """Hashable identity for a server config — used to collapse duplicates."""
520
+ if is_http(cfg):
521
+ return ("http", cfg["url"])
522
+ return ("stdio", cfg.get("command", "?"), tuple(cfg.get("args", [])))
523
+
524
+
525
+ def _truncate(text, width):
526
+ """Truncate text to width with an ellipsis if it doesn't fit."""
527
+ return text if len(text) <= width else text[: max(0, width - 1)] + "…"
528
+
529
+
440
530
  def list_servers(servers):
441
- """Print configured servers."""
531
+ """Print configured servers grouped by transport, collapsing duplicates."""
532
+ c = _colors()
533
+ term_w = shutil.get_terminal_size((100, 24)).columns
534
+ # split + group by config identity
535
+ http_groups, stdio_groups = {}, {}
442
536
  for name, cfg in servers.items():
443
- if is_http(cfg):
444
- print(f" {name:20s} → {cfg['url']} [http]")
445
- else:
446
- cmd = " ".join([cfg.get("command", "?")] + cfg.get("args", []))
447
- print(f" {name:20s} {cmd} [stdio]")
537
+ bucket = http_groups if is_http(cfg) else stdio_groups
538
+ bucket.setdefault(_config_key(cfg), []).append(name)
539
+ max_name = min(max((len(n) for n in servers), default=20), 28)
540
+
541
+ def print_group(label, color, groups, target_fn):
542
+ total = sum(len(v) for v in groups.values())
543
+ unique = len(groups)
544
+ suffix = "" if unique == total else f" {c['dim']}({unique} unique, {total} total){c['reset']}"
545
+ print(f"\n{c['bold']}{color}{label}{c['reset']} {c['dim']}({total}){c['reset']}{suffix}\n")
546
+ # sort by first name in each group, alphabetical
547
+ for key, names in sorted(groups.items(), key=lambda kv: kv[1][0].lower()):
548
+ names_sorted = sorted(names)
549
+ primary = names_sorted[0]
550
+ target = target_fn(key)
551
+ # compute remaining width for the target
552
+ base = f" ● {primary:<{max_name}} "
553
+ visible_len = len(base)
554
+ avail = max(20, term_w - visible_len - 4)
555
+ target_disp = _truncate(target, avail)
556
+ line = f" {c['green']}●{c['reset']} {c['bold']}{primary:<{max_name}}{c['reset']} {c['dim']}{target_disp}{c['reset']}"
557
+ if len(names_sorted) > 1:
558
+ line += f" {c['yellow']}×{len(names_sorted)}{c['reset']}"
559
+ print(line)
560
+ # show extra aliases under primary, indented
561
+ if len(names_sorted) > 1:
562
+ aliases = ", ".join(names_sorted[1:6])
563
+ more = f" +{len(names_sorted) - 6} more" if len(names_sorted) > 6 else ""
564
+ print(f" {c['dim']}aliases: {aliases}{more}{c['reset']}")
565
+
566
+ if http_groups:
567
+ print_group("HTTP", c["cyan"], http_groups, lambda k: k[1])
568
+ if stdio_groups:
569
+ print_group("STDIO", c["magenta"], stdio_groups,
570
+ lambda k: (k[1] + (" " + " ".join(k[2]) if k[2] else "")))
571
+ print()
448
572
 
449
573
 
450
574
  def add_server(raw_args):
@@ -513,10 +637,10 @@ def sync_from_claude():
513
637
 
514
638
  # --- Main ---
515
639
 
516
- def run_server(config, tool_name, tool_args):
640
+ def run_server(config, tool_name, tool_args, server_name=""):
517
641
  """Route to HTTP or stdio transport."""
518
642
  # tool discovery commands
519
- if tool_name in ("__tools__", "__discover__", "__schema__"):
643
+ if tool_name in ("__tools__", "__discover__", "__schema__", "__help__"):
520
644
  tools = fetch_tools(config)
521
645
  if tool_name == "__tools__":
522
646
  _print_tools(tools)
@@ -532,6 +656,14 @@ def run_server(config, tool_name, tool_args):
532
656
  return
533
657
  print(f"Error: tool '{target}' not found", file=sys.stderr)
534
658
  sys.exit(1)
659
+ elif tool_name == "__help__":
660
+ target = tool_args["_tool"]
661
+ for t in tools:
662
+ if t["name"] == target:
663
+ _print_tool_help(server_name or "<server>", t)
664
+ return
665
+ print(f"Error: tool '{target}' not found", file=sys.stderr)
666
+ sys.exit(1)
535
667
  return
536
668
  # tool calls
537
669
  if is_http(config):
@@ -574,7 +706,7 @@ def main():
574
706
  list_servers(servers)
575
707
  sys.exit(1)
576
708
 
577
- run_server(servers[server_name], tool_name, tool_args)
709
+ run_server(servers[server_name], tool_name, tool_args, server_name)
578
710
 
579
711
 
580
712
  if __name__ == "__main__":
@@ -1,2 +1,2 @@
1
1
  """MCP CLI - Call any MCP server tool from the command line."""
2
- __version__ = "0.5.0"
2
+ __version__ = "0.5.2"
@@ -7,6 +7,7 @@ import subprocess
7
7
  import sys
8
8
  import tempfile
9
9
  import re
10
+ import shutil
10
11
  import urllib.request
11
12
  import urllib.error
12
13
 
@@ -106,7 +107,8 @@ def parse_args():
106
107
  print(" mcp-call --servers", file=sys.stderr)
107
108
  print(" mcp-call <server> --tools", file=sys.stderr)
108
109
  print(" mcp-call <server> --discover", file=sys.stderr)
109
- print(" mcp-call <server> <tool> --schema", file=sys.stderr)
110
+ print(" mcp-call <server> <tool> --help (formatted help)", file=sys.stderr)
111
+ print(" mcp-call <server> <tool> --schema (raw JSON schema)", file=sys.stderr)
110
112
  print(" mcp-call --add <name> <command> [args...] [--env KEY=VAL ...]", file=sys.stderr)
111
113
  print(" mcp-call --add-http <name> <url>", file=sys.stderr)
112
114
  print(" mcp-call --remove <name>", file=sys.stderr)
@@ -152,6 +154,8 @@ def parse_args():
152
154
  arg = args[i]
153
155
  if arg == "--schema":
154
156
  return server, "__schema__", {"_tool": tool}
157
+ elif arg in ("--help", "-h"):
158
+ return server, "__help__", {"_tool": tool}
155
159
  elif arg == "--input-json" and i + 1 < len(args):
156
160
  tool_args.update(json.loads(args[i + 1]))
157
161
  i += 2
@@ -419,15 +423,89 @@ def fetch_tools(config):
419
423
  proc.kill()
420
424
 
421
425
 
426
+ def _colors():
427
+ """Return ANSI color codes if stdout is a TTY, else empty strings."""
428
+ on = sys.stdout.isatty()
429
+ return {
430
+ "bold": "\033[1m" if on else "",
431
+ "dim": "\033[2m" if on else "",
432
+ "red": "\033[31m" if on else "",
433
+ "green": "\033[32m" if on else "",
434
+ "yellow": "\033[33m" if on else "",
435
+ "blue": "\033[34m" if on else "",
436
+ "magenta": "\033[35m" if on else "",
437
+ "cyan": "\033[36m" if on else "",
438
+ "reset": "\033[0m" if on else "",
439
+ }
440
+
441
+
422
442
  def _print_tools(tools):
423
- """Print tools in human-readable format."""
443
+ """Print tools in human-readable format with colors on a TTY."""
444
+ c = _colors()
445
+ print(f"{c['dim']}{len(tools)} tools ({c['yellow']}*{c['dim']} = required){c['reset']}\n")
424
446
  for tool in tools:
425
447
  schema = tool.get("inputSchema", {})
426
448
  props = schema.get("properties", {})
427
- flags = " ".join(f"--{k}" for k in props)
428
- print(f" {tool['name']:30s} {flags}")
429
- if tool.get("description"):
430
- print(f" {tool['description']}")
449
+ required = set(schema.get("required", []))
450
+ # required flags marked with *, sorted required-first
451
+ flags = []
452
+ for k in sorted(props, key=lambda x: x not in required):
453
+ mark = f"{c['yellow']}*{c['reset']}" if k in required else ""
454
+ col = c["yellow"] if k in required else c["dim"]
455
+ flags.append(f"{col}--{k}{c['reset']}{mark}")
456
+ print(f" {c['cyan']}{c['bold']}{tool['name']}{c['reset']}")
457
+ # only the summary line — skip verbose "Args:" docstring section
458
+ desc = (tool.get("description") or "").strip().split("\n")[0]
459
+ if desc:
460
+ print(f" {c['dim']}{desc}{c['reset']}")
461
+ if flags:
462
+ print(f" {' '.join(flags)}")
463
+ print()
464
+
465
+
466
+ def _print_tool_help(server_name, tool):
467
+ """Print formatted help for a single tool — usage, params, example."""
468
+ c = _colors()
469
+ schema = tool.get("inputSchema", {})
470
+ props = schema.get("properties", {})
471
+ required = set(schema.get("required", []))
472
+ # header
473
+ print(f"\n{c['bold']}{c['cyan']}{tool['name']}{c['reset']} {c['dim']}({server_name}){c['reset']}")
474
+ desc = (tool.get("description") or "").strip()
475
+ if desc:
476
+ print()
477
+ for line in desc.split("\n"):
478
+ print(f" {c['dim']}{line}{c['reset']}")
479
+ # usage
480
+ print(f"\n{c['bold']}Usage:{c['reset']}")
481
+ req_part = " ".join(f"{c['yellow']}--{k}=<{c['reset']}{c['dim']}{props.get(k, {}).get('type', 'value')}{c['reset']}{c['yellow']}>{c['reset']}" for k in sorted(required))
482
+ print(f" mcp-call {server_name} {tool['name']} {req_part}".rstrip())
483
+ # required args
484
+ if required:
485
+ print(f"\n{c['bold']}Required:{c['reset']}")
486
+ for k in sorted(required):
487
+ _print_arg(k, props.get(k, {}), c, required=True)
488
+ # optional args
489
+ optional = [k for k in props if k not in required]
490
+ if optional:
491
+ print(f"\n{c['bold']}Optional:{c['reset']}")
492
+ for k in sorted(optional):
493
+ _print_arg(k, props.get(k, {}), c, required=False)
494
+ print()
495
+
496
+
497
+ def _print_arg(name, prop, c, required):
498
+ """Print one argument's signature + description."""
499
+ t = prop.get("type", "any")
500
+ enum = prop.get("enum")
501
+ type_str = f"{'|'.join(map(str, enum))}" if enum else t
502
+ flag_col = c["yellow"] if required else c["reset"]
503
+ mark = f"{c['yellow']}*{c['reset']}" if required else ""
504
+ print(f" {flag_col}--{name}{c['reset']}{mark} {c['dim']}<{type_str}>{c['reset']}")
505
+ desc = prop.get("description", "").strip()
506
+ if desc:
507
+ for line in desc.split("\n"):
508
+ print(f" {c['dim']}{line}{c['reset']}")
431
509
 
432
510
 
433
511
  # --- Server management ---
@@ -437,14 +515,60 @@ def is_http(config):
437
515
  return config.get("type") == "http" or "url" in config
438
516
 
439
517
 
518
+ def _config_key(cfg):
519
+ """Hashable identity for a server config — used to collapse duplicates."""
520
+ if is_http(cfg):
521
+ return ("http", cfg["url"])
522
+ return ("stdio", cfg.get("command", "?"), tuple(cfg.get("args", [])))
523
+
524
+
525
+ def _truncate(text, width):
526
+ """Truncate text to width with an ellipsis if it doesn't fit."""
527
+ return text if len(text) <= width else text[: max(0, width - 1)] + "…"
528
+
529
+
440
530
  def list_servers(servers):
441
- """Print configured servers."""
531
+ """Print configured servers grouped by transport, collapsing duplicates."""
532
+ c = _colors()
533
+ term_w = shutil.get_terminal_size((100, 24)).columns
534
+ # split + group by config identity
535
+ http_groups, stdio_groups = {}, {}
442
536
  for name, cfg in servers.items():
443
- if is_http(cfg):
444
- print(f" {name:20s} → {cfg['url']} [http]")
445
- else:
446
- cmd = " ".join([cfg.get("command", "?")] + cfg.get("args", []))
447
- print(f" {name:20s} {cmd} [stdio]")
537
+ bucket = http_groups if is_http(cfg) else stdio_groups
538
+ bucket.setdefault(_config_key(cfg), []).append(name)
539
+ max_name = min(max((len(n) for n in servers), default=20), 28)
540
+
541
+ def print_group(label, color, groups, target_fn):
542
+ total = sum(len(v) for v in groups.values())
543
+ unique = len(groups)
544
+ suffix = "" if unique == total else f" {c['dim']}({unique} unique, {total} total){c['reset']}"
545
+ print(f"\n{c['bold']}{color}{label}{c['reset']} {c['dim']}({total}){c['reset']}{suffix}\n")
546
+ # sort by first name in each group, alphabetical
547
+ for key, names in sorted(groups.items(), key=lambda kv: kv[1][0].lower()):
548
+ names_sorted = sorted(names)
549
+ primary = names_sorted[0]
550
+ target = target_fn(key)
551
+ # compute remaining width for the target
552
+ base = f" ● {primary:<{max_name}} "
553
+ visible_len = len(base)
554
+ avail = max(20, term_w - visible_len - 4)
555
+ target_disp = _truncate(target, avail)
556
+ line = f" {c['green']}●{c['reset']} {c['bold']}{primary:<{max_name}}{c['reset']} {c['dim']}{target_disp}{c['reset']}"
557
+ if len(names_sorted) > 1:
558
+ line += f" {c['yellow']}×{len(names_sorted)}{c['reset']}"
559
+ print(line)
560
+ # show extra aliases under primary, indented
561
+ if len(names_sorted) > 1:
562
+ aliases = ", ".join(names_sorted[1:6])
563
+ more = f" +{len(names_sorted) - 6} more" if len(names_sorted) > 6 else ""
564
+ print(f" {c['dim']}aliases: {aliases}{more}{c['reset']}")
565
+
566
+ if http_groups:
567
+ print_group("HTTP", c["cyan"], http_groups, lambda k: k[1])
568
+ if stdio_groups:
569
+ print_group("STDIO", c["magenta"], stdio_groups,
570
+ lambda k: (k[1] + (" " + " ".join(k[2]) if k[2] else "")))
571
+ print()
448
572
 
449
573
 
450
574
  def add_server(raw_args):
@@ -513,10 +637,10 @@ def sync_from_claude():
513
637
 
514
638
  # --- Main ---
515
639
 
516
- def run_server(config, tool_name, tool_args):
640
+ def run_server(config, tool_name, tool_args, server_name=""):
517
641
  """Route to HTTP or stdio transport."""
518
642
  # tool discovery commands
519
- if tool_name in ("__tools__", "__discover__", "__schema__"):
643
+ if tool_name in ("__tools__", "__discover__", "__schema__", "__help__"):
520
644
  tools = fetch_tools(config)
521
645
  if tool_name == "__tools__":
522
646
  _print_tools(tools)
@@ -532,6 +656,14 @@ def run_server(config, tool_name, tool_args):
532
656
  return
533
657
  print(f"Error: tool '{target}' not found", file=sys.stderr)
534
658
  sys.exit(1)
659
+ elif tool_name == "__help__":
660
+ target = tool_args["_tool"]
661
+ for t in tools:
662
+ if t["name"] == target:
663
+ _print_tool_help(server_name or "<server>", t)
664
+ return
665
+ print(f"Error: tool '{target}' not found", file=sys.stderr)
666
+ sys.exit(1)
535
667
  return
536
668
  # tool calls
537
669
  if is_http(config):
@@ -574,7 +706,7 @@ def main():
574
706
  list_servers(servers)
575
707
  sys.exit(1)
576
708
 
577
- run_server(servers[server_name], tool_name, tool_args)
709
+ run_server(servers[server_name], tool_name, tool_args, server_name)
578
710
 
579
711
 
580
712
  if __name__ == "__main__":
File without changes
File without changes