@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 +8 -0
- package/README.md +237 -0
- package/index.js +2 -0
- package/package.json +36 -0
- package/src/Agent.js +132 -0
- package/src/config.js +1 -0
- package/src/llmService.js +33 -0
- package/src/mcp/MCPClient.js +109 -0
- package/src/mcp/MCPManager.js +240 -0
- package/src/providers/gemini.js +22 -0
- package/src/providers/openai.js +42 -0
- package/src/providers/registry.js +19 -0
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
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
|
+
}
|