mcp-cli-skill 0.6.2__tar.gz → 0.7.0__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
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: mcp-cli-skill
3
- Version: 0.6.2
3
+ Version: 0.7.0
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
@@ -21,7 +21,7 @@ mcp-call <server> <tool> --key=value ... # call a tool
21
21
  mcp-call <server> <tool> --input-json '{"key":"val"}' # call with JSON args
22
22
  echo '{}' | mcp-call <server> <tool> # call with stdin JSON
23
23
  mcp-call --add <name> <cmd> [args] [--env K=V ...] # add stdio server
24
- mcp-call --add-http <name> <url> # add HTTP server
24
+ mcp-call --add-http <name> <url> [--header 'K: V' ...] # add HTTP server (auth via --header/-H)
25
25
  mcp-call --remove <name> # remove server
26
26
  mcp-call --sync # re-sync from Claude configs
27
27
  mcp-call --completion <bash|zsh|fish> # print shell completion hook
@@ -275,6 +275,22 @@ def _print_content(items):
275
275
  os.write(fd, base64.b64decode(item["data"]))
276
276
  os.close(fd)
277
277
  print(path)
278
+ elif item.get("type") == "resource_link":
279
+ line = item.get("uri", "")
280
+ if item.get("name"):
281
+ line += f" ({item['name']})"
282
+ print(line)
283
+
284
+
285
+ def _print_result(result):
286
+ """Print a tools/call result; exit 1 if the tool flagged an error."""
287
+ content = result.get("content", [])
288
+ _print_content(content)
289
+ # structured-only results (empty content) would otherwise print nothing
290
+ if not content and result.get("structuredContent") is not None:
291
+ print(json.dumps(result["structuredContent"], indent=2, default=str))
292
+ if result.get("isError"):
293
+ sys.exit(1)
278
294
 
279
295
 
280
296
  def _expand_env(val):
@@ -292,7 +308,10 @@ class HttpSession:
292
308
  self.session_id = None
293
309
  self.extra_headers = {k: _expand_env(v) for k, v in (extra_headers or {}).items()}
294
310
 
