mcptoon 0.1.0__py3-none-any.whl
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.
- mcptoon/__init__.py +18 -0
- mcptoon/cache.py +92 -0
- mcptoon/cli.py +414 -0
- mcptoon/client.py +511 -0
- mcptoon/config.py +176 -0
- mcptoon/errors.py +46 -0
- mcptoon/manifest.py +120 -0
- mcptoon/output.py +212 -0
- mcptoon/router.py +147 -0
- mcptoon/usage.py +96 -0
- mcptoon-0.1.0.dist-info/METADATA +409 -0
- mcptoon-0.1.0.dist-info/RECORD +17 -0
- mcptoon-0.1.0.dist-info/WHEEL +5 -0
- mcptoon-0.1.0.dist-info/entry_points.txt +2 -0
- mcptoon-0.1.0.dist-info/licenses/LICENSE +201 -0
- mcptoon-0.1.0.dist-info/licenses/NOTICE +20 -0
- mcptoon-0.1.0.dist-info/top_level.txt +1 -0
mcptoon/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2025 cxh
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""mcptoon — Token-efficient MCP CLI client."""
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
mcptoon/cache.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2025 cxh
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
mcptoon cache — Schema cache for tool discovery
|
|
18
|
+
|
|
19
|
+
Caches tool lists per server to avoid repeated initialize/list_tools round-trips.
|
|
20
|
+
TTL: 5 minutes (configurable via MCPTOON_CACHE_TTL env var)
|
|
21
|
+
"""
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from .config import CACHE_DIR
|
|
28
|
+
|
|
29
|
+
_DEFAULT_TTL = 300 # 5 minutes
|
|
30
|
+
_CACHE_FILE = CACHE_DIR / "schema_cache.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_ttl() -> int:
|
|
34
|
+
try:
|
|
35
|
+
return int(os.environ.get("MCPTOON_CACHE_TTL", _DEFAULT_TTL))
|
|
36
|
+
except ValueError:
|
|
37
|
+
return _DEFAULT_TTL
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_cache() -> dict:
|
|
41
|
+
"""Load the cache file."""
|
|
42
|
+
if not _CACHE_FILE.exists():
|
|
43
|
+
return {}
|
|
44
|
+
try:
|
|
45
|
+
return json.loads(_CACHE_FILE.read_text(encoding="utf-8"))
|
|
46
|
+
except (json.JSONDecodeError, OSError):
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _save_cache(data: dict):
|
|
51
|
+
"""Save the cache file."""
|
|
52
|
+
try:
|
|
53
|
+
_CACHE_FILE.write_text(
|
|
54
|
+
json.dumps(data, ensure_ascii=False),
|
|
55
|
+
encoding="utf-8",
|
|
56
|
+
)
|
|
57
|
+
except OSError:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_cached_tools(server: str) -> list[dict] | None:
|
|
62
|
+
"""Get cached tools for a server, or None if cache miss/expired."""
|
|
63
|
+
cache = _load_cache()
|
|
64
|
+
entry = cache.get(server)
|
|
65
|
+
if not entry:
|
|
66
|
+
return None
|
|
67
|
+
if time.time() - entry.get("ts", 0) > _get_ttl():
|
|
68
|
+
return None
|
|
69
|
+
return entry.get("tools", [])
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def set_cached_tools(server: str, tools: list[dict]):
|
|
73
|
+
"""Cache tools for a server."""
|
|
74
|
+
cache = _load_cache()
|
|
75
|
+
cache[server] = {"tools": tools, "ts": time.time()}
|
|
76
|
+
_save_cache(cache)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def clear_cache():
|
|
80
|
+
"""Clear all cached tools."""
|
|
81
|
+
try:
|
|
82
|
+
_CACHE_FILE.unlink()
|
|
83
|
+
except FileNotFoundError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def clear_server_cache(server: str):
|
|
88
|
+
"""Clear cached tools for a specific server."""
|
|
89
|
+
cache = _load_cache()
|
|
90
|
+
if server in cache:
|
|
91
|
+
del cache[server]
|
|
92
|
+
_save_cache(cache)
|
mcptoon/cli.py
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright 2025 cxh
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
mcptoon cli — Command-line entry point
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
mcptoon list List configured servers
|
|
21
|
+
mcptoon manifest List all tools (compact)
|
|
22
|
+
mcptoon manifest --full List all tools with params
|
|
23
|
+
mcptoon inspect <server> <tool> Show tool schema
|
|
24
|
+
mcptoon call <server> <tool> [ARGS] Call a tool
|
|
25
|
+
mcptoon init Create sample config
|
|
26
|
+
mcptoon add <name> [options] Add a server
|
|
27
|
+
mcptoon remove <name> Remove a server
|
|
28
|
+
mcptoon usage Show usage stats
|
|
29
|
+
|
|
30
|
+
Output flags (global):
|
|
31
|
+
--toon Token-efficient output (default for claude)
|
|
32
|
+
--json JSON output
|
|
33
|
+
--compact Names only
|
|
34
|
+
--raw Raw output
|
|
35
|
+
--head N Limit to N items
|
|
36
|
+
--max-chars N Truncate to N chars
|
|
37
|
+
--full No truncation
|
|
38
|
+
"""
|
|
39
|
+
import json
|
|
40
|
+
import sys
|
|
41
|
+
import os
|
|
42
|
+
|
|
43
|
+
from . import config as cfg
|
|
44
|
+
from . import manifest as manifest_mod
|
|
45
|
+
from . import output
|
|
46
|
+
from . import usage as usage_mod
|
|
47
|
+
from .router import call_tool
|
|
48
|
+
from .errors import is_error, get_error_message
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main():
|
|
52
|
+
args = sys.argv[1:]
|
|
53
|
+
|
|
54
|
+
if not args:
|
|
55
|
+
_print_help()
|
|
56
|
+
sys.exit(0)
|
|
57
|
+
|
|
58
|
+
# ─── Parse global output flags ───
|
|
59
|
+
fmt = "auto"
|
|
60
|
+
head_n = 0
|
|
61
|
+
max_chars = 0
|
|
62
|
+
full = False
|
|
63
|
+
cmd_args = []
|
|
64
|
+
|
|
65
|
+
i = 0
|
|
66
|
+
while i < len(args):
|
|
67
|
+
a = args[i]
|
|
68
|
+
if a == "--json":
|
|
69
|
+
fmt = "json"
|
|
70
|
+
elif a == "--compact":
|
|
71
|
+
fmt = "compact"
|
|
72
|
+
elif a == "--toon":
|
|
73
|
+
fmt = "toon"
|
|
74
|
+
elif a == "--raw":
|
|
75
|
+
fmt = "raw"
|
|
76
|
+
elif a == "--full":
|
|
77
|
+
full = True
|
|
78
|
+
elif a == "--head" and i + 1 < len(args):
|
|
79
|
+
try:
|
|
80
|
+
head_n = int(args[i + 1])
|
|
81
|
+
except ValueError:
|
|
82
|
+
pass
|
|
83
|
+
i += 1
|
|
84
|
+
elif a == "--max-chars" and i + 1 < len(args):
|
|
85
|
+
try:
|
|
86
|
+
max_chars = int(args[i + 1])
|
|
87
|
+
except ValueError:
|
|
88
|
+
pass
|
|
89
|
+
i += 1
|
|
90
|
+
elif a.startswith("--head="):
|
|
91
|
+
try:
|
|
92
|
+
head_n = int(a.split("=", 1)[1])
|
|
93
|
+
except ValueError:
|
|
94
|
+
pass
|
|
95
|
+
elif a.startswith("--max-chars="):
|
|
96
|
+
try:
|
|
97
|
+
max_chars = int(a.split("=", 1)[1])
|
|
98
|
+
except ValueError:
|
|
99
|
+
pass
|
|
100
|
+
else:
|
|
101
|
+
cmd_args.append(a)
|
|
102
|
+
i += 1
|
|
103
|
+
|
|
104
|
+
if not cmd_args:
|
|
105
|
+
_print_help()
|
|
106
|
+
sys.exit(0)
|
|
107
|
+
|
|
108
|
+
command = cmd_args[0]
|
|
109
|
+
rest = cmd_args[1:]
|
|
110
|
+
|
|
111
|
+
# ─── Dispatch ───
|
|
112
|
+
if command in ("list", "servers"):
|
|
113
|
+
_cmd_list(rest)
|
|
114
|
+
elif command in ("manifest", "tools"):
|
|
115
|
+
_cmd_manifest(rest, fmt, head_n, max_chars, full)
|
|
116
|
+
elif command == "inspect":
|
|
117
|
+
_cmd_inspect(rest, fmt, max_chars, full)
|
|
118
|
+
elif command == "call":
|
|
119
|
+
_cmd_call(rest, fmt, head_n, max_chars, full)
|
|
120
|
+
elif command == "init":
|
|
121
|
+
_cmd_init(rest)
|
|
122
|
+
elif command == "add":
|
|
123
|
+
_cmd_add(rest)
|
|
124
|
+
elif command == "remove":
|
|
125
|
+
_cmd_remove(rest)
|
|
126
|
+
elif command == "usage":
|
|
127
|
+
_cmd_usage(rest, fmt)
|
|
128
|
+
elif command in ("help", "-h", "--help"):
|
|
129
|
+
_print_help()
|
|
130
|
+
else:
|
|
131
|
+
# Try natural language: "mcptoon 有什么工具"
|
|
132
|
+
_try_natural(command, rest, fmt, head_n, max_chars, full)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ═══════════════════════════════════════════════════
|
|
136
|
+
# Commands
|
|
137
|
+
# ═══════════════════════════════════════════════════
|
|
138
|
+
|
|
139
|
+
def _cmd_list(_rest):
|
|
140
|
+
"""List configured servers."""
|
|
141
|
+
servers = cfg.list_servers()
|
|
142
|
+
if not servers:
|
|
143
|
+
print("No servers configured. Run: mcptoon init")
|
|
144
|
+
return
|
|
145
|
+
print(f"Configured servers ({len(servers)}):")
|
|
146
|
+
for name in servers:
|
|
147
|
+
s_cfg = cfg.get_server_config(name)
|
|
148
|
+
transport = s_cfg.get("transport", "stdio") if s_cfg else "?"
|
|
149
|
+
if transport == "stdio":
|
|
150
|
+
cmd_str = " ".join(
|
|
151
|
+
(s_cfg.get("command", []) if isinstance(s_cfg.get("command"), list) else [s_cfg.get("command", [])])
|
|
152
|
+
+ s_cfg.get("args", [])
|
|
153
|
+
)
|
|
154
|
+
print(f" {name:20s} [stdio] {cmd_str}")
|
|
155
|
+
else:
|
|
156
|
+
url = s_cfg.get("url", "?") if s_cfg else "?"
|
|
157
|
+
print(f" {name:20s} [http] {url}")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _cmd_manifest(rest, fmt, head_n, max_chars, full):
|
|
161
|
+
"""List all tools."""
|
|
162
|
+
full_mode = "--full" in rest or full
|
|
163
|
+
|
|
164
|
+
manifest = manifest_mod.get_manifest(use_cache=True)
|
|
165
|
+
if not manifest:
|
|
166
|
+
print("No tools found. Run: mcptoon init")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
if fmt in ("toon", "compact"):
|
|
170
|
+
# For toon/compact, output just the tool names
|
|
171
|
+
result = {}
|
|
172
|
+
for server, tools in manifest.items():
|
|
173
|
+
names = [t.get("name", "?") for t in tools if "error" not in t]
|
|
174
|
+
if names:
|
|
175
|
+
result[server] = names
|
|
176
|
+
print(output.render(result, fmt=fmt, head_n=head_n, max_chars=max_chars, full=full))
|
|
177
|
+
else:
|
|
178
|
+
# Human-readable or JSON
|
|
179
|
+
if fmt == "json":
|
|
180
|
+
print(output.render(manifest, fmt="json", head_n=head_n, max_chars=max_chars, full=full))
|
|
181
|
+
else:
|
|
182
|
+
text = manifest_mod.format_manifest(manifest, full=full_mode)
|
|
183
|
+
if max_chars > 0:
|
|
184
|
+
text = output._truncate(text, max_chars)
|
|
185
|
+
print(text)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _cmd_inspect(rest, fmt, max_chars, full):
|
|
189
|
+
"""Show tool schema."""
|
|
190
|
+
if len(rest) < 2:
|
|
191
|
+
print("Usage: mcptoon inspect <server> <tool>")
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
|
|
194
|
+
server = rest[0]
|
|
195
|
+
tool = rest[1]
|
|
196
|
+
|
|
197
|
+
info = manifest_mod.inspect_tool(server, tool)
|
|
198
|
+
if not info:
|
|
199
|
+
print(f"Tool not found: {server}:{tool}")
|
|
200
|
+
sys.exit(1)
|
|
201
|
+
|
|
202
|
+
print(output.render(info, fmt=fmt if fmt != "auto" else "json", max_chars=max_chars, full=full))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _cmd_call(rest, fmt, head_n, max_chars, full):
|
|
206
|
+
"""Call a tool."""
|
|
207
|
+
if len(rest) < 2:
|
|
208
|
+
print("Usage: mcptoon call <server> <tool> [JSON_ARGS] [--destructive]")
|
|
209
|
+
print("")
|
|
210
|
+
print("Examples:")
|
|
211
|
+
print(' mcptoon call fetch fetch \'{"url":"https://example.com"}\' --toon')
|
|
212
|
+
print(' mcptoon call exa search \'{"query":"AI"}\' --json')
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
server = rest[0]
|
|
216
|
+
tool = rest[1]
|
|
217
|
+
is_destructive = "--destructive" in rest
|
|
218
|
+
|
|
219
|
+
# Parse args (JSON string or key=value pairs)
|
|
220
|
+
args = {}
|
|
221
|
+
for item in rest[2:]:
|
|
222
|
+
if item == "--destructive":
|
|
223
|
+
continue
|
|
224
|
+
# Try JSON
|
|
225
|
+
if item.startswith("{"):
|
|
226
|
+
try:
|
|
227
|
+
args = json.loads(item)
|
|
228
|
+
break
|
|
229
|
+
except json.JSONDecodeError as e:
|
|
230
|
+
print(f"Error parsing JSON args: {e}")
|
|
231
|
+
sys.exit(1)
|
|
232
|
+
# key=value
|
|
233
|
+
if "=" in item:
|
|
234
|
+
k, v = item.split("=", 1)
|
|
235
|
+
# Try to parse value as JSON
|
|
236
|
+
try:
|
|
237
|
+
v = json.loads(v)
|
|
238
|
+
except json.JSONDecodeError:
|
|
239
|
+
pass
|
|
240
|
+
args[k] = v
|
|
241
|
+
|
|
242
|
+
result = call_tool(server, tool, args, is_destructive=is_destructive)
|
|
243
|
+
|
|
244
|
+
if is_error(result):
|
|
245
|
+
err = result["_error"]
|
|
246
|
+
print(f"Error [{err['code']}]: {err['message']}", file=sys.stderr)
|
|
247
|
+
if err.get("retry"):
|
|
248
|
+
print(" (retryable)", file=sys.stderr)
|
|
249
|
+
sys.exit(1)
|
|
250
|
+
|
|
251
|
+
print(output.render(result, fmt=fmt, head_n=head_n, max_chars=max_chars, full=full))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _cmd_init(_rest):
|
|
255
|
+
"""Create sample config."""
|
|
256
|
+
if cfg.init_sample_config():
|
|
257
|
+
print(f"Sample config created: {cfg.CONFIG_FILE}")
|
|
258
|
+
print("Edit it to add your MCP servers, then run: mcptoon manifest")
|
|
259
|
+
else:
|
|
260
|
+
print(f"Config already exists: {cfg.CONFIG_FILE}")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _cmd_add(rest):
|
|
264
|
+
"""Add a server to config."""
|
|
265
|
+
if not rest:
|
|
266
|
+
print("Usage: mcptoon add <name> --stdio <command> [args...]")
|
|
267
|
+
print(" mcptoon add <name> --http <url> [--header 'Key: Value']")
|
|
268
|
+
sys.exit(1)
|
|
269
|
+
|
|
270
|
+
name = rest[0]
|
|
271
|
+
flags = rest[1:]
|
|
272
|
+
|
|
273
|
+
if "--stdio" in flags:
|
|
274
|
+
idx = flags.index("--stdio")
|
|
275
|
+
cmd_parts = flags[idx + 1:]
|
|
276
|
+
if not cmd_parts:
|
|
277
|
+
print("Error: --stdio requires a command")
|
|
278
|
+
sys.exit(1)
|
|
279
|
+
# First part is command, rest are args
|
|
280
|
+
server_cfg = {
|
|
281
|
+
"transport": "stdio",
|
|
282
|
+
"command": cmd_parts[0:1] if isinstance(cmd_parts[0], str) else cmd_parts,
|
|
283
|
+
"args": cmd_parts[1:] if len(cmd_parts) > 1 else [],
|
|
284
|
+
}
|
|
285
|
+
# Actually, command should be a list for npx-style
|
|
286
|
+
# Support: mcptoon add fetch --stdio npx -y @mcp/server-fetch
|
|
287
|
+
server_cfg = {
|
|
288
|
+
"transport": "stdio",
|
|
289
|
+
"command": [cmd_parts[0]] + (cmd_parts[1:2] if len(cmd_parts) > 1 else []),
|
|
290
|
+
"args": cmd_parts[2:] if len(cmd_parts) > 2 else [],
|
|
291
|
+
}
|
|
292
|
+
cfg.add_server(name, server_cfg)
|
|
293
|
+
print(f"Added server '{name}' [stdio]: {' '.join(cmd_parts)}")
|
|
294
|
+
|
|
295
|
+
elif "--http" in flags:
|
|
296
|
+
idx = flags.index("--http")
|
|
297
|
+
if idx + 1 >= len(flags):
|
|
298
|
+
print("Error: --http requires a URL")
|
|
299
|
+
sys.exit(1)
|
|
300
|
+
url = flags[idx + 1]
|
|
301
|
+
server_cfg = {"transport": "http", "url": url}
|
|
302
|
+
|
|
303
|
+
# Parse headers
|
|
304
|
+
headers = {}
|
|
305
|
+
for j, f in enumerate(flags):
|
|
306
|
+
if f == "--header" and j + 1 < len(flags):
|
|
307
|
+
h = flags[j + 1]
|
|
308
|
+
if ":" in h:
|
|
309
|
+
k, v = h.split(":", 1)
|
|
310
|
+
headers[k.strip()] = v.strip()
|
|
311
|
+
if headers:
|
|
312
|
+
server_cfg["headers"] = headers
|
|
313
|
+
|
|
314
|
+
cfg.add_server(name, server_cfg)
|
|
315
|
+
print(f"Added server '{name}' [http]: {url}")
|
|
316
|
+
|
|
317
|
+
else:
|
|
318
|
+
print("Error: must specify --stdio or --http")
|
|
319
|
+
sys.exit(1)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _cmd_remove(rest):
|
|
323
|
+
"""Remove a server."""
|
|
324
|
+
if not rest:
|
|
325
|
+
print("Usage: mcptoon remove <name>")
|
|
326
|
+
sys.exit(1)
|
|
327
|
+
name = rest[0]
|
|
328
|
+
if cfg.remove_server(name):
|
|
329
|
+
print(f"Removed server: {name}")
|
|
330
|
+
else:
|
|
331
|
+
print(f"Server not found: {name}")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _cmd_usage(_rest, fmt):
|
|
335
|
+
"""Show usage stats."""
|
|
336
|
+
stats = usage_mod.get_usage_stats()
|
|
337
|
+
if fmt in ("toon", "compact"):
|
|
338
|
+
print(output.render(stats, fmt=fmt))
|
|
339
|
+
else:
|
|
340
|
+
print(f"Total calls: {stats['total_calls']}")
|
|
341
|
+
print(f"Success rate: {stats['success_rate']}")
|
|
342
|
+
print(f"Tokens (est): {stats['total_tokens_est']}")
|
|
343
|
+
if stats["by_server"]:
|
|
344
|
+
print("\nBy server:")
|
|
345
|
+
for s, c in stats["by_server"].items():
|
|
346
|
+
print(f" {s:20s} {c}")
|
|
347
|
+
if stats["top_tools"]:
|
|
348
|
+
print("\nTop tools:")
|
|
349
|
+
for t, c in stats["top_tools"].items():
|
|
350
|
+
print(f" {t:30s} {c}")
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# ═══════════════════════════════════════════════════
|
|
354
|
+
# Natural language fallback
|
|
355
|
+
# ═══════════════════════════════════════════════════
|
|
356
|
+
|
|
357
|
+
def _try_natural(command, rest, fmt, head_n, max_chars, full):
|
|
358
|
+
"""Try to interpret natural language input."""
|
|
359
|
+
text = " ".join([command] + rest).lower()
|
|
360
|
+
|
|
361
|
+
if any(kw in text for kw in ["什么", "有哪些", "工具", "tools", "list", "manifest"]):
|
|
362
|
+
_cmd_manifest(rest, fmt, head_n, max_chars, full)
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
if any(kw in text for kw in ["服务器", "server", "list server"]):
|
|
366
|
+
_cmd_list(rest)
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
print(f"Unknown command: {command}")
|
|
370
|
+
print("Run: mcptoon help")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ═══════════════════════════════════════════════════
|
|
374
|
+
# Help
|
|
375
|
+
# ═══════════════════════════════════════════════════
|
|
376
|
+
|
|
377
|
+
def _print_help():
|
|
378
|
+
print("""mcptoon — Token-efficient MCP CLI client
|
|
379
|
+
|
|
380
|
+
Usage:
|
|
381
|
+
mcptoon list List configured servers
|
|
382
|
+
mcptoon manifest List all tools (compact)
|
|
383
|
+
mcptoon manifest --full List all tools with params
|
|
384
|
+
mcptoon inspect <server> <tool> Show tool schema
|
|
385
|
+
mcptoon call <server> <tool> [ARGS] Call a tool
|
|
386
|
+
mcptoon init Create sample config
|
|
387
|
+
mcptoon add <name> [options] Add a server
|
|
388
|
+
mcptoon remove <name> Remove a server
|
|
389
|
+
mcptoon usage Show usage stats
|
|
390
|
+
|
|
391
|
+
Output flags:
|
|
392
|
+
--toon Token-efficient output (saves 40-60% tokens)
|
|
393
|
+
--json JSON output
|
|
394
|
+
--compact Names only
|
|
395
|
+
--head N Limit to N items
|
|
396
|
+
--max-chars N Truncate to N chars
|
|
397
|
+
--full No truncation
|
|
398
|
+
|
|
399
|
+
Examples:
|
|
400
|
+
mcptoon init
|
|
401
|
+
mcptoon manifest --toon
|
|
402
|
+
mcptoon call fetch fetch '{"url":"https://example.com"}' --toon
|
|
403
|
+
mcptoon add myserver --stdio npx -y @mcp/server-fetch
|
|
404
|
+
|
|
405
|
+
Environment:
|
|
406
|
+
MCPTOON_AGENT_TYPE=claude Auto-select --toon
|
|
407
|
+
MCPTOON_AGENT_TYPE=openai Auto-select --json
|
|
408
|
+
|
|
409
|
+
Config: ~/.mcptoon/config.json
|
|
410
|
+
""")
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if __name__ == "__main__":
|
|
414
|
+
main()
|