mcp-cli-skill 0.5.1__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.1
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,26 @@ 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
443
  """Print tools in human-readable format with colors on a TTY."""
424
- color = sys.stdout.isatty()
425
- bold = "\033[1m" if color else ""
426
- cyan = "\033[36m" if color else ""
427
- dim = "\033[2m" if color else ""
428
- yellow = "\033[33m" if color else ""
429
- reset = "\033[0m" if color else ""
430
- print(f"{dim}{len(tools)} tools ({yellow}*{dim} = required){reset}\n")
444
+ c = _colors()
445
+ print(f"{c['dim']}{len(tools)} tools ({c['yellow']}*{c['dim']} = required){c['reset']}\n")
431
446
  for tool in tools:
432
447
  schema = tool.get("inputSchema", {})
433
448
  props = schema.get("properties", {})
@@ -435,19 +450,64 @@ def _print_tools(tools):
435
450
  # required flags marked with *, sorted required-first
436
451
  flags = []
437
452
  for k in sorted(props, key=lambda x: x not in required):
438
- mark = f"{yellow}*{reset}" if k in required else ""
439
- col = yellow if k in required else dim
440
- flags.append(f"{col}--{k}{reset}{mark}")
441
- print(f" {cyan}{bold}{tool['name']}{reset}")
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']}")
442
457
  # only the summary line — skip verbose "Args:" docstring section
443
458
  desc = (tool.get("description") or "").strip().split("\n")[0]
444
459
  if desc:
445
- print(f" {dim}{desc}{reset}")
460
+ print(f" {c['dim']}{desc}{c['reset']}")
446
461
  if flags:
447
462
  print(f" {' '.join(flags)}")
448
463
  print()
449
464
 
450
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']}")
509
+
510
+
451
511
  # --- Server management ---
452
512
 
453
513
  def is_http(config):
@@ -455,14 +515,60 @@ def is_http(config):
455
515
  return config.get("type") == "http" or "url" in config
456
516
 
457
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
+
458
530
  def list_servers(servers):
459
- """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 = {}, {}
460
536
  for name, cfg in servers.items():
461
- if is_http(cfg):
462
- print(f" {name:20s} → {cfg['url']} [http]")
463
- else:
464
- cmd = " ".join([cfg.get("command", "?")] + cfg.get("args", []))
465
- 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()
466
572
 
467
573
 
468
574
  def add_server(raw_args):
@@ -531,10 +637,10 @@ def sync_from_claude():
531
637
 
532
638
  # --- Main ---
533
639
 
534
- def run_server(config, tool_name, tool_args):
640
+ def run_server(config, tool_name, tool_args, server_name=""):
535
641
  """Route to HTTP or stdio transport."""
536
642
  # tool discovery commands
537
- if tool_name in ("__tools__", "__discover__", "__schema__"):
643
+ if tool_name in ("__tools__", "__discover__", "__schema__", "__help__"):
538
644
  tools = fetch_tools(config)
539
645
  if tool_name == "__tools__":
540
646
  _print_tools(tools)
@@ -550,6 +656,14 @@ def run_server(config, tool_name, tool_args):
550
656
  return
551
657
  print(f"Error: tool '{target}' not found", file=sys.stderr)
552
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)
553
667
  return
554
668
  # tool calls
555
669
  if is_http(config):
@@ -592,7 +706,7 @@ def main():
592
706
  list_servers(servers)
593
707
  sys.exit(1)
594
708
 
595
- run_server(servers[server_name], tool_name, tool_args)
709
+ run_server(servers[server_name], tool_name, tool_args, server_name)
596
710
 
597
711
 
598
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.1"
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,26 @@ 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
443
  """Print tools in human-readable format with colors on a TTY."""
424
- color = sys.stdout.isatty()
425
- bold = "\033[1m" if color else ""
426
- cyan = "\033[36m" if color else ""
427
- dim = "\033[2m" if color else ""
428
- yellow = "\033[33m" if color else ""
429
- reset = "\033[0m" if color else ""
430
- print(f"{dim}{len(tools)} tools ({yellow}*{dim} = required){reset}\n")
444
+ c = _colors()
445
+ print(f"{c['dim']}{len(tools)} tools ({c['yellow']}*{c['dim']} = required){c['reset']}\n")
431
446
  for tool in tools:
432
447
  schema = tool.get("inputSchema", {})
433
448
  props = schema.get("properties", {})
@@ -435,19 +450,64 @@ def _print_tools(tools):
435
450
  # required flags marked with *, sorted required-first
436
451
  flags = []
437
452
  for k in sorted(props, key=lambda x: x not in required):
438
- mark = f"{yellow}*{reset}" if k in required else ""
439
- col = yellow if k in required else dim
440
- flags.append(f"{col}--{k}{reset}{mark}")
441
- print(f" {cyan}{bold}{tool['name']}{reset}")
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']}")
442
457
  # only the summary line — skip verbose "Args:" docstring section
443
458
  desc = (tool.get("description") or "").strip().split("\n")[0]
444
459
  if desc:
445
- print(f" {dim}{desc}{reset}")
460
+ print(f" {c['dim']}{desc}{c['reset']}")
446
461
  if flags:
447
462
  print(f" {' '.join(flags)}")
448
463
  print()
449
464
 
450
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']}")
509
+
510
+
451
511
  # --- Server management ---
452
512
 
453
513
  def is_http(config):
@@ -455,14 +515,60 @@ def is_http(config):
455
515
  return config.get("type") == "http" or "url" in config
456
516
 
457
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
+
458
530
  def list_servers(servers):
459
- """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 = {}, {}
460
536
  for name, cfg in servers.items():
461
- if is_http(cfg):
462
- print(f" {name:20s} → {cfg['url']} [http]")
463
- else:
464
- cmd = " ".join([cfg.get("command", "?")] + cfg.get("args", []))
465
- 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()
466
572
 
467
573
 
468
574
  def add_server(raw_args):
@@ -531,10 +637,10 @@ def sync_from_claude():
531
637
 
532
638
  # --- Main ---
533
639
 
534
- def run_server(config, tool_name, tool_args):
640
+ def run_server(config, tool_name, tool_args, server_name=""):
535
641
  """Route to HTTP or stdio transport."""
536
642
  # tool discovery commands
537
- if tool_name in ("__tools__", "__discover__", "__schema__"):
643
+ if tool_name in ("__tools__", "__discover__", "__schema__", "__help__"):
538
644
  tools = fetch_tools(config)
539
645
  if tool_name == "__tools__":
540
646
  _print_tools(tools)
@@ -550,6 +656,14 @@ def run_server(config, tool_name, tool_args):
550
656
  return
551
657
  print(f"Error: tool '{target}' not found", file=sys.stderr)
552
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)
553
667
  return
554
668
  # tool calls
555
669
  if is_http(config):
@@ -592,7 +706,7 @@ def main():
592
706
  list_servers(servers)
593
707
  sys.exit(1)
594
708
 
595
- run_server(servers[server_name], tool_name, tool_args)
709
+ run_server(servers[server_name], tool_name, tool_args, server_name)
596
710
 
597
711
 
598
712
  if __name__ == "__main__":
File without changes
File without changes