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.
- {mcp_cli_skill-0.5.1 → mcp_cli_skill-0.5.2}/PKG-INFO +1 -1
- {mcp_cli_skill-0.5.1 → mcp_cli_skill-0.5.2}/scripts/mcp_call.py +136 -22
- {mcp_cli_skill-0.5.1 → mcp_cli_skill-0.5.2}/src/mcp_cli_skill/__init__.py +1 -1
- {mcp_cli_skill-0.5.1 → mcp_cli_skill-0.5.2}/src/mcp_cli_skill/cli.py +136 -22
- {mcp_cli_skill-0.5.1 → mcp_cli_skill-0.5.2}/README.md +0 -0
- {mcp_cli_skill-0.5.1 → mcp_cli_skill-0.5.2}/SKILL.md +0 -0
- {mcp_cli_skill-0.5.1 → 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,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
|
-
|
|
425
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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.
|
|
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,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
|
-
|
|
425
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
|
File without changes
|