@toolplex/ai-engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +292 -0
  3. package/dist/adapters/index.d.ts +9 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +9 -0
  6. package/dist/adapters/index.js.map +1 -0
  7. package/dist/adapters/types.d.ts +137 -0
  8. package/dist/adapters/types.d.ts.map +1 -0
  9. package/dist/adapters/types.js +14 -0
  10. package/dist/adapters/types.js.map +1 -0
  11. package/dist/core/ChatEngine.d.ts +47 -0
  12. package/dist/core/ChatEngine.d.ts.map +1 -0
  13. package/dist/core/ChatEngine.js +355 -0
  14. package/dist/core/ChatEngine.js.map +1 -0
  15. package/dist/core/ToolBuilder.d.ts +25 -0
  16. package/dist/core/ToolBuilder.d.ts.map +1 -0
  17. package/dist/core/ToolBuilder.js +215 -0
  18. package/dist/core/ToolBuilder.js.map +1 -0
  19. package/dist/core/index.d.ts +6 -0
  20. package/dist/core/index.d.ts.map +1 -0
  21. package/dist/core/index.js +6 -0
  22. package/dist/core/index.js.map +1 -0
  23. package/dist/index.d.ts +41 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +49 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mcp/DefaultStdioTransportFactory.d.ts +27 -0
  28. package/dist/mcp/DefaultStdioTransportFactory.d.ts.map +1 -0
  29. package/dist/mcp/DefaultStdioTransportFactory.js +60 -0
  30. package/dist/mcp/DefaultStdioTransportFactory.js.map +1 -0
  31. package/dist/mcp/MCPClient.d.ts +60 -0
  32. package/dist/mcp/MCPClient.d.ts.map +1 -0
  33. package/dist/mcp/MCPClient.js +164 -0
  34. package/dist/mcp/MCPClient.js.map +1 -0
  35. package/dist/mcp/index.d.ts +10 -0
  36. package/dist/mcp/index.d.ts.map +1 -0
  37. package/dist/mcp/index.js +11 -0
  38. package/dist/mcp/index.js.map +1 -0
  39. package/dist/mcp/paths.d.ts +16 -0
  40. package/dist/mcp/paths.d.ts.map +1 -0
  41. package/dist/mcp/paths.js +58 -0
  42. package/dist/mcp/paths.js.map +1 -0
  43. package/dist/mcp/types.d.ts +85 -0
  44. package/dist/mcp/types.d.ts.map +1 -0
  45. package/dist/mcp/types.js +7 -0
  46. package/dist/mcp/types.js.map +1 -0
  47. package/dist/providers/index.d.ts +40 -0
  48. package/dist/providers/index.d.ts.map +1 -0
  49. package/dist/providers/index.js +148 -0
  50. package/dist/providers/index.js.map +1 -0
  51. package/dist/providers/toolplex.d.ts +43 -0
  52. package/dist/providers/toolplex.d.ts.map +1 -0
  53. package/dist/providers/toolplex.js +168 -0
  54. package/dist/providers/toolplex.js.map +1 -0
  55. package/dist/types/index.d.ts +218 -0
  56. package/dist/types/index.d.ts.map +1 -0
  57. package/dist/types/index.js +8 -0
  58. package/dist/types/index.js.map +1 -0
  59. package/dist/utils/index.d.ts +8 -0
  60. package/dist/utils/index.d.ts.map +1 -0
  61. package/dist/utils/index.js +8 -0
  62. package/dist/utils/index.js.map +1 -0
  63. package/dist/utils/models.d.ts +30 -0
  64. package/dist/utils/models.d.ts.map +1 -0
  65. package/dist/utils/models.js +52 -0
  66. package/dist/utils/models.js.map +1 -0
  67. package/dist/utils/schema.d.ts +74 -0
  68. package/dist/utils/schema.d.ts.map +1 -0
  69. package/dist/utils/schema.js +253 -0
  70. package/dist/utils/schema.js.map +1 -0
  71. package/package.json +70 -0
  72. package/src/adapters/index.ts +9 -0
  73. package/src/adapters/types.ts +241 -0
  74. package/src/core/ChatEngine.ts +464 -0
  75. package/src/core/ToolBuilder.ts +323 -0
  76. package/src/core/index.ts +6 -0
  77. package/src/index.ts +86 -0
  78. package/src/mcp/DefaultStdioTransportFactory.ts +71 -0
  79. package/src/mcp/MCPClient.ts +209 -0
  80. package/src/mcp/index.ts +24 -0
  81. package/src/mcp/paths.ts +91 -0
  82. package/src/mcp/types.ts +93 -0
  83. package/src/providers/index.ts +177 -0
  84. package/src/providers/toolplex.ts +217 -0
  85. package/src/types/index.ts +290 -0
  86. package/src/utils/index.ts +8 -0
  87. package/src/utils/models.ts +59 -0
  88. package/src/utils/schema.ts +307 -0
