ai-helper-agent 1.3.4 → 1.5.0
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 +119 -5
- package/bin/agent.js +16 -8
- package/package.json +3 -3
- package/src/auth.js +26 -0
- package/src/config.js +21 -7
- package/src/mcp/client.js +244 -0
- package/src/mcp/mcp-config.js +124 -0
- package/src/mcp/registry.js +165 -0
- package/src/mcp/transport.js +100 -0
- package/src/security.js +73 -5
- package/src/server.js +366 -3
- package/src/skill/executor.js +369 -0
- package/src/skill/loader.js +308 -0
- package/src/skill/markdown-loader.js +411 -0
- package/src/skill/registry.js +315 -0
- package/src/skill/skill-creator-seed.js +144 -0
- package/src/skill/template.js +96 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// mcp/registry.js - MCP 工具注册表
|
|
2
|
+
// 管理所有 MCP Client 实例,提供统一的工具发现和调用接口
|
|
3
|
+
import { McpClient } from './client.js';
|
|
4
|
+
import { loadMcpConfig, addMcpServer, removeMcpServer, toggleMcpServer } from './mcp-config.js';
|
|
5
|
+
|
|
6
|
+
// 已连接的 MCP Client 映射表: serverId → McpClient
|
|
7
|
+
const clients = new Map();
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 启动时加载并连接所有启用的 MCP Server
|
|
11
|
+
* @returns {Promise<{connected: number, failed: number}>}
|
|
12
|
+
*/
|
|
13
|
+
export async function initializeMcpRegistry() {
|
|
14
|
+
const config = loadMcpConfig();
|
|
15
|
+
let connected = 0;
|
|
16
|
+
let failed = 0;
|
|
17
|
+
|
|
18
|
+
console.log(`[MCP Registry] 初始化,共配置 ${config.servers.length} 个 MCP Server`);
|
|
19
|
+
|
|
20
|
+
for (const server of config.servers) {
|
|
21
|
+
if (!server.enabled) {
|
|
22
|
+
console.log(`[MCP Registry] 跳过禁用的 Server: ${server.id}`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const client = new McpClient(server);
|
|
27
|
+
const result = await client.connect();
|
|
28
|
+
if (result.success) {
|
|
29
|
+
clients.set(server.id, client);
|
|
30
|
+
connected++;
|
|
31
|
+
} else {
|
|
32
|
+
console.error(`[MCP Registry] 连接失败 "${server.id}":`, result.error);
|
|
33
|
+
failed++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(`[MCP Registry] 初始化完成: ${connected} 已连接, ${failed} 失败`);
|
|
38
|
+
return { connected, failed };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 获取所有 MCP Server 及其状态
|
|
43
|
+
*/
|
|
44
|
+
export function getMcpServersStatus() {
|
|
45
|
+
const config = loadMcpConfig();
|
|
46
|
+
const statuses = config.servers.map(server => {
|
|
47
|
+
const client = clients.get(server.id);
|
|
48
|
+
if (client && client.connected) {
|
|
49
|
+
return {
|
|
50
|
+
...server,
|
|
51
|
+
connected: true,
|
|
52
|
+
toolCount: client.tools.length,
|
|
53
|
+
tools: client.tools,
|
|
54
|
+
serverInfo: client.serverInfo?.serverInfo || null
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
...server,
|
|
59
|
+
connected: false,
|
|
60
|
+
toolCount: 0,
|
|
61
|
+
tools: [],
|
|
62
|
+
serverInfo: null
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return { success: true, servers: statuses };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 获取所有已连接 MCP Server 的工具列表(扁平化)
|
|
71
|
+
* @param {string} [serverId] - 可选,只获取指定 Server 的工具
|
|
72
|
+
*/
|
|
73
|
+
export function getMcpTools(serverId) {
|
|
74
|
+
const tools = [];
|
|
75
|
+
|
|
76
|
+
for (const [id, client] of clients) {
|
|
77
|
+
if (serverId && id !== serverId) continue;
|
|
78
|
+
if (!client.connected) continue;
|
|
79
|
+
|
|
80
|
+
for (const tool of client.tools) {
|
|
81
|
+
tools.push({
|
|
82
|
+
serverId: id,
|
|
83
|
+
serverName: client.serverName,
|
|
84
|
+
name: tool.name,
|
|
85
|
+
description: tool.description || '',
|
|
86
|
+
inputSchema: tool.inputSchema || { type: 'object', properties: {} }
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { success: true, tools };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 调用指定 MCP Server 的工具
|
|
96
|
+
* @param {string} serverId
|
|
97
|
+
* @param {string} toolName
|
|
98
|
+
* @param {Object} args
|
|
99
|
+
*/
|
|
100
|
+
export async function callMcpTool(serverId, toolName, args) {
|
|
101
|
+
const client = clients.get(serverId);
|
|
102
|
+
if (!client) {
|
|
103
|
+
return { success: false, error: `MCP Server "${serverId}" 不存在` };
|
|
104
|
+
}
|
|
105
|
+
if (!client.connected) {
|
|
106
|
+
return { success: false, error: `MCP Server "${serverId}" 未连接` };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(`[MCP Registry] 调用工具: ${serverId}/${toolName}`, args);
|
|
110
|
+
const result = await client.callTool(toolName, args);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 连接指定 MCP Server
|
|
116
|
+
*/
|
|
117
|
+
export async function connectMcpServer(serverId) {
|
|
118
|
+
const config = loadMcpConfig();
|
|
119
|
+
const server = config.servers.find(s => s.id === serverId);
|
|
120
|
+
if (!server) {
|
|
121
|
+
return { success: false, error: `MCP Server "${serverId}" 不存在` };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 先断开已有连接
|
|
125
|
+
const existingClient = clients.get(serverId);
|
|
126
|
+
if (existingClient) {
|
|
127
|
+
existingClient.disconnect();
|
|
128
|
+
clients.delete(serverId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const client = new McpClient(server);
|
|
132
|
+
const result = await client.connect();
|
|
133
|
+
if (result.success) {
|
|
134
|
+
clients.set(serverId, client);
|
|
135
|
+
return { success: true, toolCount: result.toolCount };
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 断开指定 MCP Server
|
|
142
|
+
*/
|
|
143
|
+
export function disconnectMcpServer(serverId) {
|
|
144
|
+
const client = clients.get(serverId);
|
|
145
|
+
if (client) {
|
|
146
|
+
client.disconnect();
|
|
147
|
+
clients.delete(serverId);
|
|
148
|
+
return { success: true };
|
|
149
|
+
}
|
|
150
|
+
return { success: false, error: `MCP Server "${serverId}" 未连接` };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 关闭所有 MCP 连接(Agent 关闭时调用)
|
|
155
|
+
*/
|
|
156
|
+
export function shutdownMcpRegistry() {
|
|
157
|
+
console.log('[MCP Registry] 关闭所有 MCP 连接...');
|
|
158
|
+
for (const [id, client] of clients) {
|
|
159
|
+
try { client.disconnect(); } catch {}
|
|
160
|
+
}
|
|
161
|
+
clients.clear();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 重新导出配置管理函数,方便 server.js 使用
|
|
165
|
+
export { loadMcpConfig, addMcpServer, removeMcpServer, toggleMcpServer };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// mcp/transport.js - MCP stdio 传输层
|
|
2
|
+
// MCP 协议使用 JSON-RPC 2.0 over stdin/stdout,每行一个 JSON 消息
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 创建 MCP 子进程传输
|
|
7
|
+
* MCP 协议中,客户端先发 initialize 请求,服务端才会响应。因此 spawn 后立即 resolve,
|
|
8
|
+
* 由上层 McpClient 负责发送 initialize 和等待响应。
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} serverConfig - { command, args, env }
|
|
11
|
+
* @returns {Promise<{send: Function, close: Function, onData: Function}>}
|
|
12
|
+
*/
|
|
13
|
+
export function createStdioTransport(serverConfig) {
|
|
14
|
+
const { command, args = [], env = {} } = serverConfig;
|
|
15
|
+
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
let resolved = false;
|
|
18
|
+
let responseBuffer = '';
|
|
19
|
+
let dataHandler = null;
|
|
20
|
+
let exitHandler = null;
|
|
21
|
+
|
|
22
|
+
const child = spawn(command, args, {
|
|
23
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
24
|
+
env: { ...process.env, ...env },
|
|
25
|
+
detached: false
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const cleanup = () => {
|
|
29
|
+
if (child.stdout) child.stdout.removeAllListeners();
|
|
30
|
+
if (child.stderr) child.stderr.removeAllListeners();
|
|
31
|
+
child.removeAllListeners();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
child.stderr.on('data', (data) => {
|
|
35
|
+
const text = data.toString();
|
|
36
|
+
if (text.trim()) {
|
|
37
|
+
console.log(`[MCP:stderr] [${serverConfig.id || command}] ${text.trim()}`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
child.on('error', (err) => {
|
|
42
|
+
if (!resolved) {
|
|
43
|
+
resolved = true;
|
|
44
|
+
cleanup();
|
|
45
|
+
reject(new Error(`无法启动 MCP Server "${command}": ${err.message}`));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
child.on('exit', (code, signal) => {
|
|
50
|
+
console.log(`[MCP] Server 进程退出: ${command} (code=${code}, signal=${signal})`);
|
|
51
|
+
if (!resolved) {
|
|
52
|
+
resolved = true;
|
|
53
|
+
cleanup();
|
|
54
|
+
reject(new Error(`MCP Server 异常退出 (code=${code}, signal=${signal}): ${command}`));
|
|
55
|
+
} else if (exitHandler) {
|
|
56
|
+
exitHandler(code, signal);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
child.stdout.on('data', (data) => {
|
|
61
|
+
responseBuffer += data.toString();
|
|
62
|
+
|
|
63
|
+
let newlineIdx;
|
|
64
|
+
while ((newlineIdx = responseBuffer.indexOf('\n')) !== -1) {
|
|
65
|
+
const line = responseBuffer.slice(0, newlineIdx).trim();
|
|
66
|
+
responseBuffer = responseBuffer.slice(newlineIdx + 1);
|
|
67
|
+
|
|
68
|
+
if (!line) continue;
|
|
69
|
+
|
|
70
|
+
if (dataHandler) {
|
|
71
|
+
try { dataHandler(line); } catch {}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!resolved) {
|
|
77
|
+
resolved = true;
|
|
78
|
+
resolve({
|
|
79
|
+
send: (jsonRpcMessage) => {
|
|
80
|
+
const msg = JSON.stringify(jsonRpcMessage) + '\n';
|
|
81
|
+
child.stdin.write(msg);
|
|
82
|
+
},
|
|
83
|
+
close: () => {
|
|
84
|
+
cleanup();
|
|
85
|
+
dataHandler = null;
|
|
86
|
+
try { child.stdin.end(); } catch {}
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
89
|
+
}, 1000);
|
|
90
|
+
},
|
|
91
|
+
onData: (handler) => {
|
|
92
|
+
dataHandler = handler;
|
|
93
|
+
},
|
|
94
|
+
onExit: (handler) => {
|
|
95
|
+
exitHandler = handler;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
package/src/security.js
CHANGED
|
@@ -1,8 +1,41 @@
|
|
|
1
1
|
// agent/src/security.js - 安全管控:文件沙箱 + 命令分级
|
|
2
|
-
import { resolve, normalize, sep, isAbsolute } from 'path';
|
|
2
|
+
import { resolve, normalize, sep, isAbsolute, join } from 'path';
|
|
3
3
|
import { realpathSync } from 'fs';
|
|
4
|
+
import { homedir } from 'os';
|
|
4
5
|
import { loadConfig } from './config.js';
|
|
5
6
|
|
|
7
|
+
// ==================== 硬阻止目录(任何情况下都不可访问) ====================
|
|
8
|
+
|
|
9
|
+
const AGENT_DIR = join(homedir(), '.ai-helper-agent');
|
|
10
|
+
|
|
11
|
+
/** 核心敏感文件/目录(硬阻止,不可绕过)。其他子目录(如 workspace)由白名单控制。 */
|
|
12
|
+
const HARD_BLOCKED_PATHS = [
|
|
13
|
+
join(AGENT_DIR, 'config.json'), // 代理配置文件
|
|
14
|
+
join(AGENT_DIR, 'pairings.json'), // 配对记录
|
|
15
|
+
join(AGENT_DIR, 'mcp_servers.json'),// MCP 服务器配置
|
|
16
|
+
join(AGENT_DIR, 'logs') + sep, // 日志目录
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 检查路径是否命中核心敏感文件
|
|
21
|
+
*/
|
|
22
|
+
function isHardBlocked(normalizedPath) {
|
|
23
|
+
for (const blocked of HARD_BLOCKED_PATHS) {
|
|
24
|
+
if (blocked.endsWith(sep)) {
|
|
25
|
+
// 目录:检查是否在该目录下
|
|
26
|
+
if (normalizedPath.startsWith(blocked) || normalizedPath === blocked.slice(0, -1)) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
// 文件:精确匹配
|
|
31
|
+
if (normalizedPath === blocked) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
6
39
|
/**
|
|
7
40
|
* 检查路径是否在白名单内
|
|
8
41
|
* 使用 realpathSync 防止符号链接绕过
|
|
@@ -20,6 +53,11 @@ function checkPath(pathStr) {
|
|
|
20
53
|
const resolved = isAbsolute(pathStr) ? resolve(pathStr) : resolve(config.workdir, pathStr);
|
|
21
54
|
const normalized = normalize(resolved);
|
|
22
55
|
|
|
56
|
+
// 0. 硬阻止检查(优先级最高,不可绕过)
|
|
57
|
+
if (isHardBlocked(normalized)) {
|
|
58
|
+
return { allowed: false, reason: '禁止访问代理系统文件' };
|
|
59
|
+
}
|
|
60
|
+
|
|
23
61
|
const allowedPaths = config.allowedPaths.length > 0 ? config.allowedPaths : [config.workdir];
|
|
24
62
|
|
|
25
63
|
// 先做前缀检查(快速路径,兼容 Windows/Unix)
|
|
@@ -43,14 +81,34 @@ function checkPath(pathStr) {
|
|
|
43
81
|
realPath = realpathSync(resolved);
|
|
44
82
|
} catch (err) {
|
|
45
83
|
if (err.code === 'ENOENT') {
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
84
|
+
// 文件/目录不存在时,向上查找直到找到存在的父目录
|
|
85
|
+
let existing = resolved;
|
|
86
|
+
while (existing !== sep && existing !== '' && existing !== '/') {
|
|
87
|
+
const parent = resolve(existing, '..');
|
|
88
|
+
try {
|
|
89
|
+
realPath = realpathSync(parent) + sep + normalized.slice(resolve(parent).length).replace(/^[/\\]/, '');
|
|
90
|
+
break;
|
|
91
|
+
} catch (parentErr) {
|
|
92
|
+
if (parentErr.code === 'ENOENT') {
|
|
93
|
+
existing = parent;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
return { allowed: false, reason: `无法解析路径: ${parentErr.message}` };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (!realPath) {
|
|
100
|
+
return { allowed: false, reason: `路径检查异常: ${err.message}` };
|
|
101
|
+
}
|
|
49
102
|
} else {
|
|
50
103
|
return { allowed: false, reason: `无法解析路径: ${err.message}` };
|
|
51
104
|
}
|
|
52
105
|
}
|
|
53
106
|
|
|
107
|
+
// 对 realPath 做硬阻止检查(防止通过符号链接绕过)
|
|
108
|
+
if (isHardBlocked(realPath)) {
|
|
109
|
+
return { allowed: false, reason: '禁止访问代理系统文件' };
|
|
110
|
+
}
|
|
111
|
+
|
|
54
112
|
// 对 realPath 再次做前缀检查
|
|
55
113
|
for (const allowed of allowedPaths) {
|
|
56
114
|
const allowedResolved = resolve(allowed);
|
|
@@ -98,7 +156,6 @@ const BLACKLIST_PATTERNS = [
|
|
|
98
156
|
/^\s*(?!(echo|printf|cat|head|tail|wc|ls|pwd|date|whoami|hostname|uname|id|env|printenv|which|type)\s).*`[^`]*`/,
|
|
99
157
|
/^\s*(?!(echo|printf|cat|head|tail|wc|ls|pwd|date|whoami|hostname|uname|id|env|printenv|which|type)\s).*\$\s*\([^)]*\)/,
|
|
100
158
|
];
|
|
101
|
-
|
|
102
159
|
const SCRIPT_EXTENSIONS = '(sh|bash|zsh|py|js|mjs|rb|pl|php|lua)';
|
|
103
160
|
const SCRIPT_INTERPRETERS = '(bash|sh|zsh|python3?|node|ruby|perl|php|lua)';
|
|
104
161
|
|
|
@@ -144,6 +201,17 @@ function checkCommand(command, force = false) {
|
|
|
144
201
|
}
|
|
145
202
|
}
|
|
146
203
|
|
|
204
|
+
// 1.5 禁止访问核心敏感文件(通过命令也一样拦截)
|
|
205
|
+
for (const blocked of HARD_BLOCKED_PATHS) {
|
|
206
|
+
if (trimmed.includes(blocked)) {
|
|
207
|
+
return {
|
|
208
|
+
safe: false,
|
|
209
|
+
level: 'deny',
|
|
210
|
+
reason: '禁止访问代理系统文件'
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
147
215
|
// 2. 强制模式:跳过灰名单检查
|
|
148
216
|
if (force) {
|
|
149
217
|
return { safe: true, level: 'allow' };
|