295
- def _send(self, data, headers, timeout=30, max_redirects=3):
311
+ def _send(self, data, headers, timeout=None, max_redirects=3):
312
+ # Long-running tool calls (bash, LLM, etc) can exceed 30s; make it tunable.
313
+ if timeout is None:
314
+ timeout = int(os.environ.get("MCP_CALL_HTTP_TIMEOUT", "300"))
296
315
  """POST data and follow 307/308 redirects preserving method+body.
297
316
 
298
317
  urllib's default HTTPRedirectHandler does NOT follow 307/308 on POST,
@@ -354,6 +373,10 @@ class HttpSession:
354
373
  except urllib.error.URLError as e:
355
374
  print(f"Error: cannot connect to {self.url}: {e.reason}", file=sys.stderr)
356
375
  sys.exit(1)
376
+ except TimeoutError:
377
+ # urlopen raises bare TimeoutError in Python 3.10+, not URLError.
378
+ print(f"Error: request to {self.url} timed out. Set MCP_CALL_HTTP_TIMEOUT=<sec> for longer.", file=sys.stderr)
379
+ sys.exit(1)
357
380
 
358
381
  def notify(self, method, params=None):
359
382
  """Send JSON-RPC notification (no id, ignore response)."""
@@ -406,7 +429,7 @@ def http_call_tool(url, tool_name, tool_args, extra_headers=None):
406
429
  if "error" in resp:
407
430
  print(json.dumps(resp["error"], indent=2), file=sys.stderr)
408
431
  sys.exit(1)
409
- _print_content(resp.get("result", {}).get("content", []))
432
+ _print_result(resp.get("result", {}))
410
433
 
411
434
 
412
435
  # --- Stdio transport ---
@@ -494,7 +517,7 @@ def stdio_call_tool(proc, tool_name, tool_args):
494
517
  if "error" in resp:
495
518
  print(json.dumps(resp["error"], indent=2), file=sys.stderr)
496
519
  sys.exit(1)
497
- _print_content(resp.get("result", {}).get("content", []))
520
+ _print_result(resp.get("result", {}))
498
521
 
499
522
 
500
523
  # --- Tool discovery ---
@@ -505,17 +528,33 @@ def fetch_tools(config, server_name=""):
505
528
  if is_http(config):
506
529
  session = HttpSession(config["url"], config.get("headers"))
507
530
  http_init(session)
508
- resp = session.rpc("tools/list", {}, msg_id=2)
509
- if resp and "result" in resp:
510
- tools = resp["result"].get("tools", [])
531
+ cursor, msg_id = None, 2
532
+ while True:
533
+ params = {"cursor": cursor} if cursor else {}
534
+ resp = session.rpc("tools/list", params, msg_id=msg_id)
535
+ if not resp or "result" not in resp:
536
+ break
537
+ tools += resp["result"].get("tools", [])
538
+ cursor = resp["result"].get("nextCursor")
539
+ if not cursor:
540
+ break
541
+ msg_id += 1
511
542
  else:
512
543
  proc = spawn_server(config)
513
544
  try:
514
545
  init_server(proc)
515
- send(proc, "tools/list", {}, msg_id=2)
516
- resp = recv(proc, expected_id=2)
517
- if resp and "result" in resp:
518
- tools = resp["result"].get("tools", [])
546
+ cursor, msg_id = None, 2
547
+ while True:
548
+ params = {"cursor": cursor} if cursor else {}
549
+ send(proc, "tools/list", params, msg_id=msg_id)
550
+ resp = recv(proc, expected_id=msg_id)
551
+ if not resp or "result" not in resp:
552
+ break
553
+ tools += resp["result"].get("tools", [])
554
+ cursor = resp["result"].get("nextCursor")
555
+ if not cursor:
556
+ break
557
+ msg_id += 1
519
558
  finally:
520
559
  proc.terminate()
521
560
  try:
@@ -529,7 +568,7 @@ def fetch_tools(config, server_name=""):
529
568
 
530
569
  def _colors():
531
570
  """Return ANSI color codes if stdout is a TTY, else empty strings."""
532
- on = sys.stdout.isatty()
571
+ on = sys.stdout.isatty() and not os.environ.get("NO_COLOR")
533
572
  return {
534
573
  "bold": "\033[1m" if on else "",
535
574
  "dim": "\033[2m" if on else "",
@@ -1,2 +1,2 @@
1
1
  """MCP CLI - Call any MCP server tool from the command line."""
2
- __version__ = "0.6.2"
2
+ __version__ = "0.7.0"
@@ -275,6 +275,22 @@ def _print_content(items):
275
275
  os.write(fd, base64.b64decode(item["data"]))
276
276
  os.close(fd)
277
277
  print(path)
278
+ elif item.get("type") == "resource_link":
279
+ line = item.get("uri", "")
280
+ if item.get("name"):
281
+ line += f" ({item['name']})"
282
+ print(line)
283
+
284
+
285
+ def _print_result(result):
286
+ """Print a tools/call result; exit 1 if the tool flagged an error."""
287
+ content = result.get("content", [])
288
+ _print_content(content)
289
+ # structured-only results (empty content) would otherwise print nothing
290
+ if not content and result.get("structuredContent") is not None:
291
+ print(json.dumps(result["structuredContent"], indent=2, default=str))
292
+ if result.get("isError"):
293
+ sys.exit(1)
278
294
 
279
295
 
280
296
  def _expand_env(val):
@@ -292,7 +308,10 @@ class HttpSession:
292
308
  self.session_id = None
293
309
  self.extra_headers = {k: _expand_env(v) for k, v in (extra_headers or {}).items()}
294
310
 
295
- def _send(self, data, headers, timeout=30, max_redirects=3):
311
+ def _send(self, data, headers, timeout=None, max_redirects=3):
312
+ # Long-running tool calls (bash, LLM, etc) can exceed 30s; make it tunable.
313
+ if timeout is None:
314
+ timeout = int(os.environ.get("MCP_CALL_HTTP_TIMEOUT", "300"))
296
315
  """POST data and follow 307/308 redirects preserving method+body.
