mcp-cli-skill 0.8.2__tar.gz → 0.8.3__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.8.2 → mcp_cli_skill-0.8.3}/PKG-INFO +1 -1
- {mcp_cli_skill-0.8.2 → mcp_cli_skill-0.8.3}/scripts/mcp_call.py +20 -6
- {mcp_cli_skill-0.8.2 → mcp_cli_skill-0.8.3}/src/mcp_cli_skill/__init__.py +1 -1
- {mcp_cli_skill-0.8.2 → mcp_cli_skill-0.8.3}/src/mcp_cli_skill/cli.py +20 -6
- {mcp_cli_skill-0.8.2 → mcp_cli_skill-0.8.3}/tests/test_oauth.py +15 -0
- {mcp_cli_skill-0.8.2 → mcp_cli_skill-0.8.3}/.github/workflows/publish.yml +0 -0
- {mcp_cli_skill-0.8.2 → mcp_cli_skill-0.8.3}/.gitignore +0 -0
- {mcp_cli_skill-0.8.2 → mcp_cli_skill-0.8.3}/README.md +0 -0
- {mcp_cli_skill-0.8.2 → mcp_cli_skill-0.8.3}/SKILL.md +0 -0
- {mcp_cli_skill-0.8.2 → mcp_cli_skill-0.8.3}/pyproject.toml +0 -0
- {mcp_cli_skill-0.8.2 → mcp_cli_skill-0.8.3}/tests/test_args.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-cli-skill
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.3
|
|
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
|
|
@@ -1036,10 +1036,11 @@ def is_http(config):
|
|
|
1036
1036
|
return config.get("type") == "http" or "url" in config
|
|
1037
1037
|
|
|
1038
1038
|
|
|
1039
|
-
def _config_key(cfg):
|
|
1040
|
-
"""Hashable identity for a server config — used to collapse
|
|
1039
|
+
def _config_key(name, cfg):
|
|
1040
|
+
"""Hashable identity for a server config — used to collapse duplicate stdio servers."""
|
|
1041
1041
|
if is_http(cfg):
|
|
1042
|
-
|
|
1042
|
+
# same URL under another name is a different account or key, never an alias
|
|
1043
|
+
return ("http", cfg["url"], name)
|
|
1043
1044
|
return ("stdio", cfg.get("command", "?"), tuple(cfg.get("args", [])))
|
|
1044
1045
|
|
|
1045
1046
|
|
|
@@ -1049,15 +1050,25 @@ def _truncate(text, width):
|
|
|
1049
1050
|
|
|
1050
1051
|
|
|
1051
1052
|
def list_servers(servers):
|
|
1052
|
-
"""Print configured servers grouped by transport, collapsing
|
|
1053
|
+
"""Print configured servers grouped by transport, collapsing duplicate stdio servers."""
|
|
1053
1054
|
c = _colors()
|
|
1054
1055
|
term_w = shutil.get_terminal_size((100, 24)).columns
|
|
1055
1056
|
# split + group by config identity
|
|
1056
1057
|
http_groups, stdio_groups = {}, {}
|
|
1057
1058
|
for name, cfg in servers.items():
|
|
1058
1059
|
bucket = http_groups if is_http(cfg) else stdio_groups
|
|
1059
|
-
bucket.setdefault(_config_key(cfg), []).append(name)
|
|
1060
|
+
bucket.setdefault(_config_key(name, cfg), []).append(name)
|
|
1060
1061
|
max_name = min(max((len(n) for n in servers), default=20), 28)
|
|
1062
|
+
tokens = _tokens_read() # local file only; no network
|
|
1063
|
+
|
|
1064
|
+
def login_mark(name):
|
|
1065
|
+
"""'🔑 logged in' / '🔑 login needed' for servers with an OAuth entry, else ''."""
|
|
1066
|
+
entry, cfg = tokens.get(name), servers[name]
|
|
1067
|
+
if entry is None or not is_http(cfg):
|
|
1068
|
+
return "", ""
|
|
1069
|
+
if entry.get("access_token") and _norm_url(entry.get("url", "")) == _norm_url(_expand_env(cfg["url"])):
|
|
1070
|
+
return "🔑 logged in", c["green"]
|
|
1071
|
+
return "🔑 login needed", c["yellow"]
|
|
1061
1072
|
|
|
1062
1073
|
def print_group(label, color, groups, target_fn):
|
|
1063
1074
|
total = sum(len(v) for v in groups.values())
|
|
@@ -1069,12 +1080,15 @@ def list_servers(servers):
|
|
|
1069
1080
|
names_sorted = sorted(names)
|
|
1070
1081
|
primary = names_sorted[0]
|
|
1071
1082
|
target = target_fn(key)
|
|
1083
|
+
mark, mark_color = login_mark(primary)
|
|
1072
1084
|
# compute remaining width for the target
|
|
1073
1085
|
base = f" ● {primary:<{max_name}} "
|
|
1074
1086
|
visible_len = len(base)
|
|
1075
|
-
avail = max(20, term_w - visible_len - 4)
|
|
1087
|
+
avail = max(20, term_w - visible_len - 4 - (len(mark) + 2 if mark else 0))
|
|
1076
1088
|
target_disp = _truncate(target, avail)
|
|
1077
1089
|
line = f" {c['green']}●{c['reset']} {c['bold']}{primary:<{max_name}}{c['reset']} {c['dim']}{target_disp}{c['reset']}"
|
|
1090
|
+
if mark:
|
|
1091
|
+
line += f" {mark_color}{mark}{c['reset']}"
|
|
1078
1092
|
if len(names_sorted) > 1:
|
|
1079
1093
|
line += f" {c['yellow']}×{len(names_sorted)}{c['reset']}"
|
|
1080
1094
|
print(line)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MCP CLI - Call any MCP server tool from the command line."""
|
|
2
|
-
__version__ = "0.8.
|
|
2
|
+
__version__ = "0.8.3"
|
|
@@ -1036,10 +1036,11 @@ def is_http(config):
|
|
|
1036
1036
|
return config.get("type") == "http" or "url" in config
|
|
1037
1037
|
|
|
1038
1038
|
|
|
1039
|
-
def _config_key(cfg):
|
|
1040
|
-
"""Hashable identity for a server config — used to collapse
|
|
1039
|
+
def _config_key(name, cfg):
|
|
1040
|
+
"""Hashable identity for a server config — used to collapse duplicate stdio servers."""
|
|
1041
1041
|
if is_http(cfg):
|
|
1042
|
-
|
|
1042
|
+
# same URL under another name is a different account or key, never an alias
|
|
1043
|
+
return ("http", cfg["url"], name)
|
|
1043
1044
|
return ("stdio", cfg.get("command", "?"), tuple(cfg.get("args", [])))
|
|
1044
1045
|
|
|
1045
1046
|
|
|
@@ -1049,15 +1050,25 @@ def _truncate(text, width):
|
|
|
1049
1050
|
|
|
1050
1051
|
|
|
1051
1052
|
def list_servers(servers):
|
|
1052
|
-
"""Print configured servers grouped by transport, collapsing
|
|
1053
|
+
"""Print configured servers grouped by transport, collapsing duplicate stdio servers."""
|
|
1053
1054
|
c = _colors()
|
|
1054
1055
|
term_w = shutil.get_terminal_size((100, 24)).columns
|
|
1055
1056
|
# split + group by config identity
|
|
1056
1057
|
http_groups, stdio_groups = {}, {}
|
|
1057
1058
|
for name, cfg in servers.items():
|
|
1058
1059
|
bucket = http_groups if is_http(cfg) else stdio_groups
|
|
1059
|
-
bucket.setdefault(_config_key(cfg), []).append(name)
|
|
1060
|
+
bucket.setdefault(_config_key(name, cfg), []).append(name)
|
|
1060
1061
|
max_name = min(max((len(n) for n in servers), default=20), 28)
|
|
1062
|
+
tokens = _tokens_read() # local file only; no network
|
|
1063
|
+
|
|
1064
|
+
def login_mark(name):
|
|
1065
|
+
"""'🔑 logged in' / '🔑 login needed' for servers with an OAuth entry, else ''."""
|
|
1066
|
+
entry, cfg = tokens.get(name), servers[name]
|
|
1067
|
+
if entry is None or not is_http(cfg):
|
|
1068
|
+
return "", ""
|
|
1069
|
+
if entry.get("access_token") and _norm_url(entry.get("url", "")) == _norm_url(_expand_env(cfg["url"])):
|
|
1070
|
+
return "🔑 logged in", c["green"]
|
|
1071
|
+
return "🔑 login needed", c["yellow"]
|
|
1061
1072
|
|
|
1062
1073
|
def print_group(label, color, groups, target_fn):
|
|
1063
1074
|
total = sum(len(v) for v in groups.values())
|
|
@@ -1069,12 +1080,15 @@ def list_servers(servers):
|
|
|
1069
1080
|
names_sorted = sorted(names)
|
|
1070
1081
|
primary = names_sorted[0]
|
|
1071
1082
|
target = target_fn(key)
|
|
1083
|
+
mark, mark_color = login_mark(primary)
|
|
1072
1084
|
# compute remaining width for the target
|
|
1073
1085
|
base = f" ● {primary:<{max_name}} "
|
|
1074
1086
|
visible_len = len(base)
|
|
1075
|
-
avail = max(20, term_w - visible_len - 4)
|
|
1087
|
+
avail = max(20, term_w - visible_len - 4 - (len(mark) + 2 if mark else 0))
|
|
1076
1088
|
target_disp = _truncate(target, avail)
|
|
1077
1089
|
line = f" {c['green']}●{c['reset']} {c['bold']}{primary:<{max_name}}{c['reset']} {c['dim']}{target_disp}{c['reset']}"
|
|
1090
|
+
if mark:
|
|
1091
|
+
line += f" {mark_color}{mark}{c['reset']}"
|
|
1078
1092
|
if len(names_sorted) > 1:
|
|
1079
1093
|
line += f" {c['yellow']}×{len(names_sorted)}{c['reset']}"
|
|
1080
1094
|
print(line)
|
|
@@ -382,6 +382,21 @@ class OAuthTest(unittest.TestCase):
|
|
|
382
382
|
self.assertIn("browser profile", err)
|
|
383
383
|
self.assertIn("Logged in to fake.", out)
|
|
384
384
|
|
|
385
|
+
def test_15_servers_lists_accounts_separately_with_login_state(self):
|
|
386
|
+
self.servers["fake-2"] = dict(self.servers["fake"]) # second account, same URL
|
|
387
|
+
cli._save_config(self.servers)
|
|
388
|
+
entry = self.seed()
|
|
389
|
+
store = {"fake": entry, "fake-2": {k: v for k, v in entry.items() if k != "access_token"}}
|
|
390
|
+
with open(cli.TOKENS_PATH, "w") as f:
|
|
391
|
+
json.dump(store, f)
|
|
392
|
+
code, out, err = self.run_cli("--servers")
|
|
393
|
+
self.assertEqual(code, 0, err)
|
|
394
|
+
self.assertNotIn("aliases", out)
|
|
395
|
+
rows = {line.split()[1]: line for line in out.splitlines() if line.strip().startswith("●")}
|
|
396
|
+
self.assertIn("logged in", rows["fake"])
|
|
397
|
+
self.assertIn("login needed", rows["fake-2"])
|
|
398
|
+
self.assertNotIn("🔑", rows["plain"]) # no OAuth entry, no marker
|
|
399
|
+
|
|
385
400
|
|
|
386
401
|
if __name__ == "__main__":
|
|
387
402
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|