@mimik/agent-kit 1.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/src/agent.js ADDED
@@ -0,0 +1,456 @@
1
+ /* eslint-disable no-await-in-loop, no-plusplus, no-continue, class-methods-use-this */
2
+
3
+ const { createChatCompletionStream } = require('./oai');
4
+ const { MCPServer, convertMCPToolsToOpenAI } = require('./mcp-server');
5
+
6
+ // MCP Agent Class
7
+ class Agent {
8
+ constructor(config) {
9
+ this.name = config.name || 'MCP Assistant';
10
+ this.instructions = config.instructions || 'You are a helpful assistant with access to MCP tools.';
11
+ this.maxIterations = config.maxIterations || 5;
12
+
13
+ // LLM configuration grouped together
14
+ const llmConfig = config.llm || {};
15
+ this.llm = {
16
+ model: llmConfig.model || 'lmstudio-community/Qwen3-4B-GGUF',
17
+ temperature: llmConfig.temperature || 0.1,
18
+ max_tokens: llmConfig.max_tokens || 2048,
19
+ endpoint: llmConfig.endpoint || 'http://127.0.0.1:8083/api/milm/v1/chat/completions',
20
+ apiKey: llmConfig.apiKey || 'bearer 1234',
21
+ no_think: llmConfig.no_think || false
22
+ };
23
+
24
+ // MCP configuration - support multiple endpoints
25
+ this.mcpEndpoints = config.mcpEndpoints || [config.mcpEndpoint];
26
+ this.httpClient = config.httpClient;
27
+ this.mcpServers = new Map(); // Map of endpoint -> MCPServer instance
28
+ this.tools = [];
29
+ this.toolToServerMap = new Map(); // Map of tool name -> server endpoint
30
+ }
31
+
32
+ async initialize() {
33
+ try {
34
+ let totalTools = 0;
35
+
36
+ // Initialize all MCP servers
37
+ for (const endpointObj of this.mcpEndpoints) {
38
+ try {
39
+ let endpoint;
40
+ let apiKey;
41
+ let options;
42
+ if (typeof endpointObj === 'object') {
43
+ endpoint = endpointObj.url;
44
+ apiKey = endpointObj.apiKey;
45
+ options = endpointObj.options;
46
+ } else {
47
+ endpoint = endpointObj;
48
+ }
49
+
50
+ if (!endpoint) {
51
+ continue;
52
+ }
53
+ // console.log(`[${this.name}] Initializing MCP server: ${endpoint}`);
54
+
55
+ const mcpServer = new MCPServer(this.httpClient, endpoint, apiKey, options);
56
+ await mcpServer.initialize();
57
+
58
+ // Load tools from this server
59
+ const toolsResult = await mcpServer.listTools();
60
+ const serverTools = convertMCPToolsToOpenAI(toolsResult.tools);
61
+
62
+ // Store server and tools
63
+ this.mcpServers.set(endpoint, mcpServer);
64
+
65
+ // Add tools to global list and map them to their server
66
+ for (const tool of serverTools) {
67
+ this.tools.push({
68
+ ...tool,
69
+ _mcpEndpoint: endpoint, // Add metadata for routing
70
+ });
71
+ this.toolToServerMap.set(tool.function.name, endpoint);
72
+ }
73
+
74
+ totalTools += serverTools.length;
75
+ // console.log(`[${this.name}] Server ${endpoint}: ${serverTools.length} tools loaded`);
76
+ } catch (error) {
77
+ // console.error(`[${this.name}] Failed to initialize ${endpoint}:`, error);
78
+ // Continue with other servers even if one fails
79
+ }
80
+ }
81
+
82
+ // console.log(`[${this.name}] Initialized with ${totalTools} tools from ${this.mcpServers.size} servers`);
83
+ return this;
84
+ } catch (error) {
85
+ // console.error(`[${this.name}] Initialization error:`, error);
86
+ this.tools = [];
87
+ return this;
88
+ }
89
+ }
90
+
91
+ async run(userMessage, options = {}) {
92
+ const { toolApproval = null } = options;
93
+
94
+ const stream = true;
95
+
96
+ if (this.mcpServers.size === 0) {
97
+ await this.initialize();
98
+ }
99
+
100
+ let userPrompt = userMessage;
101
+
102
+ if (this.llm.no_think) {
103
+ userPrompt = `${userMessage} /no_think`;
104
+ }
105
+
106
+ const messages = [
107
+ { role: 'system', content: this.instructions },
108
+ { role: 'user', content: userPrompt },
109
+ ];
110
+
111
+ if (stream) {
112
+ return this.runStreaming(messages, { toolApproval });
113
+ }
114
+ return this.runSync(messages, { toolApproval });
115
+ }
116
+
117
+ async* runStreaming(initialMessages, options = {}) {
118
+ const { toolApproval = null } = options;
119
+ const conversationMessages = [...initialMessages];
120
+ let iteration = 0;
121
+ let finalOutput = '';
122
+
123
+ while (iteration < this.maxIterations) {
124
+ iteration++;
125
+
126
+ yield {
127
+ type: 'iteration_start',
128
+ data: { iteration, messages: conversationMessages },
129
+ };
130
+
131
+ const stream = await this.createLLMStream(conversationMessages);
132
+
133
+ const assistantMessage = {
134
+ role: 'assistant',
135
+ content: '',
136
+ tool_calls: [],
137
+ };
138
+
139
+ const currentToolCalls = {};
140
+ let hasToolCalls = false;
141
+ let finishReason = null;
142
+
143
+ // Process stream
144
+ for await (const chunk of stream) {
145
+ if (chunk.data && chunk.type === 'data') {
146
+ const chunkData = chunk.data;
147
+
148
+ yield {
149
+ type: 'raw_model_stream_event',
150
+ data: { type: 'model', event: chunkData },
151
+ };
152
+
153
+ if (chunkData.choices?.[0]?.delta) {
154
+ const { delta } = chunkData.choices[0];
155
+
156
+ // Handle content
157
+ if (delta.content) {
158
+ if (delta.content.indexOf('<|processing_prompt|>') >= 0) {
159
+ }
160
+ else if (delta.content.indexOf('<|loading_model|>') >= 0) {
161
+ }
162
+ else {
163
+ assistantMessage.content += delta.content;
164
+ finalOutput += delta.content;
165
+ }
166
+
167
+ yield {
168
+ type: 'content_delta',
169
+ data: { content: delta.content },
170
+ };
171
+ }
172
+
173
+ // Handle tool calls
174
+ if (delta.tool_calls) {
175
+ hasToolCalls = true;
176
+
177
+ for (const toolCallDelta of delta.tool_calls) {
178
+ const { index } = toolCallDelta;
179
+
180
+ if (!currentToolCalls[index]) {
181
+ currentToolCalls[index] = {
182
+ id: '',
183
+ type: 'function',
184
+ function: { name: '', arguments: '' },
185
+ };
186
+ }
187
+
188
+ if (toolCallDelta.id) currentToolCalls[index].id = toolCallDelta.id;
189
+ if (toolCallDelta.function?.name) currentToolCalls[index].function.name += toolCallDelta.function.name;
190
+ if (toolCallDelta.function?.arguments) currentToolCalls[index].function.arguments += toolCallDelta.function.arguments;
191
+ }
192
+
193
+ yield {
194
+ type: 'tool_call_delta',
195
+ data: { tool_calls: delta.tool_calls },
196
+ };
197
+ }
198
+
199
+ if (chunkData.choices[0].finish_reason) {
200
+ finishReason = chunkData.choices[0].finish_reason;
201
+ }
202
+ }
203
+ } else if (chunk.type === 'done') {
204
+ break;
205
+ }
206
+ }
207
+
208
+ // Process tool calls if detected
209
+ if (finishReason === 'tool_calls' && hasToolCalls) {
210
+ assistantMessage.tool_calls = Object.values(currentToolCalls).filter((tc) => tc.id && tc.function.name);
211
+ conversationMessages.push(assistantMessage);
212
+
213
+ yield {
214
+ type: 'tool_calls_detected',
215
+ data: { toolCalls: assistantMessage.tool_calls },
216
+ };
217
+
218
+ // Handle tool approval if callback provided
219
+ let approvalResult = {
220
+ stopAfterExecution: false,
221
+ approvals: assistantMessage.tool_calls.map(() => true), // Default: approve all
222
+ };
223
+
224
+ if (toolApproval) {
225
+ try {
226
+ const userApprovalResult = await toolApproval(assistantMessage.tool_calls);
227
+ approvalResult = this.normalizeApprovalResult(userApprovalResult, assistantMessage.tool_calls.length);
228
+ } catch (error) {
229
+ // console.error(`[${this.name}] Tool approval callback error:`, error);
230
+ // On error, deny all tools
231
+ approvalResult = {
232
+ stopAfterExecution: false,
233
+ approvals: assistantMessage.tool_calls.map(() => ({
234
+ approve: false,
235
+ reason: 'Approval callback failed',
236
+ })),
237
+ };
238
+ }
239
+ }
240
+
241
+ yield {
242
+ type: 'tool_approval_result',
243
+ data: { approvalResult },
244
+ };
245
+
246
+ // Process approved and denied tools
247
+ const { approvedTools, denialMessages } = this.processToolApprovals(
248
+ assistantMessage.tool_calls,
249
+ approvalResult.approvals,
250
+ );
251
+
252
+ // Add denial messages to conversation
253
+ conversationMessages.push(...denialMessages);
254
+
255
+ // Execute approved tools
256
+ if (approvedTools.length > 0) {
257
+ const toolResults = await this.executeTools(approvedTools);
258
+ conversationMessages.push(...toolResults);
259
+
260
+ yield {
261
+ type: 'tool_results',
262
+ data: { results: toolResults },
263
+ };
264
+ }
265
+
266
+ // Check if we should stop after execution
267
+ if (approvalResult.stopAfterExecution) {
268
+ yield {
269
+ type: 'conversation_complete',
270
+ data: {
271
+ finalOutput: finalOutput || 'Tool execution completed.',
272
+ messages: conversationMessages,
273
+ iterations: iteration,
274
+ stoppedAfterToolExecution: true,
275
+ },
276
+ };
277
+
278
+ return {
279
+ finalOutput: finalOutput || 'Tool execution completed.',
280
+ messages: conversationMessages,
281
+ stoppedAfterToolExecution: true,
282
+ };
283
+ }
284
+
285
+ // Continue conversation with tool results
286
+ continue;
287
+ } else {
288
+ // Conversation finished
289
+ if (assistantMessage.content || assistantMessage.tool_calls.length > 0) {
290
+ conversationMessages.push(assistantMessage);
291
+ }
292
+
293
+ yield {
294
+ type: 'conversation_complete',
295
+ data: {
296
+ finalOutput,
297
+ messages: conversationMessages,
298
+ iterations: iteration,
299
+ },
300
+ };
301
+
302
+ return { finalOutput, messages: conversationMessages };
303
+ }
304
+ }
305
+
306
+ yield {
307
+ type: 'max_iterations_reached',
308
+ data: { maxIterations: this.maxIterations, finalOutput },
309
+ };
310
+
311
+ return { finalOutput, messages: conversationMessages };
312
+ }
313
+
314
+ async runSync(initialMessages, options = {}) {
315
+ const { toolApproval = null } = options;
316
+ const events = [];
317
+ for await (const event of this.runStreaming(initialMessages, { toolApproval })) {
318
+ events.push(event);
319
+ }
320
+
321
+ const finalEvent = events[events.length - 1];
322
+ return finalEvent.data;
323
+ }
324
+
325
+ async createLLMStream(messages) {
326
+ const body = {
327
+ model: this.llm.model,
328
+ messages,
329
+ tools: this.tools,
330
+ tool_choice: 'auto',
331
+ temperature: this.llm.temperature,
332
+ max_tokens: this.llm.max_tokens,
333
+ stream: true,
334
+ };
335
+
336
+ return createChatCompletionStream(
337
+ { http: this.httpClient },
338
+ JSON.stringify(body),
339
+ this.llm.endpoint,
340
+ this.llm.apiKey,
341
+ );
342
+ }
343
+
344
+ // Helper method to normalize approval result from user callback
345
+ normalizeApprovalResult(userResult, toolCount) {
346
+ // Handle simple boolean array format
347
+ if (Array.isArray(userResult)) {
348
+ return {
349
+ stopAfterExecution: false, // Default
350
+ approvals: userResult,
351
+ };
352
+ }
353
+
354
+ // Handle object format
355
+ if (typeof userResult === 'object' && userResult !== null) {
356
+ return {
357
+ stopAfterExecution: userResult.stopAfterExecution || false,
358
+ approvals: userResult.approvals || Array(toolCount).fill(true),
359
+ };
360
+ }
361
+
362
+ // Fallback - approve all
363
+ return {
364
+ stopAfterExecution: false,
365
+ approvals: Array(toolCount).fill(true),
366
+ };
367
+ }
368
+
369
+ // Helper method to process tool approvals and generate denial messages
370
+ processToolApprovals(toolCalls, approvals) {
371
+ const approvedTools = [];
372
+ const denialMessages = [];
373
+
374
+ for (let i = 0; i < toolCalls.length; i++) {
375
+ const toolCall = toolCalls[i];
376
+ const approval = approvals[i];
377
+
378
+ // Normalize approval to object format
379
+ let normalizedApproval;
380
+ if (typeof approval === 'boolean') {
381
+ normalizedApproval = { approve: approval, reason: null };
382
+ } else if (typeof approval === 'object' && approval !== null) {
383
+ normalizedApproval = {
384
+ approve: approval.approve !== false, // Default to true if not explicitly false
385
+ reason: approval.reason || null,
386
+ };
387
+ } else {
388
+ normalizedApproval = { approve: true, reason: null }; // Default approve
389
+ }
390
+
391
+ if (normalizedApproval.approve) {
392
+ approvedTools.push(toolCall);
393
+ } else {
394
+ // Create denial message with standard format
395
+ const reason = normalizedApproval.reason
396
+ ? ` because: ${normalizedApproval.reason}`
397
+ : '.';
398
+
399
+ denialMessages.push({
400
+ role: 'user',
401
+ content: `I denied the ${toolCall.function.name} tool${reason}`,
402
+ });
403
+ }
404
+ }
405
+
406
+ return { approvedTools, denialMessages };
407
+ }
408
+
409
+ async executeTools(toolCalls) {
410
+ const toolResults = [];
411
+
412
+ for (const toolCall of toolCalls) {
413
+ try {
414
+ // Find which server handles this tool
415
+ const serverEndpoint = this.toolToServerMap.get(toolCall.function.name);
416
+ const mcpServer = this.mcpServers.get(serverEndpoint);
417
+
418
+ if (!mcpServer) {
419
+ throw new Error(`No MCP server found for tool: ${toolCall.function.name}`);
420
+ }
421
+
422
+ let args = toolCall.function.arguments;
423
+ if (typeof args === 'string') {
424
+ args = JSON.parse(args);
425
+ }
426
+
427
+ // console.log(`[${this.name}] Calling tool ${toolCall.function.name} on server ${serverEndpoint}`);
428
+
429
+ const result = await mcpServer.callTool({
430
+ name: toolCall.function.name,
431
+ arguments: args,
432
+ });
433
+
434
+ toolResults.push({
435
+ role: 'tool',
436
+ tool_call_id: toolCall.id,
437
+ name: toolCall.function.name,
438
+ content: JSON.stringify(result),
439
+ });
440
+ } catch (error) {
441
+ // console.error(`[${this.name}] Error calling tool ${toolCall.function.name}:`, error);
442
+
443
+ toolResults.push({
444
+ role: 'tool',
445
+ tool_call_id: toolCall.id,
446
+ name: toolCall.function.name,
447
+ content: JSON.stringify({ error: error.message }),
448
+ });
449
+ }
450
+ }
451
+
452
+ return toolResults;
453
+ }
454
+ }
455
+
456
+ module.exports = { Agent };
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ const { createChatCompletionStream, parseSSEChunk } = require('./oai');
2
+ const { Agent } = require('./agent');
3
+ const { McpResponseParser } = require('./mcp-response-parser');
4
+
5
+ module.exports = {
6
+ createChatCompletionStream,
7
+ parseSSEChunk,
8
+ Agent,
9
+ McpResponseParser,
10
+ };
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Simplified parser for MCP (Model Context Protocol) tool responses
3
+ * Handles the actual MCP format without over-engineering
4
+ *
5
+ * Expected MCP Tool Response Format:
6
+ * [
7
+ * {
8
+ * "role": "tool",
9
+ * "tool_call_id": "tool_0",
10
+ * "name": "read_text_file",
11
+ * "content": "{\"content\":[{\"type\":\"text\",\"text\":\"actual file content here...\"}]}"
12
+ * }
13
+ * ]
14
+ *
15
+ * The "content" field is a JSON string that when parsed contains:
16
+ * {
17
+ * "content": [
18
+ * {
19
+ * "type": "text|image|resource|etc",
20
+ * "text": "content for text type",
21
+ * "data": "content for image/binary types"
22
+ * }
23
+ * ]
24
+ * }
25
+ */
26
+ class McpResponseParser {
27
+ constructor(toolResults) {
28
+ this.results = toolResults || [];
29
+ this.contentByType = {};
30
+
31
+ this._parseResults();
32
+ }
33
+
34
+ /**
35
+ * Parse the tool results and organize content
36
+ * @private
37
+ */
38
+ _parseResults() {
39
+ for (const result of this.results) {
40
+ if (result.content) {
41
+ try {
42
+ // Parse the JSON content string
43
+ const parsed = JSON.parse(result.content);
44
+ const contents = parsed.content || [];
45
+
46
+ // Organize content by type
47
+ for (const item of contents) {
48
+ const type = item.type || 'unknown';
49
+
50
+ if (!this.contentByType[type]) {
51
+ this.contentByType[type] = [];
52
+ }
53
+
54
+ this.contentByType[type].push({
55
+ ...item,
56
+ toolId: result.tool_call_id,
57
+ toolName: result.name,
58
+ });
59
+ }
60
+ } catch (error) {
61
+ console.log(`Failed to parse tool result content: ${error.message}`);
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ // === Tool Information ===
68
+
69
+ /**
70
+ * Get all tool IDs
71
+ */
72
+ getToolIds() {
73
+ return this.results.map(result => result.tool_call_id).filter(Boolean);
74
+ }
75
+
76
+ /**
77
+ * Get all tool names
78
+ */
79
+ getToolNames() {
80
+ return this.results.map(result => result.name).filter(Boolean);
81
+ }
82
+
83
+ /**
84
+ * Get result by tool ID
85
+ */
86
+ getResultByToolId(toolId) {
87
+ return this.results.find(result => result.tool_call_id === toolId) || null;
88
+ }
89
+
90
+ /**
91
+ * Get result by tool name
92
+ */
93
+ getResultByToolName(toolName) {
94
+ return this.results.find(result => result.name === toolName) || null;
95
+ }
96
+
97
+ // === Content Access ===
98
+
99
+ /**
100
+ * Get content by type
101
+ */
102
+ getContentByType(type) {
103
+ return this.contentByType[type] || [];
104
+ }
105
+
106
+ /**
107
+ * Get all content organized by type
108
+ */
109
+ getAllContent() {
110
+ return { ...this.contentByType };
111
+ }
112
+
113
+ /**
114
+ * Get text content (convenience method)
115
+ */
116
+ getTextContent() {
117
+ const textItems = this.getContentByType('text');
118
+ if (textItems.length === 0) return null;
119
+ if (textItems.length === 1) return textItems[0].text;
120
+ return textItems.map(item => item.text);
121
+ }
122
+
123
+ /**
124
+ * Get image content (convenience method)
125
+ */
126
+ getImageContent() {
127
+ return this.getContentByType('image');
128
+ }
129
+
130
+ /**
131
+ * Get resource content (convenience method)
132
+ */
133
+ getResourceContent() {
134
+ return this.getContentByType('resource');
135
+ }
136
+
137
+ // === Summary Info ===
138
+
139
+ /**
140
+ * Check if we have any results
141
+ */
142
+ hasResults() {
143
+ return this.results.length > 0;
144
+ }
145
+
146
+ /**
147
+ * Get available content types
148
+ */
149
+ getContentTypes() {
150
+ return Object.keys(this.contentByType);
151
+ }
152
+
153
+ /**
154
+ * Get summary of parsing results
155
+ */
156
+ getSummary() {
157
+ return {
158
+ toolCount: this.results.length,
159
+ contentTypes: this.getContentTypes(),
160
+ toolIds: this.getToolIds(),
161
+ toolNames: this.getToolNames(),
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Convert to JSON for serialization
167
+ */
168
+ toJSON() {
169
+ return {
170
+ summary: this.getSummary(),
171
+ contentByType: this.contentByType,
172
+ results: this.results,
173
+ };
174
+ }
175
+ }
176
+
177
+ module.exports = { McpResponseParser };