@hashgraphonline/conversational-agent 0.0.3 → 0.1.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/README.md +79 -0
- package/dist/cjs/agent-factory.d.ts +5 -0
- package/dist/cjs/base-agent.d.ts +95 -0
- package/dist/cjs/conversational-agent.d.ts +62 -5
- package/dist/cjs/index.cjs +1 -1
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +8 -0
- package/dist/cjs/langchain-agent.d.ts +22 -0
- package/dist/cjs/mcp/MCPClientManager.d.ts +40 -0
- package/dist/cjs/mcp/adapters/langchain.d.ts +8 -0
- package/dist/cjs/mcp/helpers.d.ts +45 -0
- package/dist/cjs/mcp/types.d.ts +27 -0
- package/dist/cjs/plugins/hbar-transfer/AccountBuilder.d.ts +13 -0
- package/dist/cjs/plugins/hbar-transfer/HbarTransferPlugin.d.ts +15 -0
- package/dist/cjs/plugins/hbar-transfer/TransferHbarTool.d.ts +61 -0
- package/dist/cjs/plugins/hbar-transfer/index.d.ts +1 -0
- package/dist/cjs/plugins/hbar-transfer/types.d.ts +10 -0
- package/dist/cjs/plugins/index.d.ts +1 -0
- package/dist/cjs/providers.d.ts +48 -0
- package/dist/esm/index.js +18 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index10.js +22 -0
- package/dist/esm/index10.js.map +1 -0
- package/dist/esm/index11.js +104 -0
- package/dist/esm/index11.js.map +1 -0
- package/dist/esm/index12.js +36 -0
- package/dist/esm/index12.js.map +1 -0
- package/dist/esm/index13.js +16 -0
- package/dist/esm/index13.js.map +1 -0
- package/dist/esm/index14.js +127 -0
- package/dist/esm/index14.js.map +1 -0
- package/dist/esm/index15.js +132 -0
- package/dist/esm/index15.js.map +1 -0
- package/dist/esm/index16.js +95 -0
- package/dist/esm/index16.js.map +1 -0
- package/dist/esm/index5.js +42 -202
- package/dist/esm/index5.js.map +1 -1
- package/dist/esm/index6.js +295 -13
- package/dist/esm/index6.js.map +1 -1
- package/dist/esm/index7.js +87 -0
- package/dist/esm/index7.js.map +1 -0
- package/dist/esm/index8.js +255 -0
- package/dist/esm/index8.js.map +1 -0
- package/dist/esm/index9.js +18 -0
- package/dist/esm/index9.js.map +1 -0
- package/dist/types/agent-factory.d.ts +5 -0
- package/dist/types/base-agent.d.ts +95 -0
- package/dist/types/conversational-agent.d.ts +62 -5
- package/dist/types/index.d.ts +8 -0
- package/dist/types/langchain-agent.d.ts +22 -0
- package/dist/types/mcp/MCPClientManager.d.ts +40 -0
- package/dist/types/mcp/adapters/langchain.d.ts +8 -0
- package/dist/types/mcp/helpers.d.ts +45 -0
- package/dist/types/mcp/types.d.ts +27 -0
- package/dist/types/plugins/hbar-transfer/AccountBuilder.d.ts +13 -0
- package/dist/types/plugins/hbar-transfer/HbarTransferPlugin.d.ts +15 -0
- package/dist/types/plugins/hbar-transfer/TransferHbarTool.d.ts +61 -0
- package/dist/types/plugins/hbar-transfer/index.d.ts +1 -0
- package/dist/types/plugins/hbar-transfer/types.d.ts +10 -0
- package/dist/types/plugins/index.d.ts +1 -0
- package/dist/types/providers.d.ts +48 -0
- package/package.json +12 -6
- package/src/agent-factory.ts +21 -0
- package/src/base-agent.ts +222 -0
- package/src/conversational-agent.ts +204 -102
- package/src/index.ts +24 -0
- package/src/langchain-agent.ts +333 -0
- package/src/mcp/MCPClientManager.ts +148 -0
- package/src/mcp/adapters/langchain.ts +185 -0
- package/src/mcp/helpers.ts +122 -0
- package/src/mcp/types.ts +29 -0
- package/src/plugins/hbar-transfer/AccountBuilder.ts +154 -0
- package/src/plugins/hbar-transfer/HbarTransferPlugin.ts +66 -0
- package/src/plugins/hbar-transfer/TransferHbarTool.ts +49 -0
- package/src/plugins/hbar-transfer/index.ts +1 -0
- package/src/plugins/hbar-transfer/types.ts +11 -0
- package/src/plugins/index.ts +2 -1
- package/src/providers.ts +82 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
2
|
+
import type { StructuredTool } from '@langchain/core/tools';
|
|
3
|
+
import { createOpenAIToolsAgent, AgentExecutor } from 'langchain/agents';
|
|
4
|
+
import {
|
|
5
|
+
ChatPromptTemplate,
|
|
6
|
+
MessagesPlaceholder,
|
|
7
|
+
} from '@langchain/core/prompts';
|
|
8
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
9
|
+
import {
|
|
10
|
+
calculateTokenCostSync,
|
|
11
|
+
getAllHederaCorePlugins,
|
|
12
|
+
HederaAgentKit,
|
|
13
|
+
TokenUsageCallbackHandler,
|
|
14
|
+
} from 'hedera-agent-kit';
|
|
15
|
+
import type { TokenUsage, CostCalculation } from 'hedera-agent-kit';
|
|
16
|
+
import {
|
|
17
|
+
BaseAgent,
|
|
18
|
+
type ConversationContext,
|
|
19
|
+
type ChatResponse,
|
|
20
|
+
type OperationalMode,
|
|
21
|
+
type UsageStats,
|
|
22
|
+
} from './base-agent';
|
|
23
|
+
import { MCPClientManager } from './mcp/MCPClientManager';
|
|
24
|
+
import { convertMCPToolToLangChain } from './mcp/adapters/langchain';
|
|
25
|
+
|
|
26
|
+
export class LangChainAgent extends BaseAgent {
|
|
27
|
+
private executor: AgentExecutor | undefined;
|
|
28
|
+
private systemMessage = '';
|
|
29
|
+
private mcpManager?: MCPClientManager;
|
|
30
|
+
|
|
31
|
+
async boot(): Promise<void> {
|
|
32
|
+
if (this.initialized) {
|
|
33
|
+
this.logger.warn('Agent already initialized');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
this.agentKit = await this.createAgentKit();
|
|
39
|
+
await this.agentKit.initialize();
|
|
40
|
+
|
|
41
|
+
const modelName =
|
|
42
|
+
this.config.ai?.modelName ||
|
|
43
|
+
process.env.OPENAI_MODEL_NAME ||
|
|
44
|
+
'gpt-4o-mini';
|
|
45
|
+
this.tokenTracker = new TokenUsageCallbackHandler(modelName);
|
|
46
|
+
|
|
47
|
+
const allTools = this.agentKit.getAggregatedLangChainTools();
|
|
48
|
+
this.tools = this.filterTools(allTools);
|
|
49
|
+
|
|
50
|
+
if (this.config.mcp?.servers && this.config.mcp.servers.length > 0) {
|
|
51
|
+
await this.initializeMCP();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.systemMessage = this.buildSystemPrompt();
|
|
55
|
+
|
|
56
|
+
await this.createExecutor();
|
|
57
|
+
|
|
58
|
+
this.initialized = true;
|
|
59
|
+
this.logger.info('LangChain Hedera agent initialized');
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.logger.error('Failed to initialize agent:', error);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async chat(
|
|
67
|
+
message: string,
|
|
68
|
+
context?: ConversationContext
|
|
69
|
+
): Promise<ChatResponse> {
|
|
70
|
+
if (!this.initialized || !this.executor) {
|
|
71
|
+
throw new Error('Agent not initialized. Call boot() first.');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const result = await this.executor.invoke({
|
|
76
|
+
input: message,
|
|
77
|
+
chat_history: context?.messages || [],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
let response: ChatResponse = {
|
|
81
|
+
output: result.output || '',
|
|
82
|
+
message: result.output || '',
|
|
83
|
+
notes: [],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const parsedSteps = result?.intermediateSteps?.[0]?.observation;
|
|
87
|
+
if (
|
|
88
|
+
parsedSteps &&
|
|
89
|
+
typeof parsedSteps === 'string' &&
|
|
90
|
+
this.isJSON(parsedSteps)
|
|
91
|
+
) {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(parsedSteps);
|
|
94
|
+
response = { ...response, ...parsed };
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.logger.error('Error parsing intermediate steps:', error);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!response.output || response.output.trim() === '') {
|
|
101
|
+
response.output = 'Agent action complete.';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (this.tokenTracker) {
|
|
105
|
+
const tokenUsage = this.tokenTracker.getLatestTokenUsage();
|
|
106
|
+
if (tokenUsage) {
|
|
107
|
+
response.tokenUsage = tokenUsage;
|
|
108
|
+
response.cost = calculateTokenCostSync(tokenUsage);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return response;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
return this.handleError(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async shutdown(): Promise<void> {
|
|
119
|
+
if (this.mcpManager) {
|
|
120
|
+
await this.mcpManager.disconnectAll();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.executor = undefined;
|
|
124
|
+
this.agentKit = undefined;
|
|
125
|
+
this.tools = [];
|
|
126
|
+
this.initialized = false;
|
|
127
|
+
this.logger.info('Agent cleaned up');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
switchMode(mode: OperationalMode): void {
|
|
131
|
+
if (this.config.execution) {
|
|
132
|
+
this.config.execution.operationalMode = mode;
|
|
133
|
+
} else {
|
|
134
|
+
this.config.execution = { operationalMode: mode };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (this.agentKit) {
|
|
138
|
+
this.agentKit.operationalMode = mode;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.systemMessage = this.buildSystemPrompt();
|
|
142
|
+
this.logger.info(`Operational mode switched to: ${mode}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getUsageStats(): UsageStats {
|
|
146
|
+
if (!this.tokenTracker) {
|
|
147
|
+
return {
|
|
148
|
+
promptTokens: 0,
|
|
149
|
+
completionTokens: 0,
|
|
150
|
+
totalTokens: 0,
|
|
151
|
+
cost: { totalCost: 0 } as CostCalculation,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const usage = this.tokenTracker.getTotalTokenUsage();
|
|
156
|
+
const cost = calculateTokenCostSync(usage);
|
|
157
|
+
return { ...usage, cost };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getUsageLog(): UsageStats[] {
|
|
161
|
+
if (!this.tokenTracker) {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this.tokenTracker.getTokenUsageHistory().map((usage) => ({
|
|
166
|
+
...usage,
|
|
167
|
+
cost: calculateTokenCostSync(usage),
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
clearUsageStats(): void {
|
|
172
|
+
if (this.tokenTracker) {
|
|
173
|
+
this.tokenTracker.reset();
|
|
174
|
+
this.logger.info('Usage statistics cleared');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private async createAgentKit(): Promise<HederaAgentKit> {
|
|
179
|
+
const corePlugins = getAllHederaCorePlugins();
|
|
180
|
+
const extensionPlugins = this.config.extensions?.plugins || [];
|
|
181
|
+
const plugins = [...corePlugins, ...extensionPlugins];
|
|
182
|
+
|
|
183
|
+
const operationalMode =
|
|
184
|
+
this.config.execution?.operationalMode || 'returnBytes';
|
|
185
|
+
const modelName = this.config.ai?.modelName || 'gpt-4o';
|
|
186
|
+
|
|
187
|
+
return new HederaAgentKit(
|
|
188
|
+
this.config.signer,
|
|
189
|
+
{ plugins },
|
|
190
|
+
operationalMode,
|
|
191
|
+
this.config.execution?.userAccountId,
|
|
192
|
+
this.config.execution?.scheduleUserTransactionsInBytesMode ?? true,
|
|
193
|
+
undefined,
|
|
194
|
+
modelName,
|
|
195
|
+
this.config.extensions?.mirrorConfig,
|
|
196
|
+
this.config.debug?.silent ?? false
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private async createExecutor(): Promise<void> {
|
|
201
|
+
let llm: BaseChatModel;
|
|
202
|
+
if (this.config.ai?.provider && this.config.ai.provider.getModel) {
|
|
203
|
+
llm = this.config.ai.provider.getModel() as BaseChatModel;
|
|
204
|
+
} else if (this.config.ai?.llm) {
|
|
205
|
+
llm = this.config.ai.llm as BaseChatModel;
|
|
206
|
+
} else {
|
|
207
|
+
const apiKey = this.config.ai?.apiKey || process.env.OPENAI_API_KEY;
|
|
208
|
+
if (!apiKey) {
|
|
209
|
+
throw new Error('OpenAI API key required');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
llm = new ChatOpenAI({
|
|
213
|
+
apiKey,
|
|
214
|
+
modelName: this.config.ai?.modelName || 'gpt-4o-mini',
|
|
215
|
+
temperature: this.config.ai?.temperature ?? 0.1,
|
|
216
|
+
callbacks: this.tokenTracker ? [this.tokenTracker] : [],
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const prompt = ChatPromptTemplate.fromMessages([
|
|
221
|
+
['system', this.systemMessage],
|
|
222
|
+
new MessagesPlaceholder('chat_history'),
|
|
223
|
+
['human', '{input}'],
|
|
224
|
+
new MessagesPlaceholder('agent_scratchpad'),
|
|
225
|
+
]);
|
|
226
|
+
|
|
227
|
+
const langchainTools = this.tools as unknown as StructuredTool[];
|
|
228
|
+
|
|
229
|
+
const agent = await createOpenAIToolsAgent({
|
|
230
|
+
llm,
|
|
231
|
+
tools: langchainTools,
|
|
232
|
+
prompt,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
this.executor = new AgentExecutor({
|
|
236
|
+
agent,
|
|
237
|
+
tools: langchainTools,
|
|
238
|
+
verbose: this.config.debug?.verbose ?? false,
|
|
239
|
+
returnIntermediateSteps: true,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private handleError(error: unknown): ChatResponse {
|
|
244
|
+
const errorMessage =
|
|
245
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
246
|
+
this.logger.error('Chat error:', error);
|
|
247
|
+
|
|
248
|
+
let tokenUsage: TokenUsage | undefined;
|
|
249
|
+
let cost: CostCalculation | undefined;
|
|
250
|
+
|
|
251
|
+
if (this.tokenTracker) {
|
|
252
|
+
tokenUsage = this.tokenTracker.getLatestTokenUsage();
|
|
253
|
+
if (tokenUsage) {
|
|
254
|
+
cost = calculateTokenCostSync(tokenUsage);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const errorResponse: ChatResponse = {
|
|
259
|
+
output: 'Sorry, I encountered an error processing your request.',
|
|
260
|
+
message: 'Error processing request.',
|
|
261
|
+
error: errorMessage,
|
|
262
|
+
notes: [],
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
if (tokenUsage) {
|
|
266
|
+
errorResponse.tokenUsage = tokenUsage;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (cost) {
|
|
270
|
+
errorResponse.cost = cost;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return errorResponse;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private async initializeMCP(): Promise<void> {
|
|
277
|
+
this.mcpManager = new MCPClientManager(this.logger);
|
|
278
|
+
|
|
279
|
+
for (const serverConfig of this.config.mcp!.servers!) {
|
|
280
|
+
if (serverConfig.autoConnect === false) {
|
|
281
|
+
this.logger.info(
|
|
282
|
+
`Skipping MCP server ${serverConfig.name} (autoConnect=false)`
|
|
283
|
+
);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const status = await this.mcpManager.connectServer(serverConfig);
|
|
288
|
+
|
|
289
|
+
if (status.connected) {
|
|
290
|
+
this.logger.info(
|
|
291
|
+
`Connected to MCP server ${status.serverName} with ${status.tools.length} tools`
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
for (const mcpTool of status.tools) {
|
|
295
|
+
const langchainTool = convertMCPToolToLangChain(
|
|
296
|
+
mcpTool,
|
|
297
|
+
this.mcpManager,
|
|
298
|
+
serverConfig
|
|
299
|
+
);
|
|
300
|
+
this.tools.push(langchainTool);
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
this.logger.error(
|
|
304
|
+
`Failed to connect to MCP server ${status.serverName}: ${status.error}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Check if a string is valid JSON
|
|
312
|
+
*/
|
|
313
|
+
private isJSON(str: string): boolean {
|
|
314
|
+
if (typeof str !== 'string') return false;
|
|
315
|
+
|
|
316
|
+
const trimmed = str.trim();
|
|
317
|
+
if (!trimmed) return false;
|
|
318
|
+
|
|
319
|
+
if (
|
|
320
|
+
!(trimmed.startsWith('{') && trimmed.endsWith('}')) &&
|
|
321
|
+
!(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
|
322
|
+
) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
JSON.parse(trimmed);
|
|
328
|
+
return true;
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import type { MCPServerConfig, MCPToolInfo, MCPConnectionStatus } from './types';
|
|
4
|
+
import { Logger } from '@hashgraphonline/standards-sdk';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Manages connections to MCP servers and tool discovery
|
|
8
|
+
*/
|
|
9
|
+
export class MCPClientManager {
|
|
10
|
+
private clients: Map<string, Client> = new Map();
|
|
11
|
+
private tools: Map<string, MCPToolInfo[]> = new Map();
|
|
12
|
+
private logger: Logger;
|
|
13
|
+
|
|
14
|
+
constructor(logger: Logger) {
|
|
15
|
+
this.logger = logger;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Connect to an MCP server and discover its tools
|
|
20
|
+
*/
|
|
21
|
+
async connectServer(config: MCPServerConfig): Promise<MCPConnectionStatus> {
|
|
22
|
+
try {
|
|
23
|
+
if (this.isServerConnected(config.name)) {
|
|
24
|
+
return {
|
|
25
|
+
serverName: config.name,
|
|
26
|
+
connected: false,
|
|
27
|
+
error: `Server ${config.name} is already connected`,
|
|
28
|
+
tools: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (config.transport && config.transport !== 'stdio') {
|
|
33
|
+
throw new Error(`Transport ${config.transport} not yet supported`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const transport = new StdioClientTransport({
|
|
37
|
+
command: config.command,
|
|
38
|
+
args: config.args,
|
|
39
|
+
...(config.env && { env: config.env }),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const client = new Client({
|
|
43
|
+
name: `conversational-agent-${config.name}`,
|
|
44
|
+
version: '1.0.0',
|
|
45
|
+
}, {
|
|
46
|
+
capabilities: {},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await client.connect(transport);
|
|
50
|
+
this.clients.set(config.name, client);
|
|
51
|
+
|
|
52
|
+
const toolsResponse = await client.listTools();
|
|
53
|
+
const toolsWithServer: MCPToolInfo[] = toolsResponse.tools.map(tool => ({
|
|
54
|
+
...tool,
|
|
55
|
+
serverName: config.name,
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
this.tools.set(config.name, toolsWithServer);
|
|
59
|
+
this.logger.info(`Connected to MCP server ${config.name} with ${toolsWithServer.length} tools`);
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
serverName: config.name,
|
|
63
|
+
connected: true,
|
|
64
|
+
tools: toolsWithServer,
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
this.logger.error(`Failed to connect to MCP server ${config.name}:`, error);
|
|
68
|
+
return {
|
|
69
|
+
serverName: config.name,
|
|
70
|
+
connected: false,
|
|
71
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
72
|
+
tools: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Execute a tool on a specific MCP server
|
|
79
|
+
*/
|
|
80
|
+
async executeTool(serverName: string, toolName: string, args: Record<string, unknown>): Promise<unknown> {
|
|
81
|
+
const client = this.clients.get(serverName);
|
|
82
|
+
if (!client) {
|
|
83
|
+
throw new Error(`MCP server ${serverName} not connected`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.logger.debug(`Executing MCP tool ${toolName} on server ${serverName}`, args);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const result = await client.callTool({
|
|
90
|
+
name: toolName,
|
|
91
|
+
arguments: args,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return result;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.logger.error(`Error executing MCP tool ${toolName}:`, error);
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Disconnect all MCP servers
|
|
103
|
+
*/
|
|
104
|
+
async disconnectAll(): Promise<void> {
|
|
105
|
+
for (const [name, client] of this.clients) {
|
|
106
|
+
try {
|
|
107
|
+
await client.close();
|
|
108
|
+
this.logger.info(`Disconnected from MCP server ${name}`);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
this.logger.error(`Error disconnecting MCP server ${name}:`, error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
this.clients.clear();
|
|
114
|
+
this.tools.clear();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get all discovered tools from all connected servers
|
|
119
|
+
*/
|
|
120
|
+
getAllTools(): MCPToolInfo[] {
|
|
121
|
+
const allTools: MCPToolInfo[] = [];
|
|
122
|
+
for (const tools of this.tools.values()) {
|
|
123
|
+
allTools.push(...tools);
|
|
124
|
+
}
|
|
125
|
+
return allTools;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get tools from a specific server
|
|
130
|
+
*/
|
|
131
|
+
getServerTools(serverName: string): MCPToolInfo[] {
|
|
132
|
+
return this.tools.get(serverName) || [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if a server is connected
|
|
137
|
+
*/
|
|
138
|
+
isServerConnected(serverName: string): boolean {
|
|
139
|
+
return this.clients.has(serverName);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get list of connected server names
|
|
144
|
+
*/
|
|
145
|
+
getConnectedServers(): string[] {
|
|
146
|
+
return Array.from(this.clients.keys());
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { MCPToolInfo, MCPServerConfig } from '../types';
|
|
4
|
+
import type { MCPClientManager } from '../MCPClientManager';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert an MCP tool to a LangChain DynamicStructuredTool
|
|
8
|
+
*/
|
|
9
|
+
export function convertMCPToolToLangChain(
|
|
10
|
+
tool: MCPToolInfo,
|
|
11
|
+
mcpManager: MCPClientManager,
|
|
12
|
+
serverConfig?: MCPServerConfig
|
|
13
|
+
): DynamicStructuredTool {
|
|
14
|
+
const zodSchema = jsonSchemaToZod(tool.inputSchema);
|
|
15
|
+
|
|
16
|
+
const sanitizedName = `${tool.serverName}_${tool.name}`.replace(
|
|
17
|
+
/[^a-zA-Z0-9_]/g,
|
|
18
|
+
'_'
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
let description = tool.description || `MCP tool ${tool.name} from ${tool.serverName}`;
|
|
22
|
+
|
|
23
|
+
if (serverConfig?.toolDescriptions?.[tool.name]) {
|
|
24
|
+
description = `${description}\n\n${serverConfig.toolDescriptions[tool.name]}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (serverConfig?.additionalContext) {
|
|
28
|
+
description = `${description}\n\nContext: ${serverConfig.additionalContext}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return new DynamicStructuredTool({
|
|
32
|
+
name: sanitizedName,
|
|
33
|
+
description,
|
|
34
|
+
schema: zodSchema,
|
|
35
|
+
func: async (input) => {
|
|
36
|
+
try {
|
|
37
|
+
const result = await mcpManager.executeTool(
|
|
38
|
+
tool.serverName,
|
|
39
|
+
tool.name,
|
|
40
|
+
input
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (typeof result === 'string') {
|
|
44
|
+
return result;
|
|
45
|
+
} else if (
|
|
46
|
+
result &&
|
|
47
|
+
typeof result === 'object' &&
|
|
48
|
+
'content' in result
|
|
49
|
+
) {
|
|
50
|
+
const content = (result as { content: unknown }).content;
|
|
51
|
+
if (Array.isArray(content)) {
|
|
52
|
+
const textParts = content
|
|
53
|
+
.filter(
|
|
54
|
+
(item): item is { type: string; text: string } =>
|
|
55
|
+
typeof item === 'object' &&
|
|
56
|
+
item !== null &&
|
|
57
|
+
'type' in item &&
|
|
58
|
+
item.type === 'text' &&
|
|
59
|
+
'text' in item
|
|
60
|
+
)
|
|
61
|
+
.map((item) => item.text);
|
|
62
|
+
return textParts.join('\n');
|
|
63
|
+
}
|
|
64
|
+
return JSON.stringify(content);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return JSON.stringify(result);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const errorMessage =
|
|
70
|
+
error instanceof Error ? error.message : 'Unknown error';
|
|
71
|
+
return `Error executing MCP tool ${tool.name}: ${errorMessage}`;
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Convert JSON Schema to Zod schema
|
|
79
|
+
* This is a simplified converter that handles common cases
|
|
80
|
+
*/
|
|
81
|
+
function jsonSchemaToZod(schema: unknown): z.ZodTypeAny {
|
|
82
|
+
if (!schema || typeof schema !== 'object') {
|
|
83
|
+
return z.object({});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const schemaObj = schema as Record<string, unknown>;
|
|
87
|
+
|
|
88
|
+
if (schemaObj.type && schemaObj.type !== 'object') {
|
|
89
|
+
return convertType(schemaObj);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!schemaObj.properties || typeof schemaObj.properties !== 'object') {
|
|
93
|
+
return z.object({});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
97
|
+
|
|
98
|
+
for (const [key, value] of Object.entries(schemaObj.properties)) {
|
|
99
|
+
let zodType = convertType(value);
|
|
100
|
+
|
|
101
|
+
const isRequired =
|
|
102
|
+
Array.isArray(schemaObj.required) && schemaObj.required.includes(key);
|
|
103
|
+
if (!isRequired) {
|
|
104
|
+
zodType = zodType.optional();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
shape[key] = zodType;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return z.object(shape);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Convert a single JSON Schema type to Zod
|
|
115
|
+
*/
|
|
116
|
+
function convertType(schema: unknown): z.ZodTypeAny {
|
|
117
|
+
if (!schema || typeof schema !== 'object' || !('type' in schema)) {
|
|
118
|
+
return z.unknown();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const schemaObj = schema as {
|
|
122
|
+
type: string;
|
|
123
|
+
enum?: unknown[];
|
|
124
|
+
items?: unknown;
|
|
125
|
+
};
|
|
126
|
+
let zodType: z.ZodTypeAny;
|
|
127
|
+
|
|
128
|
+
switch (schemaObj.type) {
|
|
129
|
+
case 'string':
|
|
130
|
+
zodType = z.string();
|
|
131
|
+
if (schemaObj.enum && Array.isArray(schemaObj.enum)) {
|
|
132
|
+
zodType = z.enum(schemaObj.enum as [string, ...string[]]);
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case 'number':
|
|
137
|
+
zodType = z.number();
|
|
138
|
+
if ('minimum' in schemaObj && typeof schemaObj.minimum === 'number') {
|
|
139
|
+
zodType = (zodType as z.ZodNumber).min(schemaObj.minimum);
|
|
140
|
+
}
|
|
141
|
+
if ('maximum' in schemaObj && typeof schemaObj.maximum === 'number') {
|
|
142
|
+
zodType = (zodType as z.ZodNumber).max(schemaObj.maximum);
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
|
|
146
|
+
case 'integer':
|
|
147
|
+
zodType = z.number().int();
|
|
148
|
+
if ('minimum' in schemaObj && typeof schemaObj.minimum === 'number') {
|
|
149
|
+
zodType = (zodType as z.ZodNumber).min(schemaObj.minimum);
|
|
150
|
+
}
|
|
151
|
+
if ('maximum' in schemaObj && typeof schemaObj.maximum === 'number') {
|
|
152
|
+
zodType = (zodType as z.ZodNumber).max(schemaObj.maximum);
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case 'boolean':
|
|
157
|
+
zodType = z.boolean();
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case 'array':
|
|
161
|
+
if (schemaObj.items) {
|
|
162
|
+
zodType = z.array(convertType(schemaObj.items));
|
|
163
|
+
} else {
|
|
164
|
+
zodType = z.array(z.unknown());
|
|
165
|
+
}
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case 'object':
|
|
169
|
+
if ('properties' in schemaObj) {
|
|
170
|
+
zodType = jsonSchemaToZod(schemaObj);
|
|
171
|
+
} else {
|
|
172
|
+
zodType = z.object({}).passthrough();
|
|
173
|
+
}
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
default:
|
|
177
|
+
zodType = z.unknown();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if ('description' in schemaObj && typeof schemaObj.description === 'string') {
|
|
181
|
+
zodType = zodType.describe(schemaObj.description);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return zodType;
|
|
185
|
+
}
|