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.
- {mcp_cli_skill-0.5.0 → mcp_cli_skill-0.5.2}/PKG-INFO +1 -1
- {mcp_cli_skill-0.5.0 → mcp_cli_skill-0.5.2}/scripts/mcp_call.py +147 -15
- {mcp_cli_skill-0.5.0 → mcp_cli_skill-0.5.2}/src/mcp_cli_skill/__init__.py +1 -1
- {mcp_cli_skill-0.5.0 → mcp_cli_skill-0.5.2}/src/mcp_cli_skill/cli.py +147 -15
- {mcp_cli_skill-0.5.0 → mcp_cli_skill-0.5.2}/README.md +0 -0
- {mcp_cli_skill-0.5.0 → mcp_cli_skill-0.5.2}/SKILL.md +0 -0
- {mcp_cli_skill-0.5.0 → mcp_cli_skill-0.5.2}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-cli-skill
|
|
3
|
-
Version: 0.5.
|
|
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> --
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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.
|
|
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> --
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
File without changes
|