n8n-nodes-smart-browser-automation 1.6.25 → 1.6.27
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/dist/nodes/SmartBrowserAutomation/SmartBrowserAutomation.node.js +56 -19
- package/dist/nodes/SmartBrowserAutomation/SmartBrowserAutomationTools.node.js +71 -173
- package/dist/nodes/SmartBrowserAutomation/utils/McpConnection.d.ts +18 -0
- package/dist/nodes/SmartBrowserAutomation/utils/McpConnection.js +61 -0
- package/dist/nodes/SmartBrowserAutomation/utils/McpUtils.d.ts +32 -0
- package/dist/nodes/SmartBrowserAutomation/utils/McpUtils.js +149 -0
- package/package.json +1 -1
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.SmartBrowserAutomation = void 0;
|
|
7
4
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
8
|
-
const BrowserSessionManager_1 = __importDefault(require("./BrowserSessionManager"));
|
|
9
5
|
const jsonSchemaToZod_1 = require("./jsonSchemaToZod");
|
|
6
|
+
const McpConnection_1 = require("./utils/McpConnection");
|
|
10
7
|
class SmartBrowserAutomation {
|
|
11
8
|
description = {
|
|
12
9
|
displayName: 'Smart Browser Automation',
|
|
@@ -97,9 +94,18 @@ class SmartBrowserAutomation {
|
|
|
97
94
|
async getAvailableTools() {
|
|
98
95
|
try {
|
|
99
96
|
const credentials = await this.getCredentials('smartBrowserAutomationApi');
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
97
|
+
// Removed sessionManager usage
|
|
98
|
+
// Use temporary config for listing options
|
|
99
|
+
const config = {
|
|
100
|
+
mcpEndpoint: (credentials.mcpEndpoint || '').trim(),
|
|
101
|
+
browserMode: credentials.browserMode,
|
|
102
|
+
};
|
|
103
|
+
const { client, mcpTools, error } = await (0, McpConnection_1.connectAndGetTools)(this, config);
|
|
104
|
+
if (error)
|
|
105
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
|
|
106
|
+
// Close immediately as we just need list
|
|
107
|
+
await client.close();
|
|
108
|
+
const tools = mcpTools;
|
|
103
109
|
return tools.map((tool) => ({
|
|
104
110
|
name: tool.name.split('_').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(' '),
|
|
105
111
|
value: tool.name,
|
|
@@ -118,13 +124,25 @@ class SmartBrowserAutomation {
|
|
|
118
124
|
const items = this.getInputData();
|
|
119
125
|
const returnData = [];
|
|
120
126
|
const credentials = await this.getCredentials('smartBrowserAutomationApi');
|
|
121
|
-
const sessionManager = BrowserSessionManager_1.default.getInstance();
|
|
122
127
|
const operation = this.getNodeParameter('operation', 0);
|
|
123
128
|
// Handle List Tools operation
|
|
124
129
|
if (operation === 'listTools') {
|
|
125
130
|
try {
|
|
126
|
-
|
|
127
|
-
const
|
|
131
|
+
// Use new McpConnection logic
|
|
132
|
+
const mcpOverride = this.getNodeParameter('mcpEndpointOverride', 0, '');
|
|
133
|
+
const cdpOverride = this.getNodeParameter('cdpOverride', 0, '');
|
|
134
|
+
const browserMode = credentials.browserMode;
|
|
135
|
+
const config = {
|
|
136
|
+
mcpEndpoint: (mcpOverride || credentials.mcpEndpoint || '').trim(),
|
|
137
|
+
browserMode: browserMode,
|
|
138
|
+
cdpEndpoint: (cdpOverride || credentials.cdpEndpoint || '').trim(),
|
|
139
|
+
};
|
|
140
|
+
const { client, mcpTools, error } = await (0, McpConnection_1.connectAndGetTools)(this, config);
|
|
141
|
+
if (error)
|
|
142
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
|
|
143
|
+
// Close client after listing since this is just a listing operation
|
|
144
|
+
await client.close();
|
|
145
|
+
const tools = mcpTools;
|
|
128
146
|
returnData.push({
|
|
129
147
|
json: {
|
|
130
148
|
tools: tools.map((tool) => ({
|
|
@@ -191,10 +209,19 @@ class SmartBrowserAutomation {
|
|
|
191
209
|
}
|
|
192
210
|
try {
|
|
193
211
|
// Initialize session if not ready
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
212
|
+
// Connect via McpConnection
|
|
213
|
+
const mcpOverride = this.getNodeParameter('mcpEndpointOverride', 0, '');
|
|
214
|
+
const browserMode = credentials.browserMode; // from credentials
|
|
215
|
+
// We might need to handle CDP Override if it's passed in tool params (legacy)
|
|
216
|
+
const cdpUrl = cdpOverride || credentials.cdpEndpoint || '';
|
|
217
|
+
const config = {
|
|
218
|
+
mcpEndpoint: (mcpOverride || credentials.mcpEndpoint || '').trim(),
|
|
219
|
+
browserMode: browserMode,
|
|
220
|
+
cdpEndpoint: cdpUrl.trim(),
|
|
221
|
+
};
|
|
222
|
+
const { client, mcpTools, error } = await (0, McpConnection_1.connectAndGetTools)(this, config);
|
|
223
|
+
if (error)
|
|
224
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error.message);
|
|
198
225
|
// Ensure toolParams is an object
|
|
199
226
|
if (typeof toolParams !== 'object' ||
|
|
200
227
|
toolParams === null ||
|
|
@@ -207,9 +234,15 @@ class SmartBrowserAutomation {
|
|
|
207
234
|
if (!endpoint) {
|
|
208
235
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'CDP endpoint is required. Provide it in tool parameters or CDP Endpoint Override field.');
|
|
209
236
|
}
|
|
210
|
-
// Reinitialize with new CDP endpoint
|
|
211
|
-
await
|
|
212
|
-
|
|
237
|
+
// Reinitialize with new CDP endpoint - Create new client
|
|
238
|
+
await client.close(); // Close existing listing client
|
|
239
|
+
const newConfig = { ...config, cdpEndpoint: endpoint };
|
|
240
|
+
const { client: newClient, error: connError } = await (0, McpConnection_1.connectAndGetTools)(this, newConfig);
|
|
241
|
+
if (connError) {
|
|
242
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Failed to reconnect with new CDP endpoint: ${connError.message}`);
|
|
243
|
+
}
|
|
244
|
+
// We just verify connection here via ListTools or similar implicitly done by connectAndGetTools
|
|
245
|
+
await newClient.close();
|
|
213
246
|
returnData.push({
|
|
214
247
|
json: {
|
|
215
248
|
result: {
|
|
@@ -221,7 +254,7 @@ class SmartBrowserAutomation {
|
|
|
221
254
|
return [returnData];
|
|
222
255
|
}
|
|
223
256
|
// Validate tool exists before executing
|
|
224
|
-
const availableTools =
|
|
257
|
+
const availableTools = mcpTools;
|
|
225
258
|
const selectedTool = availableTools.find((tool) => tool.name === toolName);
|
|
226
259
|
const toolExists = Boolean(selectedTool);
|
|
227
260
|
if (!toolExists) {
|
|
@@ -240,7 +273,11 @@ class SmartBrowserAutomation {
|
|
|
240
273
|
}
|
|
241
274
|
}
|
|
242
275
|
// Execute the tool via MCP
|
|
243
|
-
const result = await
|
|
276
|
+
const result = await client.callTool({
|
|
277
|
+
name: toolName,
|
|
278
|
+
arguments: toolParams
|
|
279
|
+
});
|
|
280
|
+
await client.close();
|
|
244
281
|
returnData.push({
|
|
245
282
|
json: { result },
|
|
246
283
|
});
|
|
@@ -1,80 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/* eslint-disable n8n-nodes-base/node-class-description-inputs-wrong-regular-node */
|
|
3
3
|
/* eslint-disable n8n-nodes-base/node-class-description-outputs-wrong */
|
|
4
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
-
if (k2 === undefined) k2 = k;
|
|
6
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
-
}
|
|
10
|
-
Object.defineProperty(o, k2, desc);
|
|
11
|
-
}) : (function(o, m, k, k2) {
|
|
12
|
-
if (k2 === undefined) k2 = k;
|
|
13
|
-
o[k2] = m[k];
|
|
14
|
-
}));
|
|
15
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
-
}) : function(o, v) {
|
|
18
|
-
o["default"] = v;
|
|
19
|
-
});
|
|
20
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
21
|
-
var ownKeys = function(o) {
|
|
22
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
23
|
-
var ar = [];
|
|
24
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
25
|
-
return ar;
|
|
26
|
-
};
|
|
27
|
-
return ownKeys(o);
|
|
28
|
-
};
|
|
29
|
-
return function (mod) {
|
|
30
|
-
if (mod && mod.__esModule) return mod;
|
|
31
|
-
var result = {};
|
|
32
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
33
|
-
__setModuleDefault(result, mod);
|
|
34
|
-
return result;
|
|
35
|
-
};
|
|
36
|
-
})();
|
|
37
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
38
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
39
|
-
};
|
|
40
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
5
|
exports.SmartBrowserAutomationTools = void 0;
|
|
42
6
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
async function importDynamicStructuredTool() {
|
|
46
|
-
try {
|
|
47
|
-
return await Promise.resolve().then(() => __importStar(require('langchain/tools')));
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
// Some n8n installations ship a langchain build where subpath exports differ.
|
|
51
|
-
return await Promise.resolve().then(() => __importStar(require('@langchain/core/tools')));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
async function importToolkitBase() {
|
|
55
|
-
// n8n uses Toolkit from @langchain/classic/agents internally.
|
|
56
|
-
return await Promise.resolve().then(() => __importStar(require('@langchain/classic/agents')));
|
|
57
|
-
}
|
|
58
|
-
function formatMcpToolResult(result) {
|
|
59
|
-
if (result === null || result === undefined)
|
|
60
|
-
return '';
|
|
61
|
-
if (typeof result === 'string')
|
|
62
|
-
return result;
|
|
63
|
-
const content = result?.content;
|
|
64
|
-
if (Array.isArray(content)) {
|
|
65
|
-
const texts = content
|
|
66
|
-
.map((c) => (c?.type === 'text' ? String(c?.text ?? '') : ''))
|
|
67
|
-
.filter((t) => t.length > 0);
|
|
68
|
-
if (texts.length)
|
|
69
|
-
return texts.join('\n');
|
|
70
|
-
}
|
|
71
|
-
try {
|
|
72
|
-
return JSON.stringify(result);
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return String(result);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
7
|
+
const McpUtils_1 = require("./utils/McpUtils");
|
|
8
|
+
const McpConnection_1 = require("./utils/McpConnection");
|
|
78
9
|
class SmartBrowserAutomationTools {
|
|
79
10
|
description = {
|
|
80
11
|
displayName: 'Smart Browser Automation Tools',
|
|
@@ -117,46 +48,60 @@ class SmartBrowserAutomationTools {
|
|
|
117
48
|
async supplyData(itemIndex) {
|
|
118
49
|
const node = this.getNode();
|
|
119
50
|
const credentials = await this.getCredentials('smartBrowserAutomationApi');
|
|
120
|
-
const sessionManager = BrowserSessionManager_1.default.getInstance();
|
|
121
51
|
const mcpOverride = this.getNodeParameter('mcpEndpointOverride', itemIndex, '');
|
|
122
52
|
const cdpOverride = this.getNodeParameter('cdpOverride', itemIndex, '');
|
|
123
|
-
const
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(mcpEndpoint)) {
|
|
134
|
-
throw new n8n_workflow_1.NodeOperationError(node, `MCP Endpoint looks incomplete: "${mcpEndpoint}" (add http:// or https://)`, { itemIndex });
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
await sessionManager.initialize(mcpEndpoint, useCDP, useCDP ? cdpUrl : undefined);
|
|
53
|
+
const browserMode = credentials.browserMode;
|
|
54
|
+
const useCDP = browserMode === 'cdp';
|
|
55
|
+
const config = {
|
|
56
|
+
mcpEndpoint: (mcpOverride || credentials.mcpEndpoint || '').trim(),
|
|
57
|
+
browserMode: browserMode,
|
|
58
|
+
cdpEndpoint: (cdpOverride || credentials.cdpEndpoint || '').trim(),
|
|
59
|
+
timeout: 60000 // Default timeout
|
|
60
|
+
};
|
|
61
|
+
if (!config.mcpEndpoint) {
|
|
62
|
+
throw new n8n_workflow_1.NodeOperationError(node, 'MCP Endpoint is required in credentials', { itemIndex });
|
|
138
63
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
throw new n8n_workflow_1.NodeOperationError(node, `Failed to connect to MCP. ${details}. Original error: ${e.message}`, {
|
|
144
|
-
itemIndex,
|
|
145
|
-
description: details,
|
|
146
|
-
});
|
|
64
|
+
// Connect to MCP Server
|
|
65
|
+
const { client, mcpTools, error } = await (0, McpConnection_1.connectAndGetTools)(this, config);
|
|
66
|
+
if (error) {
|
|
67
|
+
throw new n8n_workflow_1.NodeOperationError(node, error.message, { itemIndex });
|
|
147
68
|
}
|
|
148
|
-
|
|
149
|
-
if (!mcpTools.length) {
|
|
69
|
+
if (!mcpTools?.length) {
|
|
150
70
|
throw new n8n_workflow_1.NodeOperationError(node, 'MCP Server returned no tools', { itemIndex });
|
|
151
71
|
}
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
72
|
+
// *** Smart Browser Automation Specific Logic ***
|
|
73
|
+
// If we are in CDP mode, we MUST ensure the connection is established immediately.
|
|
74
|
+
// We call 'browser_connect_cdp' now, during initialization.
|
|
75
|
+
if (useCDP) {
|
|
76
|
+
const connectToolName = 'browser_connect_cdp';
|
|
77
|
+
const connectTool = mcpTools.find(t => t.name === connectToolName);
|
|
78
|
+
if (connectTool && config.cdpEndpoint) {
|
|
79
|
+
try {
|
|
80
|
+
console.log(`[Init] Auto-connecting to CDP: ${config.cdpEndpoint}`);
|
|
81
|
+
const connectResult = await client.callTool({
|
|
82
|
+
name: connectToolName,
|
|
83
|
+
arguments: { endpoint: config.cdpEndpoint }
|
|
84
|
+
});
|
|
85
|
+
if (connectResult.isError) {
|
|
86
|
+
throw new n8n_workflow_1.NodeOperationError(node, `CDP Connection failed: ${JSON.stringify(connectResult)}`, { itemIndex });
|
|
87
|
+
}
|
|
88
|
+
console.log(`[Init] Connected successfully.`);
|
|
89
|
+
}
|
|
90
|
+
catch (connErr) {
|
|
91
|
+
throw new n8n_workflow_1.NodeOperationError(node, `Failed to auto-connect to CDP: ${connErr.message}`, { itemIndex });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else if (!config.cdpEndpoint) {
|
|
95
|
+
throw new n8n_workflow_1.NodeOperationError(node, 'CDP Endpoint is missing for CDP mode connection.', { itemIndex });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Filter out the connect tool so the AI doesn't see it
|
|
99
|
+
const exposedTools = mcpTools.filter(t => t.name !== 'browser_connect_cdp');
|
|
100
|
+
// Debug Output
|
|
156
101
|
const debugJson = {
|
|
157
|
-
mcpEndpoint,
|
|
158
|
-
browserMode:
|
|
159
|
-
cdpEndpoint:
|
|
102
|
+
mcpEndpoint: config.mcpEndpoint,
|
|
103
|
+
browserMode: config.browserMode,
|
|
104
|
+
cdpEndpoint: config.cdpEndpoint,
|
|
160
105
|
initializationMode: 'strict_cdp_connect',
|
|
161
106
|
toolCount: exposedTools.length,
|
|
162
107
|
tools: exposedTools.map((t) => ({
|
|
@@ -164,79 +109,32 @@ class SmartBrowserAutomationTools {
|
|
|
164
109
|
description: t.description ?? '',
|
|
165
110
|
})),
|
|
166
111
|
};
|
|
112
|
+
// Using 'this.addOutputData' as per recent fix
|
|
167
113
|
this.addOutputData(n8n_workflow_1.NodeConnectionTypes.Main, 0, [[{ json: debugJson }]]);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
};
|
|
185
|
-
let runIndex = 0;
|
|
186
|
-
try {
|
|
187
|
-
runIndex = ctx.getNextRunIndex?.() ?? 0;
|
|
188
|
-
}
|
|
189
|
-
catch { }
|
|
190
|
-
const { index } = ctx.addInputData(n8n_workflow_1.NodeConnectionTypes.AiTool, [[{ json: input }]], runIndex);
|
|
191
|
-
try {
|
|
192
|
-
console.log(`[AI Agent] Executing tool ${toolName} with args:`, JSON.stringify(args));
|
|
193
|
-
const result = await sessionManager.callTool(toolName, args);
|
|
194
|
-
const output = { tool: toolName, result };
|
|
195
|
-
// Ensure the output is an array of INodeExecutionData
|
|
196
|
-
const executionData = [[{ json: output }]];
|
|
197
|
-
ctx.addOutputData(n8n_workflow_1.NodeConnectionTypes.AiTool, index, executionData);
|
|
198
|
-
return formatMcpToolResult(result);
|
|
199
|
-
}
|
|
200
|
-
catch (e) {
|
|
201
|
-
const errorMsg = e.message || String(e);
|
|
202
|
-
console.error(`[AI Agent] Tool ${toolName} failed:`, errorMsg);
|
|
203
|
-
// Fix: properly format error for addOutputData
|
|
204
|
-
const errorOutput = [[{
|
|
205
|
-
json: {
|
|
206
|
-
tool: toolName,
|
|
207
|
-
error: errorMsg,
|
|
208
|
-
stack: e.stack
|
|
209
|
-
}
|
|
210
|
-
}]];
|
|
211
|
-
// Safely attempt to report error to UI
|
|
212
|
-
try {
|
|
213
|
-
ctx.addOutputData(n8n_workflow_1.NodeConnectionTypes.AiTool, index, errorOutput);
|
|
214
|
-
}
|
|
215
|
-
catch (addError) {
|
|
216
|
-
console.error('[AI Agent] Failed to add error output:', addError);
|
|
217
|
-
}
|
|
218
|
-
// Re-throw so the AI Agent knows it failed
|
|
219
|
-
throw e;
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
metadata: { isFromToolkit: true },
|
|
223
|
-
});
|
|
114
|
+
// Create LangChain Tools
|
|
115
|
+
// Map MCP tools to DynamicStructuredTools using the official utility pattern
|
|
116
|
+
const dynamicTools = await Promise.all(exposedTools.map(async (tool) => {
|
|
117
|
+
return await (0, McpUtils_1.mcpToolToDynamicTool)(tool, (0, McpUtils_1.createCallTool)(tool.name, client, 60000, (errorMessage) => {
|
|
118
|
+
// Error callback similar to official implementation
|
|
119
|
+
const errorNode = new n8n_workflow_1.NodeOperationError(node, errorMessage, { itemIndex });
|
|
120
|
+
// We need to report this error on the AiTool output if possible,
|
|
121
|
+
// or maybe just log it since the createCallTool helper handles return values.
|
|
122
|
+
// The official node uses void this.addOutputData(...)
|
|
123
|
+
try {
|
|
124
|
+
this.addOutputData(n8n_workflow_1.NodeConnectionTypes.AiTool, itemIndex, errorNode);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
console.error('Failed to report error to AiTool output', e);
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
224
130
|
}));
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
constructor(_tools) {
|
|
228
|
-
super();
|
|
229
|
-
this._tools = _tools;
|
|
230
|
-
}
|
|
231
|
-
getTools() {
|
|
232
|
-
return this._tools;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
131
|
+
const toolkit = new McpUtils_1.McpToolkit(dynamicTools);
|
|
132
|
+
// Return the toolkit and the cleanup function
|
|
235
133
|
return {
|
|
236
|
-
response:
|
|
134
|
+
response: toolkit,
|
|
237
135
|
closeFunction: async () => {
|
|
238
|
-
|
|
239
|
-
await
|
|
136
|
+
console.log('[Cleanup] Closing MCP Client');
|
|
137
|
+
await client.close();
|
|
240
138
|
},
|
|
241
139
|
};
|
|
242
140
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { type ISupplyDataFunctions, type IExecuteFunctions } from 'n8n-workflow';
|
|
3
|
+
import { type McpTool } from './McpUtils';
|
|
4
|
+
export interface McpConfig {
|
|
5
|
+
mcpEndpoint: string;
|
|
6
|
+
browserMode: string;
|
|
7
|
+
cdpEndpoint?: string;
|
|
8
|
+
transportType?: 'sse' | 'stdio';
|
|
9
|
+
timeout?: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function connectAndGetTools(_ctx: ISupplyDataFunctions | IExecuteFunctions, config: McpConfig): Promise<{
|
|
12
|
+
client: Client;
|
|
13
|
+
mcpTools: McpTool[];
|
|
14
|
+
error?: {
|
|
15
|
+
error: string;
|
|
16
|
+
message: string;
|
|
17
|
+
};
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.connectAndGetTools = connectAndGetTools;
|
|
4
|
+
const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
5
|
+
const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
|
|
6
|
+
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
7
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
|
|
8
|
+
async function connectAndGetTools(_ctx, config) {
|
|
9
|
+
let client;
|
|
10
|
+
let Transport;
|
|
11
|
+
try {
|
|
12
|
+
const mcpEndpoint = config.mcpEndpoint.trim();
|
|
13
|
+
const isUrl = mcpEndpoint.startsWith('http://') || mcpEndpoint.startsWith('https://');
|
|
14
|
+
if (isUrl) {
|
|
15
|
+
// Check for SSE
|
|
16
|
+
let urlIsSse = false;
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(mcpEndpoint);
|
|
19
|
+
urlIsSse = /(^|\/)sse\/?(\?|#|$)/i.test(url.pathname);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
return {
|
|
23
|
+
client: null,
|
|
24
|
+
mcpTools: [],
|
|
25
|
+
error: { error: 'Invalid URL', message: `Invalid MCP Endpoint URL: ${mcpEndpoint}` }
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (urlIsSse) {
|
|
29
|
+
Transport = new sse_js_1.SSEClientTransport(new URL(mcpEndpoint));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
Transport = new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(mcpEndpoint));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
// Stdio
|
|
37
|
+
Transport = new stdio_js_1.StdioClientTransport({
|
|
38
|
+
command: 'node',
|
|
39
|
+
args: [mcpEndpoint],
|
|
40
|
+
// Pass env if needed, including CDP_URL if we want the server to pick it up via env
|
|
41
|
+
env: {
|
|
42
|
+
...process.env,
|
|
43
|
+
...(config.cdpEndpoint ? { CDP_URL: config.cdpEndpoint } : {}),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
client = new index_js_1.Client({ name: 'n8n-smart-browser-automation', version: '1.0.0' }, { capabilities: {} });
|
|
48
|
+
await client.connect(Transport);
|
|
49
|
+
// List tools
|
|
50
|
+
const result = await client.listTools();
|
|
51
|
+
const tools = (result.tools || []);
|
|
52
|
+
return { client, mcpTools: tools };
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
return {
|
|
56
|
+
client: null,
|
|
57
|
+
mcpTools: [],
|
|
58
|
+
error: { error: 'Connection Failed', message: err.message || String(err) }
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DynamicStructuredTool } from '@langchain/core/tools';
|
|
2
|
+
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
|
+
import { Toolkit } from '@langchain/classic/agents';
|
|
4
|
+
import { type IDataObject } from 'n8n-workflow';
|
|
5
|
+
export type McpToolIncludeMode = 'all' | 'selected' | 'except';
|
|
6
|
+
export interface McpTool {
|
|
7
|
+
name: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
inputSchema?: any;
|
|
10
|
+
}
|
|
11
|
+
export declare function getSelectedTools({ mode, includeTools, excludeTools, tools, }: {
|
|
12
|
+
mode: McpToolIncludeMode;
|
|
13
|
+
includeTools?: string[];
|
|
14
|
+
excludeTools?: string[];
|
|
15
|
+
tools: McpTool[];
|
|
16
|
+
}): McpTool[];
|
|
17
|
+
export declare const getErrorDescriptionFromToolCall: (result: unknown) => string | undefined;
|
|
18
|
+
export declare const createCallTool: (name: string, client: Client, timeout: number, onError: (error: string) => void) => (args: IDataObject) => Promise<{} | null>;
|
|
19
|
+
export declare function mcpToolToDynamicTool(tool: McpTool, onCallTool: (args: any) => Promise<any>): Promise<DynamicStructuredTool>;
|
|
20
|
+
/**
|
|
21
|
+
* Wraps a DynamicStructuredTool to log its execution to the n8n AI Tool output.
|
|
22
|
+
* This ensures the user sees the tool input and output in the n8n UI.
|
|
23
|
+
*/
|
|
24
|
+
export declare function logWrapper(tool: DynamicStructuredTool, nodeCtx: {
|
|
25
|
+
addOutputData: Function;
|
|
26
|
+
addInputData: Function;
|
|
27
|
+
getNextRunIndex?: Function;
|
|
28
|
+
}): DynamicStructuredTool;
|
|
29
|
+
export declare class McpToolkit extends Toolkit {
|
|
30
|
+
tools: DynamicStructuredTool[];
|
|
31
|
+
constructor(tools: DynamicStructuredTool[]);
|
|
32
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.McpToolkit = exports.createCallTool = exports.getErrorDescriptionFromToolCall = void 0;
|
|
4
|
+
exports.getSelectedTools = getSelectedTools;
|
|
5
|
+
exports.mcpToolToDynamicTool = mcpToolToDynamicTool;
|
|
6
|
+
exports.logWrapper = logWrapper;
|
|
7
|
+
const tools_1 = require("@langchain/core/tools");
|
|
8
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
9
|
+
const agents_1 = require("@langchain/classic/agents");
|
|
10
|
+
const zod_1 = require("zod");
|
|
11
|
+
const jsonSchemaToZod_1 = require("../jsonSchemaToZod");
|
|
12
|
+
function getSelectedTools({ mode, includeTools, excludeTools, tools, }) {
|
|
13
|
+
switch (mode) {
|
|
14
|
+
case 'selected': {
|
|
15
|
+
if (!includeTools?.length)
|
|
16
|
+
return tools;
|
|
17
|
+
const include = new Set(includeTools);
|
|
18
|
+
return tools.filter((tool) => include.has(tool.name));
|
|
19
|
+
}
|
|
20
|
+
case 'except': {
|
|
21
|
+
const except = new Set(excludeTools ?? []);
|
|
22
|
+
return tools.filter((tool) => !except.has(tool.name));
|
|
23
|
+
}
|
|
24
|
+
case 'all':
|
|
25
|
+
default:
|
|
26
|
+
return tools;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const getErrorDescriptionFromToolCall = (result) => {
|
|
30
|
+
if (result && typeof result === 'object') {
|
|
31
|
+
if ('content' in result && Array.isArray(result.content)) {
|
|
32
|
+
const errorMessage = result.content.find((content) => content && typeof content === 'object' && typeof content.text === 'string')?.text;
|
|
33
|
+
return errorMessage;
|
|
34
|
+
}
|
|
35
|
+
else if ('toolResult' in result && typeof result.toolResult === 'string') {
|
|
36
|
+
return result.toolResult;
|
|
37
|
+
}
|
|
38
|
+
if ('message' in result && typeof result.message === 'string') {
|
|
39
|
+
return result.message;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
exports.getErrorDescriptionFromToolCall = getErrorDescriptionFromToolCall;
|
|
45
|
+
const createCallTool = (name, client, timeout, onError) => async (args) => {
|
|
46
|
+
let result;
|
|
47
|
+
function handleError(error) {
|
|
48
|
+
const errorDescription = (0, exports.getErrorDescriptionFromToolCall)(error) ?? `Failed to execute tool "${name}"`;
|
|
49
|
+
onError(errorDescription);
|
|
50
|
+
return errorDescription;
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
result = await client.callTool({ name, arguments: args }, types_js_1.CompatibilityCallToolResultSchema, {
|
|
54
|
+
timeout,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return handleError(error);
|
|
59
|
+
}
|
|
60
|
+
if (result.isError) {
|
|
61
|
+
return handleError(result);
|
|
62
|
+
}
|
|
63
|
+
if (result.toolResult !== undefined) {
|
|
64
|
+
return result.toolResult;
|
|
65
|
+
}
|
|
66
|
+
if (result.content !== undefined) {
|
|
67
|
+
return result.content;
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
71
|
+
exports.createCallTool = createCallTool;
|
|
72
|
+
async function mcpToolToDynamicTool(tool, onCallTool) {
|
|
73
|
+
// Use our existing jsonSchemaToZod implementation
|
|
74
|
+
const rawSchema = await (0, jsonSchemaToZod_1.jsonSchemaToZod)(tool.inputSchema);
|
|
75
|
+
// Ensure we always have an object schema for structured tools
|
|
76
|
+
const objectSchema = rawSchema instanceof zod_1.z.ZodObject ? rawSchema : zod_1.z.object({ value: rawSchema });
|
|
77
|
+
return new tools_1.DynamicStructuredTool({
|
|
78
|
+
name: tool.name,
|
|
79
|
+
description: tool.description ?? '',
|
|
80
|
+
schema: objectSchema,
|
|
81
|
+
func: onCallTool,
|
|
82
|
+
metadata: { isFromToolkit: true },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Wraps a DynamicStructuredTool to log its execution to the n8n AI Tool output.
|
|
87
|
+
* This ensures the user sees the tool input and output in the n8n UI.
|
|
88
|
+
*/
|
|
89
|
+
function logWrapper(tool, nodeCtx) {
|
|
90
|
+
// Access internal properties safely via casting if needed
|
|
91
|
+
const toolAny = tool;
|
|
92
|
+
const originalFunc = toolAny.func;
|
|
93
|
+
toolAny.func = async (args) => {
|
|
94
|
+
const toolName = toolAny.name;
|
|
95
|
+
const input = {
|
|
96
|
+
tool: toolName,
|
|
97
|
+
arguments: args,
|
|
98
|
+
};
|
|
99
|
+
let runIndex = 0;
|
|
100
|
+
try {
|
|
101
|
+
// n8n type hack: try to get run index if available
|
|
102
|
+
runIndex = nodeCtx.getNextRunIndex?.() ?? 0;
|
|
103
|
+
}
|
|
104
|
+
catch { }
|
|
105
|
+
// Log Input
|
|
106
|
+
// 2 is the index for NodeConnectionTypes.AiTool usually, check your node definition
|
|
107
|
+
// In SmartBrowserAutomationTools, output[0] = AiTool, output[1] = Main.
|
|
108
|
+
// Wait, NodeConnectionTypes are strings "ai_tool", "main".
|
|
109
|
+
// The `addInputData` returns { index, runIndex }.
|
|
110
|
+
// We need to use the connection name or index.
|
|
111
|
+
// "ai_tool" is typically index 0 if listed first in outputs.
|
|
112
|
+
const { index } = nodeCtx.addInputData('ai_tool', [[{ json: input }]], runIndex);
|
|
113
|
+
try {
|
|
114
|
+
const result = await originalFunc(args);
|
|
115
|
+
const output = {
|
|
116
|
+
tool: toolName,
|
|
117
|
+
result: result,
|
|
118
|
+
};
|
|
119
|
+
// Log Output
|
|
120
|
+
// We wrap in [[ { json: ... } ]] because n8n expects INodeExecutionData[][]
|
|
121
|
+
nodeCtx.addOutputData('ai_tool', index, [[{ json: output }]]);
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// Log Error
|
|
126
|
+
const errorOutput = {
|
|
127
|
+
tool: toolName,
|
|
128
|
+
error: error.message || String(error),
|
|
129
|
+
};
|
|
130
|
+
// Try to log error output
|
|
131
|
+
try {
|
|
132
|
+
nodeCtx.addOutputData('ai_tool', index, [[{ json: errorOutput }]]);
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
console.error(`Failed to log error for tool ${toolName}`, e);
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
return tool;
|
|
141
|
+
}
|
|
142
|
+
class McpToolkit extends agents_1.Toolkit {
|
|
143
|
+
tools;
|
|
144
|
+
constructor(tools) {
|
|
145
|
+
super();
|
|
146
|
+
this.tools = tools;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
exports.McpToolkit = McpToolkit;
|