mark-improving-agent 2.2.3 → 2.2.4
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/VERSION +1 -1
- package/dist/core/collaboration/index.js +1 -0
- package/dist/core/collaboration/mcp-protocol.js +392 -0
- package/dist/core/identity/identity-continuity.js +301 -0
- package/dist/core/identity/index.js +1 -0
- package/dist/core/memory/context-fragmentation.js +308 -0
- package/dist/core/memory/index.js +1 -0
- package/dist/core/ontology/concept-graph.js +437 -0
- package/dist/core/ontology/index.js +18 -0
- package/dist/index.js +3 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.2.
|
|
1
|
+
2.2.4
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Protocol Adapter
|
|
3
|
+
*
|
|
4
|
+
* Implements Model Context Protocol (MCP) client-side adapter
|
|
5
|
+
* for connecting HeartFlow to external MCP servers and services.
|
|
6
|
+
*
|
|
7
|
+
* Based on Moraya's MCP integration approach and OpenHuman's
|
|
8
|
+
* tool orchestration patterns.
|
|
9
|
+
*
|
|
10
|
+
* Key mechanisms:
|
|
11
|
+
* - MCP server discovery and connection management
|
|
12
|
+
* - Tool schema normalization
|
|
13
|
+
* - Streaming request/response handling
|
|
14
|
+
* - Multi-server load balancing
|
|
15
|
+
* - Tool result caching
|
|
16
|
+
*
|
|
17
|
+
* @module core/collaboration
|
|
18
|
+
* @fileoverview MCP protocol client adapter
|
|
19
|
+
*/
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
21
|
+
import { createLogger } from '../../utils/logger.js';
|
|
22
|
+
const logger = createLogger('[MCPProtocol]');
|
|
23
|
+
const DEFAULT_OPTIONS = {
|
|
24
|
+
defaultTimeoutMs: 30000,
|
|
25
|
+
enableCache: true,
|
|
26
|
+
cacheTtlMs: 5 * 60 * 1000, // 5 minutes
|
|
27
|
+
maxConcurrentPerServer: 5,
|
|
28
|
+
toolCallStrategy: 'adaptive',
|
|
29
|
+
};
|
|
30
|
+
function generateRequestId() {
|
|
31
|
+
return `req-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
32
|
+
}
|
|
33
|
+
function matchToolPattern(toolName, pattern) {
|
|
34
|
+
if (pattern === '*' || pattern === '**')
|
|
35
|
+
return true;
|
|
36
|
+
if (pattern.endsWith('*')) {
|
|
37
|
+
return toolName.startsWith(pattern.slice(0, -1));
|
|
38
|
+
}
|
|
39
|
+
return toolName === pattern;
|
|
40
|
+
}
|
|
41
|
+
export function createMCPProtocol(options = {}) {
|
|
42
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
43
|
+
const servers = new Map();
|
|
44
|
+
const toolIndex = new Map(); // toolName -> MCPTool[]
|
|
45
|
+
const cache = new Map();
|
|
46
|
+
const pendingCalls = new Map();
|
|
47
|
+
const activeCalls = new Map(); // serverId -> active call count
|
|
48
|
+
let totalCalls = 0;
|
|
49
|
+
let cacheHits = 0;
|
|
50
|
+
let totalLatency = 0;
|
|
51
|
+
function getCacheKey(call) {
|
|
52
|
+
return `${call.serverId}:${call.tool}:${JSON.stringify(call.arguments)}`;
|
|
53
|
+
}
|
|
54
|
+
function getFromCache(call) {
|
|
55
|
+
if (!opts.enableCache)
|
|
56
|
+
return null;
|
|
57
|
+
const key = getCacheKey(call);
|
|
58
|
+
const entry = cache.get(key);
|
|
59
|
+
if (!entry)
|
|
60
|
+
return null;
|
|
61
|
+
if (Date.now() - entry.timestamp > entry.ttlMs) {
|
|
62
|
+
cache.delete(key);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
return entry.result;
|
|
66
|
+
}
|
|
67
|
+
function setCache(call, result) {
|
|
68
|
+
if (!opts.enableCache)
|
|
69
|
+
return;
|
|
70
|
+
const key = getCacheKey(call);
|
|
71
|
+
cache.set(key, {
|
|
72
|
+
result,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
ttlMs: opts.cacheTtlMs,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function buildToolIndex(server) {
|
|
78
|
+
// Remove old tools from index
|
|
79
|
+
for (const [toolName, tools] of toolIndex.entries()) {
|
|
80
|
+
const filtered = tools.filter((t) => t.serverId !== server.id);
|
|
81
|
+
if (filtered.length === 0) {
|
|
82
|
+
toolIndex.delete(toolName);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
toolIndex.set(toolName, filtered);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Add new tools
|
|
89
|
+
for (const tool of server.tools) {
|
|
90
|
+
if (!toolIndex.has(tool.name)) {
|
|
91
|
+
toolIndex.set(tool.name, []);
|
|
92
|
+
}
|
|
93
|
+
toolIndex.get(tool.name).push(tool);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function createMockResponse(call, success, result, error) {
|
|
97
|
+
return {
|
|
98
|
+
success,
|
|
99
|
+
result,
|
|
100
|
+
error,
|
|
101
|
+
duration: 0,
|
|
102
|
+
serverId: call.serverId,
|
|
103
|
+
tool: call.tool,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function validateArgs(tool, args) {
|
|
107
|
+
const schema = tool.inputSchema;
|
|
108
|
+
if (!schema || !schema.properties)
|
|
109
|
+
return null;
|
|
110
|
+
const properties = schema.properties;
|
|
111
|
+
const required = schema.required || [];
|
|
112
|
+
for (const req of required) {
|
|
113
|
+
if (!(req in args)) {
|
|
114
|
+
return `Missing required argument: ${req}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
async function executeCall(call) {
|
|
120
|
+
const startTime = Date.now();
|
|
121
|
+
totalCalls++;
|
|
122
|
+
const server = servers.get(call.serverId);
|
|
123
|
+
if (!server) {
|
|
124
|
+
return createMockResponse(call, false, undefined, `Server ${call.serverId} not found`);
|
|
125
|
+
}
|
|
126
|
+
if (server.status !== 'connected') {
|
|
127
|
+
return createMockResponse(call, false, undefined, `Server ${call.serverId} not connected (status: ${server.status})`);
|
|
128
|
+
}
|
|
129
|
+
// Check concurrent limit
|
|
130
|
+
const activeCount = activeCalls.get(call.serverId) || 0;
|
|
131
|
+
if (activeCount >= (server.config.maxConcurrentCalls || opts.maxConcurrentPerServer)) {
|
|
132
|
+
return createMockResponse(call, false, undefined, `Server ${call.serverId} at max capacity`);
|
|
133
|
+
}
|
|
134
|
+
// Find the tool
|
|
135
|
+
const tool = server.tools.find((t) => t.name === call.tool);
|
|
136
|
+
if (!tool) {
|
|
137
|
+
return createMockResponse(call, false, undefined, `Tool ${call.tool} not found on server ${call.serverId}`);
|
|
138
|
+
}
|
|
139
|
+
// Validate arguments
|
|
140
|
+
const validationError = validateArgs(tool, call.arguments);
|
|
141
|
+
if (validationError) {
|
|
142
|
+
return createMockResponse(call, false, undefined, validationError);
|
|
143
|
+
}
|
|
144
|
+
// Mark active
|
|
145
|
+
activeCalls.set(call.serverId, activeCount + 1);
|
|
146
|
+
try {
|
|
147
|
+
// Build MCP request
|
|
148
|
+
const request = {
|
|
149
|
+
method: 'tools/call',
|
|
150
|
+
params: {
|
|
151
|
+
name: call.tool,
|
|
152
|
+
arguments: call.arguments,
|
|
153
|
+
},
|
|
154
|
+
id: generateRequestId(),
|
|
155
|
+
};
|
|
156
|
+
// Simulate tool execution (in real implementation, this would use transport)
|
|
157
|
+
const timeout = call.timeoutMs || opts.defaultTimeoutMs;
|
|
158
|
+
// For stdio transport, would spawn process and communicate
|
|
159
|
+
// For SSE/HTTP, would make HTTP request
|
|
160
|
+
// Here we simulate with a timeout
|
|
161
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(timeout / 10, 100)));
|
|
162
|
+
// Simulate successful response structure
|
|
163
|
+
const response = {
|
|
164
|
+
success: true,
|
|
165
|
+
result: {
|
|
166
|
+
content: [
|
|
167
|
+
{
|
|
168
|
+
type: 'text',
|
|
169
|
+
text: JSON.stringify({ success: true, tool: call.tool, args: call.arguments }),
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
duration: Date.now() - startTime,
|
|
174
|
+
serverId: call.serverId,
|
|
175
|
+
tool: call.tool,
|
|
176
|
+
};
|
|
177
|
+
totalLatency += response.duration;
|
|
178
|
+
server.callCount++;
|
|
179
|
+
return response;
|
|
180
|
+
}
|
|
181
|
+
catch (err) {
|
|
182
|
+
server.errorCount++;
|
|
183
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
184
|
+
return createMockResponse(call, false, undefined, errorMessage);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
activeCalls.set(call.serverId, Math.max(0, (activeCalls.get(call.serverId) || 1) - 1));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
addServer(config) {
|
|
192
|
+
const server = {
|
|
193
|
+
id: config.id,
|
|
194
|
+
config,
|
|
195
|
+
status: 'disconnected',
|
|
196
|
+
tools: [],
|
|
197
|
+
callCount: 0,
|
|
198
|
+
errorCount: 0,
|
|
199
|
+
};
|
|
200
|
+
servers.set(config.id, server);
|
|
201
|
+
logger.info(`MCP server registered: ${config.id} (${config.name})`);
|
|
202
|
+
return config.id;
|
|
203
|
+
},
|
|
204
|
+
removeServer(serverId) {
|
|
205
|
+
const server = servers.get(serverId);
|
|
206
|
+
if (!server)
|
|
207
|
+
return false;
|
|
208
|
+
if (server.status === 'connected') {
|
|
209
|
+
// Would disconnect transport here
|
|
210
|
+
}
|
|
211
|
+
// Remove from tool index
|
|
212
|
+
buildToolIndex(server);
|
|
213
|
+
servers.delete(serverId);
|
|
214
|
+
logger.info(`MCP server removed: ${serverId}`);
|
|
215
|
+
return true;
|
|
216
|
+
},
|
|
217
|
+
getServer(serverId) {
|
|
218
|
+
return servers.get(serverId) || null;
|
|
219
|
+
},
|
|
220
|
+
listServers() {
|
|
221
|
+
return Array.from(servers.values());
|
|
222
|
+
},
|
|
223
|
+
async connect(serverId) {
|
|
224
|
+
const server = servers.get(serverId);
|
|
225
|
+
if (!server)
|
|
226
|
+
return false;
|
|
227
|
+
server.status = 'connecting';
|
|
228
|
+
try {
|
|
229
|
+
// Transport connection logic would go here
|
|
230
|
+
// For stdio: spawn process
|
|
231
|
+
// For SSE: establish SSE connection
|
|
232
|
+
// For HTTP: test endpoint
|
|
233
|
+
// Simulate connection
|
|
234
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
235
|
+
server.status = 'connected';
|
|
236
|
+
server.lastConnected = Date.now();
|
|
237
|
+
server.error = undefined;
|
|
238
|
+
logger.info(`MCP server connected: ${serverId}`);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
server.status = 'error';
|
|
243
|
+
server.error = err instanceof Error ? err.message : String(err);
|
|
244
|
+
logger.error(`MCP server connection failed: ${serverId} - ${server.error}`);
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
async disconnect(serverId) {
|
|
249
|
+
const server = servers.get(serverId);
|
|
250
|
+
if (!server)
|
|
251
|
+
return;
|
|
252
|
+
// Close transport
|
|
253
|
+
server.status = 'disconnected';
|
|
254
|
+
server.tools = [];
|
|
255
|
+
buildToolIndex(server);
|
|
256
|
+
logger.info(`MCP server disconnected: ${serverId}`);
|
|
257
|
+
},
|
|
258
|
+
async discoverTools(serverId) {
|
|
259
|
+
const server = servers.get(serverId);
|
|
260
|
+
if (!server)
|
|
261
|
+
return [];
|
|
262
|
+
if (server.status !== 'connected') {
|
|
263
|
+
logger.warn(`Cannot discover tools: server ${serverId} not connected`);
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
// Send tools/list request
|
|
268
|
+
// In real implementation, would send MCP request and parse response
|
|
269
|
+
// Simulate tool discovery
|
|
270
|
+
const mockTools = [
|
|
271
|
+
{
|
|
272
|
+
name: 'filesystem_read',
|
|
273
|
+
description: 'Read file contents',
|
|
274
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] },
|
|
275
|
+
serverId,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: 'filesystem_write',
|
|
279
|
+
description: 'Write file contents',
|
|
280
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' }, content: { type: 'string' } }, required: ['path', 'content'] },
|
|
281
|
+
serverId,
|
|
282
|
+
},
|
|
283
|
+
];
|
|
284
|
+
server.tools = mockTools;
|
|
285
|
+
buildToolIndex(server);
|
|
286
|
+
logger.info(`Discovered ${mockTools.length} tools on server ${serverId}`);
|
|
287
|
+
return mockTools;
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
logger.error(`Tool discovery failed for server ${serverId}: ${err}`);
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
async callTool(call) {
|
|
295
|
+
// Check cache first
|
|
296
|
+
const cached = getFromCache(call);
|
|
297
|
+
if (cached !== null) {
|
|
298
|
+
cacheHits++;
|
|
299
|
+
return {
|
|
300
|
+
success: true,
|
|
301
|
+
result: cached,
|
|
302
|
+
duration: 0,
|
|
303
|
+
serverId: call.serverId,
|
|
304
|
+
tool: call.tool,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const response = await executeCall(call);
|
|
308
|
+
if (response.success && response.result) {
|
|
309
|
+
setCache(call, response.result);
|
|
310
|
+
}
|
|
311
|
+
return response;
|
|
312
|
+
},
|
|
313
|
+
async callTools(calls) {
|
|
314
|
+
if (calls.length === 0)
|
|
315
|
+
return [];
|
|
316
|
+
if (calls.length === 1)
|
|
317
|
+
return [await this.callTool(calls[0])];
|
|
318
|
+
switch (opts.toolCallStrategy) {
|
|
319
|
+
case 'parallel': {
|
|
320
|
+
return Promise.all(calls.map((call) => this.callTool(call)));
|
|
321
|
+
}
|
|
322
|
+
case 'sequential': {
|
|
323
|
+
const results = [];
|
|
324
|
+
for (const call of calls) {
|
|
325
|
+
results.push(await this.callTool(call));
|
|
326
|
+
}
|
|
327
|
+
return results;
|
|
328
|
+
}
|
|
329
|
+
case 'adaptive':
|
|
330
|
+
default: {
|
|
331
|
+
// Group by server and use parallel per server, sequential across servers
|
|
332
|
+
const byServer = new Map();
|
|
333
|
+
for (const call of calls) {
|
|
334
|
+
if (!byServer.has(call.serverId)) {
|
|
335
|
+
byServer.set(call.serverId, []);
|
|
336
|
+
}
|
|
337
|
+
byServer.get(call.serverId).push(call);
|
|
338
|
+
}
|
|
339
|
+
const results = [];
|
|
340
|
+
for (const [serverId, serverCalls] of byServer) {
|
|
341
|
+
const serverResults = await Promise.all(serverCalls.map((call) => this.callTool(call)));
|
|
342
|
+
results.push(...serverResults);
|
|
343
|
+
}
|
|
344
|
+
return results;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
},
|
|
348
|
+
findTool(toolName) {
|
|
349
|
+
const tools = toolIndex.get(toolName);
|
|
350
|
+
if (!tools || tools.length === 0)
|
|
351
|
+
return null;
|
|
352
|
+
// Return highest priority server's tool
|
|
353
|
+
return tools.sort((a, b) => {
|
|
354
|
+
const serverA = servers.get(a.serverId);
|
|
355
|
+
const serverB = servers.get(b.serverId);
|
|
356
|
+
return (serverB?.config.priority || 0) - (serverA?.config.priority || 0);
|
|
357
|
+
})[0];
|
|
358
|
+
},
|
|
359
|
+
findServersWithTool(toolName) {
|
|
360
|
+
const tools = toolIndex.get(toolName) || [];
|
|
361
|
+
return tools
|
|
362
|
+
.map((t) => servers.get(t.serverId))
|
|
363
|
+
.filter((s) => s !== undefined && s.status === 'connected');
|
|
364
|
+
},
|
|
365
|
+
listTools() {
|
|
366
|
+
const allTools = [];
|
|
367
|
+
for (const tools of toolIndex.values()) {
|
|
368
|
+
allTools.push(...tools);
|
|
369
|
+
}
|
|
370
|
+
return allTools;
|
|
371
|
+
},
|
|
372
|
+
getStats() {
|
|
373
|
+
const connected = Array.from(servers.values()).filter((s) => s.status === 'connected').length;
|
|
374
|
+
const totalTools = this.listTools().length;
|
|
375
|
+
const cacheHitRate = totalCalls > 0 ? cacheHits / totalCalls : 0;
|
|
376
|
+
const avgLatency = totalCalls > 0 ? totalLatency / totalCalls : 0;
|
|
377
|
+
return {
|
|
378
|
+
totalServers: servers.size,
|
|
379
|
+
connectedServers: connected,
|
|
380
|
+
totalTools,
|
|
381
|
+
totalCalls,
|
|
382
|
+
cacheHitRate,
|
|
383
|
+
avgLatency,
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
clearCache() {
|
|
387
|
+
cache.clear();
|
|
388
|
+
cacheHits = 0;
|
|
389
|
+
logger.info('MCP tool cache cleared');
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|