297
316
 
298
317
  urllib's default HTTPRedirectHandler does NOT follow 307/308 on POST,
@@ -354,6 +373,10 @@ class HttpSession:
354
373
  except urllib.error.URLError as e:
355
374
  print(f"Error: cannot connect to {self.url}: {e.reason}", file=sys.stderr)
356
375
  sys.exit(1)
376
+ except TimeoutError:
377
+ # urlopen raises bare TimeoutError in Python 3.10+, not URLError.
378
+ print(f"Error: request to {self.url} timed out. Set MCP_CALL_HTTP_TIMEOUT=<sec> for longer.", file=sys.stderr)
379
+ sys.exit(1)
357
380
 
358
381
  def notify(self, method, params=None):
359
382
  """Send JSON-RPC notification (no id, ignore response)."""
@@ -406,7 +429,7 @@ def http_call_tool(url, tool_name, tool_args, extra_headers=None):
406
429
  if "error" in resp:
407
430
  print(json.dumps(resp["error"], indent=2), file=sys.stderr)
408
431
  sys.exit(1)
409
- _print_content(resp.get("result", {}).get("content", []))
432
+ _print_result(resp.get("result", {}))
410
433
 
411
434
 
412
435
  # --- Stdio transport ---
@@ -494,7 +517,7 @@ def stdio_call_tool(proc, tool_name, tool_args):
494
517
  if "error" in resp:
495
518
  print(json.dumps(resp["error"], indent=2), file=sys.stderr)
496
519
  sys.exit(1)
497
- _print_content(resp.get("result", {}).get("content", []))
520
+ _print_result(resp.get("result", {}))
498
521
 
499
522
 
500
523
  # --- Tool discovery ---
@@ -505,17 +528,33 @@ def fetch_tools(config, server_name=""):
505
528
  if is_http(config):
506
529
  session = HttpSession(config["url"], config.get("headers"))
507
530
  http_init(session)
508
- resp = session.rpc("tools/list", {}, msg_id=2)
509
- if resp and "result" in resp:
510
- tools = resp["result"].get("tools", [])
531
+ cursor, msg_id = None, 2
532
+ while True:
533
+ params = {"cursor": cursor} if cursor else {}
534
+ resp = session.rpc("tools/list", params, msg_id=msg_id)
535
+ if not resp or "result" not in resp:
536
+ break
537
+ tools += resp["result"].get("tools", [])
538
+ cursor = resp["result"].get("nextCursor")
539
+ if not cursor:
540
+ break
541
+ msg_id += 1
511
542
  else:
512
543
  proc = spawn_server(config)
513
544
  try:
514
545
  init_server(proc)
515
- send(proc, "tools/list", {}, msg_id=2)
516
- resp = recv(proc, expected_id=2)
517
- if resp and "result" in resp:
518
- tools = resp["result"].get("tools", [])
546
+ cursor, msg_id = None, 2
547
+ while True:
548
+ params = {"cursor": cursor} if cursor else {}
549
+ send(proc, "tools/list", params, msg_id=msg_id)
550
+ resp = recv(proc, expected_id=msg_id)
551
+ if not resp or "result" not in resp:
552
+ break
553
+ tools += resp["result"].get("tools", [])
554
+ cursor = resp["result"].get("nextCursor")
555
+ if not cursor:
556
+ break
557
+ msg_id += 1
519
558
  finally:
520
559
  proc.terminate()
521
560
  try:
@@ -529,7 +568,7 @@ def fetch_tools(config, server_name=""):
529
568
 
530
569
  def _colors():
531
570
  """Return ANSI color codes if stdout is a TTY, else empty strings."""
532
- on = sys.stdout.isatty()
571
+ on = sys.stdout.isatty() and not os.environ.get("NO_COLOR")
533
572
  return {
534
573
  "bold": "\033[1m" if on else "",
535
574
  "dim": "\033[2m" if on else "",
File without changes
File without changes