beast-agent 1.9.0 → 2.0.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/LICENSE +21 -21
- package/README.md +130 -130
- package/bin/beast-agent.js +137 -137
- package/package.json +1 -1
- package/scripts/fix-electron.js +138 -138
- package/scripts/release.js +137 -131
- package/scripts/swap-electron.js +40 -40
- package/src/agent/agentdefs.js +122 -122
- package/src/agent/bots.js +580 -580
- package/src/agent/bus.js +389 -389
- package/src/agent/computeruse.js +200 -200
- package/src/agent/config.js +284 -284
- package/src/agent/discord.js +332 -332
- package/src/agent/engine.js +75 -10
- package/src/agent/kb.js +123 -123
- package/src/agent/llm.js +430 -430
- package/src/agent/logger.js +90 -90
- package/src/agent/mcp.js +427 -427
- package/src/agent/mem0.js +605 -605
- package/src/agent/memory.js +427 -427
- package/src/agent/mqueue.js +124 -124
- package/src/agent/pdf.js +20 -20
- package/src/agent/research.js +133 -133
- package/src/agent/scripts/news.py +113 -113
- package/src/agent/scripts/stealthsearch.py +30 -30
- package/src/agent/scripts/websearch.py +225 -225
- package/src/agent/searxng.js +325 -325
- package/src/agent/seeds/brainstorming/SKILL.md +90 -90
- package/src/agent/seeds/dispatching-parallel-agents/SKILL.md +120 -120
- package/src/agent/seeds/executing-plans/SKILL.md +60 -60
- package/src/agent/seeds/subagent-driven-development/SKILL.md +167 -167
- package/src/agent/seeds/systematic-debugging/SKILL.md +131 -131
- package/src/agent/seeds/test-driven-development/SKILL.md +152 -152
- package/src/agent/seeds/verification-before-completion/SKILL.md +63 -63
- package/src/agent/seeds/writing-plans/SKILL.md +162 -162
- package/src/agent/seeds/writing-skills/SKILL.md +229 -229
- package/src/agent/skills.js +652 -652
- package/src/agent/store.js +378 -378
- package/src/agent/telegram.js +155 -155
- package/src/agent/tokens.js +39 -39
- package/src/agent/usage.js +125 -125
- package/src/agent/watchers.js +312 -312
- package/src/agent/watext.js +80 -80
- package/src/agent/whatsapp.js +555 -555
- package/src/cron.js +255 -255
- package/src/main.js +348 -6
- package/src/preload.js +9 -0
- package/src/renderer/browserPreload.js +73 -73
- package/src/renderer/i18n.js +4 -0
- package/src/renderer/index.html +448 -409
- package/src/renderer/renderer.js +824 -41
- package/src/renderer/style.css +264 -5
package/src/agent/mcp.js
CHANGED
|
@@ -1,427 +1,427 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
/* Beast MCP (Model Context Protocol) istemcisi — el yazımı minimal, sıfır bağımlılık.
|
|
4
|
-
stdio transport: JSON-RPC 2.0, satır-bazlı (newline-delimited).
|
|
5
|
-
|
|
6
|
-
%APPDATA%\beast\mcp.json:
|
|
7
|
-
{
|
|
8
|
-
"servers": {
|
|
9
|
-
"fetch": { "command": "uvx", "args": ["mcp-server-fetch"], "enabled": true },
|
|
10
|
-
"git": { "command": "uvx", "args": ["mcp-server-git", "--repository", "C:/repo"],
|
|
11
|
-
"enabled": true, "tools": ["git_status", "git_log"], "timeoutMs": 60000 }
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
Tool'lar modele `mcp__<server>__<tool>` adıyla açılır; çağrı tools/call'a gider.
|
|
16
|
-
Server çökerse 3 dk cooldown'a girer, sonra otomatik yeniden denenir. */
|
|
17
|
-
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const path = require('path');
|
|
20
|
-
const { spawn } = require('child_process');
|
|
21
|
-
const { beastRoot } = require('./memory');
|
|
22
|
-
const log = require('./logger');
|
|
23
|
-
|
|
24
|
-
const INIT_TIMEOUT_MS = 15000;
|
|
25
|
-
const LIST_TIMEOUT_MS = 15000;
|
|
26
|
-
const CALL_TIMEOUT_MS = 120000;
|
|
27
|
-
const COOLDOWN_MS = 3 * 60 * 1000;
|
|
28
|
-
const MAX_TOOLS_PER_SERVER = 40;
|
|
29
|
-
const MAX_TOOLS_TOTAL = 80;
|
|
30
|
-
const MAX_DESC_LEN = 220;
|
|
31
|
-
|
|
32
|
-
/* server adı → { cfg, proc, nextId, pending:Map, tools:[], status, lastError } */
|
|
33
|
-
const conns = new Map();
|
|
34
|
-
/* başarısız server adı → yeniden deneme zamanı */
|
|
35
|
-
const downUntil = new Map();
|
|
36
|
-
|
|
37
|
-
/* ---------- config ---------- */
|
|
38
|
-
|
|
39
|
-
function configPath() {
|
|
40
|
-
return path.join(beastRoot(), 'mcp.json');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let cfgCache = { mtime: -1, cfg: { servers: {} } };
|
|
44
|
-
|
|
45
|
-
function readConfig(force) {
|
|
46
|
-
try {
|
|
47
|
-
const p = configPath();
|
|
48
|
-
const st = fs.statSync(p);
|
|
49
|
-
if (!force && st.mtimeMs === cfgCache.mtime) return cfgCache.cfg;
|
|
50
|
-
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
51
|
-
const cfg = { servers: {} };
|
|
52
|
-
if (parsed && typeof parsed.servers === 'object') {
|
|
53
|
-
for (const [name, s] of Object.entries(parsed.servers)) {
|
|
54
|
-
if (!s || typeof s !== 'object' || !s.command) continue;
|
|
55
|
-
cfg.servers[sanitizeName(name)] = {
|
|
56
|
-
command: String(s.command),
|
|
57
|
-
args: Array.isArray(s.args) ? s.args.map(String) : [],
|
|
58
|
-
env: s.env && typeof s.env === 'object' ? Object.fromEntries(Object.entries(s.env).map(([k, v]) => [String(k), String(v)])) : {},
|
|
59
|
-
enabled: s.enabled !== false,
|
|
60
|
-
tools: Array.isArray(s.tools) ? s.tools.map(String) : null,
|
|
61
|
-
timeoutMs: Number(s.timeoutMs) > 1000 ? Math.min(Number(s.timeoutMs), 600000) : CALL_TIMEOUT_MS,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
cfgCache = { mtime: st.mtimeMs, cfg };
|
|
66
|
-
return cfg;
|
|
67
|
-
} catch {
|
|
68
|
-
/* dosya yok/bozuk → boş yapı (mtime -1 kalır; dosya oluşturulunca yeniden okunur) */
|
|
69
|
-
try {
|
|
70
|
-
const st = fs.statSync(configPath());
|
|
71
|
-
cfgCache = { mtime: st.mtimeMs, cfg: { servers: {} } };
|
|
72
|
-
} catch {}
|
|
73
|
-
return { servers: {} };
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function saveConfig(cfg) {
|
|
78
|
-
const p = configPath();
|
|
79
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
80
|
-
fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n');
|
|
81
|
-
cfgCache = { mtime: -1, cfg }; /* sonraki okuma diskten gelsin */
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function sanitizeName(name) {
|
|
85
|
-
return String(name || '')
|
|
86
|
-
.toLowerCase()
|
|
87
|
-
.replace(/[^a-z0-9_-]+/g, '-')
|
|
88
|
-
.replace(/^-+|-+$/g, '')
|
|
89
|
-
.slice(0, 32) || 'server';
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/* ---------- süreç yaşam döngüsü ---------- */
|
|
93
|
-
|
|
94
|
-
function spawnServer(cfg) {
|
|
95
|
-
/* Windows: npx/uvx gibi .cmd sarmalayıcıları düz spawn ile başlamaz —
|
|
96
|
-
cmd /d /s /c üzerinden, argümanlar elle tırnaklanarak geç. */
|
|
97
|
-
const quote = (t) => (/[\s"]/u.test(t) ? '"' + t.replace(/"/g, '\\"') + '"' : t);
|
|
98
|
-
const line = [cfg.command, ...cfg.args].map(quote).join(' ');
|
|
99
|
-
const child = spawn(line, {
|
|
100
|
-
shell: true,
|
|
101
|
-
windowsHide: true,
|
|
102
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
103
|
-
env: { ...process.env, ...(cfg.env || {}) },
|
|
104
|
-
});
|
|
105
|
-
return child;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function connFor(name) {
|
|
109
|
-
return conns.get(name);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function killConn(name) {
|
|
113
|
-
const c = conns.get(name);
|
|
114
|
-
if (!c) return;
|
|
115
|
-
conns.delete(name);
|
|
116
|
-
try {
|
|
117
|
-
if (c.timer) clearTimeout(c.timer);
|
|
118
|
-
for (const [, p] of c.pending) {
|
|
119
|
-
try { p.reject(new Error('MCP server kapatıldı')); } catch {}
|
|
120
|
-
}
|
|
121
|
-
c.pending.clear();
|
|
122
|
-
try { c.proc.stdin.end(); } catch {}
|
|
123
|
-
try { c.proc.kill(); } catch {}
|
|
124
|
-
} catch {}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function request(conn, method, params, timeoutMs, signal) {
|
|
128
|
-
return new Promise((resolve, reject) => {
|
|
129
|
-
const id = conn.nextId++;
|
|
130
|
-
const timer = setTimeout(() => {
|
|
131
|
-
conn.pending.delete(id);
|
|
132
|
-
reject(new Error(`MCP ${method} zaman aşımı (${Math.round(timeoutMs / 1000)}s)`));
|
|
133
|
-
}, timeoutMs);
|
|
134
|
-
const onAbort = () => {
|
|
135
|
-
clearTimeout(timer);
|
|
136
|
-
conn.pending.delete(id);
|
|
137
|
-
reject(new Error('iptal edildi'));
|
|
138
|
-
};
|
|
139
|
-
if (signal) {
|
|
140
|
-
if (signal.aborted) return onAbort();
|
|
141
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
142
|
-
}
|
|
143
|
-
conn.pending.set(id, {
|
|
144
|
-
resolve: (v) => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', onAbort); resolve(v); },
|
|
145
|
-
reject: (e) => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', onAbort); reject(e); },
|
|
146
|
-
});
|
|
147
|
-
try {
|
|
148
|
-
conn.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
149
|
-
} catch (e) {
|
|
150
|
-
clearTimeout(timer);
|
|
151
|
-
conn.pending.delete(id);
|
|
152
|
-
reject(e);
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function handleLine(conn, line) {
|
|
158
|
-
if (!line || !line.trim()) return;
|
|
159
|
-
let msg;
|
|
160
|
-
try { msg = JSON.parse(line); } catch { return; }
|
|
161
|
-
if (msg.id !== undefined && msg.id !== null && conn.pending.has(msg.id)) {
|
|
162
|
-
const p = conn.pending.get(msg.id);
|
|
163
|
-
conn.pending.delete(msg.id);
|
|
164
|
-
if (msg.error) p.reject(new Error(String(msg.error.message || JSON.stringify(msg.error)).slice(0, 300)));
|
|
165
|
-
else p.resolve(msg.result);
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
/* bildirimler (id'siz) — şimdilik yok sayılır */
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function startServer(name, cfg) {
|
|
172
|
-
killConn(name);
|
|
173
|
-
const conn = {
|
|
174
|
-
cfg,
|
|
175
|
-
proc: null,
|
|
176
|
-
nextId: 1,
|
|
177
|
-
pending: new Map(),
|
|
178
|
-
tools: [],
|
|
179
|
-
status: 'starting',
|
|
180
|
-
lastError: '',
|
|
181
|
-
stdout: '',
|
|
182
|
-
};
|
|
183
|
-
conns.set(name, conn);
|
|
184
|
-
try {
|
|
185
|
-
conn.proc = spawnServer(cfg);
|
|
186
|
-
} catch (e) {
|
|
187
|
-
conn.status = 'down';
|
|
188
|
-
conn.lastError = String((e && e.message) || e).slice(0, 200);
|
|
189
|
-
downUntil.set(name, Date.now() + COOLDOWN_MS);
|
|
190
|
-
return Promise.resolve(false);
|
|
191
|
-
}
|
|
192
|
-
conn.proc.stdout.setEncoding('utf8');
|
|
193
|
-
conn.proc.stdout.on('data', (chunk) => {
|
|
194
|
-
conn.stdout += chunk;
|
|
195
|
-
let idx;
|
|
196
|
-
while ((idx = conn.stdout.indexOf('\n')) >= 0) {
|
|
197
|
-
const line = conn.stdout.slice(0, idx);
|
|
198
|
-
conn.stdout = conn.stdout.slice(idx + 1);
|
|
199
|
-
try { handleLine(conn, line); } catch {}
|
|
200
|
-
}
|
|
201
|
-
});
|
|
202
|
-
conn.proc.stderr.setEncoding('utf8');
|
|
203
|
-
conn.proc.stderr.on('data', () => {}); /* server logları — yok sayılır */
|
|
204
|
-
const fail = (err) => {
|
|
205
|
-
conn.status = 'down';
|
|
206
|
-
conn.lastError = String((err && err.message) || err).slice(0, 200);
|
|
207
|
-
conn.tools = [];
|
|
208
|
-
downUntil.set(name, Date.now() + COOLDOWN_MS);
|
|
209
|
-
for (const [, p] of conn.pending) { try { p.reject(new Error(conn.lastError)); } catch {} }
|
|
210
|
-
conn.pending.clear();
|
|
211
|
-
try { conn.proc.kill(); } catch {}
|
|
212
|
-
};
|
|
213
|
-
conn.proc.on('error', fail);
|
|
214
|
-
conn.proc.on('exit', (code) => {
|
|
215
|
-
if (conn.status === 'up') {
|
|
216
|
-
fail(new Error(`MCP server '${name}' beklenmedik çıkış yaptı (kod ${code})`));
|
|
217
|
-
} else if (conn.status === 'starting') {
|
|
218
|
-
fail(new Error(`MCP server '${name}' başlamadan kapandı (kod ${code})`));
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
return (async () => {
|
|
222
|
-
try {
|
|
223
|
-
await request(conn, 'initialize', {
|
|
224
|
-
protocolVersion: '2024-11-05',
|
|
225
|
-
capabilities: {},
|
|
226
|
-
clientInfo: { name: 'beast-agent', version: '1.9.0' },
|
|
227
|
-
}, INIT_TIMEOUT_MS);
|
|
228
|
-
try {
|
|
229
|
-
conn.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
|
|
230
|
-
} catch {}
|
|
231
|
-
const listed = await request(conn, 'tools/list', {}, LIST_TIMEOUT_MS);
|
|
232
|
-
const tools = Array.isArray(listed && listed.tools) ? listed.tools : [];
|
|
233
|
-
conn.tools = tools.slice(0, MAX_TOOLS_PER_SERVER).map((t) => ({
|
|
234
|
-
name: String(t.name || ''),
|
|
235
|
-
description: String(t.description || '').slice(0, MAX_DESC_LEN),
|
|
236
|
-
inputSchema: t.inputSchema && typeof t.inputSchema === 'object' ? t.inputSchema : { type: 'object', properties: {} },
|
|
237
|
-
}));
|
|
238
|
-
conn.status = 'up';
|
|
239
|
-
downUntil.delete(name);
|
|
240
|
-
log.info(`[mcp] '${name}' bağlı — ${conn.tools.length} araç`);
|
|
241
|
-
return true;
|
|
242
|
-
} catch (e) {
|
|
243
|
-
fail(e);
|
|
244
|
-
log.warn(`[mcp] '${name}' bağlanamadı: ${conn.lastError}`);
|
|
245
|
-
return false;
|
|
246
|
-
}
|
|
247
|
-
})();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/* Tüm etkin serverlara bağlan (zaten bağlılar hariç; cooldown'dakiler atlanır).
|
|
251
|
-
Toplam `waitMs`'ten uzun beklemez — hangisi hazır değilse bu turda öyle kalır. */
|
|
252
|
-
async function ensureAll(waitMs = 8000) {
|
|
253
|
-
const cfg = readConfig();
|
|
254
|
-
const kicks = [];
|
|
255
|
-
for (const [name, s] of Object.entries(cfg.servers)) {
|
|
256
|
-
if (!s.enabled) continue;
|
|
257
|
-
if (conns.has(name) || (downUntil.get(name) || 0) > Date.now()) continue;
|
|
258
|
-
kicks.push(startServer(name, s));
|
|
259
|
-
}
|
|
260
|
-
if (!kicks.length) return;
|
|
261
|
-
await Promise.race([
|
|
262
|
-
Promise.allSettled(kicks),
|
|
263
|
-
new Promise((r) => setTimeout(r, waitMs)),
|
|
264
|
-
]);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/* ---------- şemalar ---------- */
|
|
268
|
-
|
|
269
|
-
function toolFullNames(cfg, serverName, tools) {
|
|
270
|
-
let list = tools;
|
|
271
|
-
if (cfg.tools && cfg.tools.length) {
|
|
272
|
-
const allow = new Set(cfg.tools);
|
|
273
|
-
list = tools.filter((t) => allow.has(t.name));
|
|
274
|
-
}
|
|
275
|
-
return list;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function toolSchemas() {
|
|
279
|
-
const cfg = readConfig();
|
|
280
|
-
const out = [];
|
|
281
|
-
for (const [name, conn] of conns) {
|
|
282
|
-
if (conn.status !== 'up' || !conn.tools.length) continue;
|
|
283
|
-
const s = cfg.servers[name] || {};
|
|
284
|
-
const list = toolFullNames(s, name, conn.tools);
|
|
285
|
-
for (const t of list) {
|
|
286
|
-
out.push({
|
|
287
|
-
type: 'function',
|
|
288
|
-
function: {
|
|
289
|
-
name: `mcp__${name}__${t.name}`,
|
|
290
|
-
description: `[mcp:${name}] ${t.description}`.trim(),
|
|
291
|
-
parameters: t.inputSchema,
|
|
292
|
-
},
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return out.slice(0, MAX_TOOLS_TOTAL);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/* engine._chatTurn başında çağrılır: MCP araç şemalarını listeye ekler */
|
|
300
|
-
async function mergeTools(toolsList) {
|
|
301
|
-
try {
|
|
302
|
-
if (!readConfig().servers || !Object.keys(readConfig().servers).length) return toolsList;
|
|
303
|
-
if (toolsList.some((t) => t && t.function && String(t.function.name).startsWith('mcp__'))) return toolsList;
|
|
304
|
-
await ensureAll();
|
|
305
|
-
const extra = toolSchemas();
|
|
306
|
-
return extra.length ? toolsList.concat(extra) : toolsList;
|
|
307
|
-
} catch {
|
|
308
|
-
return toolsList;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/* ---------- çağrı ---------- */
|
|
313
|
-
|
|
314
|
-
function parseFullName(fullName) {
|
|
315
|
-
const m = String(fullName || '').match(/^mcp__([a-z0-9_-]{1,32})__([A-Za-z0-9_.-]{1,64})$/);
|
|
316
|
-
return m ? { server: m[1], tool: m[2] } : null;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function extractText(result) {
|
|
320
|
-
if (!result) return '';
|
|
321
|
-
const content = Array.isArray(result.content) ? result.content : [];
|
|
322
|
-
const parts = [];
|
|
323
|
-
for (const c of content) {
|
|
324
|
-
if (!c || typeof c !== 'object') continue;
|
|
325
|
-
if (c.type === 'text' && typeof c.text === 'string') parts.push(c.text);
|
|
326
|
-
else if (c.type === 'resource') parts.push(JSON.stringify(c.resource || {}).slice(0, 4000));
|
|
327
|
-
else parts.push(JSON.stringify(c).slice(0, 2000));
|
|
328
|
-
}
|
|
329
|
-
return parts.join('\n').slice(0, 60000);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
async function call(fullName, args, signal) {
|
|
333
|
-
const parsed = parseFullName(fullName);
|
|
334
|
-
if (!parsed) return { ok: false, error: `geçersiz MCP araç adı: ${fullName}` };
|
|
335
|
-
const { server, tool } = parsed;
|
|
336
|
-
const cfg = readConfig();
|
|
337
|
-
const scfg = cfg.servers[server];
|
|
338
|
-
if (!scfg || !scfg.enabled) return { ok: false, error: `MCP server '${server}' kapalı` };
|
|
339
|
-
let conn = conns.get(server);
|
|
340
|
-
if (!conn || conn.status !== 'up') {
|
|
341
|
-
if ((downUntil.get(server) || 0) > Date.now()) {
|
|
342
|
-
return { ok: false, error: `MCP server '${server}' şu an erişilemiyor (${conn ? conn.lastError : 'cooldown'}) — az sonra yeniden dene` };
|
|
343
|
-
}
|
|
344
|
-
await ensureAll(15000);
|
|
345
|
-
conn = conns.get(server);
|
|
346
|
-
}
|
|
347
|
-
if (!conn || conn.status !== 'up') {
|
|
348
|
-
return { ok: false, error: `MCP server '${server}' bağlı değil` };
|
|
349
|
-
}
|
|
350
|
-
if (scfg.tools && scfg.tools.length && !scfg.tools.includes(tool)) {
|
|
351
|
-
return { ok: false, error: `araç '${tool}' server '${server}' için yetkili listede değil` };
|
|
352
|
-
}
|
|
353
|
-
try {
|
|
354
|
-
const result = await request(conn, 'tools/call', { name: tool, arguments: args || {} }, scfg.timeoutMs || CALL_TIMEOUT_MS, signal);
|
|
355
|
-
const text = extractText(result);
|
|
356
|
-
if (result && result.isError) return { ok: false, error: text.slice(0, 4000) || 'MCP araç hatası' };
|
|
357
|
-
return { ok: true, result: text };
|
|
358
|
-
} catch (e) {
|
|
359
|
-
const msg = String((e && e.message) || e);
|
|
360
|
-
/* ölü süreç → sonraki turda yeniden doğsun */
|
|
361
|
-
if (conn.status !== 'up') killConn(server);
|
|
362
|
-
return { ok: false, error: `MCP çağrısı başarısız: ${msg.slice(0, 300)}` };
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/* ---------- yönetim (settings UI) ---------- */
|
|
367
|
-
|
|
368
|
-
function status() {
|
|
369
|
-
const cfg = readConfig(true);
|
|
370
|
-
const now = Date.now();
|
|
371
|
-
const servers = [];
|
|
372
|
-
for (const [name, s] of Object.entries(cfg.servers)) {
|
|
373
|
-
const conn = conns.get(name);
|
|
374
|
-
const down = (downUntil.get(name) || 0) > now;
|
|
375
|
-
servers.push({
|
|
376
|
-
name,
|
|
377
|
-
command: s.command,
|
|
378
|
-
args: s.args,
|
|
379
|
-
enabled: !!s.enabled,
|
|
380
|
-
tools: s.tools || null,
|
|
381
|
-
timeoutMs: s.timeoutMs,
|
|
382
|
-
state: !s.enabled ? 'disabled' : conn ? conn.status : down ? 'down' : 'idle',
|
|
383
|
-
toolCount: conn ? conn.tools.length : 0,
|
|
384
|
-
toolNames: conn ? conn.tools.map((t) => t.name) : [],
|
|
385
|
-
lastError: conn ? conn.lastError : '',
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
return { path: configPath(), servers };
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/* Tek serverı yeniden başlat (settings "yenile" düğmesi) */
|
|
392
|
-
async function refresh(name) {
|
|
393
|
-
const cfg = readConfig(true);
|
|
394
|
-
const s = cfg.servers[sanitizeName(name)];
|
|
395
|
-
if (!s || !s.enabled) return false;
|
|
396
|
-
downUntil.delete(name);
|
|
397
|
-
await startServer(sanitizeName(name), s);
|
|
398
|
-
return true;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function stopAll() {
|
|
402
|
-
for (const name of Array.from(conns.keys())) killConn(name);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/* test/ayalar: tüm bağlantı ve cooldown durumunu sıfırla */
|
|
406
|
-
function _reset() {
|
|
407
|
-
stopAll();
|
|
408
|
-
downUntil.clear();
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
process.on('exit', stopAll);
|
|
412
|
-
|
|
413
|
-
module.exports = {
|
|
414
|
-
configPath,
|
|
415
|
-
readConfig,
|
|
416
|
-
saveConfig,
|
|
417
|
-
mergeTools,
|
|
418
|
-
toolSchemas,
|
|
419
|
-
call,
|
|
420
|
-
status,
|
|
421
|
-
refresh,
|
|
422
|
-
stopAll,
|
|
423
|
-
_reset,
|
|
424
|
-
parseFullName,
|
|
425
|
-
extractText,
|
|
426
|
-
sanitizeName,
|
|
427
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/* Beast MCP (Model Context Protocol) istemcisi — el yazımı minimal, sıfır bağımlılık.
|
|
4
|
+
stdio transport: JSON-RPC 2.0, satır-bazlı (newline-delimited).
|
|
5
|
+
|
|
6
|
+
%APPDATA%\beast\mcp.json:
|
|
7
|
+
{
|
|
8
|
+
"servers": {
|
|
9
|
+
"fetch": { "command": "uvx", "args": ["mcp-server-fetch"], "enabled": true },
|
|
10
|
+
"git": { "command": "uvx", "args": ["mcp-server-git", "--repository", "C:/repo"],
|
|
11
|
+
"enabled": true, "tools": ["git_status", "git_log"], "timeoutMs": 60000 }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Tool'lar modele `mcp__<server>__<tool>` adıyla açılır; çağrı tools/call'a gider.
|
|
16
|
+
Server çökerse 3 dk cooldown'a girer, sonra otomatik yeniden denenir. */
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { spawn } = require('child_process');
|
|
21
|
+
const { beastRoot } = require('./memory');
|
|
22
|
+
const log = require('./logger');
|
|
23
|
+
|
|
24
|
+
const INIT_TIMEOUT_MS = 15000;
|
|
25
|
+
const LIST_TIMEOUT_MS = 15000;
|
|
26
|
+
const CALL_TIMEOUT_MS = 120000;
|
|
27
|
+
const COOLDOWN_MS = 3 * 60 * 1000;
|
|
28
|
+
const MAX_TOOLS_PER_SERVER = 40;
|
|
29
|
+
const MAX_TOOLS_TOTAL = 80;
|
|
30
|
+
const MAX_DESC_LEN = 220;
|
|
31
|
+
|
|
32
|
+
/* server adı → { cfg, proc, nextId, pending:Map, tools:[], status, lastError } */
|
|
33
|
+
const conns = new Map();
|
|
34
|
+
/* başarısız server adı → yeniden deneme zamanı */
|
|
35
|
+
const downUntil = new Map();
|
|
36
|
+
|
|
37
|
+
/* ---------- config ---------- */
|
|
38
|
+
|
|
39
|
+
function configPath() {
|
|
40
|
+
return path.join(beastRoot(), 'mcp.json');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let cfgCache = { mtime: -1, cfg: { servers: {} } };
|
|
44
|
+
|
|
45
|
+
function readConfig(force) {
|
|
46
|
+
try {
|
|
47
|
+
const p = configPath();
|
|
48
|
+
const st = fs.statSync(p);
|
|
49
|
+
if (!force && st.mtimeMs === cfgCache.mtime) return cfgCache.cfg;
|
|
50
|
+
const parsed = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
51
|
+
const cfg = { servers: {} };
|
|
52
|
+
if (parsed && typeof parsed.servers === 'object') {
|
|
53
|
+
for (const [name, s] of Object.entries(parsed.servers)) {
|
|
54
|
+
if (!s || typeof s !== 'object' || !s.command) continue;
|
|
55
|
+
cfg.servers[sanitizeName(name)] = {
|
|
56
|
+
command: String(s.command),
|
|
57
|
+
args: Array.isArray(s.args) ? s.args.map(String) : [],
|
|
58
|
+
env: s.env && typeof s.env === 'object' ? Object.fromEntries(Object.entries(s.env).map(([k, v]) => [String(k), String(v)])) : {},
|
|
59
|
+
enabled: s.enabled !== false,
|
|
60
|
+
tools: Array.isArray(s.tools) ? s.tools.map(String) : null,
|
|
61
|
+
timeoutMs: Number(s.timeoutMs) > 1000 ? Math.min(Number(s.timeoutMs), 600000) : CALL_TIMEOUT_MS,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
cfgCache = { mtime: st.mtimeMs, cfg };
|
|
66
|
+
return cfg;
|
|
67
|
+
} catch {
|
|
68
|
+
/* dosya yok/bozuk → boş yapı (mtime -1 kalır; dosya oluşturulunca yeniden okunur) */
|
|
69
|
+
try {
|
|
70
|
+
const st = fs.statSync(configPath());
|
|
71
|
+
cfgCache = { mtime: st.mtimeMs, cfg: { servers: {} } };
|
|
72
|
+
} catch {}
|
|
73
|
+
return { servers: {} };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function saveConfig(cfg) {
|
|
78
|
+
const p = configPath();
|
|
79
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
80
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 2) + '\n');
|
|
81
|
+
cfgCache = { mtime: -1, cfg }; /* sonraki okuma diskten gelsin */
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function sanitizeName(name) {
|
|
85
|
+
return String(name || '')
|
|
86
|
+
.toLowerCase()
|
|
87
|
+
.replace(/[^a-z0-9_-]+/g, '-')
|
|
88
|
+
.replace(/^-+|-+$/g, '')
|
|
89
|
+
.slice(0, 32) || 'server';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ---------- süreç yaşam döngüsü ---------- */
|
|
93
|
+
|
|
94
|
+
function spawnServer(cfg) {
|
|
95
|
+
/* Windows: npx/uvx gibi .cmd sarmalayıcıları düz spawn ile başlamaz —
|
|
96
|
+
cmd /d /s /c üzerinden, argümanlar elle tırnaklanarak geç. */
|
|
97
|
+
const quote = (t) => (/[\s"]/u.test(t) ? '"' + t.replace(/"/g, '\\"') + '"' : t);
|
|
98
|
+
const line = [cfg.command, ...cfg.args].map(quote).join(' ');
|
|
99
|
+
const child = spawn(line, {
|
|
100
|
+
shell: true,
|
|
101
|
+
windowsHide: true,
|
|
102
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
103
|
+
env: { ...process.env, ...(cfg.env || {}) },
|
|
104
|
+
});
|
|
105
|
+
return child;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function connFor(name) {
|
|
109
|
+
return conns.get(name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function killConn(name) {
|
|
113
|
+
const c = conns.get(name);
|
|
114
|
+
if (!c) return;
|
|
115
|
+
conns.delete(name);
|
|
116
|
+
try {
|
|
117
|
+
if (c.timer) clearTimeout(c.timer);
|
|
118
|
+
for (const [, p] of c.pending) {
|
|
119
|
+
try { p.reject(new Error('MCP server kapatıldı')); } catch {}
|
|
120
|
+
}
|
|
121
|
+
c.pending.clear();
|
|
122
|
+
try { c.proc.stdin.end(); } catch {}
|
|
123
|
+
try { c.proc.kill(); } catch {}
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function request(conn, method, params, timeoutMs, signal) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const id = conn.nextId++;
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
conn.pending.delete(id);
|
|
132
|
+
reject(new Error(`MCP ${method} zaman aşımı (${Math.round(timeoutMs / 1000)}s)`));
|
|
133
|
+
}, timeoutMs);
|
|
134
|
+
const onAbort = () => {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
conn.pending.delete(id);
|
|
137
|
+
reject(new Error('iptal edildi'));
|
|
138
|
+
};
|
|
139
|
+
if (signal) {
|
|
140
|
+
if (signal.aborted) return onAbort();
|
|
141
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
142
|
+
}
|
|
143
|
+
conn.pending.set(id, {
|
|
144
|
+
resolve: (v) => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', onAbort); resolve(v); },
|
|
145
|
+
reject: (e) => { clearTimeout(timer); if (signal) signal.removeEventListener('abort', onAbort); reject(e); },
|
|
146
|
+
});
|
|
147
|
+
try {
|
|
148
|
+
conn.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n');
|
|
149
|
+
} catch (e) {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
conn.pending.delete(id);
|
|
152
|
+
reject(e);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function handleLine(conn, line) {
|
|
158
|
+
if (!line || !line.trim()) return;
|
|
159
|
+
let msg;
|
|
160
|
+
try { msg = JSON.parse(line); } catch { return; }
|
|
161
|
+
if (msg.id !== undefined && msg.id !== null && conn.pending.has(msg.id)) {
|
|
162
|
+
const p = conn.pending.get(msg.id);
|
|
163
|
+
conn.pending.delete(msg.id);
|
|
164
|
+
if (msg.error) p.reject(new Error(String(msg.error.message || JSON.stringify(msg.error)).slice(0, 300)));
|
|
165
|
+
else p.resolve(msg.result);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
/* bildirimler (id'siz) — şimdilik yok sayılır */
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function startServer(name, cfg) {
|
|
172
|
+
killConn(name);
|
|
173
|
+
const conn = {
|
|
174
|
+
cfg,
|
|
175
|
+
proc: null,
|
|
176
|
+
nextId: 1,
|
|
177
|
+
pending: new Map(),
|
|
178
|
+
tools: [],
|
|
179
|
+
status: 'starting',
|
|
180
|
+
lastError: '',
|
|
181
|
+
stdout: '',
|
|
182
|
+
};
|
|
183
|
+
conns.set(name, conn);
|
|
184
|
+
try {
|
|
185
|
+
conn.proc = spawnServer(cfg);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
conn.status = 'down';
|
|
188
|
+
conn.lastError = String((e && e.message) || e).slice(0, 200);
|
|
189
|
+
downUntil.set(name, Date.now() + COOLDOWN_MS);
|
|
190
|
+
return Promise.resolve(false);
|
|
191
|
+
}
|
|
192
|
+
conn.proc.stdout.setEncoding('utf8');
|
|
193
|
+
conn.proc.stdout.on('data', (chunk) => {
|
|
194
|
+
conn.stdout += chunk;
|
|
195
|
+
let idx;
|
|
196
|
+
while ((idx = conn.stdout.indexOf('\n')) >= 0) {
|
|
197
|
+
const line = conn.stdout.slice(0, idx);
|
|
198
|
+
conn.stdout = conn.stdout.slice(idx + 1);
|
|
199
|
+
try { handleLine(conn, line); } catch {}
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
conn.proc.stderr.setEncoding('utf8');
|
|
203
|
+
conn.proc.stderr.on('data', () => {}); /* server logları — yok sayılır */
|
|
204
|
+
const fail = (err) => {
|
|
205
|
+
conn.status = 'down';
|
|
206
|
+
conn.lastError = String((err && err.message) || err).slice(0, 200);
|
|
207
|
+
conn.tools = [];
|
|
208
|
+
downUntil.set(name, Date.now() + COOLDOWN_MS);
|
|
209
|
+
for (const [, p] of conn.pending) { try { p.reject(new Error(conn.lastError)); } catch {} }
|
|
210
|
+
conn.pending.clear();
|
|
211
|
+
try { conn.proc.kill(); } catch {}
|
|
212
|
+
};
|
|
213
|
+
conn.proc.on('error', fail);
|
|
214
|
+
conn.proc.on('exit', (code) => {
|
|
215
|
+
if (conn.status === 'up') {
|
|
216
|
+
fail(new Error(`MCP server '${name}' beklenmedik çıkış yaptı (kod ${code})`));
|
|
217
|
+
} else if (conn.status === 'starting') {
|
|
218
|
+
fail(new Error(`MCP server '${name}' başlamadan kapandı (kod ${code})`));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return (async () => {
|
|
222
|
+
try {
|
|
223
|
+
await request(conn, 'initialize', {
|
|
224
|
+
protocolVersion: '2024-11-05',
|
|
225
|
+
capabilities: {},
|
|
226
|
+
clientInfo: { name: 'beast-agent', version: '1.9.0' },
|
|
227
|
+
}, INIT_TIMEOUT_MS);
|
|
228
|
+
try {
|
|
229
|
+
conn.proc.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
|
|
230
|
+
} catch {}
|
|
231
|
+
const listed = await request(conn, 'tools/list', {}, LIST_TIMEOUT_MS);
|
|
232
|
+
const tools = Array.isArray(listed && listed.tools) ? listed.tools : [];
|
|
233
|
+
conn.tools = tools.slice(0, MAX_TOOLS_PER_SERVER).map((t) => ({
|
|
234
|
+
name: String(t.name || ''),
|
|
235
|
+
description: String(t.description || '').slice(0, MAX_DESC_LEN),
|
|
236
|
+
inputSchema: t.inputSchema && typeof t.inputSchema === 'object' ? t.inputSchema : { type: 'object', properties: {} },
|
|
237
|
+
}));
|
|
238
|
+
conn.status = 'up';
|
|
239
|
+
downUntil.delete(name);
|
|
240
|
+
log.info(`[mcp] '${name}' bağlı — ${conn.tools.length} araç`);
|
|
241
|
+
return true;
|
|
242
|
+
} catch (e) {
|
|
243
|
+
fail(e);
|
|
244
|
+
log.warn(`[mcp] '${name}' bağlanamadı: ${conn.lastError}`);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
})();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* Tüm etkin serverlara bağlan (zaten bağlılar hariç; cooldown'dakiler atlanır).
|
|
251
|
+
Toplam `waitMs`'ten uzun beklemez — hangisi hazır değilse bu turda öyle kalır. */
|
|
252
|
+
async function ensureAll(waitMs = 8000) {
|
|
253
|
+
const cfg = readConfig();
|
|
254
|
+
const kicks = [];
|
|
255
|
+
for (const [name, s] of Object.entries(cfg.servers)) {
|
|
256
|
+
if (!s.enabled) continue;
|
|
257
|
+
if (conns.has(name) || (downUntil.get(name) || 0) > Date.now()) continue;
|
|
258
|
+
kicks.push(startServer(name, s));
|
|
259
|
+
}
|
|
260
|
+
if (!kicks.length) return;
|
|
261
|
+
await Promise.race([
|
|
262
|
+
Promise.allSettled(kicks),
|
|
263
|
+
new Promise((r) => setTimeout(r, waitMs)),
|
|
264
|
+
]);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* ---------- şemalar ---------- */
|
|
268
|
+
|
|
269
|
+
function toolFullNames(cfg, serverName, tools) {
|
|
270
|
+
let list = tools;
|
|
271
|
+
if (cfg.tools && cfg.tools.length) {
|
|
272
|
+
const allow = new Set(cfg.tools);
|
|
273
|
+
list = tools.filter((t) => allow.has(t.name));
|
|
274
|
+
}
|
|
275
|
+
return list;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function toolSchemas() {
|
|
279
|
+
const cfg = readConfig();
|
|
280
|
+
const out = [];
|
|
281
|
+
for (const [name, conn] of conns) {
|
|
282
|
+
if (conn.status !== 'up' || !conn.tools.length) continue;
|
|
283
|
+
const s = cfg.servers[name] || {};
|
|
284
|
+
const list = toolFullNames(s, name, conn.tools);
|
|
285
|
+
for (const t of list) {
|
|
286
|
+
out.push({
|
|
287
|
+
type: 'function',
|
|
288
|
+
function: {
|
|
289
|
+
name: `mcp__${name}__${t.name}`,
|
|
290
|
+
description: `[mcp:${name}] ${t.description}`.trim(),
|
|
291
|
+
parameters: t.inputSchema,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return out.slice(0, MAX_TOOLS_TOTAL);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* engine._chatTurn başında çağrılır: MCP araç şemalarını listeye ekler */
|
|
300
|
+
async function mergeTools(toolsList) {
|
|
301
|
+
try {
|
|
302
|
+
if (!readConfig().servers || !Object.keys(readConfig().servers).length) return toolsList;
|
|
303
|
+
if (toolsList.some((t) => t && t.function && String(t.function.name).startsWith('mcp__'))) return toolsList;
|
|
304
|
+
await ensureAll();
|
|
305
|
+
const extra = toolSchemas();
|
|
306
|
+
return extra.length ? toolsList.concat(extra) : toolsList;
|
|
307
|
+
} catch {
|
|
308
|
+
return toolsList;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* ---------- çağrı ---------- */
|
|
313
|
+
|
|
314
|
+
function parseFullName(fullName) {
|
|
315
|
+
const m = String(fullName || '').match(/^mcp__([a-z0-9_-]{1,32})__([A-Za-z0-9_.-]{1,64})$/);
|
|
316
|
+
return m ? { server: m[1], tool: m[2] } : null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function extractText(result) {
|
|
320
|
+
if (!result) return '';
|
|
321
|
+
const content = Array.isArray(result.content) ? result.content : [];
|
|
322
|
+
const parts = [];
|
|
323
|
+
for (const c of content) {
|
|
324
|
+
if (!c || typeof c !== 'object') continue;
|
|
325
|
+
if (c.type === 'text' && typeof c.text === 'string') parts.push(c.text);
|
|
326
|
+
else if (c.type === 'resource') parts.push(JSON.stringify(c.resource || {}).slice(0, 4000));
|
|
327
|
+
else parts.push(JSON.stringify(c).slice(0, 2000));
|
|
328
|
+
}
|
|
329
|
+
return parts.join('\n').slice(0, 60000);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function call(fullName, args, signal) {
|
|
333
|
+
const parsed = parseFullName(fullName);
|
|
334
|
+
if (!parsed) return { ok: false, error: `geçersiz MCP araç adı: ${fullName}` };
|
|
335
|
+
const { server, tool } = parsed;
|
|
336
|
+
const cfg = readConfig();
|
|
337
|
+
const scfg = cfg.servers[server];
|
|
338
|
+
if (!scfg || !scfg.enabled) return { ok: false, error: `MCP server '${server}' kapalı` };
|
|
339
|
+
let conn = conns.get(server);
|
|
340
|
+
if (!conn || conn.status !== 'up') {
|
|
341
|
+
if ((downUntil.get(server) || 0) > Date.now()) {
|
|
342
|
+
return { ok: false, error: `MCP server '${server}' şu an erişilemiyor (${conn ? conn.lastError : 'cooldown'}) — az sonra yeniden dene` };
|
|
343
|
+
}
|
|
344
|
+
await ensureAll(15000);
|
|
345
|
+
conn = conns.get(server);
|
|
346
|
+
}
|
|
347
|
+
if (!conn || conn.status !== 'up') {
|
|
348
|
+
return { ok: false, error: `MCP server '${server}' bağlı değil` };
|
|
349
|
+
}
|
|
350
|
+
if (scfg.tools && scfg.tools.length && !scfg.tools.includes(tool)) {
|
|
351
|
+
return { ok: false, error: `araç '${tool}' server '${server}' için yetkili listede değil` };
|
|
352
|
+
}
|
|
353
|
+
try {
|
|
354
|
+
const result = await request(conn, 'tools/call', { name: tool, arguments: args || {} }, scfg.timeoutMs || CALL_TIMEOUT_MS, signal);
|
|
355
|
+
const text = extractText(result);
|
|
356
|
+
if (result && result.isError) return { ok: false, error: text.slice(0, 4000) || 'MCP araç hatası' };
|
|
357
|
+
return { ok: true, result: text };
|
|
358
|
+
} catch (e) {
|
|
359
|
+
const msg = String((e && e.message) || e);
|
|
360
|
+
/* ölü süreç → sonraki turda yeniden doğsun */
|
|
361
|
+
if (conn.status !== 'up') killConn(server);
|
|
362
|
+
return { ok: false, error: `MCP çağrısı başarısız: ${msg.slice(0, 300)}` };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* ---------- yönetim (settings UI) ---------- */
|
|
367
|
+
|
|
368
|
+
function status() {
|
|
369
|
+
const cfg = readConfig(true);
|
|
370
|
+
const now = Date.now();
|
|
371
|
+
const servers = [];
|
|
372
|
+
for (const [name, s] of Object.entries(cfg.servers)) {
|
|
373
|
+
const conn = conns.get(name);
|
|
374
|
+
const down = (downUntil.get(name) || 0) > now;
|
|
375
|
+
servers.push({
|
|
376
|
+
name,
|
|
377
|
+
command: s.command,
|
|
378
|
+
args: s.args,
|
|
379
|
+
enabled: !!s.enabled,
|
|
380
|
+
tools: s.tools || null,
|
|
381
|
+
timeoutMs: s.timeoutMs,
|
|
382
|
+
state: !s.enabled ? 'disabled' : conn ? conn.status : down ? 'down' : 'idle',
|
|
383
|
+
toolCount: conn ? conn.tools.length : 0,
|
|
384
|
+
toolNames: conn ? conn.tools.map((t) => t.name) : [],
|
|
385
|
+
lastError: conn ? conn.lastError : '',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return { path: configPath(), servers };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/* Tek serverı yeniden başlat (settings "yenile" düğmesi) */
|
|
392
|
+
async function refresh(name) {
|
|
393
|
+
const cfg = readConfig(true);
|
|
394
|
+
const s = cfg.servers[sanitizeName(name)];
|
|
395
|
+
if (!s || !s.enabled) return false;
|
|
396
|
+
downUntil.delete(name);
|
|
397
|
+
await startServer(sanitizeName(name), s);
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function stopAll() {
|
|
402
|
+
for (const name of Array.from(conns.keys())) killConn(name);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* test/ayalar: tüm bağlantı ve cooldown durumunu sıfırla */
|
|
406
|
+
function _reset() {
|
|
407
|
+
stopAll();
|
|
408
|
+
downUntil.clear();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
process.on('exit', stopAll);
|
|
412
|
+
|
|
413
|
+
module.exports = {
|
|
414
|
+
configPath,
|
|
415
|
+
readConfig,
|
|
416
|
+
saveConfig,
|
|
417
|
+
mergeTools,
|
|
418
|
+
toolSchemas,
|
|
419
|
+
call,
|
|
420
|
+
status,
|
|
421
|
+
refresh,
|
|
422
|
+
stopAll,
|
|
423
|
+
_reset,
|
|
424
|
+
parseFullName,
|
|
425
|
+
extractText,
|
|
426
|
+
sanitizeName,
|
|
427
|
+
};
|