@peebles-group/agentlib-js 1.0.1

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/LICENSE.txt ADDED
@@ -0,0 +1,8 @@
1
+ Copyright 2025 Peebles Group
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # AgentLib
2
+
3
+ A lightweight Node.js library for building AI agents with LLM providers and MCP (Model Context Protocol) server integration.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install peebles-agentlib
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ 1. **Set up API keys**
14
+ ```bash
15
+ # Create .env file
16
+ OPENAI_API_KEY=your_openai_key
17
+ GEMINI_API_KEY=your_gemini_key
18
+ ```
19
+
20
+ 2. **Create a new project**
21
+ ```bash
22
+ mkdir my-agent-project
23
+ cd my-agent-project
24
+ npm init -y
25
+ npm install peebles-agentlib dotenv
26
+ ```
27
+
28
+ ## Features
29
+
30
+ - **Multi-Provider LLM Support**: OpenAI, Gemini
31
+ - **MCP Integration**: Browser automation, filesystem, web search, memory
32
+ - **Tool Calling**: Native function execution with type safety
33
+ - **Structured Output**: Zod schema validation
34
+ - **Agent Orchestration**: Multi-step reasoning with tool use
35
+
36
+ ## Basic Usage
37
+
38
+ ```javascript
39
+ import { Agent } from 'peebles-agentlib';
40
+ import dotenv from 'dotenv';
41
+ dotenv.config();
42
+
43
+ // Simple agent
44
+ const agent = new Agent('openai', process.env.OPENAI_API_KEY, {
45
+ model: 'gpt-4o-mini'
46
+ });
47
+ agent.addInput({ role: 'user', content: 'Hello!' });
48
+ const response = await agent.run();
49
+ console.log(response.output_text);
50
+
51
+ // Agent with MCP servers (auto-installs packages)
52
+ const mcpAgent = new Agent('openai', process.env.OPENAI_API_KEY, {
53
+ model: 'gpt-4o-mini',
54
+ enableMCP: true
55
+ });
56
+
57
+ await mcpAgent.addMCPServer('browser', {
58
+ type: 'stdio',
59
+ command: 'npx',
60
+ args: ['@playwright/mcp@latest']
61
+ });
62
+ ```
63
+
64
+ ## Structured Outputs
65
+
66
+ AgentLib supports type-safe structured outputs using Zod schemas for reliable JSON responses.
67
+
68
+ ```javascript
69
+ import { Agent } from 'peebles-agentlib';
70
+ import { z } from 'zod';
71
+ import dotenv from 'dotenv';
72
+ dotenv.config();
73
+
74
+ // Define schema with Zod
75
+ const ResponseSchema = z.object({
76
+ answer: z.string(),
77
+ confidence: z.number(),
78
+ sources: z.array(z.string())
79
+ });
80
+
81
+ const agent = new Agent('openai', process.env.OPENAI_API_KEY, {
82
+ model: 'gpt-4o-mini',
83
+ outputSchema: ResponseSchema // Pass Zod object directly
84
+ });
85
+
86
+ agent.addInput({ role: 'user', content: 'What is the capital of France?' });
87
+ const result = await agent.run();
88
+
89
+ // Access structured data from the result
90
+ const parsedData = result.output_parsed; // Structured data when schema is used
91
+ const text = result.output_text; // Raw text response
92
+ ```
93
+
94
+ **Key Points:**
95
+ - **Input/Output Schemas**: Pass Zod objects directly to `inputSchema`/`outputSchema`
96
+ - **Raw Text**: Access via `result.output_text` (when no schema)
97
+ - **Type Safety**: Automatic validation and TypeScript support
98
+ - **Model Support**: Works with `gpt-4o-mini` and `gpt-4o` models
99
+
100
+ ## Examples
101
+
102
+ The repository includes several development examples that demonstrate different features:
103
+
104
+ - **`examples/simpleAgent/`** - Basic agent usage with tools
105
+ - **`examples/mcp-example/`** - Full MCP integration demo
106
+ - **`examples/translatorExample/`** - Multi-agent orchestration
107
+ - **`examples/sqlAgent/`** - Database operations
108
+ - **`examples/schema-example/`** - Structured input/output with Zod schemas
109
+ - **`examples/rag-example/`** - Agentic RAG example with mongodb hybrid search
110
+
111
+ **Note:** These examples use relative imports for development. In your projects, use the npm package:
112
+
113
+ ```javascript
114
+ // In your project
115
+ import { Agent } from 'peebles-agentlib';
116
+
117
+ // Instead of (development only)
118
+ import { Agent } from './src/Agent.js';
119
+ ```
120
+
121
+ ## API Reference
122
+
123
+ ### Agent Constructor
124
+ ```javascript
125
+ const agent = new Agent(provider, apiKey, options);
126
+ ```
127
+
128
+ **Parameters:**
129
+ - `provider` (string): LLM provider name ('openai', 'gemini')
130
+ - `apiKey` (string): API key for the provider
131
+ - `options` (object): Configuration options
132
+ - `model` (string): LLM model name (default: 'gpt-4o-mini')
133
+ - `tools` (array): Native function tools
134
+ - `enableMCP` (boolean): Enable MCP servers
135
+ - `inputSchema` (Zod object): Input validation schema
136
+ - `outputSchema` (Zod object): Output validation schema
137
+
138
+ **Example:**
139
+ ```javascript
140
+ import { Agent } from 'peebles-agentlib';
141
+
142
+ const agent = new Agent('openai', process.env.OPENAI_API_KEY, {
143
+ model: 'gpt-4o-mini',
144
+ tools: [],
145
+ enableMCP: true,
146
+ inputSchema: zodSchema,
147
+ outputSchema: zodSchema
148
+ });
149
+ ```
150
+
151
+ ### LLM Providers
152
+ - **OpenAI**: `gpt-4o-mini`, `gpt-4o`, `gpt-3.5-turbo`
153
+ - **Gemini**: `gemini-2.5-flash-lite`
154
+
155
+ Input format follows OpenAI's message structure:
156
+ ```javascript
157
+ [{ role: 'user', content: 'Hello' }]
158
+ ```
159
+
160
+ ### LLM Result Format
161
+
162
+ When calling an LLM, the result object has the following structure:
163
+
164
+ ```javascript
165
+ {
166
+ "id": "resp_67ccd2bed1ec8190b14f964abc0542670bb6a6b452d3795b",
167
+ "object": "response",
168
+ "created_at": 1741476542,
169
+ "status": "completed",
170
+ "error": null,
171
+ "incomplete_details": null,
172
+ "instructions": null,
173
+ "max_output_tokens": null,
174
+ "model": "gpt-4.1-2025-04-14",
175
+ "output": [
176
+ {
177
+ "type": "message",
178
+ "id": "msg_67ccd2bf17f0819081ff3bb2cf6508e60bb6a6b452d3795b",
179
+ "status": "completed",
180
+ "role": "assistant",
181
+ "content": [
182
+ {
183
+ "type": "output_text",
184
+ "text": "In a peaceful grove beneath a silver moon...",
185
+ "annotations": []
186
+ }
187
+ ]
188
+ },
189
+ {
190
+ type: 'function',
191
+ description: 'Search the web for information',
192
+ name: 'web_search',
193
+ parameters: [Object],
194
+ strict: true
195
+ }
196
+ ],
197
+ "parallel_tool_calls": true,
198
+ "previous_response_id": null,
199
+ "reasoning": {
200
+ "effort": null,
201
+ "summary": null
202
+ },
203
+ "store": true,
204
+ "temperature": 1.0,
205
+ "text": {
206
+ "format": {
207
+ "type": "text"
208
+ }
209
+ },
210
+ "tool_choice": "auto",
211
+ "tools": [],
212
+ "top_p": 1.0,
213
+ "truncation": "disabled",
214
+ "usage": {
215
+ "input_tokens": 36,
216
+ "input_tokens_details": {
217
+ "cached_tokens": 0
218
+ },
219
+ "output_tokens": 87,
220
+ "output_tokens_details": {
221
+ "reasoning_tokens": 0
222
+ },
223
+ "total_tokens": 123
224
+ },
225
+ "user": null,
226
+ "metadata": {}
227
+ }
228
+ ```
229
+
230
+ **Key Fields:**
231
+ - `output_text` - The actual response text
232
+ - `output_parsed` - Response ONLY WHEN OUTPUT SCHEMA IS PRESENT
233
+ - `usage` - Token consumption details
234
+ - `model` - The model used for the response
235
+ - `status` - Response status ("completed", "failed", etc.)
236
+
237
+
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { LLMService } from './src/llmService.js';
2
+ export { Agent } from './src/Agent.js';
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@peebles-group/agentlib-js",
3
+ "version": "1.0.1",
4
+ "description": "A minimal JavaScript library implementing concurrent async agents for illustrating multi-agent systems and other agentic design patterns including recursive ones purely through function calling loops.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node index.js",
9
+ "test": "echo \"Error: no test specified\" && exit 1"
10
+ },
11
+ "author": "Peebles Group",
12
+ "license": "MIT",
13
+ "dependencies": {
14
+ "@google/genai": "^1.16.0",
15
+ "@modelcontextprotocol/sdk": "^1.18.2",
16
+ "openai": "^6.0.0"
17
+ },
18
+ "devDependencies": {
19
+ "dotenv": "^16.6.1",
20
+ "js-yaml": "^4.1.0",
21
+ "mongodb": "^6.20.0",
22
+ "openai": "^5.16.0",
23
+ "agentlib-js": "^1.0.0",
24
+ "playwright": "^1.55.0",
25
+ "prompt-sync": "^4.2.0",
26
+ "sqlite": "^5.1.1",
27
+ "sqlite3": "^5.1.7",
28
+ "zod": "^3.25.76"
29
+ },
30
+ "files": [
31
+ "index.js",
32
+ "src/",
33
+ "README.md",
34
+ "LICENSE"
35
+ ]
36
+ }
package/src/Agent.js ADDED
@@ -0,0 +1,132 @@
1
+ import { LLMService } from "./llmService.js";
2
+ import { defaultModel } from "./config.js";
3
+ import { MCPManager } from "./mcp/MCPManager.js";
4
+
5
+ export class Agent {
6
+ constructor(provider, apiKey, {model = defaultModel, tools = [], inputSchema = null, outputSchema = null, enableMCP = false} = {}) {
7
+ this.llmService = new LLMService(provider, apiKey);
8
+ this.model = model;
9
+ this.nativeTools = tools;
10
+ this.inputSchema = inputSchema;
11
+ this.outputSchema = outputSchema;
12
+ this.mcpManager = enableMCP ? new MCPManager() : null;
13
+ this.updateSystemPrompt();
14
+ }
15
+
16
+ async addMCPServer(serverName, config) {
17
+ if (!this.mcpManager) {
18
+ throw new Error("MCP is not enabled for this agent");
19
+ }
20
+
21
+ const result = await this.mcpManager.addServer(serverName, config);
22
+ this.updateSystemPrompt();
23
+ return result;
24
+ }
25
+
26
+ async removeMCPServer(serverName) {
27
+ if (!this.mcpManager) return false;
28
+
29
+ const result = await this.mcpManager.removeServer(serverName);
30
+ if (result) this.updateSystemPrompt();
31
+ return result;
32
+ }
33
+
34
+ getAllTools() {
35
+ const mcpTools = this.mcpManager ? this.mcpManager.getAllTools() : [];
36
+ return [...this.nativeTools, ...mcpTools];
37
+ }
38
+
39
+ getMCPInfo() {
40
+ return this.mcpManager ? this.mcpManager.getServerInfo() : { enabled: false };
41
+ }
42
+
43
+ // Mentioning the tools in the system prompt for maximum reliability
44
+ updateSystemPrompt() {
45
+ const allTools = this.getAllTools();
46
+ this.input = [{
47
+ role: 'system',
48
+ content: 'You are a tool-calling agent. You have access to the following tools: ' +
49
+ allTools.map(tool => `${tool.name}: ${tool.description}`).join('; ') +
50
+ '. Use these tools to answer the user\'s questions.'
51
+ }];
52
+ }
53
+
54
+ addInput(input) {
55
+ if (this.inputSchema) {
56
+ this.inputSchema.parse(input);
57
+ }
58
+ this.input.push(input);
59
+ }
60
+
61
+ /**
62
+ * Run the agent for a single step
63
+ */
64
+ async run() {
65
+ const allTools = this.getAllTools();
66
+
67
+ // Step 1: send input to model
68
+ let response = await this.llmService.chat(this.input, {
69
+ model: this.model,
70
+ outputSchema: this.outputSchema,
71
+ tools: allTools,
72
+ });
73
+
74
+ const { output, rawResponse } = response;
75
+
76
+ // Step 2: Clean and add the response to input history
77
+ // Remove parsed_arguments (if it exists) from function calls before adding to history
78
+ const cleanedOutput = rawResponse.output.map(item => {
79
+ if (item.type === "function_call" && item.parsed_arguments) {
80
+ const { parsed_arguments, ...cleanItem } = item;
81
+ return cleanItem;
82
+ }
83
+ return item;
84
+ });
85
+
86
+ this.input = this.input.concat(cleanedOutput);
87
+
88
+ // Step 3: collect all function calls
89
+ const functionCalls = rawResponse.output.filter(item => item.type === "function_call");
90
+
91
+ if (functionCalls.length > 0) {
92
+ for (const call of functionCalls) {
93
+ let args;
94
+ try {
95
+ args = JSON.parse(call.arguments);
96
+ } catch (err) {
97
+ console.error("Failed to parse function call arguments:", call.arguments);
98
+ continue;
99
+ }
100
+
101
+ const tool = allTools.find(t => t.name === call.name);
102
+ if (!tool || !tool.func) {
103
+ throw new Error(`Tool ${call.name} not found or missing implementation.`);
104
+ }
105
+
106
+ // Step 4: execute the function
107
+ const result = await tool.func(args);
108
+
109
+ // Step 5: append function call output to input
110
+ this.input.push({
111
+ type: "function_call_output",
112
+ call_id: call.call_id,
113
+ output: JSON.stringify(result),
114
+ });
115
+ }
116
+
117
+ // Step 6: send updated input back to model for final response
118
+ response = await this.llmService.chat(this.input, {
119
+ tools: allTools,
120
+ model: this.model,
121
+ outputSchema: this.outputSchema,
122
+ });
123
+ }
124
+ return response;
125
+ }
126
+
127
+ async cleanup() {
128
+ if (this.mcpManager) {
129
+ await this.mcpManager.cleanup();
130
+ }
131
+ }
132
+ }
package/src/config.js ADDED
@@ -0,0 +1 @@
1
+ export const defaultModel = 'gpt-4o-mini';
@@ -0,0 +1,33 @@
1
+ import { validateProviderName } from './providers/registry.js';
2
+
3
+ export class LLMService {
4
+ constructor(provider, apiKey) {
5
+ this.provider = validateProviderName(provider);
6
+ this.apiKey = apiKey;
7
+ this.client = null;
8
+
9
+ if (!apiKey) {
10
+ throw new Error(`API key is required for provider: ${provider}`);
11
+ }
12
+ }
13
+
14
+ async _getProviderClient() {
15
+
16
+ if (!this.client) {
17
+ const provider = await import(`./providers/${this.provider}.js`);
18
+ this.client = provider.createClient(this.apiKey);
19
+ }
20
+ return this.client;
21
+ }
22
+
23
+ async chat(input, {inputSchema = null, outputSchema = null, ...options} = {}) {
24
+ const client = await this._getProviderClient();
25
+ const provider = await import(`./providers/${this.provider}.js`);
26
+
27
+ return provider.chat(client, input, {
28
+ inputSchema,
29
+ outputSchema,
30
+ ...options
31
+ });
32
+ }
33
+ }
@@ -0,0 +1,109 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
4
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
+
6
+ class MCPClient {
7
+ constructor() {
8
+ this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
9
+ this.tools = [];
10
+ this.isConnected = false;
11
+ }
12
+
13
+ async connectToServer(server) {
14
+ try {
15
+ switch (server.type) {
16
+ case "stdio":
17
+ this.transport = new StdioClientTransport(server);
18
+ break;
19
+ case "sse":
20
+ this.transport = new SSEClientTransport(server.url);
21
+ break;
22
+ case "streamableHttp":
23
+ this.transport = new StreamableHTTPClientTransport(server.url);
24
+ break;
25
+ default:
26
+ throw new Error("Invalid server type");
27
+ }
28
+ await this.mcp.connect(this.transport);
29
+
30
+ const toolsResult = await this.mcp.listTools();
31
+ this.tools = toolsResult.tools.map((tool) => {
32
+ return {
33
+ type: "function",
34
+ name: tool.name,
35
+ description: tool.description,
36
+ parameters: tool.inputSchema,
37
+ func: async (args) => {
38
+ return await this.executeTool(tool.name, args);
39
+ }
40
+ };
41
+ });
42
+ this.isConnected = true;
43
+ console.log(
44
+ "Connected to MCP server with tools:",
45
+ this.tools.map(({ name }) => name)
46
+ );
47
+
48
+ return this.tools;
49
+ } catch (e) {
50
+ console.log("Failed to connect to MCP server: ", e);
51
+ throw e;
52
+ }
53
+ }
54
+
55
+ async executeTool(toolName, args) {
56
+ if (!this.isConnected) {
57
+ throw new Error("MCP client is not connected to a server");
58
+ }
59
+
60
+ const tool = this.tools.find(t => t.name === toolName);
61
+ if (!tool) {
62
+ throw new Error(`Tool '${toolName}' not found on MCP server`);
63
+ }
64
+
65
+ try {
66
+ const result = await this.mcp.callTool({
67
+ name: toolName,
68
+ arguments: args,
69
+ });
70
+ return result.content;
71
+ } catch (error) {
72
+ console.error(`Error executing MCP tool '${toolName}':`, error);
73
+ throw error;
74
+ }
75
+ }
76
+
77
+ getTools() {
78
+ if (!this.isConnected) {
79
+ return [];
80
+ }
81
+ return this.tools;
82
+ }
83
+
84
+ getToolNames() {
85
+ return this.tools.map(tool => tool.name);
86
+ }
87
+
88
+ getAvailableTools() {
89
+ return this.tools.map(tool => tool.name);
90
+ }
91
+
92
+ getAgentTools() {
93
+ return this.tools;
94
+ }
95
+
96
+ isServerConnected() {
97
+ return this.isConnected;
98
+ }
99
+
100
+ async disconnect() {
101
+ if (this.transport && this.isConnected) {
102
+ await this.transport.close();
103
+ this.isConnected = false;
104
+ this.tools = [];
105
+ }
106
+ }
107
+ }
108
+
109
+ export default MCPClient;
@@ -0,0 +1,240 @@
1
+ import MCPClient from "./MCPClient.js";
2
+
3
+ export class MCPManager {
4
+ constructor() {
5
+ this.clients = new Map();
6
+ this.serverConfigs = new Map();
7
+ }
8
+
9
+ async addServer(serverName, serverConfig) {
10
+ if (this.clients.has(serverName)) {
11
+ throw new Error(`MCP server '${serverName}' already exists`);
12
+ }
13
+
14
+ try {
15
+ const client = new MCPClient();
16
+ const tools = await client.connectToServer(serverConfig);
17
+
18
+ this.clients.set(serverName, client);
19
+ this.serverConfigs.set(serverName, serverConfig);
20
+
21
+ console.log(`MCPManager: Connected to server '${serverName}' with ${tools.length} tools`);
22
+ return { serverName, tools, toolCount: tools.length };
23
+
24
+ } catch (error) {
25
+ console.error(`MCPManager: Failed to connect to server '${serverName}':`, error);
26
+ throw error;
27
+ }
28
+ }
29
+
30
+ async removeServer(serverName) {
31
+ const client = this.clients.get(serverName);
32
+ if (!client) {
33
+ console.warn(`MCPManager: Server '${serverName}' not found`);
34
+ return false;
35
+ }
36
+
37
+ try {
38
+ await client.disconnect();
39
+ this.clients.delete(serverName);
40
+ this.serverConfigs.delete(serverName);
41
+ console.log(`MCPManager: Disconnected from server '${serverName}'`);
42
+ return true;
43
+ } catch (error) {
44
+ console.error(`MCPManager: Error disconnecting from server '${serverName}':`, error);
45
+ throw error;
46
+ }
47
+ }
48
+
49
+ // Get all tools from all connected servers
50
+ getAllTools() {
51
+ const allTools = [];
52
+
53
+ for (const [serverName, client] of this.clients) {
54
+ allTools.push(...client.getTools());
55
+ }
56
+
57
+ return allTools;
58
+ }
59
+
60
+ // Execute a tool by finding the right server
61
+ async executeTool(toolName, args) {
62
+ for (const [serverName, client] of this.clients) {
63
+ if (client.isServerConnected()) {
64
+ const availableTools = client.getAvailableTools();
65
+ if (availableTools.includes(toolName)) {
66
+ console.log(`MCPManager: Executing '${toolName}' on server '${serverName}'`);
67
+ return await client.executeTool(toolName, args);
68
+ }
69
+ }
70
+ }
71
+
72
+ throw new Error(`MCPManager: Tool '${toolName}' not found on any connected server`);
73
+ }
74
+
75
+ // Get comprehensive server information
76
+ getServerInfo() {
77
+ const info = {
78
+ totalServers: this.clients.size,
79
+ connectedServers: 0,
80
+ totalTools: 0,
81
+ servers: {}
82
+ };
83
+
84
+ for (const [serverName, client] of this.clients) {
85
+ const isConnected = client.isServerConnected();
86
+ const tools = isConnected ? client.getAvailableTools() : [];
87
+
88
+ if (isConnected) {
89
+ info.connectedServers++;
90
+ info.totalTools += tools.length;
91
+ }
92
+
93
+ info.servers[serverName] = {
94
+ connected: isConnected,
95
+ toolCount: tools.length,
96
+ tools: tools,
97
+ };
98
+ }
99
+
100
+ return info;
101
+ }
102
+
103
+ // Health check all servers
104
+ async healthCheck() {
105
+ const results = {
106
+ manager: 'healthy',
107
+ servers: {}
108
+ };
109
+
110
+ for (const [serverName, client] of this.clients) {
111
+ try {
112
+ if (client.isServerConnected()) {
113
+ // Try to get tools as a health check
114
+ client.getAvailableTools();
115
+ results.servers[serverName] = 'healthy';
116
+ } else {
117
+ results.servers[serverName] = 'disconnected';
118
+ }
119
+ } catch (error) {
120
+ results.servers[serverName] = 'unhealthy';
121
+ console.warn(`MCPManager: Health check failed for '${serverName}':`, error.message);
122
+ }
123
+ }
124
+
125
+ return results;
126
+ }
127
+
128
+ // Reconnect a specific server
129
+ async reconnectServer(serverName) {
130
+ const config = this.serverConfigs.get(serverName);
131
+ if (!config) {
132
+ throw new Error(`MCPManager: No config found for server '${serverName}'`);
133
+ }
134
+
135
+ // Remove existing connection
136
+ await this.removeServer(serverName);
137
+
138
+ // Reconnect
139
+ return await this.addServer(serverName, config);
140
+ }
141
+
142
+ // Reconnect all disconnected servers
143
+ async reconnectAll() {
144
+ const results = [];
145
+
146
+ for (const [serverName, client] of this.clients) {
147
+ if (!client.isServerConnected()) {
148
+ try {
149
+ const result = await this.reconnectServer(serverName);
150
+ results.push({ serverName, status: 'reconnected', ...result });
151
+ } catch (error) {
152
+ results.push({ serverName, status: 'failed', error: error.message });
153
+ }
154
+ }
155
+ }
156
+
157
+ return results;
158
+ }
159
+
160
+ // Batch operations
161
+ async addMultipleServers(serverConfigs) {
162
+ const results = [];
163
+
164
+ for (const { name, config } of serverConfigs) {
165
+ try {
166
+ const result = await this.addServer(name, config);
167
+ results.push({ ...result, status: 'success' });
168
+ } catch (error) {
169
+ results.push({ serverName: name, status: 'failed', error: error.message });
170
+ }
171
+ }
172
+
173
+ return results;
174
+ }
175
+
176
+ // Get servers by capability (tools matching a pattern)
177
+ getServersByTool(toolNamePattern) {
178
+ const matchingServers = [];
179
+ const regex = new RegExp(toolNamePattern, 'i');
180
+
181
+ for (const [serverName, client] of this.clients) {
182
+ if (client.isServerConnected()) {
183
+ const tools = client.getAvailableTools();
184
+ const matchingTools = tools.filter(tool => regex.test(tool));
185
+
186
+ if (matchingTools.length > 0) {
187
+ matchingServers.push({
188
+ serverName,
189
+ matchingTools
190
+ });
191
+ }
192
+ }
193
+ }
194
+
195
+ return matchingServers;
196
+ }
197
+
198
+ // Cleanup all connections
199
+ async cleanup() {
200
+ console.log(`MCPManager: Cleaning up ${this.clients.size} connections...`);
201
+
202
+ const disconnectPromises = [];
203
+ for (const [serverName, client] of this.clients) {
204
+ disconnectPromises.push(
205
+ client.disconnect().catch(error =>
206
+ console.error(`MCPManager: Error disconnecting '${serverName}':`, error)
207
+ )
208
+ );
209
+ }
210
+
211
+ await Promise.allSettled(disconnectPromises);
212
+ this.clients.clear();
213
+ this.serverConfigs.clear();
214
+
215
+ console.log('MCPManager: Cleanup completed');
216
+ }
217
+
218
+ // Iterator support for easy looping
219
+ [Symbol.iterator]() {
220
+ return this.clients.entries();
221
+ }
222
+
223
+ // Get list of connected server names
224
+ getConnectedServerNames() {
225
+ return Array.from(this.clients.keys()).filter(serverName =>
226
+ this.clients.get(serverName).isServerConnected()
227
+ );
228
+ }
229
+
230
+ // Get total tool count across all servers
231
+ getTotalToolCount() {
232
+ let count = 0;
233
+ for (const client of this.clients.values()) {
234
+ if (client.isServerConnected()) {
235
+ count += client.getAvailableTools().length;
236
+ }
237
+ }
238
+ return count;
239
+ }
240
+ }
@@ -0,0 +1,22 @@
1
+ import { GoogleGenAI } from "@google/genai";
2
+
3
+ export function createClient(apiKey) {
4
+ return new GoogleGenAI(apiKey);
5
+ }
6
+
7
+ export async function chat(client, input, { inputSchema, outputSchema, ...options }) {
8
+ // TODO: Add translation of input to provide
9
+ const defaultOptions = { model: 'gemini-2.5-flash-lite' };
10
+ const finalOptions = { ...defaultOptions, ...options };
11
+
12
+ try {
13
+ const response = await client.models.generateContent({
14
+ contents: input,
15
+ ...finalOptions,
16
+ });
17
+ return response;
18
+ } catch (error) {
19
+ console.error(`Error during Gemini chat completion:`, error);
20
+ throw error;
21
+ }
22
+ }
@@ -0,0 +1,42 @@
1
+ import OpenAI from 'openai';
2
+ import { zodTextFormat } from "openai/helpers/zod";
3
+ import { defaultModel } from "../config.js";
4
+
5
+ // Factory function to create client
6
+ export function createClient(apiKey) {
7
+ return new OpenAI({ apiKey });
8
+ }
9
+
10
+ // Now accepts the client as first parameter
11
+ export async function chat(client, input, { inputSchema, outputSchema, ...options }) {
12
+ const defaultOptions = { model: defaultModel };
13
+ const finalOptions = { ...defaultOptions, ...options };
14
+
15
+ if (inputSchema) {
16
+ input = inputSchema.parse(input);
17
+ }
18
+
19
+ try {
20
+ let response, output;
21
+ if (outputSchema) {
22
+ response = await client.responses.parse({
23
+ input: input,
24
+ text: {
25
+ format: zodTextFormat(outputSchema, "output")
26
+ },
27
+ ...finalOptions,
28
+ });
29
+ output = response.output_parsed;
30
+ } else {
31
+ response = await client.responses.create({
32
+ input: input,
33
+ ...finalOptions,
34
+ });
35
+ output = response.output_text;
36
+ }
37
+ return {output: output, rawResponse: response};
38
+ } catch (error) {
39
+ console.error(`Error during OpenAI chat completion:`, error);
40
+ throw error;
41
+ }
42
+ }
@@ -0,0 +1,19 @@
1
+ const ALLOWED_PROVIDERS = Object.freeze(['openai', 'gemini']);
2
+
3
+ export function getAllowedProviders() {
4
+ return [...ALLOWED_PROVIDERS];
5
+ }
6
+
7
+ export function validateProviderName(providerName) {
8
+ if (typeof providerName !== 'string') {
9
+ throw new TypeError('Provider name must be a string.');
10
+ }
11
+
12
+ const normalizedName = providerName.trim().toLowerCase();
13
+
14
+ if (!ALLOWED_PROVIDERS.includes(normalizedName)) {
15
+ throw new Error(`Unsupported provider. Allowed providers: ${ALLOWED_PROVIDERS.join(', ')}`);
16
+ }
17
+
18
+ return normalizedName;
19
+ }