@ai-zen/agents-sdk 1.0.0-alpha.22 → 1.0.0-alpha.23
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.
- package/CHANGELOG.md +216 -184
- package/README.md +13 -0
- package/README.zh.md +13 -0
- package/dist/agent-plugins/AutoMigratePlugin.js +1 -1
- package/dist/agent-plugins/UnknownToolHintPlugin.js +4 -4
- package/dist/config/constants.js +24 -24
- package/dist/config/factory-skills.js +56 -56
- package/dist/runtime/HandoffGuardPlugin.js +2 -2
- package/dist/runtime/HandoffService.js +22 -22
- package/dist/runtime/createAgent.js +2 -2
- package/dist/runtime/createModel.js +3 -3
- package/dist/scope/PermissionEvaluator.js +1 -1
- package/dist/scope/Scope.js +1 -1
- package/dist/scope/plugins/builtin/BatchEditTool.js +8 -8
- package/dist/scope/plugins/builtin/CopyTool.js +4 -4
- package/dist/scope/plugins/builtin/CwdTool.js +1 -1
- package/dist/scope/plugins/builtin/DownloadFileTool.js +8 -8
- package/dist/scope/plugins/builtin/EditTool.js +9 -9
- package/dist/scope/plugins/builtin/ExecTool.js +8 -8
- package/dist/scope/plugins/builtin/ExistTool.js +2 -2
- package/dist/scope/plugins/builtin/FindTextTool.js +10 -10
- package/dist/scope/plugins/builtin/GenerateImageTool.js +13 -13
- package/dist/scope/plugins/builtin/GlobTool.js +4 -4
- package/dist/scope/plugins/builtin/InspectFileTool.js +7 -7
- package/dist/scope/plugins/builtin/LsTool.js +5 -5
- package/dist/scope/plugins/builtin/MkdirTool.js +3 -3
- package/dist/scope/plugins/builtin/ReadTool.js +8 -8
- package/dist/scope/plugins/builtin/RenameTool.js +3 -3
- package/dist/scope/plugins/builtin/RmTool.js +3 -3
- package/dist/scope/plugins/builtin/SleepTool.js +6 -6
- package/dist/scope/plugins/builtin/SpawnTool.js +9 -9
- package/dist/scope/plugins/builtin/ViewImageTool.js +12 -12
- package/dist/scope/plugins/builtin/WriteTool.js +3 -3
- package/dist/scope/plugins/mcp/McpConnectionManager.js +7 -7
- package/dist/scope/plugins/mcp/discover.js +1 -1
- package/dist/scope/plugins/mcp/mcpTools.js +29 -29
- package/dist/scope/plugins/skills/discover.js +7 -7
- package/dist/scope/plugins/skills/skillTools.js +17 -17
- package/dist/scope/plugins/subagents/subAgentTools.js +2 -2
- package/dist/scope/plugins/usertools/discover.js +2 -2
- package/dist/shared/content-blocks.js +9 -9
- package/dist/shared/errors.js +3 -3
- package/package.json +10 -1
|
@@ -3,17 +3,17 @@ import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
|
3
3
|
export class RenameTool extends SdkCallbackTool {
|
|
4
4
|
function = {
|
|
5
5
|
name: "rename",
|
|
6
|
-
description: "
|
|
6
|
+
description: "Rename or move a file/directory (equivalent to the mv command)",
|
|
7
7
|
parameters: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
10
10
|
oldPath: {
|
|
11
11
|
type: "string",
|
|
12
|
-
description: "
|
|
12
|
+
description: "Original path",
|
|
13
13
|
},
|
|
14
14
|
newPath: {
|
|
15
15
|
type: "string",
|
|
16
|
-
description: "
|
|
16
|
+
description: "New path",
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
19
|
required: ["oldPath", "newPath"],
|
|
@@ -3,17 +3,17 @@ import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
|
3
3
|
export class RmTool extends SdkCallbackTool {
|
|
4
4
|
function = {
|
|
5
5
|
name: "rm",
|
|
6
|
-
description: "
|
|
6
|
+
description: "Delete a file or directory",
|
|
7
7
|
parameters: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
10
10
|
path: {
|
|
11
11
|
type: "string",
|
|
12
|
-
description: "
|
|
12
|
+
description: "File or directory path",
|
|
13
13
|
},
|
|
14
14
|
recursive: {
|
|
15
15
|
type: "boolean",
|
|
16
|
-
description: "
|
|
16
|
+
description: "Whether to delete subdirectories recursively",
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
19
|
required: ["path"],
|
|
@@ -2,13 +2,13 @@ import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
|
2
2
|
export class SleepTool extends SdkCallbackTool {
|
|
3
3
|
function = {
|
|
4
4
|
name: "sleep",
|
|
5
|
-
description: "
|
|
5
|
+
description: "Wait for the specified time in milliseconds. Use it when a delay is needed, for example to wait for another operation to finish or to space out actions. Note: long waits slow down responses; keep it at or below 30000 milliseconds (30 seconds).",
|
|
6
6
|
parameters: {
|
|
7
7
|
type: "object",
|
|
8
8
|
properties: {
|
|
9
9
|
ms: {
|
|
10
10
|
type: "number",
|
|
11
|
-
description: "
|
|
11
|
+
description: "Milliseconds to wait (1 second = 1000 milliseconds). Values above 30000 (30 seconds) are not recommended.",
|
|
12
12
|
},
|
|
13
13
|
},
|
|
14
14
|
required: ["ms"],
|
|
@@ -23,14 +23,14 @@ export class SleepTool extends SdkCallbackTool {
|
|
|
23
23
|
if (typeof ms !== "number" || !Number.isFinite(ms) || ms < 0) {
|
|
24
24
|
return JSON.stringify({
|
|
25
25
|
success: false,
|
|
26
|
-
error: "ms
|
|
26
|
+
error: "ms must be a positive integer",
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
if (ms > 300_000) {
|
|
30
30
|
// 超过 5 分钟给出警告
|
|
31
31
|
return JSON.stringify({
|
|
32
32
|
success: false,
|
|
33
|
-
error:
|
|
33
|
+
error: `Wait time ${ms}ms is too long, keep it at or below 300000ms (5 minutes). For longer waits, split them into shorter steps`,
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
// 若 signal 已处于 aborted,直接返回中断结果,避免不必要的等待
|
|
@@ -53,14 +53,14 @@ export class SleepTool extends SdkCallbackTool {
|
|
|
53
53
|
return JSON.stringify({
|
|
54
54
|
success: false,
|
|
55
55
|
aborted: true,
|
|
56
|
-
error: "
|
|
56
|
+
error: "Wait aborted",
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
return JSON.stringify({
|
|
60
60
|
success: true,
|
|
61
61
|
waitedMs: ms,
|
|
62
62
|
waitedSeconds: (ms / 1000).toFixed(1),
|
|
63
|
-
message:
|
|
63
|
+
message: `Waited ${ms}ms (${(ms / 1000).toFixed(1)} seconds)`,
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -3,21 +3,21 @@ import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
|
3
3
|
export class SpawnTool extends SdkCallbackTool {
|
|
4
4
|
function = {
|
|
5
5
|
name: "spawn",
|
|
6
|
-
description: "
|
|
6
|
+
description: "Spawn a command asynchronously and return immediately without waiting for the result. Use it for long-running processes (servers, watchers, GUI apps, etc.) or commands whose result you do not need to wait for. Note: this tool does not capture the command's output itself; to retain or inspect output, use shell redirection to a file inside the command (e.g. `> /path/file`; `>>` append and `|` pipes are supported). The command is parsed and executed by the shell.",
|
|
7
7
|
parameters: {
|
|
8
8
|
type: "object",
|
|
9
9
|
properties: {
|
|
10
10
|
command: {
|
|
11
11
|
type: "string",
|
|
12
|
-
description: "
|
|
12
|
+
description: "Command to execute (including arguments)",
|
|
13
13
|
},
|
|
14
14
|
cwd: {
|
|
15
15
|
type: "string",
|
|
16
|
-
description: "
|
|
16
|
+
description: "Working directory (optional, defaults to the current working directory)",
|
|
17
17
|
},
|
|
18
18
|
detached: {
|
|
19
19
|
type: "boolean",
|
|
20
|
-
description: "
|
|
20
|
+
description: "Whether to fully detach from the current process group (optional, defaults to false). When true, the spawned program keeps running even after the current Agent process exits. Use it for daemons or GUI programs that must run independently.",
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
23
|
required: ["command"],
|
|
@@ -48,7 +48,7 @@ export class SpawnTool extends SdkCallbackTool {
|
|
|
48
48
|
resolve(JSON.stringify({
|
|
49
49
|
success: false,
|
|
50
50
|
pid: null,
|
|
51
|
-
message:
|
|
51
|
+
message: `Failed to start: ${err.message}`,
|
|
52
52
|
}));
|
|
53
53
|
});
|
|
54
54
|
child.on("spawn", () => {
|
|
@@ -58,7 +58,7 @@ export class SpawnTool extends SdkCallbackTool {
|
|
|
58
58
|
resolve(JSON.stringify({
|
|
59
59
|
success: true,
|
|
60
60
|
pid: child.pid ?? null,
|
|
61
|
-
message:
|
|
61
|
+
message: `Process started (PID: ${child.pid ?? "unknown"})`,
|
|
62
62
|
}));
|
|
63
63
|
});
|
|
64
64
|
child.on("exit", (code) => {
|
|
@@ -70,8 +70,8 @@ export class SpawnTool extends SdkCallbackTool {
|
|
|
70
70
|
pid: child.pid ?? null,
|
|
71
71
|
exitCode: code,
|
|
72
72
|
message: code === null
|
|
73
|
-
?
|
|
74
|
-
:
|
|
73
|
+
? `Process exited (PID: ${child.pid ?? "unknown"})`
|
|
74
|
+
: `Process exited (PID: ${child.pid ?? "unknown"}, exit code: ${code})`,
|
|
75
75
|
}));
|
|
76
76
|
});
|
|
77
77
|
const timeout = setTimeout(() => {
|
|
@@ -81,7 +81,7 @@ export class SpawnTool extends SdkCallbackTool {
|
|
|
81
81
|
resolve(JSON.stringify({
|
|
82
82
|
success: true,
|
|
83
83
|
pid: child.pid ?? null,
|
|
84
|
-
message:
|
|
84
|
+
message: `Process started (PID: ${child.pid ?? "unknown"})`,
|
|
85
85
|
}));
|
|
86
86
|
}, 500);
|
|
87
87
|
const cleanup = () => {
|
|
@@ -35,13 +35,13 @@ export class ViewImageTool extends SdkCallbackTool {
|
|
|
35
35
|
}
|
|
36
36
|
function = {
|
|
37
37
|
name: "view_image",
|
|
38
|
-
description: "
|
|
38
|
+
description: "View an image and analyze its content. Pass a local image path or an http(s) image URL. Local images are uploaded when the endpoint supports the Files API and are otherwise referenced as inline base64; remote images are referenced directly. Local URLs are not supported (such as the file:// protocol, or http(s) addresses pointing to localhost/127.0.0.1 on this machine); use a file path for local images. Suitable for reading text in images, describing image content and analyzing charts.",
|
|
39
39
|
parameters: {
|
|
40
40
|
type: "object",
|
|
41
41
|
properties: {
|
|
42
42
|
path_or_url: {
|
|
43
43
|
type: "string",
|
|
44
|
-
description: "
|
|
44
|
+
description: "Local image path (relative or absolute) or an http(s) image URL. Supported formats: JPEG, PNG, GIF, WebP. Local URLs are not supported (such as the file:// protocol, or http(s) addresses pointing to localhost/127.0.0.1 on this machine); use a file path for local images.",
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
47
|
required: ["path_or_url"],
|
|
@@ -55,12 +55,12 @@ export class ViewImageTool extends SdkCallbackTool {
|
|
|
55
55
|
const signal = ctx?.signal;
|
|
56
56
|
const target = input?.path_or_url;
|
|
57
57
|
if (!target || !target.trim()) {
|
|
58
|
-
return "view_image
|
|
58
|
+
return "view_image requires the path_or_url parameter (a local image path or an http(s) URL)";
|
|
59
59
|
}
|
|
60
60
|
const value = target.trim();
|
|
61
61
|
// 拒绝服务商无法访问的"本地 URL":file:// 等本地协议,或指向本机回环地址的 http(s) 地址
|
|
62
62
|
if (isLocalUrl(value)) {
|
|
63
|
-
return "
|
|
63
|
+
return "Local URLs are not supported (the API provider cannot download them). Use a file path for local images, not a local link";
|
|
64
64
|
}
|
|
65
65
|
// 按当前模型定位端点:决定走 Files API 上传还是 base64 内联
|
|
66
66
|
const { model, endpoint } = resolveModelAndEndpoint(this.env.config, ctx?.agent?.model);
|
|
@@ -74,11 +74,11 @@ export class ViewImageTool extends SdkCallbackTool {
|
|
|
74
74
|
await fs.stat(abs);
|
|
75
75
|
}
|
|
76
76
|
catch {
|
|
77
|
-
return
|
|
77
|
+
return `Local image not found: ${abs}`;
|
|
78
78
|
}
|
|
79
79
|
const mime = mimeOf(abs);
|
|
80
80
|
if (!mime || !mime.startsWith("image/")) {
|
|
81
|
-
return
|
|
81
|
+
return `Unsupported image format "${path.extname(abs) || "(no extension)"}". Supported formats: JPEG, PNG, GIF, WebP`;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
const result = await fileToContentSection({
|
|
@@ -87,21 +87,21 @@ export class ViewImageTool extends SdkCallbackTool {
|
|
|
87
87
|
model,
|
|
88
88
|
signal,
|
|
89
89
|
});
|
|
90
|
-
const label = isUrl ?
|
|
90
|
+
const label = isUrl ? `Fetched remote image: ${value}` : `Fetched local image (${path.basename(value)})`;
|
|
91
91
|
const how = result.transport === "files-api"
|
|
92
|
-
?
|
|
92
|
+
? `, uploaded to the Files API (file_id: ${result.section.file_id ?? "?"})`
|
|
93
93
|
: result.transport === "inline"
|
|
94
|
-
?
|
|
94
|
+
? `, inlined as base64 (${(result.bytes / 1024).toFixed(0)} KB)`
|
|
95
95
|
: "";
|
|
96
|
-
const extra = result.warning ?
|
|
96
|
+
const extra = result.warning ? ` (fallback: ${result.warning})` : "";
|
|
97
97
|
return [{ type: "text", text: `${label}${how}${extra}` }, result.section];
|
|
98
98
|
}
|
|
99
99
|
catch (error) {
|
|
100
100
|
// 中断(abort)时返回明确的中断结果
|
|
101
101
|
if (signal?.aborted) {
|
|
102
|
-
return "
|
|
102
|
+
return "Image view aborted";
|
|
103
103
|
}
|
|
104
|
-
return
|
|
104
|
+
return `Failed to view image: ${error instanceof Error ? error.message : String(error)}`;
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -4,17 +4,17 @@ import { SdkCallbackTool } from "./SdkCallbackTool.js";
|
|
|
4
4
|
export class WriteTool extends SdkCallbackTool {
|
|
5
5
|
function = {
|
|
6
6
|
name: "write",
|
|
7
|
-
description: "
|
|
7
|
+
description: "Write a file",
|
|
8
8
|
parameters: {
|
|
9
9
|
type: "object",
|
|
10
10
|
properties: {
|
|
11
11
|
path: {
|
|
12
12
|
type: "string",
|
|
13
|
-
description: "
|
|
13
|
+
description: "File path",
|
|
14
14
|
},
|
|
15
15
|
content: {
|
|
16
16
|
type: "string",
|
|
17
|
-
description: "
|
|
17
|
+
description: "File content",
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
required: ["path", "content"],
|
|
@@ -141,7 +141,7 @@ export class McpConnectionManager {
|
|
|
141
141
|
}));
|
|
142
142
|
}
|
|
143
143
|
catch (err) {
|
|
144
|
-
getLogger().error(`[McpConnectionManager]
|
|
144
|
+
getLogger().error(`[McpConnectionManager] Failed to list tools for "${name}": ${err?.message ?? err}`);
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
if (serverCaps?.resources) {
|
|
@@ -155,7 +155,7 @@ export class McpConnectionManager {
|
|
|
155
155
|
}));
|
|
156
156
|
}
|
|
157
157
|
catch (err) {
|
|
158
|
-
getLogger().error(`[McpConnectionManager]
|
|
158
|
+
getLogger().error(`[McpConnectionManager] Failed to list resources for "${name}": ${err?.message ?? err}`);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
if (serverCaps?.prompts) {
|
|
@@ -167,7 +167,7 @@ export class McpConnectionManager {
|
|
|
167
167
|
}));
|
|
168
168
|
}
|
|
169
169
|
catch (err) {
|
|
170
|
-
getLogger().error(`[McpConnectionManager]
|
|
170
|
+
getLogger().error(`[McpConnectionManager] Failed to list prompts for "${name}": ${err?.message ?? err}`);
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
const entry = this.servers.get(name) ?? { state: "connecting" };
|
|
@@ -190,7 +190,7 @@ export class McpConnectionManager {
|
|
|
190
190
|
}
|
|
191
191
|
if (config.transport === "stdio") {
|
|
192
192
|
if (!config.command) {
|
|
193
|
-
throw new Error(`MCP stdio server
|
|
193
|
+
throw new Error(`MCP stdio server is missing command`);
|
|
194
194
|
}
|
|
195
195
|
return new StdioClientTransport({
|
|
196
196
|
command: config.command,
|
|
@@ -201,7 +201,7 @@ export class McpConnectionManager {
|
|
|
201
201
|
// http 使用 Streamable HTTP 传输(POST + 可选 SSE 流)
|
|
202
202
|
if (config.transport === "http") {
|
|
203
203
|
if (!config.url) {
|
|
204
|
-
throw new Error(`MCP http server
|
|
204
|
+
throw new Error(`MCP http server is missing url`);
|
|
205
205
|
}
|
|
206
206
|
return new StreamableHTTPClientTransport(new URL(config.url), {
|
|
207
207
|
requestInit: config.headers ? { headers: config.headers } : undefined,
|
|
@@ -210,13 +210,13 @@ export class McpConnectionManager {
|
|
|
210
210
|
// sse 使用旧版 HTTP+SSE 传输(GET 建立 SSE 流,POST 发送消息)
|
|
211
211
|
if (config.transport === "sse") {
|
|
212
212
|
if (!config.url) {
|
|
213
|
-
throw new Error(`MCP sse server
|
|
213
|
+
throw new Error(`MCP sse server is missing url`);
|
|
214
214
|
}
|
|
215
215
|
return new SSEClientTransport(new URL(config.url), {
|
|
216
216
|
requestInit: config.headers ? { headers: config.headers } : undefined,
|
|
217
217
|
});
|
|
218
218
|
}
|
|
219
|
-
throw new Error(
|
|
219
|
+
throw new Error(`Unsupported MCP transport type: ${config.transport}`);
|
|
220
220
|
}
|
|
221
221
|
// ---- 断开 ----
|
|
222
222
|
async disconnect(name) {
|
|
@@ -82,7 +82,7 @@ export async function discoverMcpServers(paths) {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
catch (err) {
|
|
85
|
-
getLogger().warn(`[sdk]
|
|
85
|
+
getLogger().warn(`[sdk] Failed to parse MCP config: ${path} — ${err?.message ?? err}`);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
return items;
|
|
@@ -3,7 +3,7 @@ import { fileToContentSection, resolveModelAndEndpoint } from "../../../shared/c
|
|
|
3
3
|
import { createDisclosureParam } from "../../disclosure.js";
|
|
4
4
|
import { DEFAULT_MAX_TOOL_OUTPUT, guardOutput, headTailPreview } from "../builtin/outputGuard.js";
|
|
5
5
|
import { getLogger } from "../../../shared/logger.js";
|
|
6
|
-
const EMPTY_HINT = "
|
|
6
|
+
const EMPTY_HINT = "(No MCP servers available, ask the user to add one)";
|
|
7
7
|
/**
|
|
8
8
|
* 创建 load_mcp 工具。
|
|
9
9
|
* server 枚举由 filteredMcps (完整 McpServerConfig[])自动推导,
|
|
@@ -16,16 +16,16 @@ export function createLoadMcpTool(mcpManager, filteredMcps) {
|
|
|
16
16
|
configMap.set(s.id, s);
|
|
17
17
|
}
|
|
18
18
|
const ids = filteredMcps.map((s) => s.id);
|
|
19
|
-
const param = createDisclosureParam(ids, "
|
|
19
|
+
const param = createDisclosureParam(ids, "MCP server name", EMPTY_HINT);
|
|
20
20
|
// 拼接各 MCP 服务器的描述供 LLM 参考(对齐 load_skill 的处理方式)
|
|
21
21
|
const mcpDescriptions = filteredMcps
|
|
22
|
-
.map((s) => ` - ${s.id}: ${s.description || "
|
|
22
|
+
.map((s) => ` - ${s.id}: ${s.description || "No description"}`)
|
|
23
23
|
.join("\n");
|
|
24
|
-
const serverIdDescription = `${param.description}\n\
|
|
24
|
+
const serverIdDescription = `${param.description}\n\nAvailable MCP servers:\n${mcpDescriptions || " No MCP servers available"}`;
|
|
25
25
|
return new CallbackTool({
|
|
26
26
|
function: {
|
|
27
27
|
name: "load_mcp",
|
|
28
|
-
description: "
|
|
28
|
+
description: "Connect to the specified MCP server and get the list of tools and resources it exposes. Pass include_manifest=false to only establish the connection without returning the manifest (useful when the manifest is already in context).",
|
|
29
29
|
parameters: {
|
|
30
30
|
type: "object",
|
|
31
31
|
properties: {
|
|
@@ -36,7 +36,7 @@ export function createLoadMcpTool(mcpManager, filteredMcps) {
|
|
|
36
36
|
},
|
|
37
37
|
include_manifest: {
|
|
38
38
|
type: "boolean",
|
|
39
|
-
description: "
|
|
39
|
+
description: "Whether to return the server's full capability manifest (tools and resources). Optional, defaults to true. If the manifest is already in your context (for example it was loaded earlier in this session but the connection was dropped after a process restart), set it to false to only establish the connection and return a summary, saving context tokens",
|
|
40
40
|
default: true,
|
|
41
41
|
},
|
|
42
42
|
},
|
|
@@ -48,7 +48,7 @@ export function createLoadMcpTool(mcpManager, filteredMcps) {
|
|
|
48
48
|
const serverName = input.server;
|
|
49
49
|
const config = configMap.get(serverName);
|
|
50
50
|
if (!config) {
|
|
51
|
-
return `❌ MCP
|
|
51
|
+
return `❌ MCP server "${serverName}" is not configured`;
|
|
52
52
|
}
|
|
53
53
|
// 披露开关:默认返回完整清单;显式传 false 时仅建立连接、只返回摘要
|
|
54
54
|
const includeManifest = input.include_manifest !== false;
|
|
@@ -64,12 +64,12 @@ export function createLoadMcpTool(mcpManager, filteredMcps) {
|
|
|
64
64
|
manifest = await mcpManager.connect(serverName, config);
|
|
65
65
|
}
|
|
66
66
|
catch (error) {
|
|
67
|
-
return
|
|
67
|
+
return `Failed to connect to "${serverName}": ${error?.message ?? error}`;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
// 静默模式:只回吐摘要(清单仍留在管理器内,供 call_mcp_tool 使用)
|
|
71
71
|
if (!includeManifest) {
|
|
72
|
-
getLogger().info(`[load_mcp] ${serverName}:
|
|
72
|
+
getLogger().info(`[load_mcp] ${serverName}: connected (manifest not disclosed: ${manifest.tools.length} tools, ${manifest.resources.length} resources)`);
|
|
73
73
|
return JSON.stringify({
|
|
74
74
|
server: serverName,
|
|
75
75
|
connected: true,
|
|
@@ -117,7 +117,7 @@ async function mediaSection(data, mime, media) {
|
|
|
117
117
|
catch (error) {
|
|
118
118
|
return {
|
|
119
119
|
type: "text",
|
|
120
|
-
text: `[
|
|
120
|
+
text: `[media] Failed to convert (${mime}): ${error?.message ?? error}`,
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -161,11 +161,11 @@ async function toMessageSection(block, media) {
|
|
|
161
161
|
const mime = r.mimeType ?? "application/octet-stream";
|
|
162
162
|
return await mediaSection(r.blob, mime, media);
|
|
163
163
|
}
|
|
164
|
-
return { type: "text", text: `[
|
|
164
|
+
return { type: "text", text: `[resource] ${r.uri ?? "(unknown URI)"}` };
|
|
165
165
|
}
|
|
166
166
|
case "resource_link": {
|
|
167
167
|
const parts = [block.name, block.uri, block.mimeType].filter(Boolean);
|
|
168
|
-
return { type: "text", text: `[
|
|
168
|
+
return { type: "text", text: `[resource_link] ${parts.join(" · ")}` };
|
|
169
169
|
}
|
|
170
170
|
case "text":
|
|
171
171
|
return { type: "text", text: block.text ?? "" };
|
|
@@ -200,8 +200,8 @@ async function protectCallOutput(output, env) {
|
|
|
200
200
|
return JSON.stringify({
|
|
201
201
|
outputTooLarge: true,
|
|
202
202
|
isError: isError || undefined,
|
|
203
|
-
warning:
|
|
204
|
-
|
|
203
|
+
warning: `Text output too large: ${chars} characters, exceeding the limit of ${limit} characters. ` +
|
|
204
|
+
`The full text has been written to disk (result.txt); use read to page through it.`,
|
|
205
205
|
files: ctx.files,
|
|
206
206
|
chars,
|
|
207
207
|
...preview,
|
|
@@ -228,9 +228,9 @@ async function protectCallOutput(output, env) {
|
|
|
228
228
|
// 未超限:无媒体 → 纯字符串;有媒体 → 原序块数组
|
|
229
229
|
const hasMedia = sections.some((s) => s.type !== "text");
|
|
230
230
|
if (!hasMedia)
|
|
231
|
-
return isError ? `❌
|
|
231
|
+
return isError ? `❌ Tool execution failed:\n${fullText}` : fullText;
|
|
232
232
|
return isError
|
|
233
|
-
? [{ type: "text", text: "❌
|
|
233
|
+
? [{ type: "text", text: "❌ Tool execution failed:" }, ...sections]
|
|
234
234
|
: sections;
|
|
235
235
|
}
|
|
236
236
|
/**
|
|
@@ -247,13 +247,13 @@ export function createCallMcpTool(mcpManager, env) {
|
|
|
247
247
|
return new CallbackTool({
|
|
248
248
|
function: {
|
|
249
249
|
name: "call_mcp_tool",
|
|
250
|
-
description: "
|
|
250
|
+
description: "Call a tool exposed by a connected MCP server. Connect the server with load_mcp first. Large text output is written to disk and only a head/tail preview is returned; images, audio and other media are preserved: uploaded and referenced by file_id when the endpoint supports the Files API, otherwise inlined as base64.",
|
|
251
251
|
parameters: {
|
|
252
252
|
type: "object",
|
|
253
253
|
properties: {
|
|
254
|
-
server: { type: "string", description: "MCP
|
|
255
|
-
tool: { type: "string", description: "
|
|
256
|
-
arguments: { type: "object", description: "
|
|
254
|
+
server: { type: "string", description: "MCP server name" },
|
|
255
|
+
tool: { type: "string", description: "Tool name" },
|
|
256
|
+
arguments: { type: "object", description: "Tool arguments" },
|
|
257
257
|
},
|
|
258
258
|
required: ["server", "tool", "arguments"],
|
|
259
259
|
additionalProperties: false,
|
|
@@ -263,11 +263,11 @@ export function createCallMcpTool(mcpManager, env) {
|
|
|
263
263
|
const serverName = input.server;
|
|
264
264
|
const state = mcpManager.getState(serverName);
|
|
265
265
|
if (state !== "connected") {
|
|
266
|
-
return
|
|
266
|
+
return `Call load_mcp to connect "${serverName}" first`;
|
|
267
267
|
}
|
|
268
268
|
const client = mcpManager.getClient(serverName);
|
|
269
269
|
if (!client) {
|
|
270
|
-
return `MCP
|
|
270
|
+
return `MCP server "${serverName}" client is not available`;
|
|
271
271
|
}
|
|
272
272
|
try {
|
|
273
273
|
mcpManager.touch(serverName);
|
|
@@ -294,7 +294,7 @@ export function createCallMcpTool(mcpManager, env) {
|
|
|
294
294
|
return await protectCallOutput({ sections, fullText, chars, isError }, env);
|
|
295
295
|
}
|
|
296
296
|
catch (error) {
|
|
297
|
-
return `❌
|
|
297
|
+
return `❌ Failed to call "${input.tool}": ${error?.message ?? error}`;
|
|
298
298
|
}
|
|
299
299
|
},
|
|
300
300
|
});
|
|
@@ -306,12 +306,12 @@ export function createReadMcpResourceTool(mcpManager, env) {
|
|
|
306
306
|
return new CallbackTool({
|
|
307
307
|
function: {
|
|
308
308
|
name: "read_mcp_resource",
|
|
309
|
-
description: "
|
|
309
|
+
description: "Read a resource (document, data, image, etc.) from a connected MCP server. Text is returned as-is; images and other media are uploaded (file_id) or inlined as base64 depending on endpoint capabilities, and are never dropped.",
|
|
310
310
|
parameters: {
|
|
311
311
|
type: "object",
|
|
312
312
|
properties: {
|
|
313
|
-
server: { type: "string", description: "MCP
|
|
314
|
-
uri: { type: "string", description: "
|
|
313
|
+
server: { type: "string", description: "MCP server name" },
|
|
314
|
+
uri: { type: "string", description: "Resource URI" },
|
|
315
315
|
},
|
|
316
316
|
required: ["server", "uri"],
|
|
317
317
|
additionalProperties: false,
|
|
@@ -321,11 +321,11 @@ export function createReadMcpResourceTool(mcpManager, env) {
|
|
|
321
321
|
const serverName = input.server;
|
|
322
322
|
const state = mcpManager.getState(serverName);
|
|
323
323
|
if (state !== "connected") {
|
|
324
|
-
return
|
|
324
|
+
return `Call load_mcp to connect "${serverName}" first`;
|
|
325
325
|
}
|
|
326
326
|
const client = mcpManager.getClient(serverName);
|
|
327
327
|
if (!client) {
|
|
328
|
-
return `MCP
|
|
328
|
+
return `MCP server "${serverName}" client is not available`;
|
|
329
329
|
}
|
|
330
330
|
try {
|
|
331
331
|
mcpManager.touch(serverName);
|
|
@@ -349,7 +349,7 @@ export function createReadMcpResourceTool(mcpManager, env) {
|
|
|
349
349
|
return await protectCallOutput({ sections, fullText, chars, isError: false }, env);
|
|
350
350
|
}
|
|
351
351
|
catch (error) {
|
|
352
|
-
return `❌
|
|
352
|
+
return `❌ Failed to read resource "${input.uri}": ${error?.message ?? error}`;
|
|
353
353
|
}
|
|
354
354
|
},
|
|
355
355
|
});
|
|
@@ -165,27 +165,27 @@ const VALID_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
|
165
165
|
export function validateSkill(skillId, fm) {
|
|
166
166
|
const errors = [];
|
|
167
167
|
if (!fm.name || fm.name.length === 0) {
|
|
168
|
-
errors.push({ skillId, field: "name", message: "
|
|
168
|
+
errors.push({ skillId, field: "name", message: "Must be present and non-empty" });
|
|
169
169
|
}
|
|
170
170
|
else {
|
|
171
171
|
if (fm.name.length > 64) {
|
|
172
|
-
errors.push({ skillId, field: "name", message:
|
|
172
|
+
errors.push({ skillId, field: "name", message: `Exceeds 64 characters (currently ${fm.name.length})` });
|
|
173
173
|
}
|
|
174
174
|
if (!VALID_NAME_RE.test(fm.name)) {
|
|
175
|
-
errors.push({ skillId, field: "name", message: `"${fm.name}"
|
|
175
|
+
errors.push({ skillId, field: "name", message: `"${fm.name}" is not valid: only lowercase letters, digits and hyphens are allowed, and it must not start, end or contain consecutive hyphens` });
|
|
176
176
|
}
|
|
177
177
|
if (fm.name !== skillId) {
|
|
178
|
-
errors.push({ skillId, field: "name", message: `"${fm.name}"
|
|
178
|
+
errors.push({ skillId, field: "name", message: `"${fm.name}" does not match the directory name "${skillId}"` });
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
if (!fm.description || fm.description.length === 0) {
|
|
182
|
-
errors.push({ skillId, field: "description", message: "
|
|
182
|
+
errors.push({ skillId, field: "description", message: "Must be present and non-empty" });
|
|
183
183
|
}
|
|
184
184
|
else if (fm.description.length > 1024) {
|
|
185
|
-
errors.push({ skillId, field: "description", message:
|
|
185
|
+
errors.push({ skillId, field: "description", message: `Exceeds 1024 characters (currently ${fm.description.length})` });
|
|
186
186
|
}
|
|
187
187
|
if (fm.compatibility && fm.compatibility.length > 500) {
|
|
188
|
-
errors.push({ skillId, field: "compatibility", message:
|
|
188
|
+
errors.push({ skillId, field: "compatibility", message: `Exceeds 500 characters (currently ${fm.compatibility.length})` });
|
|
189
189
|
}
|
|
190
190
|
return errors;
|
|
191
191
|
}
|