@next-open-ai/openbot 0.2.1 → 0.3.2
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/README.md +117 -17
- package/apps/desktop/renderer/dist/assets/index-DKtaRFW4.js +89 -0
- package/apps/desktop/renderer/dist/assets/index-QHuqXpWQ.css +10 -0
- package/apps/desktop/renderer/dist/index.html +3 -3
- package/dist/cli/cli.js +27 -1
- package/dist/cli/service.d.ts +13 -0
- package/dist/cli/service.js +243 -0
- package/dist/core/agent/agent-manager.d.ts +3 -0
- package/dist/core/agent/agent-manager.js +12 -6
- package/dist/core/config/desktop-config.d.ts +4 -0
- package/dist/core/config/desktop-config.js +5 -1
- package/dist/core/installer/index.d.ts +1 -1
- package/dist/core/installer/index.js +1 -1
- package/dist/core/installer/skill-installer.d.ts +9 -0
- package/dist/core/installer/skill-installer.js +94 -0
- package/dist/core/mcp/adapter.d.ts +17 -0
- package/dist/core/mcp/adapter.js +49 -0
- package/dist/core/mcp/client.d.ts +24 -0
- package/dist/core/mcp/client.js +70 -0
- package/dist/core/mcp/config.d.ts +22 -0
- package/dist/core/mcp/config.js +69 -0
- package/dist/core/mcp/index.d.ts +18 -0
- package/dist/core/mcp/index.js +20 -0
- package/dist/core/mcp/operator.d.ts +15 -0
- package/dist/core/mcp/operator.js +72 -0
- package/dist/core/mcp/transport/index.d.ts +11 -0
- package/dist/core/mcp/transport/index.js +16 -0
- package/dist/core/mcp/transport/sse.d.ts +20 -0
- package/dist/core/mcp/transport/sse.js +82 -0
- package/dist/core/mcp/transport/stdio.d.ts +32 -0
- package/dist/core/mcp/transport/stdio.js +132 -0
- package/dist/core/mcp/types.d.ts +72 -0
- package/dist/core/mcp/types.js +5 -0
- package/dist/core/tools/bookmark-tool.d.ts +9 -0
- package/dist/core/tools/bookmark-tool.js +118 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/gateway/methods/agent-chat.js +1 -0
- package/dist/gateway/methods/install-skill-from-upload.d.ts +14 -0
- package/dist/gateway/methods/install-skill-from-upload.js +13 -0
- package/dist/gateway/methods/run-scheduled-task.js +1 -0
- package/dist/gateway/server.js +26 -1
- package/dist/server/agent-config/agent-config.controller.d.ts +1 -1
- package/dist/server/agent-config/agent-config.service.d.ts +4 -1
- package/dist/server/agent-config/agent-config.service.js +2 -0
- package/dist/server/agents/agents.controller.js +3 -5
- package/dist/server/agents/agents.service.d.ts +1 -1
- package/dist/server/agents/agents.service.js +4 -2
- package/dist/server/app.module.js +2 -0
- package/dist/server/database/database.service.d.ts +7 -0
- package/dist/server/database/database.service.js +54 -5
- package/dist/server/saved-items/saved-items.controller.d.ts +26 -0
- package/dist/server/saved-items/saved-items.controller.js +78 -0
- package/dist/server/saved-items/saved-items.module.d.ts +2 -0
- package/dist/server/saved-items/saved-items.module.js +23 -0
- package/dist/server/saved-items/saved-items.service.d.ts +31 -0
- package/dist/server/saved-items/saved-items.service.js +105 -0
- package/dist/server/saved-items/tags.controller.d.ts +30 -0
- package/dist/server/saved-items/tags.controller.js +85 -0
- package/dist/server/saved-items/tags.service.d.ts +24 -0
- package/dist/server/saved-items/tags.service.js +84 -0
- package/dist/server/skills/skills.service.d.ts +2 -0
- package/dist/server/skills/skills.service.js +80 -16
- package/package.json +8 -2
- package/skills/url-bookmark/SKILL.md +36 -0
- package/apps/desktop/renderer/dist/assets/index-BOS-F8a4.js +0 -89
- package/apps/desktop/renderer/dist/assets/index-DxqxayUL.css +0 -10
|
@@ -3,7 +3,8 @@ import { join } from "node:path";
|
|
|
3
3
|
import { existsSync, mkdirSync } from "node:fs";
|
|
4
4
|
import { createCompactionMemoryExtensionFactory } from "../memory/compaction-extension.js";
|
|
5
5
|
import { getCompactionContextForSystemPrompt } from "../memory/index.js";
|
|
6
|
-
import { createBrowserTool, createSaveExperienceTool, createInstallSkillTool } from "../tools/index.js";
|
|
6
|
+
import { createBrowserTool, createSaveExperienceTool, createInstallSkillTool, createGetBookmarkTagsTool, createSaveBookmarkTool } from "../tools/index.js";
|
|
7
|
+
import { createMcpToolsForSession } from "../mcp/index.js";
|
|
7
8
|
import { registerBuiltInApiProviders } from "@mariozechner/pi-ai/dist/providers/register-builtins.js";
|
|
8
9
|
import { getOpenbotAgentDir, getOpenbotWorkspaceDir, ensureDefaultAgentDir } from "./agent-dir.js";
|
|
9
10
|
import { formatSkillsForPrompt } from "./skills.js";
|
|
@@ -233,6 +234,15 @@ For downloads, provide either a direct URL or a selector to click.`;
|
|
|
233
234
|
grep: createGrepTool(sessionWorkspaceDir),
|
|
234
235
|
ls: createLsTool(sessionWorkspaceDir),
|
|
235
236
|
};
|
|
237
|
+
const mcpTools = await createMcpToolsForSession({ mcpServers: options.mcpServers });
|
|
238
|
+
const customTools = [
|
|
239
|
+
createBrowserTool(sessionWorkspaceDir),
|
|
240
|
+
createSaveExperienceTool(sessionId),
|
|
241
|
+
createInstallSkillTool(options.targetAgentId),
|
|
242
|
+
createGetBookmarkTagsTool(),
|
|
243
|
+
createSaveBookmarkTool(),
|
|
244
|
+
...mcpTools,
|
|
245
|
+
];
|
|
236
246
|
const { session } = await createAgentSession({
|
|
237
247
|
agentDir: this.agentDir,
|
|
238
248
|
sessionManager: CoreSessionManager.inMemory(),
|
|
@@ -240,11 +250,7 @@ For downloads, provide either a direct URL or a selector to click.`;
|
|
|
240
250
|
modelRegistry,
|
|
241
251
|
cwd: sessionWorkspaceDir,
|
|
242
252
|
resourceLoader: loader,
|
|
243
|
-
customTools
|
|
244
|
-
createBrowserTool(sessionWorkspaceDir),
|
|
245
|
-
createSaveExperienceTool(sessionId),
|
|
246
|
-
createInstallSkillTool(options.targetAgentId),
|
|
247
|
-
],
|
|
253
|
+
customTools,
|
|
248
254
|
baseToolsOverride: coreTools,
|
|
249
255
|
});
|
|
250
256
|
const model = modelRegistry.find(provider, modelId);
|
|
@@ -7,6 +7,8 @@ export interface RagEmbeddingConfig {
|
|
|
7
7
|
apiKey?: string;
|
|
8
8
|
baseUrl?: string;
|
|
9
9
|
}
|
|
10
|
+
/** MCP 服务器配置(与 core/mcp 类型一致,避免 core/config 依赖 core/mcp 实现) */
|
|
11
|
+
export type DesktopMcpServerConfig = import("../mcp/index.js").McpServerConfig;
|
|
10
12
|
/**
|
|
11
13
|
* 同步读取桌面全局配置中的 maxAgentSessions 等。
|
|
12
14
|
* Gateway 进程内使用,用于会话上限等。
|
|
@@ -22,6 +24,8 @@ export interface DesktopAgentConfig {
|
|
|
22
24
|
apiKey?: string;
|
|
23
25
|
/** 工作区名,来自 agents.json 的 agent.workspace 或 agent.id */
|
|
24
26
|
workspace?: string;
|
|
27
|
+
/** MCP 服务器配置,创建 Session 时传入 */
|
|
28
|
+
mcpServers?: DesktopMcpServerConfig[];
|
|
25
29
|
}
|
|
26
30
|
/**
|
|
27
31
|
* 从 config.json 读取缺省智能体 id(defaultAgentId)。
|
|
@@ -145,6 +145,7 @@ export async function loadDesktopAgentConfig(agentId) {
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
let workspaceName = resolvedAgentId;
|
|
148
|
+
let mcpServers;
|
|
148
149
|
if (existsSync(agentsPath)) {
|
|
149
150
|
try {
|
|
150
151
|
const raw = await readFile(agentsPath, "utf-8");
|
|
@@ -156,6 +157,9 @@ export async function loadDesktopAgentConfig(agentId) {
|
|
|
156
157
|
workspaceName = agent.workspace;
|
|
157
158
|
else if (agent.id)
|
|
158
159
|
workspaceName = agent.id;
|
|
160
|
+
if (agent.mcpServers && Array.isArray(agent.mcpServers)) {
|
|
161
|
+
mcpServers = agent.mcpServers;
|
|
162
|
+
}
|
|
159
163
|
if (agent.modelItemCode && Array.isArray(config.configuredModels)) {
|
|
160
164
|
const configured = config.configuredModels.find((m) => m.modelItemCode === agent.modelItemCode);
|
|
161
165
|
if (configured) {
|
|
@@ -185,7 +189,7 @@ export async function loadDesktopAgentConfig(agentId) {
|
|
|
185
189
|
const apiKey = provConfig?.apiKey && typeof provConfig.apiKey === "string" && provConfig.apiKey.trim()
|
|
186
190
|
? provConfig.apiKey.trim()
|
|
187
191
|
: undefined;
|
|
188
|
-
return { provider, model, apiKey: apiKey ?? undefined, workspace: workspaceName };
|
|
192
|
+
return { provider, model, apiKey: apiKey ?? undefined, workspace: workspaceName, mcpServers };
|
|
189
193
|
}
|
|
190
194
|
function ensureDesktopDir() {
|
|
191
195
|
const desktopDir = getDesktopDir();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { resolveInstallTarget, installSkillByUrl, installSkillFromPath, type InstallByUrlOptions, type InstallByUrlResult, type InstallFromPathOptions, type InstallFromPathResult, } from "./skill-installer.js";
|
|
1
|
+
export { resolveInstallTarget, installSkillByUrl, installSkillFromPath, installSkillFromUpload, type InstallByUrlOptions, type InstallByUrlResult, type InstallFromPathOptions, type InstallFromPathResult, type InstallFromUploadOptions, } from "./skill-installer.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { resolveInstallTarget, installSkillByUrl, installSkillFromPath, } from "./skill-installer.js";
|
|
1
|
+
export { resolveInstallTarget, installSkillByUrl, installSkillFromPath, installSkillFromUpload, } from "./skill-installer.js";
|
|
@@ -28,3 +28,12 @@ export interface InstallFromPathResult {
|
|
|
28
28
|
* 从本地目录安装技能:将指定目录复制到目标 skills 目录。
|
|
29
29
|
*/
|
|
30
30
|
export declare function installSkillFromPath(localPath: string, options?: InstallFromPathOptions): Promise<InstallFromPathResult>;
|
|
31
|
+
export interface InstallFromUploadOptions {
|
|
32
|
+
scope: "global" | "workspace";
|
|
33
|
+
workspace?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* 从上传的 zip 安装技能:解压到临时目录,校验为单个技能目录(含 SKILL.md),再复制到目标。
|
|
37
|
+
* 支持两种 zip 结构:① 单个顶层目录且内含 SKILL.md;② 根目录直接含 SKILL.md(将视为技能根目录)。
|
|
38
|
+
*/
|
|
39
|
+
export declare function installSkillFromUpload(zipBuffer: Buffer, options?: InstallFromUploadOptions): Promise<InstallFromPathResult>;
|
|
@@ -7,6 +7,7 @@ import { existsSync } from "fs";
|
|
|
7
7
|
import { join, resolve, basename } from "path";
|
|
8
8
|
import { tmpdir } from "os";
|
|
9
9
|
import { randomUUID } from "crypto";
|
|
10
|
+
import AdmZip from "adm-zip";
|
|
10
11
|
import { exec } from "child_process";
|
|
11
12
|
import { promisify } from "util";
|
|
12
13
|
import { homedir } from "os";
|
|
@@ -119,3 +120,96 @@ export async function installSkillFromPath(localPath, options = { scope: "global
|
|
|
119
120
|
await cp(srcResolved, destPath, { recursive: true });
|
|
120
121
|
return { installDir: targetDir, name: baseName };
|
|
121
122
|
}
|
|
123
|
+
/** 上传 zip 包最大体积(字节) */
|
|
124
|
+
const MAX_UPLOAD_ZIP_BYTES = 10 * 1024 * 1024;
|
|
125
|
+
/** 解压后忽略的条目(系统/打包产生的噪音) */
|
|
126
|
+
const IGNORED_ZIP_ENTRIES = new Set(["__MACOSX", ".DS_Store", ".git", "Thumbs.db"]);
|
|
127
|
+
function isIgnoredZipEntry(name) {
|
|
128
|
+
if (!name || name.includes(".."))
|
|
129
|
+
return true;
|
|
130
|
+
if (IGNORED_ZIP_ENTRIES.has(name))
|
|
131
|
+
return true;
|
|
132
|
+
if (name.startsWith("."))
|
|
133
|
+
return true;
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 从上传的 zip 安装技能:解压到临时目录,校验为单个技能目录(含 SKILL.md),再复制到目标。
|
|
138
|
+
* 支持两种 zip 结构:① 单个顶层目录且内含 SKILL.md;② 根目录直接含 SKILL.md(将视为技能根目录)。
|
|
139
|
+
*/
|
|
140
|
+
export async function installSkillFromUpload(zipBuffer, options = { scope: "global", workspace: "default" }) {
|
|
141
|
+
if (zipBuffer.length > MAX_UPLOAD_ZIP_BYTES) {
|
|
142
|
+
throw new Error("zip 包不能超过 10MB");
|
|
143
|
+
}
|
|
144
|
+
const tempDir = join(tmpdir(), `openbot-upload-${randomUUID()}`);
|
|
145
|
+
try {
|
|
146
|
+
await mkdir(tempDir, { recursive: true });
|
|
147
|
+
const zip = new AdmZip(zipBuffer);
|
|
148
|
+
zip.extractAllTo(tempDir, true);
|
|
149
|
+
const allEntries = await readdir(tempDir);
|
|
150
|
+
const entries = allEntries.filter((e) => !isIgnoredZipEntry(e));
|
|
151
|
+
let skillPath;
|
|
152
|
+
if (entries.length === 0) {
|
|
153
|
+
throw new Error("zip 解压后未得到有效内容,请检查 zip 是否包含技能目录或 SKILL.md");
|
|
154
|
+
}
|
|
155
|
+
if (entries.length === 1) {
|
|
156
|
+
const singleName = entries[0];
|
|
157
|
+
const candidatePath = join(tempDir, singleName);
|
|
158
|
+
const statEntry = await stat(candidatePath);
|
|
159
|
+
if (statEntry.isDirectory()) {
|
|
160
|
+
const skillMdInDir = join(candidatePath, "SKILL.md");
|
|
161
|
+
if (existsSync(skillMdInDir)) {
|
|
162
|
+
skillPath = candidatePath;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
throw new Error("该目录下未找到 SKILL.md,请确保 zip 内技能目录根目录含有 SKILL.md 文件");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
if (existsSync(join(tempDir, "SKILL.md"))) {
|
|
170
|
+
throw new Error("zip 根目录含有 SKILL.md,但根目录下还有其它文件,请将整个技能放在一个子目录内再打包");
|
|
171
|
+
}
|
|
172
|
+
throw new Error("zip 内未找到包含 SKILL.md 的技能目录,请检查打包方式");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const skillMdAtRoot = existsSync(join(tempDir, "SKILL.md"));
|
|
177
|
+
const dirsWithSkillMd = [];
|
|
178
|
+
for (const e of entries) {
|
|
179
|
+
const p = join(tempDir, e);
|
|
180
|
+
const st = await stat(p).catch(() => null);
|
|
181
|
+
if (st?.isDirectory() && existsSync(join(p, "SKILL.md")))
|
|
182
|
+
dirsWithSkillMd.push(p);
|
|
183
|
+
}
|
|
184
|
+
if (dirsWithSkillMd.length === 1) {
|
|
185
|
+
skillPath = dirsWithSkillMd[0];
|
|
186
|
+
}
|
|
187
|
+
else if (dirsWithSkillMd.length > 1) {
|
|
188
|
+
throw new Error("zip 内包含多个技能目录,请只保留一个技能目录再打包");
|
|
189
|
+
}
|
|
190
|
+
else if (skillMdAtRoot) {
|
|
191
|
+
skillPath = tempDir;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
throw new Error("zip 内未找到包含 SKILL.md 的技能目录;若为多文件打包,请将 SKILL.md 放在 zip 根目录或单个子目录内");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (skillPath === tempDir) {
|
|
198
|
+
const baseName = "skill";
|
|
199
|
+
const wrappedDir = join(tmpdir(), `openbot-skill-wrap-${randomUUID()}`);
|
|
200
|
+
try {
|
|
201
|
+
await mkdir(wrappedDir, { recursive: true });
|
|
202
|
+
const destPath = join(wrappedDir, baseName);
|
|
203
|
+
await cp(tempDir, destPath, { recursive: true });
|
|
204
|
+
return await installSkillFromPath(destPath, options);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
await rm(wrappedDir, { recursive: true, force: true }).catch(() => { });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return await installSkillFromPath(skillPath, options);
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool 转为 pi-coding-agent ToolDefinition 的适配层。
|
|
3
|
+
*/
|
|
4
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import type { McpTool } from "./types.js";
|
|
6
|
+
import type { McpClient } from "./client.js";
|
|
7
|
+
/**
|
|
8
|
+
* 将单个 MCP 工具封装为 pi-coding-agent 的 ToolDefinition。
|
|
9
|
+
* @param tool MCP tools/list 返回的项
|
|
10
|
+
* @param client 已连接的 McpClient,用于 callTool
|
|
11
|
+
* @param serverId 可选前缀,用于避免多 MCP 时工具名冲突
|
|
12
|
+
*/
|
|
13
|
+
export declare function mcpToolToToolDefinition(tool: McpTool, client: McpClient, serverId?: string): ToolDefinition;
|
|
14
|
+
/**
|
|
15
|
+
* 将某 MCP 客户端的全部工具转为 ToolDefinition 数组。
|
|
16
|
+
*/
|
|
17
|
+
export declare function mcpToolsToToolDefinitions(tools: McpTool[], client: McpClient, serverId?: string): ToolDefinition[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool 转为 pi-coding-agent ToolDefinition 的适配层。
|
|
3
|
+
*/
|
|
4
|
+
import { Type } from "@sinclair/typebox";
|
|
5
|
+
/** 通用参数:MCP 工具接受任意 JSON 对象作为 arguments */
|
|
6
|
+
const McpToolParamsSchema = Type.Record(Type.String(), Type.Any());
|
|
7
|
+
/**
|
|
8
|
+
* 将单个 MCP 工具封装为 pi-coding-agent 的 ToolDefinition。
|
|
9
|
+
* @param tool MCP tools/list 返回的项
|
|
10
|
+
* @param client 已连接的 McpClient,用于 callTool
|
|
11
|
+
* @param serverId 可选前缀,用于避免多 MCP 时工具名冲突
|
|
12
|
+
*/
|
|
13
|
+
export function mcpToolToToolDefinition(tool, client, serverId) {
|
|
14
|
+
const name = serverId ? `${serverId}_${tool.name}` : tool.name;
|
|
15
|
+
const description = (tool.description ?? "").trim() || `MCP tool: ${tool.name}`;
|
|
16
|
+
return {
|
|
17
|
+
name,
|
|
18
|
+
label: tool.name,
|
|
19
|
+
description,
|
|
20
|
+
parameters: McpToolParamsSchema,
|
|
21
|
+
execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
|
|
22
|
+
const args = params && typeof params === "object" ? params : {};
|
|
23
|
+
try {
|
|
24
|
+
const result = await client.callTool(tool.name, args);
|
|
25
|
+
const text = result.content
|
|
26
|
+
?.filter((c) => c.type === "text")
|
|
27
|
+
.map((c) => c.text)
|
|
28
|
+
.join("\n") ?? (result.isError ? "MCP 调用返回错误" : "");
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text }],
|
|
31
|
+
details: result.isError ? { isError: true } : undefined,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: "text", text: `MCP 调用失败: ${msg}` }],
|
|
38
|
+
details: undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 将某 MCP 客户端的全部工具转为 ToolDefinition 数组。
|
|
46
|
+
*/
|
|
47
|
+
export function mcpToolsToToolDefinitions(tools, client, serverId) {
|
|
48
|
+
return tools.map((t) => mcpToolToToolDefinition(t, client, serverId));
|
|
49
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 客户端:连接、list_tools、call_tool、断开。
|
|
3
|
+
* 支持 stdio(本地进程)与 sse(远程 HTTP)两种传输。
|
|
4
|
+
*/
|
|
5
|
+
import type { McpTool, McpToolCallResult, IMcpTransport } from "./types.js";
|
|
6
|
+
import type { McpServerConfig } from "./types.js";
|
|
7
|
+
export interface McpClientOptions {
|
|
8
|
+
initTimeoutMs?: number;
|
|
9
|
+
requestTimeoutMs?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare class McpClient {
|
|
12
|
+
private transport;
|
|
13
|
+
private _tools;
|
|
14
|
+
private _requestId;
|
|
15
|
+
constructor(configOrTransport: McpServerConfig | IMcpTransport, options?: McpClientOptions);
|
|
16
|
+
/** 建立连接并完成握手;成功后可使用 listTools / callTool */
|
|
17
|
+
connect(): Promise<void>;
|
|
18
|
+
/** 获取工具列表(会缓存;断开重连后需重新 list) */
|
|
19
|
+
listTools(): Promise<McpTool[]>;
|
|
20
|
+
/** 调用指定工具 */
|
|
21
|
+
callTool(name: string, args: Record<string, unknown>): Promise<McpToolCallResult>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
get isConnected(): boolean;
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 客户端:连接、list_tools、call_tool、断开。
|
|
3
|
+
* 支持 stdio(本地进程)与 sse(远程 HTTP)两种传输。
|
|
4
|
+
*/
|
|
5
|
+
import { createTransport } from "./transport/index.js";
|
|
6
|
+
export class McpClient {
|
|
7
|
+
transport;
|
|
8
|
+
_tools = null;
|
|
9
|
+
_requestId = 0;
|
|
10
|
+
constructor(configOrTransport, options = {}) {
|
|
11
|
+
if (typeof configOrTransport.request === "function" &&
|
|
12
|
+
typeof configOrTransport.start === "function") {
|
|
13
|
+
this.transport = configOrTransport;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
this.transport = createTransport(configOrTransport, {
|
|
17
|
+
initTimeoutMs: options.initTimeoutMs,
|
|
18
|
+
requestTimeoutMs: options.requestTimeoutMs,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** 建立连接并完成握手;成功后可使用 listTools / callTool */
|
|
23
|
+
async connect() {
|
|
24
|
+
await this.transport.start();
|
|
25
|
+
}
|
|
26
|
+
/** 获取工具列表(会缓存;断开重连后需重新 list) */
|
|
27
|
+
async listTools() {
|
|
28
|
+
if (this._tools !== null) {
|
|
29
|
+
return this._tools;
|
|
30
|
+
}
|
|
31
|
+
const res = await this.transport.request({
|
|
32
|
+
jsonrpc: "2.0",
|
|
33
|
+
id: ++this._requestId,
|
|
34
|
+
method: "tools/list",
|
|
35
|
+
});
|
|
36
|
+
if (res.error) {
|
|
37
|
+
throw new Error(`MCP tools/list failed: ${res.error.message}`);
|
|
38
|
+
}
|
|
39
|
+
const list = res.result?.tools;
|
|
40
|
+
this._tools = Array.isArray(list) ? list : [];
|
|
41
|
+
return this._tools;
|
|
42
|
+
}
|
|
43
|
+
/** 调用指定工具 */
|
|
44
|
+
async callTool(name, args) {
|
|
45
|
+
const res = await this.transport.request({
|
|
46
|
+
jsonrpc: "2.0",
|
|
47
|
+
id: ++this._requestId,
|
|
48
|
+
method: "tools/call",
|
|
49
|
+
params: { name, arguments: args ?? {} },
|
|
50
|
+
});
|
|
51
|
+
if (res.error) {
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: "text", text: `MCP call_tool error: ${res.error.message}` }],
|
|
54
|
+
isError: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const result = res.result;
|
|
58
|
+
if (!result || !Array.isArray(result.content)) {
|
|
59
|
+
return { content: [{ type: "text", text: "Invalid MCP tools/call result" }], isError: true };
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
async close() {
|
|
64
|
+
this._tools = null;
|
|
65
|
+
await this.transport.close();
|
|
66
|
+
}
|
|
67
|
+
get isConnected() {
|
|
68
|
+
return this.transport.isConnected;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 配置解析与校验。
|
|
3
|
+
* 从 getOrCreateSession 的 options.mcpServers 中取出并规范化,支持 stdio 与 sse。
|
|
4
|
+
*/
|
|
5
|
+
import type { McpServerConfig, McpServerConfigStdio, McpServerConfigSse } from "./types.js";
|
|
6
|
+
/**
|
|
7
|
+
* 从会话选项里解析出本会话启用的 MCP 服务器配置列表。
|
|
8
|
+
* 支持 stdio(本地进程)与 sse(远程 HTTP)。
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveMcpServersForSession(mcpServers: McpServerConfig[] | undefined): McpServerConfig[];
|
|
11
|
+
/**
|
|
12
|
+
* 为 stdio 配置生成缓存键(用于 Operator 复用同一进程连接)
|
|
13
|
+
*/
|
|
14
|
+
export declare function stdioConfigKey(config: McpServerConfigStdio): string;
|
|
15
|
+
/**
|
|
16
|
+
* 为 sse 配置生成缓存键
|
|
17
|
+
*/
|
|
18
|
+
export declare function sseConfigKey(config: McpServerConfigSse): string;
|
|
19
|
+
/**
|
|
20
|
+
* 任意 MCP 配置的缓存键
|
|
21
|
+
*/
|
|
22
|
+
export declare function mcpConfigKey(config: McpServerConfig): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 配置解析与校验。
|
|
3
|
+
* 从 getOrCreateSession 的 options.mcpServers 中取出并规范化,支持 stdio 与 sse。
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 从会话选项里解析出本会话启用的 MCP 服务器配置列表。
|
|
7
|
+
* 支持 stdio(本地进程)与 sse(远程 HTTP)。
|
|
8
|
+
*/
|
|
9
|
+
export function resolveMcpServersForSession(mcpServers) {
|
|
10
|
+
if (!Array.isArray(mcpServers) || mcpServers.length === 0) {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
const result = [];
|
|
14
|
+
for (const s of mcpServers) {
|
|
15
|
+
if (!s || typeof s !== "object")
|
|
16
|
+
continue;
|
|
17
|
+
if (s.transport === "stdio") {
|
|
18
|
+
if (typeof s.command !== "string" || !s.command.trim()) {
|
|
19
|
+
console.warn("[mcp] 跳过无效 stdio 配置:缺少或无效 command");
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
result.push({
|
|
23
|
+
transport: "stdio",
|
|
24
|
+
command: s.command.trim(),
|
|
25
|
+
args: Array.isArray(s.args) ? s.args : undefined,
|
|
26
|
+
env: s.env && typeof s.env === "object" ? s.env : undefined,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else if (s.transport === "sse") {
|
|
30
|
+
const url = s.url?.trim();
|
|
31
|
+
if (!url) {
|
|
32
|
+
console.warn("[mcp] 跳过无效 sse 配置:缺少或无效 url");
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
result.push({
|
|
36
|
+
transport: "sse",
|
|
37
|
+
url,
|
|
38
|
+
headers: s.headers && typeof s.headers === "object" ? s.headers : undefined,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.warn("[mcp] 未知 transport:", s.transport);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 为 stdio 配置生成缓存键(用于 Operator 复用同一进程连接)
|
|
49
|
+
*/
|
|
50
|
+
export function stdioConfigKey(config) {
|
|
51
|
+
const args = (config.args ?? []).join(" ");
|
|
52
|
+
const envStr = config.env ? JSON.stringify(config.env) : "";
|
|
53
|
+
return `stdio:${config.command}\0${args}\0${envStr}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 为 sse 配置生成缓存键
|
|
57
|
+
*/
|
|
58
|
+
export function sseConfigKey(config) {
|
|
59
|
+
const headersStr = config.headers ? JSON.stringify(config.headers) : "";
|
|
60
|
+
return `sse:${config.url}\0${headersStr}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 任意 MCP 配置的缓存键
|
|
64
|
+
*/
|
|
65
|
+
export function mcpConfigKey(config) {
|
|
66
|
+
if (config.transport === "stdio")
|
|
67
|
+
return stdioConfigKey(config);
|
|
68
|
+
return sseConfigKey(config);
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core MCP 模块:为 SessionAgent 提供 MCP Tools 能力(Client 端)。
|
|
3
|
+
* 配置在创建 Session 时通过 options.mcpServers 传入,与 Skill 类似。
|
|
4
|
+
*/
|
|
5
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
6
|
+
import type { McpServerConfig } from "./types.js";
|
|
7
|
+
export type { McpServerConfig, McpServerConfigStdio, McpServerConfigSse, McpTool } from "./types.js";
|
|
8
|
+
export { resolveMcpServersForSession, stdioConfigKey, sseConfigKey, mcpConfigKey } from "./config.js";
|
|
9
|
+
export { McpClient } from "./client.js";
|
|
10
|
+
export { getMcpToolDefinitions, shutdownMcpClients } from "./operator.js";
|
|
11
|
+
export { mcpToolToToolDefinition, mcpToolsToToolDefinitions } from "./adapter.js";
|
|
12
|
+
/**
|
|
13
|
+
* 根据会话选项中的 mcpServers 配置,返回该会话可用的 MCP 工具(ToolDefinition 数组)。
|
|
14
|
+
* 在 AgentManager.getOrCreateSession 中调用,并入 customTools。
|
|
15
|
+
*/
|
|
16
|
+
export declare function createMcpToolsForSession(options: {
|
|
17
|
+
mcpServers?: McpServerConfig[];
|
|
18
|
+
}): Promise<ToolDefinition[]>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core MCP 模块:为 SessionAgent 提供 MCP Tools 能力(Client 端)。
|
|
3
|
+
* 配置在创建 Session 时通过 options.mcpServers 传入,与 Skill 类似。
|
|
4
|
+
*/
|
|
5
|
+
import { resolveMcpServersForSession } from "./config.js";
|
|
6
|
+
import { getMcpToolDefinitions } from "./operator.js";
|
|
7
|
+
export { resolveMcpServersForSession, stdioConfigKey, sseConfigKey, mcpConfigKey } from "./config.js";
|
|
8
|
+
export { McpClient } from "./client.js";
|
|
9
|
+
export { getMcpToolDefinitions, shutdownMcpClients } from "./operator.js";
|
|
10
|
+
export { mcpToolToToolDefinition, mcpToolsToToolDefinitions } from "./adapter.js";
|
|
11
|
+
/**
|
|
12
|
+
* 根据会话选项中的 mcpServers 配置,返回该会话可用的 MCP 工具(ToolDefinition 数组)。
|
|
13
|
+
* 在 AgentManager.getOrCreateSession 中调用,并入 customTools。
|
|
14
|
+
*/
|
|
15
|
+
export async function createMcpToolsForSession(options) {
|
|
16
|
+
const configs = resolveMcpServersForSession(options.mcpServers);
|
|
17
|
+
if (configs.length === 0)
|
|
18
|
+
return [];
|
|
19
|
+
return getMcpToolDefinitions(configs);
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Operator:管理 MCP 客户端生命周期,按配置为会话提供 ToolDefinition 列表。
|
|
3
|
+
* 支持 stdio(本地)与 sse(远程)两种传输。
|
|
4
|
+
*/
|
|
5
|
+
import type { McpServerConfig } from "./types.js";
|
|
6
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
7
|
+
/**
|
|
8
|
+
* 为给定 MCP 服务器配置列表获取或创建客户端,并返回其工具对应的 ToolDefinition 数组。
|
|
9
|
+
* 连接失败或 list_tools 失败的 server 会被跳过并打日志,不阻塞整体。
|
|
10
|
+
*/
|
|
11
|
+
export declare function getMcpToolDefinitions(serverConfigs: McpServerConfig[]): Promise<ToolDefinition[]>;
|
|
12
|
+
/**
|
|
13
|
+
* 关闭并移除所有缓存的 MCP 客户端(进程退出或显式清理时调用)。
|
|
14
|
+
*/
|
|
15
|
+
export declare function shutdownMcpClients(): Promise<void>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Operator:管理 MCP 客户端生命周期,按配置为会话提供 ToolDefinition 列表。
|
|
3
|
+
* 支持 stdio(本地)与 sse(远程)两种传输。
|
|
4
|
+
*/
|
|
5
|
+
import { mcpConfigKey } from "./config.js";
|
|
6
|
+
import { McpClient } from "./client.js";
|
|
7
|
+
import { mcpToolsToToolDefinitions } from "./adapter.js";
|
|
8
|
+
/** 按配置键缓存的客户端 */
|
|
9
|
+
const clientCache = new Map();
|
|
10
|
+
function configLabel(config) {
|
|
11
|
+
if (config.transport === "stdio")
|
|
12
|
+
return config.command;
|
|
13
|
+
return config.url;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 为给定 MCP 服务器配置列表获取或创建客户端,并返回其工具对应的 ToolDefinition 数组。
|
|
17
|
+
* 连接失败或 list_tools 失败的 server 会被跳过并打日志,不阻塞整体。
|
|
18
|
+
*/
|
|
19
|
+
export async function getMcpToolDefinitions(serverConfigs) {
|
|
20
|
+
const allTools = [];
|
|
21
|
+
for (let i = 0; i < serverConfigs.length; i++) {
|
|
22
|
+
const config = serverConfigs[i];
|
|
23
|
+
const key = mcpConfigKey(config);
|
|
24
|
+
let client = clientCache.get(key);
|
|
25
|
+
if (!client) {
|
|
26
|
+
client = new McpClient(config);
|
|
27
|
+
try {
|
|
28
|
+
await client.connect();
|
|
29
|
+
clientCache.set(key, client);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.warn(`[mcp] 连接失败 (${configLabel(config)}):`, err instanceof Error ? err.message : err);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (!client.isConnected) {
|
|
37
|
+
clientCache.delete(key);
|
|
38
|
+
try {
|
|
39
|
+
await client.close();
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
const newClient = new McpClient(config);
|
|
43
|
+
try {
|
|
44
|
+
await newClient.connect();
|
|
45
|
+
clientCache.set(key, newClient);
|
|
46
|
+
client = newClient;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.warn(`[mcp] 重连失败 (${configLabel(config)}):`, err instanceof Error ? err.message : err);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const tools = await client.listTools();
|
|
55
|
+
const serverId = `mcp${i}`;
|
|
56
|
+
const definitions = mcpToolsToToolDefinitions(tools, client, serverId);
|
|
57
|
+
allTools.push(...definitions);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
console.warn(`[mcp] list_tools 失败 (${configLabel(config)}):`, err instanceof Error ? err.message : err);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return allTools;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* 关闭并移除所有缓存的 MCP 客户端(进程退出或显式清理时调用)。
|
|
67
|
+
*/
|
|
68
|
+
export async function shutdownMcpClients() {
|
|
69
|
+
const closeAll = Array.from(clientCache.values()).map((c) => c.close().catch(() => { }));
|
|
70
|
+
clientCache.clear();
|
|
71
|
+
await Promise.all(closeAll);
|
|
72
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpServerConfig, IMcpTransport } from "../types.js";
|
|
2
|
+
export interface TransportOptions {
|
|
3
|
+
initTimeoutMs?: number;
|
|
4
|
+
requestTimeoutMs?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 根据配置创建对应的传输层实例。
|
|
8
|
+
*/
|
|
9
|
+
export declare function createTransport(config: McpServerConfig, options?: TransportOptions): IMcpTransport;
|
|
10
|
+
export { StdioTransport } from "./stdio.js";
|
|
11
|
+
export { SseTransport } from "./sse.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StdioTransport } from "./stdio.js";
|
|
2
|
+
import { SseTransport } from "./sse.js";
|
|
3
|
+
/**
|
|
4
|
+
* 根据配置创建对应的传输层实例。
|
|
5
|
+
*/
|
|
6
|
+
export function createTransport(config, options) {
|
|
7
|
+
if (config.transport === "stdio") {
|
|
8
|
+
return new StdioTransport(config, options);
|
|
9
|
+
}
|
|
10
|
+
if (config.transport === "sse") {
|
|
11
|
+
return new SseTransport(config, options);
|
|
12
|
+
}
|
|
13
|
+
throw new Error(`Unsupported MCP transport: ${config.transport}`);
|
|
14
|
+
}
|
|
15
|
+
export { StdioTransport } from "./stdio.js";
|
|
16
|
+
export { SseTransport } from "./sse.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP SSE/HTTP 传输:通过 HTTP POST 向远程 URL 发送 JSON-RPC 请求,响应在 response body。
|
|
3
|
+
*/
|
|
4
|
+
import type { McpServerConfigSse } from "../types.js";
|
|
5
|
+
import type { JsonRpcRequest, JsonRpcResponse } from "../types.js";
|
|
6
|
+
export interface SseTransportOptions {
|
|
7
|
+
initTimeoutMs?: number;
|
|
8
|
+
requestTimeoutMs?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare class SseTransport {
|
|
11
|
+
private config;
|
|
12
|
+
private initTimeoutMs;
|
|
13
|
+
private requestTimeoutMs;
|
|
14
|
+
private _connected;
|
|
15
|
+
constructor(config: McpServerConfigSse, options?: SseTransportOptions);
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
request(req: JsonRpcRequest, timeoutMs?: number): Promise<JsonRpcResponse>;
|
|
18
|
+
close(): Promise<void>;
|
|
19
|
+
get isConnected(): boolean;
|
|
20
|
+
}
|