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.
- {mcp_cli_skill-0.6.2 → mcp_cli_skill-0.7.0}/PKG-INFO +2 -2
- {mcp_cli_skill-0.6.2 → mcp_cli_skill-0.7.0}/SKILL.md +1 -1
- {mcp_cli_skill-0.6.2 → mcp_cli_skill-0.7.0}/scripts/mcp_call.py +50 -11
- {mcp_cli_skill-0.6.2 → mcp_cli_skill-0.7.0}/src/mcp_cli_skill/__init__.py +1 -1
- {mcp_cli_skill-0.6.2 → mcp_cli_skill-0.7.0}/src/mcp_cli_skill/cli.py +50 -11
- {mcp_cli_skill-0.6.2 → mcp_cli_skill-0.7.0}/.gitignore +0 -0
- {mcp_cli_skill-0.6.2 → mcp_cli_skill-0.7.0}/README.md +0 -0
- {mcp_cli_skill-0.6.2 → mcp_cli_skill-0.7.0}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: mcp-cli-skill
|
|
3
|
-
Version: 0.
|
|
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>
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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.
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
|
File without changes
|