neoagent 2.4.1-beta.34 → 2.4.1-beta.36
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/.env.example +10 -3
- package/flutter_app/lib/main_admin.dart +13 -0
- package/flutter_app/lib/main_controller.dart +24 -6
- package/flutter_app/lib/main_models.dart +14 -0
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +13767 -13696
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +1 -0
- package/server/services/ai/engine.js +35 -6
- package/server/services/ai/systemPrompt.js +28 -22
- package/server/services/ai/taskAnalysis.js +4 -1
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +12 -3
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/manager.js +51 -53
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/tasks/runtime.js +133 -21
|
@@ -1,137 +1,154 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const EventEmitter = require('events');
|
|
4
|
-
const crypto = require('crypto');
|
|
5
4
|
const db = require('../../db/database');
|
|
6
5
|
const { Client } = require('@modelcontextprotocol/sdk/client/index.js');
|
|
7
6
|
const { SSEClientTransport } = require('@modelcontextprotocol/sdk/client/sse.js');
|
|
8
7
|
const { validateRemoteMcpEndpoint } = require('../runtime/mcp');
|
|
8
|
+
const {
|
|
9
|
+
buildTransportOptions,
|
|
10
|
+
extractErrorMessage,
|
|
11
|
+
normalizeServerId,
|
|
12
|
+
} = require('./client_support');
|
|
13
|
+
const recovery = require('./recovery');
|
|
14
|
+
const toolOperations = require('./tool_operations');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_RECONNECT_DELAY_MS = 60_000;
|
|
17
|
+
const DEFAULT_MAX_RECONNECT_DELAY_MS = 15 * 60_000;
|
|
18
|
+
const DEFAULT_OPERATION_TIMEOUT_MS = 30_000;
|
|
9
19
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
state() {
|
|
30
|
-
return `${this.serverId}::${crypto.randomBytes(16).toString('hex')}`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
clientInformation() {
|
|
34
|
-
return { client_id: this.clientId };
|
|
20
|
+
class MCPClient extends EventEmitter {
|
|
21
|
+
constructor(options = {}) {
|
|
22
|
+
super();
|
|
23
|
+
this.servers = new Map();
|
|
24
|
+
this._reconnectTimers = new Map();
|
|
25
|
+
this._reconnectAttempts = new Map();
|
|
26
|
+
this._lifecycleVersions = new Map();
|
|
27
|
+
this._isShuttingDown = false;
|
|
28
|
+
this._reconnectDelayMs = options.reconnectDelayMs ?? DEFAULT_RECONNECT_DELAY_MS;
|
|
29
|
+
this._maxReconnectDelayMs = options.maxReconnectDelayMs ?? DEFAULT_MAX_RECONNECT_DELAY_MS;
|
|
30
|
+
this._operationTimeoutMs = options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS;
|
|
31
|
+
this._toolFailureThreshold = options.toolFailureThreshold ?? 3;
|
|
32
|
+
this._createTransport = options.createTransport
|
|
33
|
+
|| ((endpoint, transportOptions) => new SSEClientTransport(new URL(endpoint), transportOptions));
|
|
34
|
+
this._createClient = options.createClient
|
|
35
|
+
|| (() => new Client(
|
|
36
|
+
{ name: 'NeoAgent', version: '1.0.0' },
|
|
37
|
+
{ capabilities: { tools: {} } },
|
|
38
|
+
));
|
|
35
39
|
}
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
_nextLifecycleVersion(serverId) {
|
|
42
|
+
serverId = normalizeServerId(serverId);
|
|
43
|
+
const version = (this._lifecycleVersions.get(serverId) || 0) + 1;
|
|
44
|
+
this._lifecycleVersions.set(serverId, version);
|
|
45
|
+
return version;
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
_isCurrentLifecycle(serverId, server, version) {
|
|
49
|
+
serverId = normalizeServerId(serverId);
|
|
50
|
+
return (
|
|
51
|
+
!this._isShuttingDown
|
|
52
|
+
&& this._lifecycleVersions.get(serverId) === version
|
|
53
|
+
&& this.servers.get(serverId) === server
|
|
54
|
+
);
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
_cancelReconnect(serverId) {
|
|
58
|
+
serverId = normalizeServerId(serverId);
|
|
59
|
+
const timer = this._reconnectTimers.get(serverId);
|
|
60
|
+
if (!timer) return;
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
this._reconnectTimers.delete(serverId);
|
|
48
63
|
}
|
|
49
64
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
config.auth.tokens = tokens;
|
|
54
|
-
this._saveConfig(config);
|
|
65
|
+
_persistStatus(serverId, status) {
|
|
66
|
+
serverId = normalizeServerId(serverId);
|
|
67
|
+
db.prepare('UPDATE mcp_servers SET status = ? WHERE id = ?').run(status, serverId);
|
|
55
68
|
}
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
_setServerStatus(server, status, extra = {}) {
|
|
71
|
+
if (!server) return;
|
|
72
|
+
server.status = status;
|
|
73
|
+
Object.assign(server, extra);
|
|
74
|
+
this._persistStatus(server.id, status);
|
|
75
|
+
this.emit('server_status', {
|
|
76
|
+
serverId: server.id,
|
|
77
|
+
status,
|
|
78
|
+
error: server.lastError || null,
|
|
79
|
+
consecutiveFails: server.consecutiveFails || 0,
|
|
80
|
+
nextRetryAt: server.nextRetryAt || null,
|
|
81
|
+
});
|
|
59
82
|
}
|
|
60
83
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
async _closeClient(server) {
|
|
85
|
+
if (!server?.client) return;
|
|
86
|
+
try {
|
|
87
|
+
await this._runWithTimeout(
|
|
88
|
+
() => server.client.close(),
|
|
89
|
+
`Closing MCP server "${server.name || server.id}"`,
|
|
90
|
+
);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(`[MCP] Error closing client ${server.id}:`, extractErrorMessage(err));
|
|
93
|
+
}
|
|
66
94
|
}
|
|
67
95
|
|
|
68
|
-
|
|
69
|
-
|
|
96
|
+
_recordFailure(server, err) {
|
|
97
|
+
const message = extractErrorMessage(err);
|
|
98
|
+
const attempts = (this._reconnectAttempts.get(server.id) || 0) + 1;
|
|
99
|
+
this._reconnectAttempts.set(server.id, attempts);
|
|
100
|
+
server.consecutiveFails = attempts;
|
|
101
|
+
server.lastError = message;
|
|
102
|
+
server.nextRetryAt = null;
|
|
103
|
+
this._setServerStatus(server, err?.requiresAuth ? 'auth_required' : 'error');
|
|
104
|
+
return message;
|
|
70
105
|
}
|
|
71
|
-
}
|
|
72
106
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// ECONNREFUSED: pull out just the host/port
|
|
83
|
-
if (err?.code === 'ECONNREFUSED' || raw.includes('ECONNREFUSED')) {
|
|
84
|
-
const addrMatch = raw.match(/connect ECONNREFUSED ([^\s,]+)/);
|
|
85
|
-
return addrMatch
|
|
86
|
-
? `Connection refused at ${addrMatch[1]} — is the MCP server running?`
|
|
87
|
-
: 'Connection refused — the MCP server is not reachable';
|
|
88
|
-
}
|
|
89
|
-
return raw.split('\n')[0].trim();
|
|
90
|
-
}
|
|
107
|
+
async _runWithTimeout(operation, label) {
|
|
108
|
+
let timer;
|
|
109
|
+
const timeout = new Promise((_, reject) => {
|
|
110
|
+
timer = setTimeout(() => {
|
|
111
|
+
const error = new Error(`${label} timed out after ${this._operationTimeoutMs} ms`);
|
|
112
|
+
error.code = 'MCP_OPERATION_TIMEOUT';
|
|
113
|
+
reject(error);
|
|
114
|
+
}, this._operationTimeoutMs);
|
|
115
|
+
});
|
|
91
116
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
117
|
+
try {
|
|
118
|
+
return await Promise.race([Promise.resolve().then(operation), timeout]);
|
|
119
|
+
} finally {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
97
122
|
}
|
|
98
123
|
|
|
99
124
|
async startServer(serverId, url, name = '', userId = null, options = {}) {
|
|
100
|
-
|
|
101
|
-
|
|
125
|
+
serverId = normalizeServerId(serverId);
|
|
126
|
+
if (this._isShuttingDown) {
|
|
127
|
+
throw new Error('MCP client is shutting down');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const isReconnect = options._isReconnect === true;
|
|
131
|
+
if (!isReconnect) {
|
|
132
|
+
this._cancelReconnect(serverId);
|
|
133
|
+
this._reconnectAttempts.delete(serverId);
|
|
102
134
|
}
|
|
103
135
|
|
|
104
136
|
const slug = (name || String(serverId)).toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
|
|
137
|
+
const previous = this.servers.get(serverId);
|
|
138
|
+
const lifecycleVersion = this._nextLifecycleVersion(serverId);
|
|
139
|
+
if (previous) {
|
|
140
|
+
await this._closeClient(previous);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
let serverObj = null;
|
|
105
144
|
try {
|
|
106
145
|
const endpoint = validateRemoteMcpEndpoint(url);
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
configObj = JSON.parse(serverRow.config || '{}');
|
|
112
|
-
authObj = configObj.auth || {};
|
|
113
|
-
}
|
|
146
|
+
const transportOpts = buildTransportOptions(serverId);
|
|
147
|
+
const transport = this._createTransport(endpoint, transportOpts);
|
|
148
|
+
const client = this._createClient();
|
|
149
|
+
const previousFails = this._reconnectAttempts.get(serverId) || 0;
|
|
114
150
|
|
|
115
|
-
|
|
116
|
-
requestInit: { headers: {} },
|
|
117
|
-
eventSourceInit: { headers: {} },
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
if (authObj.type === 'bearer' && authObj.token) {
|
|
121
|
-
const h = `Bearer ${authObj.token}`;
|
|
122
|
-
transportOpts.requestInit.headers['Authorization'] = h;
|
|
123
|
-
transportOpts.eventSourceInit.headers['Authorization'] = h;
|
|
124
|
-
} else if (authObj.type === 'oauth') {
|
|
125
|
-
transportOpts.authProvider = new DBAuthProvider(serverId, authObj.clientId, authObj.authServerUrl);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const transport = new SSEClientTransport(new URL(endpoint), transportOpts);
|
|
129
|
-
const client = new Client(
|
|
130
|
-
{ name: 'NeoAgent', version: '1.0.0' },
|
|
131
|
-
{ capabilities: { tools: {} } },
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
const serverObj = {
|
|
151
|
+
serverObj = {
|
|
135
152
|
id: serverId,
|
|
136
153
|
userId,
|
|
137
154
|
agentId: options.agentId || null,
|
|
@@ -143,101 +160,123 @@ class MCPClient extends EventEmitter {
|
|
|
143
160
|
transport,
|
|
144
161
|
tools: [],
|
|
145
162
|
status: 'starting',
|
|
146
|
-
consecutiveFails:
|
|
163
|
+
consecutiveFails: previousFails,
|
|
147
164
|
lastError: null,
|
|
165
|
+
nextRetryAt: null,
|
|
148
166
|
};
|
|
149
167
|
|
|
150
168
|
this.servers.set(serverId, serverObj);
|
|
169
|
+
this._setServerStatus(serverObj, 'starting');
|
|
151
170
|
|
|
152
|
-
await
|
|
171
|
+
await this._runWithTimeout(
|
|
172
|
+
() => client.connect(transport),
|
|
173
|
+
`Connecting to MCP server "${serverObj.name}"`,
|
|
174
|
+
);
|
|
175
|
+
const response = await this._runWithTimeout(
|
|
176
|
+
() => client.listTools(),
|
|
177
|
+
`Discovering tools from MCP server "${serverObj.name}"`,
|
|
178
|
+
);
|
|
153
179
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.emit('server_status', { serverId, status: 'running' });
|
|
180
|
+
if (!this._isCurrentLifecycle(serverId, serverObj, lifecycleVersion)) {
|
|
181
|
+
await this._closeClient(serverObj);
|
|
182
|
+
const superseded = new Error(`MCP server ${serverId} start was superseded`);
|
|
183
|
+
superseded.code = 'MCP_START_SUPERSEDED';
|
|
184
|
+
throw superseded;
|
|
160
185
|
}
|
|
161
186
|
|
|
162
|
-
|
|
187
|
+
serverObj.tools = response.tools || [];
|
|
188
|
+
serverObj.consecutiveFails = 0;
|
|
189
|
+
serverObj.lastError = null;
|
|
190
|
+
serverObj.nextRetryAt = null;
|
|
191
|
+
this._reconnectAttempts.delete(serverId);
|
|
192
|
+
this._setServerStatus(serverObj, 'running');
|
|
193
|
+
|
|
194
|
+
return { status: 'running', tools: serverObj.tools };
|
|
163
195
|
} catch (err) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (server) {
|
|
167
|
-
server.consecutiveFails = (server.consecutiveFails || 0) + 1;
|
|
168
|
-
server.lastError = message;
|
|
169
|
-
server.status = 'error';
|
|
170
|
-
this.emit('server_status', { serverId, status: 'error', error: message });
|
|
196
|
+
if (err?.code === 'MCP_START_SUPERSEDED') {
|
|
197
|
+
throw err;
|
|
171
198
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
199
|
+
if (serverObj && this.servers.get(serverId) === serverObj) {
|
|
200
|
+
const requiresAuth = String(err?.message || '').startsWith('OAUTH_REDIRECT:');
|
|
201
|
+
if (requiresAuth) err.requiresAuth = true;
|
|
202
|
+
const message = this._recordFailure(serverObj, err);
|
|
203
|
+
if (!requiresAuth) {
|
|
204
|
+
await this._closeClient(serverObj);
|
|
205
|
+
}
|
|
206
|
+
const friendlyErr = new Error(message);
|
|
207
|
+
friendlyErr.originalError = err;
|
|
208
|
+
friendlyErr.requiresAuth = requiresAuth;
|
|
209
|
+
throw friendlyErr;
|
|
210
|
+
}
|
|
211
|
+
if (previous && this.servers.get(serverId) === previous) {
|
|
212
|
+
const message = this._recordFailure(previous, err);
|
|
213
|
+
const friendlyErr = new Error(message);
|
|
214
|
+
friendlyErr.originalError = err;
|
|
215
|
+
throw friendlyErr;
|
|
216
|
+
}
|
|
217
|
+
throw err;
|
|
175
218
|
}
|
|
176
219
|
}
|
|
177
220
|
|
|
178
221
|
async finishOAuth(serverId, code) {
|
|
222
|
+
serverId = normalizeServerId(serverId);
|
|
179
223
|
const server = this.servers.get(serverId);
|
|
180
224
|
if (!server || !server.transport) {
|
|
181
225
|
throw new Error(`Server ${serverId} transport not initialized`);
|
|
182
226
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
227
|
+
try {
|
|
228
|
+
await this._runWithTimeout(
|
|
229
|
+
() => server.transport.finishAuth(code),
|
|
230
|
+
`Completing OAuth for MCP server "${server.name}"`,
|
|
231
|
+
);
|
|
232
|
+
await this._runWithTimeout(
|
|
233
|
+
() => server.client.connect(server.transport),
|
|
234
|
+
`Connecting to MCP server "${server.name}" after OAuth`,
|
|
235
|
+
);
|
|
236
|
+
const response = await this._runWithTimeout(
|
|
237
|
+
() => server.client.listTools(),
|
|
238
|
+
`Discovering tools from MCP server "${server.name}"`,
|
|
239
|
+
);
|
|
240
|
+
server.tools = response.tools || [];
|
|
241
|
+
server.consecutiveFails = 0;
|
|
242
|
+
server.lastError = null;
|
|
243
|
+
server.nextRetryAt = null;
|
|
244
|
+
this._reconnectAttempts.delete(serverId);
|
|
245
|
+
this._setServerStatus(server, 'running');
|
|
246
|
+
return { status: 'running', tools: server.tools };
|
|
247
|
+
} catch (err) {
|
|
248
|
+
const message = this._recordFailure(server, err);
|
|
249
|
+
const friendlyErr = new Error(message);
|
|
250
|
+
friendlyErr.originalError = err;
|
|
251
|
+
throw friendlyErr;
|
|
252
|
+
}
|
|
190
253
|
}
|
|
191
254
|
|
|
192
255
|
async stopServer(serverId) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
256
|
+
serverId = normalizeServerId(serverId);
|
|
257
|
+
this._cancelReconnect(serverId);
|
|
258
|
+
this._reconnectAttempts.delete(serverId);
|
|
259
|
+
this._nextLifecycleVersion(serverId);
|
|
198
260
|
|
|
199
261
|
const server = this.servers.get(serverId);
|
|
200
|
-
if (!server)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (server.client) await server.client.close();
|
|
204
|
-
} catch (err) {
|
|
205
|
-
console.error(`[MCP] Error closing client ${serverId}:`, err.message);
|
|
262
|
+
if (!server) {
|
|
263
|
+
this._persistStatus(serverId, 'stopped');
|
|
264
|
+
return { status: 'stopped' };
|
|
206
265
|
}
|
|
207
266
|
|
|
267
|
+
await this._closeClient(server);
|
|
208
268
|
this.servers.delete(serverId);
|
|
269
|
+
this._persistStatus(serverId, 'stopped');
|
|
209
270
|
this.emit('server_status', { serverId, status: 'stopped' });
|
|
271
|
+
return { status: 'stopped' };
|
|
210
272
|
}
|
|
211
273
|
|
|
212
274
|
_scheduleReconnect(serverId, userId, options) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const timer = setTimeout(async () => {
|
|
216
|
-
this._reconnectTimers.delete(serverId);
|
|
217
|
-
const server = this.servers.get(serverId);
|
|
218
|
-
if (!server || server.status === 'running') return;
|
|
219
|
-
|
|
220
|
-
const row = db.prepare('SELECT * FROM mcp_servers WHERE id = ? AND enabled = 1').get(serverId);
|
|
221
|
-
if (!row) return;
|
|
222
|
-
|
|
223
|
-
try {
|
|
224
|
-
await this.startServer(serverId, row.command, row.name, userId, options);
|
|
225
|
-
await this.listTools(serverId, userId);
|
|
226
|
-
console.log(`[MCP] Reconnected to ${row.name}`);
|
|
227
|
-
} catch (err) {
|
|
228
|
-
const server = this.servers.get(serverId);
|
|
229
|
-
if (server && server.consecutiveFails < CONSECUTIVE_FAIL_LIMIT) {
|
|
230
|
-
this._scheduleReconnect(serverId, userId, options);
|
|
231
|
-
} else {
|
|
232
|
-
console.warn(`[MCP] ${row.name} disabled after ${CONSECUTIVE_FAIL_LIMIT} consecutive failures: ${err.message}`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}, RECONNECT_DELAY_MS);
|
|
236
|
-
|
|
237
|
-
this._reconnectTimers.set(serverId, timer);
|
|
275
|
+
return recovery.scheduleReconnect(this, serverId, userId, options);
|
|
238
276
|
}
|
|
239
277
|
|
|
240
278
|
_getOwnedServer(serverId, userId = null) {
|
|
279
|
+
serverId = normalizeServerId(serverId);
|
|
241
280
|
const server = this.servers.get(serverId);
|
|
242
281
|
if (!server) return null;
|
|
243
282
|
if (userId != null && server.userId !== userId) return null;
|
|
@@ -245,130 +284,31 @@ class MCPClient extends EventEmitter {
|
|
|
245
284
|
}
|
|
246
285
|
|
|
247
286
|
async listTools(serverId, userId = null) {
|
|
248
|
-
|
|
249
|
-
if (!server || server.status !== 'running') {
|
|
250
|
-
throw new Error(`Server ${serverId} not running`);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const response = await server.client.listTools();
|
|
254
|
-
server.tools = response.tools || [];
|
|
255
|
-
return server.tools;
|
|
287
|
+
return toolOperations.listTools(this, serverId, userId);
|
|
256
288
|
}
|
|
257
289
|
|
|
258
290
|
async callTool(serverId, toolName, args = {}, userId = null) {
|
|
259
|
-
|
|
260
|
-
if (!server) throw new Error(`Server ${serverId} not found`);
|
|
261
|
-
|
|
262
|
-
if (server.status !== 'running') {
|
|
263
|
-
const hint = server.lastError ? ` (${server.lastError})` : '';
|
|
264
|
-
throw new Error(`MCP server "${server.name}" is not available${hint}`);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
const result = await server.client.callTool({ name: toolName, arguments: args });
|
|
269
|
-
server.consecutiveFails = 0;
|
|
270
|
-
return result;
|
|
271
|
-
} catch (err) {
|
|
272
|
-
const message = extractErrorMessage(err);
|
|
273
|
-
server.consecutiveFails = (server.consecutiveFails || 0) + 1;
|
|
274
|
-
server.lastError = message;
|
|
275
|
-
|
|
276
|
-
if (server.consecutiveFails >= CONSECUTIVE_FAIL_LIMIT) {
|
|
277
|
-
server.status = 'error';
|
|
278
|
-
this.emit('server_status', { serverId, status: 'error', error: message });
|
|
279
|
-
this._scheduleReconnect(serverId, server.userId, { agentId: server.agentId });
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
throw new Error(`MCP tool "${toolName}" failed: ${message}`);
|
|
283
|
-
}
|
|
291
|
+
return toolOperations.callTool(this, serverId, toolName, args, userId);
|
|
284
292
|
}
|
|
285
293
|
|
|
286
294
|
async callToolByName(fullName, args = {}, userId = null, options = {}) {
|
|
287
|
-
|
|
288
|
-
if (userId != null && server.userId !== userId) continue;
|
|
289
|
-
if (options.agentId && server.agentId && server.agentId !== options.agentId) continue;
|
|
290
|
-
const prefix = `mcp_${server.slug}_`;
|
|
291
|
-
if (!fullName.startsWith(prefix)) continue;
|
|
292
|
-
|
|
293
|
-
const originalName = fullName.substring(prefix.length);
|
|
294
|
-
return await this.callTool(serverId, originalName, args, userId);
|
|
295
|
-
}
|
|
296
|
-
return null;
|
|
295
|
+
return toolOperations.callToolByName(this, fullName, args, userId, options);
|
|
297
296
|
}
|
|
298
297
|
|
|
299
298
|
getAllTools(userId = null, options = {}) {
|
|
300
|
-
|
|
301
|
-
for (const [serverId, server] of this.servers) {
|
|
302
|
-
if (userId != null && server.userId !== userId) continue;
|
|
303
|
-
if (options.agentId && server.agentId && server.agentId !== options.agentId) continue;
|
|
304
|
-
if (server.status !== 'running') continue;
|
|
305
|
-
for (const tool of server.tools) {
|
|
306
|
-
allTools.push({
|
|
307
|
-
...tool,
|
|
308
|
-
name: `mcp_${server.slug}_${tool.name}`,
|
|
309
|
-
originalName: tool.name,
|
|
310
|
-
parameters: tool.inputSchema || tool.parameters,
|
|
311
|
-
serverId,
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return allTools;
|
|
299
|
+
return toolOperations.getAllTools(this, userId, options);
|
|
316
300
|
}
|
|
317
301
|
|
|
318
302
|
getStatus(userId = null, options = {}) {
|
|
319
|
-
|
|
320
|
-
const agentId = options.agentId || options.agent_id || null;
|
|
321
|
-
for (const [serverId, server] of this.servers) {
|
|
322
|
-
if (userId != null && server.userId !== userId) continue;
|
|
323
|
-
if (agentId && server.agentId && server.agentId !== agentId) continue;
|
|
324
|
-
statuses[serverId] = {
|
|
325
|
-
status: server.status,
|
|
326
|
-
command: server.url,
|
|
327
|
-
args: [],
|
|
328
|
-
toolCount: server.tools.length,
|
|
329
|
-
error: server.lastError || null,
|
|
330
|
-
consecutiveFails: server.consecutiveFails || 0,
|
|
331
|
-
serverInfo: null,
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
return statuses;
|
|
303
|
+
return toolOperations.getStatus(this, userId, options);
|
|
335
304
|
}
|
|
336
305
|
|
|
337
306
|
async loadFromDB(userId) {
|
|
338
|
-
|
|
339
|
-
const results = [];
|
|
340
|
-
|
|
341
|
-
for (const srv of servers) {
|
|
342
|
-
try {
|
|
343
|
-
await this.startServer(srv.id, srv.command, srv.name, userId, { agentId: srv.agent_id });
|
|
344
|
-
await this.listTools(srv.id, userId);
|
|
345
|
-
results.push({ id: srv.id, name: srv.name, status: 'running' });
|
|
346
|
-
} catch (err) {
|
|
347
|
-
const message = err.message;
|
|
348
|
-
console.error(`[MCP] Failed to start "${srv.name}":`, message);
|
|
349
|
-
// Schedule a reconnect attempt for transient failures (not auth errors)
|
|
350
|
-
const server = this.servers.get(srv.id);
|
|
351
|
-
if (server) {
|
|
352
|
-
this._scheduleReconnect(srv.id, userId, { agentId: srv.agent_id });
|
|
353
|
-
}
|
|
354
|
-
results.push({ id: srv.id, name: srv.name, status: 'error', error: message });
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
return results;
|
|
307
|
+
return recovery.loadFromDB(this, userId);
|
|
359
308
|
}
|
|
360
309
|
|
|
361
310
|
async shutdown() {
|
|
362
|
-
|
|
363
|
-
clearTimeout(timer);
|
|
364
|
-
}
|
|
365
|
-
this._reconnectTimers.clear();
|
|
366
|
-
|
|
367
|
-
const promises = [];
|
|
368
|
-
for (const serverId of this.servers.keys()) {
|
|
369
|
-
promises.push(this.stopServer(serverId));
|
|
370
|
-
}
|
|
371
|
-
await Promise.allSettled(promises);
|
|
311
|
+
return recovery.shutdown(this);
|
|
372
312
|
}
|
|
373
313
|
}
|
|
374
314
|
|