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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: mcp-cli-skill
3
- Version: 0.8.2
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 duplicates."""
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
- return ("http", cfg["url"])
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 duplicates."""
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"
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 duplicates."""
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
- return ("http", cfg["url"])
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 duplicates."""
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