@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.
- package/LICENSE +98 -0
- package/README.md +292 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +9 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +137 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +14 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/core/ChatEngine.d.ts +47 -0
- package/dist/core/ChatEngine.d.ts.map +1 -0
- package/dist/core/ChatEngine.js +355 -0
- package/dist/core/ChatEngine.js.map +1 -0
- package/dist/core/ToolBuilder.d.ts +25 -0
- package/dist/core/ToolBuilder.d.ts.map +1 -0
- package/dist/core/ToolBuilder.js +215 -0
- package/dist/core/ToolBuilder.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/DefaultStdioTransportFactory.d.ts +27 -0
- package/dist/mcp/DefaultStdioTransportFactory.d.ts.map +1 -0
- package/dist/mcp/DefaultStdioTransportFactory.js +60 -0
- package/dist/mcp/DefaultStdioTransportFactory.js.map +1 -0
- package/dist/mcp/MCPClient.d.ts +60 -0
- package/dist/mcp/MCPClient.d.ts.map +1 -0
- package/dist/mcp/MCPClient.js +164 -0
- package/dist/mcp/MCPClient.js.map +1 -0
- package/dist/mcp/index.d.ts +10 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/paths.d.ts +16 -0
- package/dist/mcp/paths.d.ts.map +1 -0
- package/dist/mcp/paths.js +58 -0
- package/dist/mcp/paths.js.map +1 -0
- package/dist/mcp/types.d.ts +85 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/providers/index.d.ts +40 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +148 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/toolplex.d.ts +43 -0
- package/dist/providers/toolplex.d.ts.map +1 -0
- package/dist/providers/toolplex.js +168 -0
- package/dist/providers/toolplex.js.map +1 -0
- package/dist/types/index.d.ts +218 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/models.d.ts +30 -0
- package/dist/utils/models.d.ts.map +1 -0
- package/dist/utils/models.js +52 -0
- package/dist/utils/models.js.map +1 -0
- package/dist/utils/schema.d.ts +74 -0
- package/dist/utils/schema.d.ts.map +1 -0
- package/dist/utils/schema.js +253 -0
- package/dist/utils/schema.js.map +1 -0
- package/package.json +70 -0
- package/src/adapters/index.ts +9 -0
- package/src/adapters/types.ts +241 -0
- package/src/core/ChatEngine.ts +464 -0
- package/src/core/ToolBuilder.ts +323 -0
- package/src/core/index.ts +6 -0
- package/src/index.ts +86 -0
- package/src/mcp/DefaultStdioTransportFactory.ts +71 -0
- package/src/mcp/MCPClient.ts +209 -0
- package/src/mcp/index.ts +24 -0
- package/src/mcp/paths.ts +91 -0
- package/src/mcp/types.ts +93 -0
- package/src/providers/index.ts +177 -0
- package/src/providers/toolplex.ts +217 -0
- package/src/types/index.ts +290 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/models.ts +59 -0
- 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
|
+
}
|
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
|
+
}
|