@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.
@@ -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 };