@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/README.md +718 -0
- package/package.json +11 -0
- package/src/agent.js +456 -0
- package/src/index.js +10 -0
- package/src/mcp-response-parser.js +177 -0
- package/src/mcp-server.js +290 -0
- package/src/oai.js +205 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/* eslint-disable no-plusplus */
|
|
2
|
+
|
|
3
|
+
class MCPServer {
|
|
4
|
+
constructor(httpClient, proxyEndpoint, apiKey, options = {}) {
|
|
5
|
+
this.http = httpClient;
|
|
6
|
+
this.endpoint = proxyEndpoint;
|
|
7
|
+
this.apiKey = apiKey;
|
|
8
|
+
this.requestId = 1;
|
|
9
|
+
this.initialized = false;
|
|
10
|
+
|
|
11
|
+
// Tool whitelist configuration
|
|
12
|
+
this.toolWhitelist = options.toolWhitelist || null; // null means all tools allowed
|
|
13
|
+
this.whitelistMode = options.whitelistMode || 'include'; // 'include' or 'exclude'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Set tool whitelist after construction
|
|
17
|
+
setToolWhitelist(whitelist, mode = 'include') {
|
|
18
|
+
this.toolWhitelist = whitelist;
|
|
19
|
+
this.whitelistMode = mode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Clear tool whitelist (allow all tools)
|
|
23
|
+
clearToolWhitelist() {
|
|
24
|
+
this.toolWhitelist = null;
|
|
25
|
+
this.whitelistMode = 'include';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Helper method to filter tools based on whitelist
|
|
29
|
+
filterToolsByWhitelist(tools) {
|
|
30
|
+
if (!this.toolWhitelist || !Array.isArray(this.toolWhitelist)) {
|
|
31
|
+
return tools;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (this.whitelistMode === 'exclude') {
|
|
35
|
+
// Exclude mode: remove tools that are in the whitelist
|
|
36
|
+
return tools.filter(tool => !this.toolWhitelist.includes(tool.name));
|
|
37
|
+
} else {
|
|
38
|
+
// Include mode (default): only include tools that are in the whitelist
|
|
39
|
+
return tools.filter(tool => this.toolWhitelist.includes(tool.name));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Helper method to check if a tool is allowed
|
|
44
|
+
isToolAllowed(toolName) {
|
|
45
|
+
if (!this.toolWhitelist || !Array.isArray(this.toolWhitelist)) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.whitelistMode === 'exclude') {
|
|
50
|
+
return !this.toolWhitelist.includes(toolName);
|
|
51
|
+
} else {
|
|
52
|
+
return this.toolWhitelist.includes(toolName);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Helper method to make JSON-RPC requests
|
|
57
|
+
async makeRequest(method, params = null) {
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const request = {
|
|
60
|
+
jsonrpc: '2.0',
|
|
61
|
+
id: this.requestId++,
|
|
62
|
+
method,
|
|
63
|
+
...(params && { params }),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.http.request({
|
|
67
|
+
url: this.endpoint,
|
|
68
|
+
type: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
'Authorization': this.apiKey && `bearer ${this.apiKey}`,
|
|
72
|
+
},
|
|
73
|
+
data: request,
|
|
74
|
+
success: (result) => {
|
|
75
|
+
try {
|
|
76
|
+
const json = JSON.parse(result.data);
|
|
77
|
+
resolve(json);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
reject(new Error(err.message || 'MCP Error'));
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
error: (err) => {
|
|
83
|
+
reject(err);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Initialize the MCP connection
|
|
90
|
+
async initialize(clientInfo = null) {
|
|
91
|
+
if (this.initialized) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const params = {
|
|
96
|
+
protocolVersion: '2024-11-05',
|
|
97
|
+
capabilities: {},
|
|
98
|
+
clientInfo: clientInfo || { name: 'http-mcp-client', version: '1.0.0' },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const response = await this.makeRequest('initialize', params);
|
|
102
|
+
this.initialized = true;
|
|
103
|
+
return response;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// List available tools (filtered by whitelist)
|
|
107
|
+
async listTools() {
|
|
108
|
+
if (!this.initialized) {
|
|
109
|
+
await this.initialize();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const response = await this.makeRequest('tools/list');
|
|
113
|
+
|
|
114
|
+
// Apply whitelist filtering
|
|
115
|
+
if (response.result && response.result.tools) {
|
|
116
|
+
response.result.tools = this.filterToolsByWhitelist(response.result.tools);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return response.result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Get raw tools list without whitelist filtering
|
|
123
|
+
async listAllTools() {
|
|
124
|
+
if (!this.initialized) {
|
|
125
|
+
await this.initialize();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const response = await this.makeRequest('tools/list');
|
|
129
|
+
return response.result;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Call a specific tool (with whitelist check)
|
|
133
|
+
async callTool({ name, arguments: args }) {
|
|
134
|
+
if (!this.initialized) {
|
|
135
|
+
await this.initialize();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check if tool is allowed by whitelist
|
|
139
|
+
if (!this.isToolAllowed(name)) {
|
|
140
|
+
throw new Error(`Tool '${name}' is not allowed by the current whitelist configuration`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const params = {
|
|
144
|
+
name,
|
|
145
|
+
arguments: args || {},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const response = await this.makeRequest('tools/call', params);
|
|
149
|
+
return response.result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// List available resources
|
|
153
|
+
async listResources() {
|
|
154
|
+
if (!this.initialized) {
|
|
155
|
+
await this.initialize();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const response = await this.makeRequest('resources/list');
|
|
159
|
+
return response.result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Read a resource
|
|
163
|
+
async readResource(uri) {
|
|
164
|
+
if (!this.initialized) {
|
|
165
|
+
await this.initialize();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const params = { uri };
|
|
169
|
+
const response = await this.makeRequest('resources/read', params);
|
|
170
|
+
return response.result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// List available prompts
|
|
174
|
+
async listPrompts() {
|
|
175
|
+
if (!this.initialized) {
|
|
176
|
+
await this.initialize();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const response = await this.makeRequest('prompts/list');
|
|
180
|
+
return response.result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Get a prompt
|
|
184
|
+
async getPrompt(name, args = {}) {
|
|
185
|
+
if (!this.initialized) {
|
|
186
|
+
await this.initialize();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const params = {
|
|
190
|
+
name,
|
|
191
|
+
arguments: args,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const response = await this.makeRequest('prompts/get', params);
|
|
195
|
+
return response.result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Generic method to call any MCP method
|
|
199
|
+
async call(method, params = null) {
|
|
200
|
+
if (!this.initialized && method !== 'initialize') {
|
|
201
|
+
await this.initialize();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return this.makeRequest(method, params);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Get whitelist configuration info
|
|
208
|
+
getWhitelistInfo() {
|
|
209
|
+
return {
|
|
210
|
+
whitelist: this.toolWhitelist,
|
|
211
|
+
mode: this.whitelistMode,
|
|
212
|
+
isActive: this.toolWhitelist !== null && Array.isArray(this.toolWhitelist)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Convert MCP tools to OpenAI function format (respects whitelist)
|
|
218
|
+
function convertMCPToolsToOpenAI(mcpTools, toolWhitelist = null, whitelistMode = 'include') {
|
|
219
|
+
let toolsToConvert = mcpTools;
|
|
220
|
+
|
|
221
|
+
// Apply whitelist filtering if provided
|
|
222
|
+
if (toolWhitelist && Array.isArray(toolWhitelist)) {
|
|
223
|
+
if (whitelistMode === 'exclude') {
|
|
224
|
+
toolsToConvert = mcpTools.filter(tool => !toolWhitelist.includes(tool.name));
|
|
225
|
+
} else {
|
|
226
|
+
toolsToConvert = mcpTools.filter(tool => toolWhitelist.includes(tool.name));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const converted = toolsToConvert.map((tool) => ({
|
|
231
|
+
type: 'function',
|
|
232
|
+
function: {
|
|
233
|
+
name: tool.name,
|
|
234
|
+
description: tool.description,
|
|
235
|
+
parameters: tool.inputSchema || {
|
|
236
|
+
type: 'object',
|
|
237
|
+
properties: {},
|
|
238
|
+
required: [],
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
}));
|
|
242
|
+
|
|
243
|
+
// this.log('🔄 Converted MCP tools to OpenAI format', {
|
|
244
|
+
// originalCount: mcpTools.length,
|
|
245
|
+
// filteredCount: toolsToConvert.length,
|
|
246
|
+
// convertedCount: converted.length,
|
|
247
|
+
// tools: converted.map(t => t.function.name),
|
|
248
|
+
// whitelist: toolWhitelist,
|
|
249
|
+
// whitelistMode: whitelistMode
|
|
250
|
+
// });
|
|
251
|
+
|
|
252
|
+
return converted;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Usage example:
|
|
256
|
+
/*
|
|
257
|
+
const mcpClient = new MCPClient(http, 'http://localhost:3000/mcp');
|
|
258
|
+
|
|
259
|
+
// Example usage:
|
|
260
|
+
async function example() {
|
|
261
|
+
try {
|
|
262
|
+
// Initialize (optional - will auto-initialize on first call)
|
|
263
|
+
await mcpClient.initialize();
|
|
264
|
+
|
|
265
|
+
// List tools
|
|
266
|
+
const toolsResult = await mcpClient.listTools();
|
|
267
|
+
console.log('Available tools:', toolsResult);
|
|
268
|
+
|
|
269
|
+
// Call a tool
|
|
270
|
+
const result = await mcpClient.callTool({
|
|
271
|
+
name: 'add',
|
|
272
|
+
arguments: { a: 5, b: 3 }
|
|
273
|
+
});
|
|
274
|
+
console.log('Tool result:', result);
|
|
275
|
+
|
|
276
|
+
// List resources
|
|
277
|
+
const resources = await mcpClient.listResources();
|
|
278
|
+
console.log('Available resources:', resources);
|
|
279
|
+
|
|
280
|
+
// Read a resource
|
|
281
|
+
const resourceContent = await mcpClient.readResource('file://example.txt');
|
|
282
|
+
console.log('Resource content:', resourceContent);
|
|
283
|
+
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error('MCP Error:', error);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
*/
|
|
289
|
+
|
|
290
|
+
module.exports = { MCPServer, convertMCPToolsToOpenAI };
|
package/src/oai.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/* eslint-disable no-await-in-loop, no-loop-func, no-plusplus, no-continue */
|
|
2
|
+
|
|
3
|
+
// Parse SSE (Server-Sent Events) data
|
|
4
|
+
function parseSSEChunk(rawData) {
|
|
5
|
+
if (typeof rawData !== 'string') {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Split by double newlines to separate SSE messages
|
|
10
|
+
const messages = rawData.split('\n\n');
|
|
11
|
+
const parsedMessages = [];
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < messages.length; i++) {
|
|
14
|
+
const message = messages[i].trim();
|
|
15
|
+
if (!message) continue;
|
|
16
|
+
|
|
17
|
+
const lines = message.split('\n');
|
|
18
|
+
let data = null;
|
|
19
|
+
let eventType = null;
|
|
20
|
+
let id = null;
|
|
21
|
+
|
|
22
|
+
for (const line of lines) {
|
|
23
|
+
const trimmedLine = line.trim();
|
|
24
|
+
if (trimmedLine.startsWith('data: ')) {
|
|
25
|
+
data = trimmedLine.substring(6).trim();
|
|
26
|
+
} else if (trimmedLine.startsWith('event: ')) {
|
|
27
|
+
eventType = trimmedLine.substring(7).trim();
|
|
28
|
+
} else if (trimmedLine.startsWith('id: ')) {
|
|
29
|
+
id = trimmedLine.substring(4).trim();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (data !== null) {
|
|
34
|
+
// Handle special SSE termination message (comes as: data: [DONE])
|
|
35
|
+
if (data === '[DONE]') {
|
|
36
|
+
parsedMessages.push({ type: 'done', data: '[DONE]' });
|
|
37
|
+
} else {
|
|
38
|
+
try {
|
|
39
|
+
// Try to parse as JSON
|
|
40
|
+
const jsonData = JSON.parse(data);
|
|
41
|
+
parsedMessages.push({
|
|
42
|
+
type: 'data',
|
|
43
|
+
data: jsonData,
|
|
44
|
+
event: eventType,
|
|
45
|
+
id,
|
|
46
|
+
raw: data,
|
|
47
|
+
});
|
|
48
|
+
} catch (e) {
|
|
49
|
+
// If not valid JSON, return as raw data
|
|
50
|
+
parsedMessages.push({
|
|
51
|
+
type: 'data',
|
|
52
|
+
data,
|
|
53
|
+
event: eventType,
|
|
54
|
+
id,
|
|
55
|
+
raw: data,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parsedMessages.length > 0 ? parsedMessages : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Alternative implementation using a more direct approach with queues
|
|
66
|
+
async function* createChatCompletionStream(context, body, genAiChatUri, genAiApiKey, stream = true) {
|
|
67
|
+
const { http } = context;
|
|
68
|
+
|
|
69
|
+
let headers;
|
|
70
|
+
if (genAiApiKey) {
|
|
71
|
+
headers = { authorization: genAiApiKey };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Use a queue-based approach
|
|
75
|
+
const chunkQueue = [];
|
|
76
|
+
let streamEnded = false;
|
|
77
|
+
let streamError = null;
|
|
78
|
+
let resolveNext = null;
|
|
79
|
+
let sseBuffer = ''; // Buffer for incomplete SSE messages
|
|
80
|
+
|
|
81
|
+
const streamCb = {
|
|
82
|
+
ondata: (result) => {
|
|
83
|
+
if (stream && result.data) {
|
|
84
|
+
// Handle SSE parsing
|
|
85
|
+
sseBuffer += result.data;
|
|
86
|
+
|
|
87
|
+
// Check if we have complete SSE messages (ending with \n\n)
|
|
88
|
+
if (sseBuffer.includes('\n\n')) {
|
|
89
|
+
const parts = sseBuffer.split('\n\n');
|
|
90
|
+
|
|
91
|
+
// Process all complete messages (all parts except the last one)
|
|
92
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
93
|
+
const completeMessage = `${parts[i]}\n\n`;
|
|
94
|
+
|
|
95
|
+
const parsed = parseSSEChunk(completeMessage);
|
|
96
|
+
if (parsed) {
|
|
97
|
+
for (const message of parsed) {
|
|
98
|
+
chunkQueue.push(message);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Keep the last part (potentially incomplete) in buffer
|
|
104
|
+
sseBuffer = parts[parts.length - 1];
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
// Non-SSE data, push as-is
|
|
108
|
+
chunkQueue.push(result);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (resolveNext) {
|
|
112
|
+
resolveNext();
|
|
113
|
+
resolveNext = null;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
onend: (result) => {
|
|
117
|
+
// Process any remaining buffer content
|
|
118
|
+
if (stream && sseBuffer.length > 0) {
|
|
119
|
+
// Add \n\n if not present to make it a complete message
|
|
120
|
+
const finalMessage = sseBuffer.endsWith('\n\n') ? sseBuffer : `${sseBuffer}\n\n`;
|
|
121
|
+
const parsed = parseSSEChunk(finalMessage);
|
|
122
|
+
if (parsed) {
|
|
123
|
+
for (const message of parsed) {
|
|
124
|
+
chunkQueue.push(message);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
sseBuffer = '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (result) {
|
|
131
|
+
chunkQueue.push(result);
|
|
132
|
+
}
|
|
133
|
+
streamEnded = true;
|
|
134
|
+
if (resolveNext) {
|
|
135
|
+
resolveNext();
|
|
136
|
+
resolveNext = null;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
onerror: (err) => {
|
|
140
|
+
streamError = err;
|
|
141
|
+
if (resolveNext) {
|
|
142
|
+
resolveNext();
|
|
143
|
+
resolveNext = null;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// Start the request
|
|
149
|
+
const requestPromise = new Promise((resolve, reject) => {
|
|
150
|
+
http.request({
|
|
151
|
+
url: genAiChatUri,
|
|
152
|
+
type: 'POST',
|
|
153
|
+
headers,
|
|
154
|
+
data: body,
|
|
155
|
+
mode: 'stream',
|
|
156
|
+
success: (result) => {
|
|
157
|
+
if (result.done === undefined) {
|
|
158
|
+
streamCb.onend(result);
|
|
159
|
+
} else if (result.done === true) {
|
|
160
|
+
streamCb.onend();
|
|
161
|
+
} else if (result.done === false) {
|
|
162
|
+
streamCb.ondata(result);
|
|
163
|
+
}
|
|
164
|
+
resolve();
|
|
165
|
+
},
|
|
166
|
+
error: (err) => {
|
|
167
|
+
streamCb.onerror(err);
|
|
168
|
+
reject(err);
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Start the request (don't await it, let it run in background)
|
|
174
|
+
requestPromise.catch(() => { }); // Handle errors through streamError
|
|
175
|
+
|
|
176
|
+
// Yield chunks as they arrive
|
|
177
|
+
while (true) {
|
|
178
|
+
if (streamError) {
|
|
179
|
+
throw streamError;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (chunkQueue.length > 0) {
|
|
183
|
+
const chunk = chunkQueue.shift();
|
|
184
|
+
|
|
185
|
+
// Always yield the chunk first
|
|
186
|
+
yield chunk;
|
|
187
|
+
|
|
188
|
+
// THEN check if this was the termination message and break AFTER yielding
|
|
189
|
+
if (stream && chunk.type === 'done' && chunk.data === '[DONE]') {
|
|
190
|
+
// Stream is complete, break out of the loop
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
} else if (streamEnded) {
|
|
194
|
+
// No more chunks and stream is ended
|
|
195
|
+
break;
|
|
196
|
+
} else {
|
|
197
|
+
// Wait for next chunk
|
|
198
|
+
await new Promise((resolve) => {
|
|
199
|
+
resolveNext = resolve;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = { createChatCompletionStream, parseSSEChunk };
|