mcp2cli 3.5.0__tar.gz → 3.6.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.
- {mcp2cli-3.5.0 → mcp2cli-3.6.0}/PKG-INFO +1 -1
- {mcp2cli-3.5.0 → mcp2cli-3.6.0}/pyproject.toml +1 -1
- {mcp2cli-3.5.0 → mcp2cli-3.6.0}/pyproject.toml.orig +1 -1
- {mcp2cli-3.5.0 → mcp2cli-3.6.0}/src/mcp2cli/__init__.py +215 -39
- {mcp2cli-3.5.0 → mcp2cli-3.6.0}/README.md +0 -0
- {mcp2cli-3.5.0 → mcp2cli-3.6.0}/src/mcp2cli/__main__.py +0 -0
- {mcp2cli-3.5.0 → mcp2cli-3.6.0}/src/mcp2cli/py.typed +0 -0
|
@@ -61,6 +61,7 @@ class ParamDef:
|
|
|
61
61
|
choices: list | None = None
|
|
62
62
|
location: str = "body" # path|query|header|body|tool_input
|
|
63
63
|
schema: dict = field(default_factory=dict)
|
|
64
|
+
cli_name: str | None = None # collision-free argparse flag name
|
|
64
65
|
|
|
65
66
|
|
|
66
67
|
@dataclass
|
|
@@ -178,8 +179,17 @@ def read_stdin_json(context: str):
|
|
|
178
179
|
sys.exit(1)
|
|
179
180
|
|
|
180
181
|
|
|
182
|
+
def _normalize_schema_type(t):
|
|
183
|
+
"""JSON Schema allows "type": ["integer", "null"] (array form). Reduce it
|
|
184
|
+
to the single concrete type, dropping "null"; anything else passes through."""
|
|
185
|
+
if isinstance(t, list):
|
|
186
|
+
concrete = [x for x in t if x != "null"]
|
|
187
|
+
return concrete[0] if len(concrete) == 1 else None
|
|
188
|
+
return t
|
|
189
|
+
|
|
190
|
+
|
|
181
191
|
def schema_type_to_python(schema: dict) -> tuple[type | None, str]:
|
|
182
|
-
t = schema.get("type")
|
|
192
|
+
t = _normalize_schema_type(schema.get("type"))
|
|
183
193
|
if t == "integer":
|
|
184
194
|
return int, ""
|
|
185
195
|
if t == "number":
|
|
@@ -190,6 +200,16 @@ def schema_type_to_python(schema: dict) -> tuple[type | None, str]:
|
|
|
190
200
|
return str, " (JSON array)"
|
|
191
201
|
if t == "object":
|
|
192
202
|
return str, " (JSON object)"
|
|
203
|
+
if t is None:
|
|
204
|
+
# No "type" but an enum: infer the argparse type from the values, so
|
|
205
|
+
# numeric enums stay callable (otherwise argparse parses the flag as a
|
|
206
|
+
# string and rejects it against numeric choices).
|
|
207
|
+
enum = schema.get("enum")
|
|
208
|
+
if enum and not any(isinstance(v, bool) for v in enum):
|
|
209
|
+
if all(isinstance(v, int) for v in enum):
|
|
210
|
+
return int, ""
|
|
211
|
+
if all(isinstance(v, (int, float)) for v in enum):
|
|
212
|
+
return float, ""
|
|
193
213
|
return str, ""
|
|
194
214
|
|
|
195
215
|
|
|
@@ -207,7 +227,7 @@ def _coerce_item(value: str, item_type: str | None):
|
|
|
207
227
|
def coerce_value(value, schema: dict):
|
|
208
228
|
if value is None:
|
|
209
229
|
return None
|
|
210
|
-
t = schema.get("type")
|
|
230
|
+
t = _normalize_schema_type(schema.get("type"))
|
|
211
231
|
if t == "array":
|
|
212
232
|
if isinstance(value, list):
|
|
213
233
|
return value
|
|
@@ -218,7 +238,7 @@ def coerce_value(value, schema: dict):
|
|
|
218
238
|
return parsed
|
|
219
239
|
except (json.JSONDecodeError, TypeError):
|
|
220
240
|
pass
|
|
221
|
-
item_type = schema.get("items", {}).get("type")
|
|
241
|
+
item_type = _normalize_schema_type(schema.get("items", {}).get("type"))
|
|
222
242
|
if "," in value:
|
|
223
243
|
return [_coerce_item(v.strip(), item_type) for v in value.split(",")]
|
|
224
244
|
return [_coerce_item(value, item_type)]
|
|
@@ -252,13 +272,16 @@ def to_kebab(name: str) -> str:
|
|
|
252
272
|
return s.replace("_", "-").lower()
|
|
253
273
|
|
|
254
274
|
|
|
255
|
-
def _find_toon_cli() -> str | None:
|
|
256
|
-
"""Return
|
|
275
|
+
def _find_toon_cli() -> tuple[str, ...] | None:
|
|
276
|
+
"""Return argv for the TOON CLI, or None if unavailable."""
|
|
257
277
|
if shutil.which("toon"):
|
|
258
|
-
return "toon"
|
|
259
|
-
#
|
|
278
|
+
return ("toon",)
|
|
279
|
+
# npx ships with Node.js, but having it says nothing about the package.
|
|
280
|
+
# `--no` forbids npx's implicit registry download, so a missing package
|
|
281
|
+
# fails in about a second instead of turning --toon into a network fetch
|
|
282
|
+
# racing the timeout in _toon_encode.
|
|
260
283
|
if shutil.which("npx"):
|
|
261
|
-
return "npx @toon-format/cli"
|
|
284
|
+
return ("npx", "--no", "@toon-format/cli")
|
|
262
285
|
return None
|
|
263
286
|
|
|
264
287
|
|
|
@@ -269,7 +292,7 @@ def _toon_encode(json_str: str) -> str | None:
|
|
|
269
292
|
return None
|
|
270
293
|
try:
|
|
271
294
|
result = subprocess.run(
|
|
272
|
-
cmd
|
|
295
|
+
cmd,
|
|
273
296
|
input=json_str,
|
|
274
297
|
capture_output=True,
|
|
275
298
|
text=True,
|
|
@@ -499,9 +522,34 @@ def _python_type_name(t: type | None) -> str:
|
|
|
499
522
|
return getattr(t, "__name__", str(t))
|
|
500
523
|
|
|
501
524
|
|
|
525
|
+
def _param_dest(p: "ParamDef") -> str:
|
|
526
|
+
"""Return the argparse destination allocated for a parameter."""
|
|
527
|
+
return (p.cli_name or p.name).replace("-", "_")
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
def _allocate_param_cli_names(cmd: "CommandDef") -> None:
|
|
531
|
+
"""Assign unique parameter flags without shadowing built-in options."""
|
|
532
|
+
natural_names = {p.name for p in cmd.params}
|
|
533
|
+
used = {"help"}
|
|
534
|
+
if cmd.has_body:
|
|
535
|
+
used.add("stdin")
|
|
536
|
+
|
|
537
|
+
for p in cmd.params:
|
|
538
|
+
name = p.name
|
|
539
|
+
if name in used:
|
|
540
|
+
stem = f"arg-{name}"
|
|
541
|
+
name = stem
|
|
542
|
+
suffix = 2
|
|
543
|
+
while name in used or name in natural_names:
|
|
544
|
+
name = f"{stem}-{suffix}"
|
|
545
|
+
suffix += 1
|
|
546
|
+
p.cli_name = name
|
|
547
|
+
used.add(name)
|
|
548
|
+
|
|
549
|
+
|
|
502
550
|
def _param_to_dict(p: "ParamDef") -> dict:
|
|
503
551
|
d = {
|
|
504
|
-
"name": p.name,
|
|
552
|
+
"name": p.cli_name or p.name,
|
|
505
553
|
"type": _python_type_name(p.python_type),
|
|
506
554
|
"required": p.required,
|
|
507
555
|
"description": p.description,
|
|
@@ -1494,8 +1542,34 @@ def extract_openapi_commands(spec: dict) -> list[CommandDef]:
|
|
|
1494
1542
|
|
|
1495
1543
|
def extract_mcp_commands(tools: list[dict]) -> list[CommandDef]:
|
|
1496
1544
|
commands: list[CommandDef] = []
|
|
1497
|
-
for tool in tools
|
|
1498
|
-
|
|
1545
|
+
base_names = [to_kebab(tool.get("name", "unknown")) for tool in tools]
|
|
1546
|
+
reserved_names = set(base_names)
|
|
1547
|
+
cli_names = [""] * len(tools)
|
|
1548
|
+
used_names: set[str] = set()
|
|
1549
|
+
groups: dict[str, list[int]] = {}
|
|
1550
|
+
for index, base_name in enumerate(base_names):
|
|
1551
|
+
groups.setdefault(base_name, []).append(index)
|
|
1552
|
+
|
|
1553
|
+
# Assign aliases from the complete tool-name set so a generated suffix can
|
|
1554
|
+
# never shadow another tool's natural name. Sorting each collision group by
|
|
1555
|
+
# wire name keeps aliases stable when a server reorders tools/list.
|
|
1556
|
+
for base_name in sorted(groups):
|
|
1557
|
+
indices = sorted(
|
|
1558
|
+
groups[base_name],
|
|
1559
|
+
key=lambda index: (tools[index].get("name", "unknown"), index),
|
|
1560
|
+
)
|
|
1561
|
+
for rank, index in enumerate(indices):
|
|
1562
|
+
name = base_name
|
|
1563
|
+
if rank:
|
|
1564
|
+
suffix = 2
|
|
1565
|
+
name = f"{base_name}-{suffix}"
|
|
1566
|
+
while name in reserved_names or name in used_names:
|
|
1567
|
+
suffix += 1
|
|
1568
|
+
name = f"{base_name}-{suffix}"
|
|
1569
|
+
cli_names[index] = name
|
|
1570
|
+
used_names.add(name)
|
|
1571
|
+
|
|
1572
|
+
for tool, name in zip(tools, cli_names):
|
|
1499
1573
|
desc = tool.get("description", "")
|
|
1500
1574
|
schema = tool.get("inputSchema", {})
|
|
1501
1575
|
required_fields = set(schema.get("required", []))
|
|
@@ -1934,12 +2008,12 @@ def _build_graphql_document(
|
|
|
1934
2008
|
types_by_name = {t["name"]: t for t in schema.get("types", []) if t.get("name")}
|
|
1935
2009
|
|
|
1936
2010
|
# Build variables dict from args
|
|
1937
|
-
if getattr(args, "stdin", False):
|
|
2011
|
+
if getattr(args, "stdin", False) is True:
|
|
1938
2012
|
variables = read_stdin_json("GraphQL variables")
|
|
1939
2013
|
else:
|
|
1940
2014
|
variables = {}
|
|
1941
2015
|
for p in cmd.params:
|
|
1942
|
-
val = getattr(args, p
|
|
2016
|
+
val = getattr(args, _param_dest(p), None)
|
|
1943
2017
|
if val is not None:
|
|
1944
2018
|
variables[p.original_name] = coerce_value(val, p.schema)
|
|
1945
2019
|
|
|
@@ -2473,6 +2547,7 @@ def build_argparse(
|
|
|
2473
2547
|
help=escape_argparse_help(cmd.description),
|
|
2474
2548
|
description=escape_argparse_help(cmd.description),
|
|
2475
2549
|
)
|
|
2550
|
+
_allocate_param_cli_names(cmd)
|
|
2476
2551
|
sub.set_defaults(_cmd=cmd)
|
|
2477
2552
|
|
|
2478
2553
|
if cmd.has_body:
|
|
@@ -2483,12 +2558,8 @@ def build_argparse(
|
|
|
2483
2558
|
help="Read JSON body/arguments from stdin",
|
|
2484
2559
|
)
|
|
2485
2560
|
|
|
2486
|
-
seen_flags: set[str] = set()
|
|
2487
2561
|
for p in cmd.params:
|
|
2488
|
-
flag = f"--{p.name}"
|
|
2489
|
-
if flag in seen_flags:
|
|
2490
|
-
continue # skip duplicate param names (e.g. path + body both have same name)
|
|
2491
|
-
seen_flags.add(flag)
|
|
2562
|
+
flag = f"--{p.cli_name or p.name}"
|
|
2492
2563
|
kwargs: dict = {}
|
|
2493
2564
|
if p.python_type is not None:
|
|
2494
2565
|
kwargs["type"] = p.python_type
|
|
@@ -2506,6 +2577,7 @@ def build_argparse(
|
|
|
2506
2577
|
kwargs["help"] = escape_argparse_help(p.description)
|
|
2507
2578
|
if p.choices:
|
|
2508
2579
|
kwargs["choices"] = p.choices
|
|
2580
|
+
kwargs["dest"] = _param_dest(p)
|
|
2509
2581
|
sub.add_argument(flag, **kwargs)
|
|
2510
2582
|
|
|
2511
2583
|
return parser
|
|
@@ -2634,13 +2706,13 @@ def _collect_openapi_params(
|
|
|
2634
2706
|
|
|
2635
2707
|
for p in cmd.params:
|
|
2636
2708
|
if p.location == "path":
|
|
2637
|
-
val = getattr(args, p
|
|
2709
|
+
val = getattr(args, _param_dest(p), None)
|
|
2638
2710
|
if val is not None:
|
|
2639
2711
|
path = path.replace(f"{{{p.original_name}}}", str(val))
|
|
2640
2712
|
|
|
2641
2713
|
if cmd.method == "get":
|
|
2642
2714
|
for p in cmd.params:
|
|
2643
|
-
val = getattr(args, p
|
|
2715
|
+
val = getattr(args, _param_dest(p), None)
|
|
2644
2716
|
if val is None:
|
|
2645
2717
|
continue
|
|
2646
2718
|
if p.location == "query":
|
|
@@ -2648,12 +2720,12 @@ def _collect_openapi_params(
|
|
|
2648
2720
|
elif p.location == "header":
|
|
2649
2721
|
extra_headers[p.original_name] = str(val)
|
|
2650
2722
|
else:
|
|
2651
|
-
if getattr(args, "stdin", False):
|
|
2723
|
+
if getattr(args, "stdin", False) is True:
|
|
2652
2724
|
body = read_stdin_json("OpenAPI request body")
|
|
2653
2725
|
else:
|
|
2654
2726
|
body = {}
|
|
2655
2727
|
for p in cmd.params:
|
|
2656
|
-
val = getattr(args, p
|
|
2728
|
+
val = getattr(args, _param_dest(p), None)
|
|
2657
2729
|
if p.location == "header":
|
|
2658
2730
|
if val is not None:
|
|
2659
2731
|
extra_headers[p.original_name] = str(val)
|
|
@@ -2678,7 +2750,7 @@ def _collect_openapi_params(
|
|
|
2678
2750
|
# Also collect query params for non-GET
|
|
2679
2751
|
for p in cmd.params:
|
|
2680
2752
|
if p.location == "query":
|
|
2681
|
-
val = getattr(args, p
|
|
2753
|
+
val = getattr(args, _param_dest(p), None)
|
|
2682
2754
|
if val is not None:
|
|
2683
2755
|
query_params[p.original_name] = coerce_value(val, p.schema)
|
|
2684
2756
|
|
|
@@ -2763,6 +2835,55 @@ def execute_openapi(
|
|
|
2763
2835
|
# ---------------------------------------------------------------------------
|
|
2764
2836
|
|
|
2765
2837
|
|
|
2838
|
+
def _exc_leaves(exc: BaseException) -> list[BaseException]:
|
|
2839
|
+
"""Flatten nested exception groups into their leaf exceptions."""
|
|
2840
|
+
nested = getattr(exc, "exceptions", None)
|
|
2841
|
+
if not nested:
|
|
2842
|
+
return [exc]
|
|
2843
|
+
return [leaf for child in nested for leaf in _exc_leaves(child)]
|
|
2844
|
+
|
|
2845
|
+
|
|
2846
|
+
def _exc_message(exc: BaseException) -> str:
|
|
2847
|
+
"""Flatten an exception group into one terminal-safe line."""
|
|
2848
|
+
parts = []
|
|
2849
|
+
for leaf in _exc_leaves(exc):
|
|
2850
|
+
message = str(leaf) or leaf.__class__.__name__
|
|
2851
|
+
parts.append("; ".join(line.strip() for line in message.splitlines() if line.strip()))
|
|
2852
|
+
return "; ".join(part for part in parts if part) or exc.__class__.__name__
|
|
2853
|
+
|
|
2854
|
+
|
|
2855
|
+
def _run_mcp_clean(fn, source: str):
|
|
2856
|
+
"""Run an MCP coroutine, reporting failures as one clean error line.
|
|
2857
|
+
|
|
2858
|
+
Transport failures (bad URL, refused connection, 401/403) otherwise reach
|
|
2859
|
+
the terminal as a multi-level anyio ExceptionGroup traceback with the
|
|
2860
|
+
actual cause buried at the bottom. Set MCP2CLI_DEBUG=1 for the traceback.
|
|
2861
|
+
"""
|
|
2862
|
+
try:
|
|
2863
|
+
return anyio.run(fn)
|
|
2864
|
+
except SystemExit:
|
|
2865
|
+
raise
|
|
2866
|
+
except KeyboardInterrupt: # pragma: no cover - interactive only
|
|
2867
|
+
raise
|
|
2868
|
+
except BaseException as exc:
|
|
2869
|
+
leaves = _exc_leaves(exc)
|
|
2870
|
+
if len(leaves) == 1 and isinstance(leaves[0], (SystemExit, KeyboardInterrupt)):
|
|
2871
|
+
raise leaves[0]
|
|
2872
|
+
if os.environ.get("MCP2CLI_DEBUG"):
|
|
2873
|
+
raise
|
|
2874
|
+
message = _exc_message(exc)
|
|
2875
|
+
lowered = message.lower()
|
|
2876
|
+
if "401" in lowered or "403" in lowered:
|
|
2877
|
+
hint = (
|
|
2878
|
+
" — the server rejected the request; pass credentials with "
|
|
2879
|
+
"--auth-header 'Name:Value' or use the --oauth-* options"
|
|
2880
|
+
)
|
|
2881
|
+
else:
|
|
2882
|
+
hint = ""
|
|
2883
|
+
print(f"Error: cannot use MCP server at {source}: {message}{hint}", file=sys.stderr)
|
|
2884
|
+
sys.exit(1)
|
|
2885
|
+
|
|
2886
|
+
|
|
2766
2887
|
def run_mcp_http(
|
|
2767
2888
|
url: str,
|
|
2768
2889
|
auth_headers: list[tuple[str, str]],
|
|
@@ -2865,7 +2986,9 @@ def run_mcp_http(
|
|
|
2865
2986
|
except Exception:
|
|
2866
2987
|
return await _with_sse()
|
|
2867
2988
|
|
|
2868
|
-
|
|
2989
|
+
rc = _run_mcp_clean(_run, url)
|
|
2990
|
+
if rc:
|
|
2991
|
+
sys.exit(rc)
|
|
2869
2992
|
|
|
2870
2993
|
|
|
2871
2994
|
def run_mcp_stdio(
|
|
@@ -2923,7 +3046,7 @@ def run_mcp_stdio(
|
|
|
2923
3046
|
async with stdio_client(params) as (read, write):
|
|
2924
3047
|
async with ClientSession(read, write) as session:
|
|
2925
3048
|
await session.initialize()
|
|
2926
|
-
await _mcp_session(
|
|
3049
|
+
return await _mcp_session(
|
|
2927
3050
|
session,
|
|
2928
3051
|
tool_name,
|
|
2929
3052
|
arguments,
|
|
@@ -2937,7 +3060,9 @@ def run_mcp_stdio(
|
|
|
2937
3060
|
**extra,
|
|
2938
3061
|
)
|
|
2939
3062
|
|
|
2940
|
-
|
|
3063
|
+
rc = _run_mcp_clean(_run, command_str)
|
|
3064
|
+
if rc:
|
|
3065
|
+
sys.exit(rc)
|
|
2941
3066
|
|
|
2942
3067
|
|
|
2943
3068
|
async def _mcp_session(
|
|
@@ -3028,10 +3153,26 @@ async def _mcp_session(
|
|
|
3028
3153
|
# isError) with the camelCase wire names, so the envelope does not
|
|
3029
3154
|
# change shape with the installed SDK major.
|
|
3030
3155
|
output_result(_mcp_dump(result), pretty=pretty, head=head, json_output=True)
|
|
3031
|
-
|
|
3156
|
+
# A failed tool still exits non-zero under --json so callers can detect
|
|
3157
|
+
# it; the envelope on stdout already carries isError for machines.
|
|
3158
|
+
return 1 if _mcp_attr(result, "isError") else 0
|
|
3032
3159
|
|
|
3033
3160
|
text = _extract_content_parts(result.content)
|
|
3034
|
-
|
|
3161
|
+
payload = text
|
|
3162
|
+
if not payload:
|
|
3163
|
+
# A tool may return only structuredContent with an empty content list.
|
|
3164
|
+
structured = _mcp_attr(result, "structuredContent")
|
|
3165
|
+
if structured is not None:
|
|
3166
|
+
payload = structured
|
|
3167
|
+
|
|
3168
|
+
if _mcp_attr(result, "isError"):
|
|
3169
|
+
# Return the code instead of raising inside the anyio task group, which
|
|
3170
|
+
# would wrap SystemExit in a BaseExceptionGroup traceback.
|
|
3171
|
+
error = payload if isinstance(payload, str) else json.dumps(payload, ensure_ascii=False)
|
|
3172
|
+
print(f"Error: {error or f'tool {tool_name!r} reported an error'}", file=sys.stderr)
|
|
3173
|
+
return 1
|
|
3174
|
+
output_result(payload, pretty=pretty, raw=raw, toon=toon, head=head)
|
|
3175
|
+
return 0
|
|
3035
3176
|
|
|
3036
3177
|
|
|
3037
3178
|
# ---------------------------------------------------------------------------
|
|
@@ -3280,13 +3421,25 @@ def session_start(
|
|
|
3280
3421
|
|
|
3281
3422
|
|
|
3282
3423
|
def _extract_content_parts(content_list, *, attrs=("text", "data")) -> str:
|
|
3283
|
-
"""Extract text/data/blob from MCP content objects, joined by newline.
|
|
3424
|
+
"""Extract text/data/blob from MCP content objects, joined by newline.
|
|
3425
|
+
|
|
3426
|
+
``resource_link`` blocks carry neither ``text`` nor ``data`` — only
|
|
3427
|
+
``uri``/``name`` — so they used to be dropped silently. Render them as
|
|
3428
|
+
``name: uri`` (or just the URI) instead.
|
|
3429
|
+
"""
|
|
3284
3430
|
parts = []
|
|
3285
|
-
for
|
|
3431
|
+
for content in content_list:
|
|
3432
|
+
is_mapping = isinstance(content, dict)
|
|
3286
3433
|
for attr in attrs:
|
|
3287
|
-
if
|
|
3288
|
-
|
|
3434
|
+
value = content.get(attr) if is_mapping else getattr(content, attr, None)
|
|
3435
|
+
if value is not None:
|
|
3436
|
+
parts.append(value)
|
|
3289
3437
|
break
|
|
3438
|
+
else:
|
|
3439
|
+
uri = content.get("uri") if is_mapping else getattr(content, "uri", None)
|
|
3440
|
+
if uri is not None:
|
|
3441
|
+
name = content.get("name") if is_mapping else getattr(content, "name", None)
|
|
3442
|
+
parts.append(f"{name}: {uri}" if name else str(uri))
|
|
3290
3443
|
return "\n".join(parts) if parts else ""
|
|
3291
3444
|
|
|
3292
3445
|
|
|
@@ -3320,7 +3473,7 @@ async def _dispatch_list_tools(session, params):
|
|
|
3320
3473
|
|
|
3321
3474
|
async def _dispatch_call_tool(session, params):
|
|
3322
3475
|
result = await session.call_tool(params["name"], params.get("arguments", {}))
|
|
3323
|
-
return
|
|
3476
|
+
return _mcp_dump(result)
|
|
3324
3477
|
|
|
3325
3478
|
|
|
3326
3479
|
async def _dispatch_list_resources(session, params):
|
|
@@ -3772,12 +3925,12 @@ def handle_mcp(
|
|
|
3772
3925
|
|
|
3773
3926
|
cmd: CommandDef = args._cmd
|
|
3774
3927
|
|
|
3775
|
-
if getattr(args, "stdin", False):
|
|
3928
|
+
if getattr(args, "stdin", False) is True:
|
|
3776
3929
|
arguments = read_stdin_json("MCP tool arguments")
|
|
3777
3930
|
else:
|
|
3778
3931
|
arguments = {}
|
|
3779
3932
|
for p in cmd.params:
|
|
3780
|
-
val = getattr(args, p
|
|
3933
|
+
val = getattr(args, _param_dest(p), None)
|
|
3781
3934
|
if val is not None:
|
|
3782
3935
|
arguments[p.original_name] = coerce_value(val, p.schema)
|
|
3783
3936
|
|
|
@@ -3861,7 +4014,7 @@ def _fetch_mcp_tools(
|
|
|
3861
4014
|
except Exception:
|
|
3862
4015
|
await _via_sse()
|
|
3863
4016
|
|
|
3864
|
-
|
|
4017
|
+
_run_mcp_clean(_run, source)
|
|
3865
4018
|
return tools_result
|
|
3866
4019
|
|
|
3867
4020
|
|
|
@@ -4366,18 +4519,41 @@ def _handle_session_operations(
|
|
|
4366
4519
|
sys.exit(1)
|
|
4367
4520
|
|
|
4368
4521
|
cmd: CommandDef = args._cmd
|
|
4369
|
-
if getattr(args, "stdin", False):
|
|
4522
|
+
if getattr(args, "stdin", False) is True:
|
|
4370
4523
|
arguments = read_stdin_json(f"session {sess_name} tool arguments")
|
|
4371
4524
|
else:
|
|
4372
4525
|
arguments = {}
|
|
4373
4526
|
for p in cmd.params:
|
|
4374
|
-
val = getattr(args, p
|
|
4527
|
+
val = getattr(args, _param_dest(p), None)
|
|
4375
4528
|
if val is not None:
|
|
4376
4529
|
arguments[p.original_name] = coerce_value(val, p.schema)
|
|
4377
4530
|
|
|
4378
4531
|
result = _session_request(
|
|
4379
4532
|
sess_name, "call_tool", {"name": cmd.tool_name, "arguments": arguments}
|
|
4380
4533
|
)
|
|
4534
|
+
if isinstance(result, dict) and "isError" in result:
|
|
4535
|
+
content = result.get("content") or []
|
|
4536
|
+
text = _extract_content_parts(content) if isinstance(content, list) else content
|
|
4537
|
+
payload = text or result.get("structuredContent") or ""
|
|
4538
|
+
|
|
4539
|
+
if pre_args.json_output:
|
|
4540
|
+
output_result(result, **_sess_out)
|
|
4541
|
+
if result.get("isError"):
|
|
4542
|
+
sys.exit(1)
|
|
4543
|
+
return True
|
|
4544
|
+
|
|
4545
|
+
if result.get("isError"):
|
|
4546
|
+
error = (
|
|
4547
|
+
payload
|
|
4548
|
+
if isinstance(payload, str)
|
|
4549
|
+
else json.dumps(payload, ensure_ascii=False)
|
|
4550
|
+
)
|
|
4551
|
+
print(
|
|
4552
|
+
f"Error: {error or f'tool {cmd.tool_name!r} reported an error'}",
|
|
4553
|
+
file=sys.stderr,
|
|
4554
|
+
)
|
|
4555
|
+
sys.exit(1)
|
|
4556
|
+
result = payload
|
|
4381
4557
|
output_result(result, **_sess_out)
|
|
4382
4558
|
return True
|
|
4383
4559
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|