@@ -0,0 +1,323 @@
1
+ /**
2
+ * @toolplex/ai-engine - Tool Builder
3
+ *
4
+ * Builds AI SDK tools from MCP tools, handling:
5
+ * - Schema cleaning and sanitization
6
+ * - Tool confirmation flows
7
+ * - Tool execution with cancellation support
8
+ */
9
+
10
+ import { tool, jsonSchema } from "ai";
11
+ import type { EngineAdapter } from "../adapters/types.js";
12
+ import type {
13
+ MCPTool,
14
+ MCPToolResult,
15
+ ConfirmationRequest,
16
+ } from "../types/index.js";
17
+ import { deepSanitizeParams, cleanToolSchema } from "../utils/schema.js";
18
+ import { isChatGPTModel, isGoogleGeminiModel } from "../utils/models.js";
19
+
20
+ export interface BuildToolsOptions {
21
+ sessionId: string;
22
+ streamId: string;
23
+ modelId: string;
24
+ abortSignal: AbortSignal;
25
+ adapter: EngineAdapter;
26
+ /** Tools to hide from AI agents (e.g., 'initialize_toolplex') */
27
+ hiddenTools?: string[];
28
+ /** Callback for when tool args are edited during confirmation */
29
+ onArgsEdited?: (
30
+ toolName: string,
31
+ editedArgs: any,
32
+ configEdited: boolean,
33
+ ) => void;
34
+ }
35
+
36
+ /**
37
+ * Build AI SDK tools from MCP tools
38
+ */
39
+ export async function buildMCPTools(
40
+ options: BuildToolsOptions,
41
+ ): Promise<Record<string, any>> {
42
+ const {
43
+ sessionId,
44
+ streamId,
45
+ modelId,
46
+ abortSignal,
47
+ adapter,
48
+ hiddenTools = ["initialize_toolplex"],
49
+ onArgsEdited,
50
+ } = options;
51
+
52
+ const logger = adapter.logger;
53
+ const isGemini = isGoogleGeminiModel(modelId);
54
+
55
+ // Get tools from MCP
56
+ const mcpToolsResult = await adapter.mcp.listTools(sessionId);
57
+ const mcpTools: MCPTool[] = mcpToolsResult?.tools || [];
58
+
59
+ const aiSdkTools: Record<string, any> = {};
60
+
61
+ // Track active tool executions for cancellation
62
+ const activeToolExecutions = new Map<string, AbortController>();
63
+
64
+ // Clean up when stream is aborted
65
+ abortSignal.addEventListener("abort", () => {
66
+ logger.debug(
67
+ "Tool builder: Stream aborted, cancelling active tool executions",
68
+ {
69
+ sessionId,
70
+ streamId,
71
+ activeToolCount: activeToolExecutions.size,
72
+ },
73
+ );
74
+
75
+ for (const [
76
+ toolExecutionId,
77
+ controller,
78
+ ] of activeToolExecutions.entries()) {
79
+ logger.debug("Tool builder: Aborting tool execution", {
80
+ toolExecutionId,
81
+ });
82
+ controller.abort();
83
+ }
84
+
85
+ activeToolExecutions.clear();
86
+ });
87
+
88
+ for (const mcpTool of mcpTools) {
89
+ // Skip hidden tools
90
+ if (hiddenTools.includes(mcpTool.name)) {
91
+ continue;
92
+ }
93
+
94
+ const toolSchema = mcpTool.inputSchema || {
95
+ type: "object",
96
+ properties: {},
97
+ };
98
+ const finalSchema = cleanToolSchema(toolSchema, isGemini, logger);
99
+
100
+ aiSdkTools[mcpTool.name] = tool({
101
+ description: mcpTool.description || `Tool: ${mcpTool.name}`,
102
+ inputSchema: jsonSchema<any>(finalSchema),
103
+ execute: async (params: any): Promise<MCPToolResult> => {
104
+ const toolExecutionId = `${mcpTool.name}-${Date.now()}`;
105
+ const toolAbortController = new AbortController();
106
+ activeToolExecutions.set(toolExecutionId, toolAbortController);
107
+
108
+ // Check if stream was already aborted
109
+ if (abortSignal.aborted) {
110
+ logger.debug(
111
+ "Tool builder: Stream already aborted, skipping tool execution",
112
+ {
113
+ toolName: mcpTool.name,
114
+ sessionId,
115
+ },
116
+ );
117
+ activeToolExecutions.delete(toolExecutionId);
118
+ throw new Error("Stream cancelled");
119
+ }
120
+
121
+ try {
122
+ // Normalize ChatGPT args -> arguments workaround
123
+ if (
124
+ mcpTool.name === "call_tool" &&
125
+ isChatGPTModel(modelId) &&
126
+ params?.args &&
127
+ !params?.arguments
128
+ ) {
129
+ logger.info(
130
+ "Tool builder: Normalizing call_tool params for ChatGPT",
131
+ {
132
+ modelId,
133
+ originalKeys: Object.keys(params),
134
+ },
135
+ );
136
+ const { args, ...rest } = params;
137
+ params = { ...rest, arguments: args };
138
+ }
139
+
140
+ // Deep sanitize params
141
+ const sanitizedParams = deepSanitizeParams(
142
+ params,
143
+ toolSchema,
144
+ undefined,
145
+ logger,
146
+ );
147
+
148
+ // Log when sanitization modifies parameters
149
+ if (JSON.stringify(params) !== JSON.stringify(sanitizedParams)) {
150
+ logger.debug(
151
+ "Tool builder: deepSanitizeParams modified tool arguments",
152
+ {
153
+ toolName: mcpTool.name,
154
+ sessionId,
155
+ },
156
+ );
157
+ }
158
+
159
+ // Check abort before confirmation
160
+ if (toolAbortController.signal.aborted) {
161
+ throw new Error("Tool execution cancelled");
162
+ }
163
+
164
+ // Handle confirmation if adapter supports it
165
+ let finalParams = sanitizedParams;
166
+
167
+ if (adapter.confirmations.isInteractive()) {
168
+ const confirmationRequest = await checkToolConfirmation(
169
+ mcpTool.name,
170
+ sanitizedParams,
171
+ { sessionId, streamId },
172
+ );
173
+
174
+ if (confirmationRequest) {
175
+ logger.debug("Tool builder: Tool requires user confirmation", {
176
+ sessionId,
177
+ toolName: mcpTool.name,
178
+ confirmationType: confirmationRequest.type,
179
+ });
180
+
181
+ try {
182
+ const result = await adapter.confirmations.requestConfirmation(
183
+ streamId,
184
+ confirmationRequest,
185
+ );
186
+
187
+ if (!result.allowed) {
188
+ return {
189
+ content: [
190
+ {
191
+ type: "text",
192
+ text: `Operation cancelled: ${result.reason || "User denied the operation"}`,
193
+ },
194
+ ],
195
+ };
196
+ }
197
+
198
+ // Apply edited config if provided
199
+ if (result.editedConfig) {
200
+ finalParams = { ...sanitizedParams };
201
+ if (confirmationRequest.type === "install") {
202
+ finalParams.config = result.editedConfig;
203
+ }
204
+
205
+ if (onArgsEdited) {
206
+ onArgsEdited(
207
+ mcpTool.name,
208
+ finalParams,
209
+ result.wasEdited === true,
210
+ );
211
+ }
212
+ }
213
+ } catch (confirmationError: any) {
214
+ if (confirmationError?.message === "Stream cancelled by user") {
215
+ throw new Error("Tool execution cancelled");
216
+ }
217
+ throw confirmationError;
218
+ }
219
+ }
220
+ }
221
+
222
+ // Check abort before MCP call
223
+ if (toolAbortController.signal.aborted) {
224
+ throw new Error("Tool execution cancelled");
225
+ }
226
+
227
+ // Execute the MCP tool
228
+ const result = await adapter.mcp.callTool(
229
+ sessionId,
230
+ mcpTool.name,
231
+ finalParams,
232
+ );
233
+
234
+ return result;
235
+ } catch (error: any) {
236
+ activeToolExecutions.delete(toolExecutionId);
237
+
238
+ if (
239
+ toolAbortController.signal.aborted ||
240
+ abortSignal.aborted ||
241
+ error?.message === "Tool execution cancelled"
242
+ ) {
243
+ throw new Error("Tool execution cancelled");
244
+ }
245
+
246
+ logger.error("Tool builder: Tool execution failed", {
247
+ sessionId,
248
+ toolName: mcpTool.name,
249
+ error,
250
+ });
251
+
252
+ return {
253
+ isError: true,
254
+ content: [
255
+ {
256
+ type: "text",
257
+ text: `Tool execution failed: ${error instanceof Error ? error.message : String(error)}`,
258
+ },
259
+ ],
260
+ };
261
+ }
262
+ },
263
+ } as any);
264
+ }
265
+
266
+ return aiSdkTools;
267
+ }
268
+
269
+ /**
270
+ * Check if a tool requires confirmation
271
+ * This is a simplified version - the full implementation would be in the confirmation registry
272
+ */
273
+ async function checkToolConfirmation(
274
+ toolName: string,
275
+ params: any,
276
+ _context: { sessionId: string; streamId: string },
277
+ ): Promise<ConfirmationRequest | null> {
278
+ // Install/uninstall operations always require confirmation
279
+ if (toolName === "install_server" || toolName === "install_mcp_server") {
280
+ return {
281
+ type: "install",
282
+ data: {
283
+ serverId: params.server_id,
284
+ serverName: params.server_name,
285
+ config: params.config,
286
+ },
287
+ };
288
+ }
289
+
290
+ if (toolName === "uninstall_server" || toolName === "uninstall_mcp_server") {
291
+ return {
292
+ type: "uninstall",
293
+ data: {
294
+ serverId: params.server_id,
295
+ serverName: params.server_name,
296
+ },
297
+ };
298
+ }
299
+
300
+ if (toolName === "save_playbook") {
301
+ return {
302
+ type: "save-playbook",
303
+ data: {
304
+ playbookName: params.playbook_name,
305
+ description: params.description,
306
+ actions: params.actions,
307
+ privacy: params.privacy,
308
+ },
309
+ };
310
+ }
311
+
312
+ if (toolName === "submit_feedback") {
313
+ return {
314
+ type: "submit-feedback",
315
+ data: {
316
+ vote: params.vote,
317
+ message: params.message,
318
+ },
319
+ };
320
+ }
321
+
322
+ return null;
323
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @toolplex/ai-engine - Core Components
3
+ */
4
+
5
+ export { ChatEngine, type ChatEngineOptions } from "./ChatEngine.js";
6
+ export { buildMCPTools, type BuildToolsOptions } from "./ToolBuilder.js";
package/src/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @toolplex/ai-engine
3
+ *
4
+ * Core AI chat engine for ToolPlex.
5
+ * Powers desktop, cloud, and CLI environments through adapter pattern.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { ChatEngine, createElectronAdapter } from '@toolplex/ai-engine';
10
+ *
11
+ * const adapter = createElectronAdapter({ webContents });
12
+ * const engine = new ChatEngine(adapter);
13
+ *
14
+ * await engine.stream({
15
+ * sessionId: 'session-123',
16
+ * modelId: 'anthropic/claude-sonnet-4',
17
+ * messages: [{ role: 'user', content: 'Hello!' }],
18
+ * });
19
+ * ```
20
+ */
21
+
22
+ // Types
23
+ export * from "./types/index.js";
24
+
25
+ // Adapters
26
+ export * from "./adapters/index.js";
27
+
28
+ // Providers
29
+ export {
30
+ getProvider,
31
+ getModel,
32
+ isProviderAvailable,
33
+ toolplexUsageMap,
34
+ type GetProviderOptions,
35
+ } from "./providers/index.js";
36
+ export { createToolPlex, type ToolPlexConfig } from "./providers/toolplex.js";
37
+
38
+ // Utilities
39
+ export {
40
+ deepSanitizeParams,
41
+ resolveSchemaRefs,
42
+ sanitizeSchemaForGemini,
43
+ cleanToolSchema,
44
+ } from "./utils/schema.js";
45
+ export {
46
+ isChatGPTModel,
47
+ isGoogleGeminiModel,
48
+ isAnthropicModel,
49
+ parseModelId,
50
+ } from "./utils/models.js";
51
+
52
+ // Core engine
53
+ export { ChatEngine, type ChatEngineOptions } from "./core/ChatEngine.js";
54
+ export { buildMCPTools, type BuildToolsOptions } from "./core/ToolBuilder.js";
55
+
56
+ // MCP Client
57
+ export { MCPClient } from "./mcp/MCPClient.js";
58
+ export type {
59
+ MCPSession,
60
+ MCPResult,
61
+ MCPTool,
62
+ MCPToolResult,
63
+ TransportFactory,
64
+ MCPClientConfig,
65
+ } from "./mcp/types.js";
66
+
67
+ // MCP path utilities and default transport
68
+ export { getToolplexClientPath } from "./mcp/paths.js";
69
+ export {
70
+ DefaultStdioTransportFactory,
71
+ defaultStdioTransportFactory,
72
+ } from "./mcp/DefaultStdioTransportFactory.js";
73
+
74
+ // Re-export AI SDK primitives for consumers
75
+ export { streamText, tool, jsonSchema, stepCountIs } from "ai";
76
+ export type { ToolResultPart, ToolCallPart, TextPart, ImagePart } from "ai";
77
+
78
+ // Re-export provider factory functions
79
+ export { createOpenAI } from "@ai-sdk/openai";
80
+ export { createAnthropic } from "@ai-sdk/anthropic";
81
+ export { createGoogleGenerativeAI } from "@ai-sdk/google";
82
+ export { createOpenRouter } from "@openrouter/ai-sdk-provider";
83
+
84
+ // Re-export MCP SDK for transport implementations
85
+ export { Client as MCPSDKClient } from "@modelcontextprotocol/sdk/client/index.js";
86
+ export { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @toolplex/ai-engine - Default Stdio Transport Factory
3
+ *
4
+ * Default implementation of TransportFactory that spawns @toolplex/client
5
+ * using the system's Node.js. This works out-of-box for CLI usage.
6
+ *
7
+ * For desktop apps with bundled dependencies, override this with a custom
8
+ * TransportFactory implementation.
9
+ */
10
+
11
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
12
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
13
+ import type { TransportFactory, MCPSession } from "./types.js";
14
+ import { getToolplexClientPath } from "./paths.js";
15
+
16
+ /**
17
+ * Default Transport Factory - spawns @toolplex/client using system Node.js
18
+ *
19
+ * This is suitable for:
20
+ * - CLI applications
21
+ * - Development environments
22
+ * - Any context where system Node.js is available
23
+ *
24
+ * For Electron/desktop apps with bundled Node.js, create a custom
25
+ * TransportFactory that uses the bundled runtime.
26
+ */
27
+ export class DefaultStdioTransportFactory implements TransportFactory {
28
+ async createTransport(
29
+ apiKey: string,
30
+ sessionResumeHistory?: string,
31
+ ): Promise<MCPSession> {
32
+ const toolplexPath = getToolplexClientPath();
33
+
34
+ // Build environment for the MCP server
35
+ const env: Record<string, string> = {
36
+ ...(process.env as Record<string, string>),
37
+ TOOLPLEX_API_KEY: apiKey,
38
+ CLIENT_NAME: "toolplex-ai-engine",
39
+ };
40
+
41
+ // Add session resume history if provided
42
+ if (sessionResumeHistory) {
43
+ env.TOOLPLEX_SESSION_RESUME_HISTORY = sessionResumeHistory;
44
+ }
45
+
46
+ const transport = new StdioClientTransport({
47
+ command: "node", // Uses system Node.js
48
+ args: [toolplexPath],
49
+ env,
50
+ });
51
+
52
+ const client = new Client({
53
+ name: "toolplex-ai-engine-client",
54
+ version: "1.0.0",
55
+ });
56
+
57
+ await client.connect(transport);
58
+ return { transport, client };
59
+ }
60
+
61
+ async closeTransport(session: MCPSession): Promise<void> {
62
+ try {
63
+ await session.client.close();
64
+ } catch {
65
+ // Silently continue to ensure cleanup
66
+ }
67
+ }
68
+ }
69
+
70
+ // Export singleton instance for convenience
71
+ export const defaultStdioTransportFactory = new DefaultStdioTransportFactory();
@@ -0,0 +1,209 @@
1
+ /**
2
+ * @toolplex/ai-engine - MCP Client
3
+ *
4
+ * Core MCP client that manages sessions and tool operations.
5
+ * Uses a TransportFactory for platform-specific transport creation.
6
+ */
7
+
8
+ import type {
9
+ MCPSession,
10
+ MCPResult,
11
+ MCPToolResult,
12
+ MCPClientConfig,
13
+ TransportFactory,
14
+ } from "./types.js";
15
+
16
+ /**
17
+ * MCP Client - manages sessions and provides tool operations
18
+ */
19
+ export class MCPClient {
20
+ private sessions = new Map<string, MCPSession>();
21
+ private transportFactory: TransportFactory;
22
+ private logger: MCPClientConfig["logger"];
23
+ private imageHandler: MCPClientConfig["imageHandler"];
24
+ private getCurrentUserId: MCPClientConfig["getCurrentUserId"];
25
+
26
+ constructor(config: MCPClientConfig) {
27
+ this.transportFactory = config.transportFactory;
28
+ this.logger = config.logger;
29
+ this.imageHandler = config.imageHandler;
30
+ this.getCurrentUserId = config.getCurrentUserId;
31
+ }
32
+
33
+ /**
34
+ * Creates an MCP session and waits for tools to be initialized
35
+ *
36
+ * CRITICAL: The ToolPlex MCP server fetches tool schemas from the API during startup.
37
+ * This is asynchronous and can take time. We MUST wait for this to complete before
38
+ * returning, otherwise listTools() will return empty schemas.
39
+ */
40
+ async createSession(
41
+ sessionId: string,
42
+ apiKey: string,
43
+ sessionResumeHistory?: string,
44
+ ): Promise<MCPResult> {
45
+ try {
46
+ // Clean up existing session if it exists
47
+ await this.destroySession(sessionId);
48
+
49
+ this.logger?.debug("MCPClient: Creating session", { sessionId });
50
+
51
+ const session = await this.transportFactory.createTransport(
52
+ apiKey,
53
+ sessionResumeHistory,
54
+ );
55
+
56
+ this.sessions.set(sessionId, session);
57
+ this.logger?.debug("MCPClient: Session created and stored", {
58
+ sessionId,
59
+ });
60
+
61
+ return { success: true };
62
+ } catch (error) {
63
+ this.logger?.error("MCPClient: Transport creation failed", { error });
64
+ return {
65
+ success: false,
66
+ error: error instanceof Error ? error.message : "Unknown error",
67
+ };
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Lists available tools for a session
73
+ */
74
+ async listTools(sessionId: string): Promise<{ tools: any[] }> {
75
+ this.logger?.debug("MCPClient: Listing tools for session", {
76
+ sessionId,
77
+ availableSessions: Array.from(this.sessions.keys()),
78
+ });
79
+
80
+ const session = this.sessions.get(sessionId);
81
+ if (!session) {
82
+ this.logger?.error("MCPClient: No session found in registry", {
83
+ sessionId,
84
+ availableSessions: Array.from(this.sessions.keys()),
85
+ });
86
+ throw new Error(`No MCP client found for session: ${sessionId}`);
87
+ }
88
+
89
+ try {
90
+ const result = await session.client.listTools();
91
+ this.logger?.debug("MCPClient: Tools listed successfully", {
92
+ sessionId,
93
+ toolCount: result?.tools?.length || 0,
94
+ });
95
+ return result;
96
+ } catch (error) {
97
+ this.logger?.error("MCPClient: Failed to list tools", {
98
+ sessionId,
99
+ error,
100
+ });
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Calls a tool for a session
107
+ */
108
+ async callTool(
109
+ sessionId: string,
110
+ toolName: string,
111
+ args: any,
112
+ ): Promise<MCPToolResult> {
113
+ const session = this.sessions.get(sessionId);
114
+ if (!session) {
115
+ throw new Error(`No MCP client found for session: ${sessionId}`);
116
+ }
117
+
118
+ const toolCall = {
119
+ name: toolName,
120
+ arguments: args || {},
121
+ };
122
+ const result = await session.client.callTool(toolCall);
123
+
124
+ // Process the result to handle images if handler is available
125
+ if (
126
+ this.imageHandler &&
127
+ this.getCurrentUserId &&
128
+ result?.content &&
129
+ Array.isArray(result.content)
130
+ ) {
131
+ const hasImages = result.content.some(
132
+ (item: any) => item?.type === "image" && item?.data,
133
+ );
134
+
135
+ if (hasImages) {
136
+ const userId = this.getCurrentUserId();
137
+ if (userId) {
138
+ await this.imageHandler.initialize(userId);
139
+ const processed = await this.imageHandler.processToolResult(result);
140
+
141
+ return {
142
+ ...result,
143
+ content: processed.content,
144
+ savedImages: processed.savedImages,
145
+ };
146
+ }
147
+ }
148
+ }
149
+
150
+ return result as MCPToolResult;
151
+ }
152
+
153
+ /**
154
+ * Destroys an MCP session
155
+ */
156
+ async destroySession(sessionId: string): Promise<MCPResult> {
157
+ try {
158
+ const session = this.sessions.get(sessionId);
159
+ if (session) {
160
+ await this.transportFactory.closeTransport(session);
161
+ this.sessions.delete(sessionId);
162
+ }
163
+
164
+ return { success: true };
165
+ } catch (error) {
166
+ return {
167
+ success: false,
168
+ error: error instanceof Error ? error.message : "Unknown error",
169
+ };
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Gets all active session IDs
175
+ */
176
+ getActiveSessions(): string[] {
177
+ return Array.from(this.sessions.keys());
178
+ }
179
+
180
+ /**
181
+ * Destroys all MCP sessions (cleanup)
182
+ */
183
+ async destroyAllSessions(): Promise<void> {
184
+ const sessionIds = this.getActiveSessions();
185
+ await Promise.all(sessionIds.map((id) => this.destroySession(id)));
186
+ }
187
+
188
+ /**
189
+ * Gets session info
190
+ */
191
+ getSessionInfo(sessionId: string): {
192
+ exists: boolean;
193
+ isConnected?: boolean;
194
+ } {
195
+ const session = this.sessions.get(sessionId);
196
+ if (!session) {
197
+ return { exists: false };
198
+ }
199
+
200
+ return { exists: true, isConnected: true };
201
+ }
202
+
203
+ /**
204
+ * Check if a session exists
205
+ */
206
+ hasSession(sessionId: string): boolean {
207
+ return this.sessions.has(sessionId);
208
+ }
209
+ }