This repository was archived by the owner on May 21, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 33
Expand file tree
/
Copy pathmcp_agent.ts
More file actions
437 lines (383 loc) · 15.1 KB
/
Copy pathmcp_agent.ts
File metadata and controls
437 lines (383 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
import type { BaseLanguageModelInterface, LanguageModelLike } from '@langchain/core/language_models/base'
import type {
BaseMessage,
} from '@langchain/core/messages'
import type { StructuredToolInterface, ToolInterface } from '@langchain/core/tools'
import type { AgentFinish, AgentStep } from 'langchain/agents'
import type { MCPClient } from '../client.js'
import type { BaseConnector } from '../connectors/base.js'
import type { ServerManager } from '../managers/server_manager.js'
import type { MCPSession } from '../session.js'
import {
AIMessage,
HumanMessage,
SystemMessage,
} from '@langchain/core/messages'
import { OutputParserException } from '@langchain/core/output_parsers'
import {
ChatPromptTemplate,
MessagesPlaceholder,
} from '@langchain/core/prompts'
import {
AgentExecutor,
createToolCallingAgent,
} from 'langchain/agents'
import { LangChainAdapter } from '../adapters/langchain_adapter.js'
import { logger } from '../logging.js'
import { createSystemMessage } from './prompts/system_prompt_builder.js'
import { DEFAULT_SYSTEM_PROMPT_TEMPLATE, SERVER_MANAGER_SYSTEM_PROMPT_TEMPLATE } from './prompts/templates.js'
export class MCPAgent {
private llm: BaseLanguageModelInterface
private client?: MCPClient
private connectors: BaseConnector[]
private maxSteps: number
private autoInitialize: boolean
private memoryEnabled: boolean
private disallowedTools: string[]
private useServerManager: boolean
private verbose: boolean
private systemPrompt?: string | null
private systemPromptTemplateOverride?: string | null
private additionalInstructions?: string | null
private initialized = false
private conversationHistory: BaseMessage[] = []
private agentExecutor: AgentExecutor | null = null
private sessions: Record<string, MCPSession> = {}
private systemMessage: SystemMessage | null = null
private tools: StructuredToolInterface[] = []
private adapter: LangChainAdapter
private serverManager: ServerManager | null = null
constructor(options: {
llm: BaseLanguageModelInterface
client?: MCPClient
connectors?: BaseConnector[]
maxSteps?: number
autoInitialize?: boolean
memoryEnabled?: boolean
systemPrompt?: string | null
systemPromptTemplate?: string | null
additionalInstructions?: string | null
disallowedTools?: string[]
useServerManager?: boolean
verbose?: boolean
adapter?: LangChainAdapter
serverManagerFactory?: (client: MCPClient) => ServerManager
}) {
this.llm = options.llm
this.client = options.client
this.connectors = options.connectors ?? []
this.maxSteps = options.maxSteps ?? 5
this.autoInitialize = options.autoInitialize ?? false
this.memoryEnabled = options.memoryEnabled ?? true
this.systemPrompt = options.systemPrompt ?? null
this.systemPromptTemplateOverride = options.systemPromptTemplate ?? null
this.additionalInstructions = options.additionalInstructions ?? null
this.disallowedTools = options.disallowedTools ?? []
this.useServerManager = options.useServerManager ?? false
this.verbose = options.verbose ?? false
if (!this.client && this.connectors.length === 0) {
throw new Error('Either \'client\' or at least one \'connector\' must be provided.')
}
if (this.useServerManager) {
if (!this.client) {
throw new Error('\'client\' must be provided when \'useServerManager\' is true.')
}
if (options.serverManagerFactory) {
this.serverManager = options.serverManagerFactory(this.client)
}
else {
throw new Error('No serverManagerFactory passed to MCPAgent constructor.')
}
}
// Let consumers swap allowed tools dynamically
this.adapter = options.adapter ?? new LangChainAdapter(this.disallowedTools)
}
public async initialize(): Promise<void> {
logger.info('🚀 Initializing MCP agent and connecting to services...')
// If using server manager, initialize it
if (this.useServerManager && this.serverManager) {
await this.serverManager.initialize()
// Get server management tools
const managementTools = this.serverManager.tools
this.tools = managementTools
logger.info(
`🔧 Server manager mode active with ${managementTools.length} management tools`,
)
// Create the system message based on available tools
await this.createSystemMessageFromTools(this.tools)
}
else {
// Standard initialization - if using client, get or create sessions
if (this.client) {
// First try to get existing sessions
this.sessions = await this.client.getAllActiveSessions()
logger.info(`🔌 Found ${Object.keys(this.sessions).length} existing sessions`)
// If no active sessions exist, create new ones
if (Object.keys(this.sessions).length === 0) {
logger.info('🔄 No active sessions found, creating new ones...')
this.sessions = await this.client.createAllSessions()
logger.info(`✅ Created ${Object.keys(this.sessions).length} new sessions`)
}
// Create LangChain tools directly from the client using the adapter
this.tools = await LangChainAdapter.createTools(this.client)
logger.info(`🛠️ Created ${this.tools.length} LangChain tools from client`)
}
else {
// Using direct connector - only establish connection
logger.info(`🔗 Connecting to ${this.connectors.length} direct connectors...`)
for (const connector of this.connectors) {
if (!connector.isClientConnected) {
await connector.connect()
}
}
// Create LangChain tools using the adapter with connectors
this.tools = await this.adapter.createToolsFromConnectors(this.connectors)
logger.info(`🛠️ Created ${this.tools.length} LangChain tools from connectors`)
}
// Get all tools for system message generation
logger.info(`🧰 Found ${this.tools.length} tools across all connectors`)
// Create the system message based on available tools
await this.createSystemMessageFromTools(this.tools)
}
// Create the agent executor and mark initialized
this.agentExecutor = this.createAgent()
this.initialized = true
logger.info('✨ Agent initialization complete')
}
private async createSystemMessageFromTools(tools: StructuredToolInterface[]): Promise<void> {
const systemPromptTemplate
= this.systemPromptTemplateOverride
?? DEFAULT_SYSTEM_PROMPT_TEMPLATE
this.systemMessage = createSystemMessage(
tools,
systemPromptTemplate,
SERVER_MANAGER_SYSTEM_PROMPT_TEMPLATE,
this.useServerManager,
this.disallowedTools,
this.systemPrompt ?? undefined,
this.additionalInstructions ?? undefined,
)
if (this.memoryEnabled) {
this.conversationHistory = [
this.systemMessage,
...this.conversationHistory.filter(m => !(m instanceof SystemMessage)),
]
}
}
private createAgent(): AgentExecutor {
const systemContent = this.systemMessage?.content ?? 'You are a helpful assistant.'
const prompt = ChatPromptTemplate.fromMessages([
['system', systemContent],
new MessagesPlaceholder('chat_history'),
['human', '{input}'],
new MessagesPlaceholder('agent_scratchpad'),
])
const agent = createToolCallingAgent({
llm: this.llm as unknown as LanguageModelLike,
tools: this.tools,
prompt,
})
return new AgentExecutor({
agent,
tools: this.tools,
maxIterations: this.maxSteps,
verbose: this.verbose,
returnIntermediateSteps: true,
})
}
public getConversationHistory(): BaseMessage[] {
return [...this.conversationHistory]
}
public clearConversationHistory(): void {
this.conversationHistory = this.memoryEnabled && this.systemMessage ? [this.systemMessage] : []
}
private addToHistory(message: BaseMessage): void {
if (this.memoryEnabled)
this.conversationHistory.push(message)
}
public getSystemMessage(): SystemMessage | null {
return this.systemMessage
}
public setSystemMessage(message: string): void {
this.systemMessage = new SystemMessage(message)
if (this.memoryEnabled) {
this.conversationHistory = this.conversationHistory.filter(m => !(m instanceof SystemMessage))
this.conversationHistory.unshift(this.systemMessage)
}
if (this.initialized && this.tools.length) {
this.agentExecutor = this.createAgent()
logger.debug('Agent recreated with new system message')
}
}
public setDisallowedTools(disallowedTools: string[]): void {
this.disallowedTools = disallowedTools
this.adapter = new LangChainAdapter(this.disallowedTools)
if (this.initialized) {
logger.debug('Agent already initialized. Changes will take effect on next initialization.')
}
}
public getDisallowedTools(): string[] {
return this.disallowedTools
}
public async run(
query: string,
maxSteps?: number,
manageConnector = true,
externalHistory?: BaseMessage[],
): Promise<string> {
let result = ''
let initializedHere = false
try {
if (manageConnector && !this.initialized) {
await this.initialize()
initializedHere = true
}
else if (!this.initialized && this.autoInitialize) {
await this.initialize()
initializedHere = true
}
if (!this.agentExecutor) {
throw new Error('MCP agent failed to initialize')
}
const steps = maxSteps ?? this.maxSteps
this.agentExecutor.maxIterations = steps
const display_query
= query.length > 50 ? `${query.slice(0, 50).replace(/\n/g, ' ')}...` : query.replace(/\n/g, ' ')
logger.info(`💬 Received query: '${display_query}'`)
// —–– Record user message
if (this.memoryEnabled) {
this.addToHistory(new HumanMessage(query))
}
const historyToUse = externalHistory ?? this.conversationHistory
const langchainHistory: BaseMessage[] = []
for (const msg of historyToUse) {
if (msg instanceof HumanMessage || msg instanceof AIMessage) {
langchainHistory.push(msg)
}
}
const intermediateSteps: AgentStep[] = []
const inputs = { input: query, chat_history: langchainHistory } as Record<string, unknown>
let nameToToolMap: Record<string, StructuredToolInterface> = Object.fromEntries(this.tools.map(t => [t.name, t]))
logger.info(`🏁 Starting agent execution with max_steps=${steps}`)
for (let stepNum = 0; stepNum < steps; stepNum++) {
if (this.useServerManager && this.serverManager) {
const currentTools = this.serverManager.tools
const currentToolNames = new Set(currentTools.map(t => t.name))
const existingToolNames = new Set(this.tools.map(t => t.name))
const changed
= currentTools.length !== this.tools.length
|| [...currentToolNames].some(n => !existingToolNames.has(n))
if (changed) {
logger.info(
`🔄 Tools changed before step ${stepNum + 1}, updating agent. New tools: ${[...currentToolNames].join(', ')}`,
)
this.tools = currentTools
await this.createSystemMessageFromTools(this.tools)
this.agentExecutor = this.createAgent()
this.agentExecutor.maxIterations = steps
nameToToolMap = Object.fromEntries(this.tools.map(t => [t.name, t]))
}
}
logger.info(`👣 Step ${stepNum + 1}/${steps}`)
try {
logger.debug('Starting agent step execution')
const nextStepOutput = await this.agentExecutor._takeNextStep(
nameToToolMap as Record<string, ToolInterface>,
inputs,
intermediateSteps,
)
if ((nextStepOutput as AgentFinish).returnValues) {
logger.info(`✅ Agent finished at step ${stepNum + 1}`)
result = (nextStepOutput as AgentFinish).returnValues?.output ?? 'No output generated'
break
}
const stepArray = nextStepOutput as AgentStep[]
intermediateSteps.push(...stepArray)
for (const step of stepArray) {
const { action, observation } = step
const toolName = action.tool
let toolInputStr = String(action.toolInput)
if (toolInputStr.length > 100)
toolInputStr = `${toolInputStr.slice(0, 97)}...`
logger.info(`🔧 Tool call: ${toolName} with input: ${toolInputStr}`)
let outputStr = String(observation)
if (outputStr.length > 100)
outputStr = `${outputStr.slice(0, 97)}...`
outputStr = outputStr.replace(/\n/g, ' ')
logger.info(`📄 Tool result: ${outputStr}`)
}
// Detect direct return
if (stepArray.length) {
const lastStep = stepArray[stepArray.length - 1]
const toolReturn = await this.agentExecutor._getToolReturn(lastStep)
if (toolReturn) {
logger.info(`🏆 Tool returned directly at step ${stepNum + 1}`)
result = (toolReturn as unknown as AgentFinish).returnValues?.output ?? 'No output generated'
break
}
}
}
catch (e) {
if (e instanceof OutputParserException) {
logger.error(`❌ Output parsing error during step ${stepNum + 1}: ${e}`)
result = `Agent stopped due to a parsing error: ${e}`
break
}
logger.error(`❌ Error during agent execution step ${stepNum + 1}: ${e}`)
console.error(e)
result = `Agent stopped due to an error: ${e}`
break
}
}
// —–– Post‑loop handling
if (!result) {
logger.warn(`⚠️ Agent stopped after reaching max iterations (${steps})`)
result = `Agent stopped after reaching the maximum number of steps (${steps}).`
}
if (this.memoryEnabled) {
this.addToHistory(new AIMessage(result))
}
logger.info('🎉 Agent execution complete')
return result
}
catch (e) {
logger.error(`❌ Error running query: ${e}`)
if (initializedHere && manageConnector) {
logger.info('🧹 Cleaning up resources after initialization error in run')
await this.close()
}
throw e
}
finally {
if (manageConnector && !this.client && initializedHere) {
logger.info('🧹 Closing agent after query completion')
await this.close()
}
}
}
public async close(): Promise<void> {
logger.info('🔌 Closing MCPAgent resources…')
try {
this.agentExecutor = null
this.tools = []
if (this.client) {
logger.info('🔄 Closing sessions through client')
await this.client.closeAllSessions()
this.sessions = {}
}
else {
for (const connector of this.connectors) {
logger.info('🔄 Disconnecting connector')
await connector.disconnect()
}
}
if ('connectorToolMap' in this.adapter) {
this.adapter = new LangChainAdapter()
}
}
finally {
this.initialized = false
logger.info('👋 Agent closed successfully')
}
}
}