n8n-nodes-smart-browser-automation 1.6.24 → 1.6.26
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/BrowserSessionManager.js +65 -53
- package/dist/nodes/SmartBrowserAutomation/SmartBrowserAutomation.node.js +56 -19
- package/dist/nodes/SmartBrowserAutomation/SmartBrowserAutomationTools.node.js +74 -172
- 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 +23 -0
- package/dist/nodes/SmartBrowserAutomation/utils/McpUtils.js +91 -0
- package/package.json +1 -1
|
@@ -85,70 +85,82 @@ class BrowserSessionManager {
|
|
|
85
85
|
throw new Error(`Invalid CDP Endpoint: "${trimmedCdp}". CDP Endpoint must be a ws(s) URL (e.g. ws://localhost:9222 or wss://.../devtools/...).`);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
//
|
|
89
|
-
if
|
|
88
|
+
// Check checks if we are already initialized with the same config
|
|
89
|
+
// BUT if we are using CDP, we want to ensure we call the connect tool again
|
|
90
|
+
// to make sure the browser session is active (referencing user request).
|
|
91
|
+
const isConfigMatch = this.isInitialized &&
|
|
90
92
|
this.config.mcpEndpoint === mcpEndpoint &&
|
|
91
93
|
this.config.useCDP === useCDP &&
|
|
92
|
-
this.config.cdpEndpoint === cdpEndpoint
|
|
94
|
+
this.config.cdpEndpoint === cdpEndpoint;
|
|
95
|
+
if (isConfigMatch && !useCDP) {
|
|
96
|
+
// For non-CDP (standard MCP tools), we can safely return cached tools
|
|
93
97
|
return this.tools;
|
|
94
98
|
}
|
|
95
|
-
//
|
|
96
|
-
if (this.mcpClient) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.mcpClient = new index_js_1.Client({ name: 'n8n-browser-automation', version: '1.0.0' }, { capabilities: {} });
|
|
101
|
-
// Determine transport based on endpoint type
|
|
102
|
-
const isUrl = trimmedMcpEndpoint.startsWith('http://') || trimmedMcpEndpoint.startsWith('https://');
|
|
103
|
-
let urlIsSse = false;
|
|
104
|
-
if (isUrl) {
|
|
105
|
-
try {
|
|
106
|
-
urlIsSse = /(^|\/)sse\/?(\?|#|$)/i.test(mcpUrlObj.pathname);
|
|
107
|
-
}
|
|
108
|
-
catch (e) {
|
|
109
|
-
// Should be caught by parseUrl above, but just in case
|
|
110
|
-
throw new Error(`Invalid MCP Endpoint path: "${trimmedMcpEndpoint}".`);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (isUrl) {
|
|
114
|
-
if (urlIsSse) {
|
|
115
|
-
// Connect via SSE
|
|
116
|
-
const { SSEClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/sse.js')));
|
|
117
|
-
this.transport = new SSEClientTransport(new URL(trimmedMcpEndpoint)); // Safe because we parsed it above
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
// Connect via Streamable HTTP
|
|
121
|
-
const { StreamableHTTPClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/streamableHttp.js')));
|
|
122
|
-
this.transport = new StreamableHTTPClientTransport(new URL(trimmedMcpEndpoint)); // Safe because we parsed it above
|
|
99
|
+
// If config changed, or strict strict re-connect is needed (though we try to reuse client if endpoint is same)
|
|
100
|
+
if (this.config.mcpEndpoint !== mcpEndpoint || !this.mcpClient) {
|
|
101
|
+
// Full re-initialization needed
|
|
102
|
+
if (this.mcpClient) {
|
|
103
|
+
await this.close();
|
|
123
104
|
}
|
|
105
|
+
this.isInitialized = false;
|
|
124
106
|
}
|
|
125
107
|
else {
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
this.transport = new StdioClientTransport({
|
|
129
|
-
command: 'node',
|
|
130
|
-
args: [mcpEndpoint],
|
|
131
|
-
env: {
|
|
132
|
-
...process.env,
|
|
133
|
-
...(useCDP && cdpEndpoint ? { CDP_URL: cdpEndpoint } : {}),
|
|
134
|
-
},
|
|
135
|
-
});
|
|
108
|
+
// MCP endpoint is same, just need to re-run tools logic (like CDP connect)
|
|
109
|
+
// We can skip client creation
|
|
136
110
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
//
|
|
111
|
+
if (!this.mcpClient) {
|
|
112
|
+
// Initialize MCP client
|
|
113
|
+
this.mcpClient = new index_js_1.Client({ name: 'n8n-browser-automation', version: '1.0.0' }, { capabilities: {} });
|
|
114
|
+
// Determine transport based on endpoint type
|
|
115
|
+
const isUrl = trimmedMcpEndpoint.startsWith('http://') || trimmedMcpEndpoint.startsWith('https://');
|
|
116
|
+
let urlIsSse = false;
|
|
141
117
|
if (isUrl) {
|
|
142
|
-
|
|
143
|
-
|
|
118
|
+
try {
|
|
119
|
+
urlIsSse = /(^|\/)sse\/?(\?|#|$)/i.test(mcpUrlObj.pathname);
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
throw new Error(`Invalid MCP Endpoint path: "${trimmedMcpEndpoint}".`);
|
|
144
123
|
}
|
|
145
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
146
124
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
125
|
+
if (isUrl) {
|
|
126
|
+
if (urlIsSse) {
|
|
127
|
+
// Connect via SSE
|
|
128
|
+
const { SSEClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/sse.js')));
|
|
129
|
+
this.transport = new SSEClientTransport(new URL(trimmedMcpEndpoint)); // Safe because we parsed it above
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
// Connect via Streamable HTTP
|
|
133
|
+
const { StreamableHTTPClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/streamableHttp.js')));
|
|
134
|
+
this.transport = new StreamableHTTPClientTransport(new URL(trimmedMcpEndpoint)); // Safe because we parsed it above
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
// Connect via Stdio (lazy import so environments that only use SSE don't need stdio deps)
|
|
139
|
+
const { StdioClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/stdio.js')));
|
|
140
|
+
this.transport = new StdioClientTransport({
|
|
141
|
+
command: 'node',
|
|
142
|
+
args: [mcpEndpoint],
|
|
143
|
+
env: {
|
|
144
|
+
...process.env,
|
|
145
|
+
...(useCDP && cdpEndpoint ? { CDP_URL: cdpEndpoint } : {}),
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await this.mcpClient.connect(this.transport);
|
|
151
|
+
// Stability delay for HTTP connections to ensure session is fully open
|
|
152
|
+
if (isUrl) {
|
|
153
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
154
|
+
console.log(`[MCP] Connected to ${trimmedMcpEndpoint}. Waiting for session stabilization...`);
|
|
155
|
+
}
|
|
156
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
this.isInitialized = false;
|
|
161
|
+
const transportType = isUrl ? (urlIsSse ? 'SSE' : 'Streamable HTTP') : 'Stdio';
|
|
162
|
+
throw new Error(`Failed to connect to MCP server via ${transportType} at ${trimmedMcpEndpoint}. Error: ${error.message}`);
|
|
163
|
+
}
|
|
152
164
|
}
|
|
153
165
|
this.isInitialized = true;
|
|
154
166
|
this.config = { mcpEndpoint, useCDP, cdpEndpoint };
|
|
@@ -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,122 +48,93 @@ 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
|
-
//
|
|
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
|
|
153
101
|
const debugJson = {
|
|
154
|
-
mcpEndpoint,
|
|
155
|
-
browserMode:
|
|
156
|
-
cdpEndpoint:
|
|
157
|
-
|
|
158
|
-
|
|
102
|
+
mcpEndpoint: config.mcpEndpoint,
|
|
103
|
+
browserMode: config.browserMode,
|
|
104
|
+
cdpEndpoint: config.cdpEndpoint,
|
|
105
|
+
initializationMode: 'strict_cdp_connect',
|
|
106
|
+
toolCount: exposedTools.length,
|
|
107
|
+
tools: exposedTools.map((t) => ({
|
|
159
108
|
name: t.name,
|
|
160
109
|
description: t.description ?? '',
|
|
161
110
|
})),
|
|
162
111
|
};
|
|
112
|
+
// Using 'this.addOutputData' as per recent fix
|
|
163
113
|
this.addOutputData(n8n_workflow_1.NodeConnectionTypes.Main, 0, [[{ json: debugJson }]]);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
};
|
|
181
|
-
let runIndex = 0;
|
|
182
|
-
try {
|
|
183
|
-
runIndex = ctx.getNextRunIndex?.() ?? 0;
|
|
184
|
-
}
|
|
185
|
-
catch { }
|
|
186
|
-
const { index } = ctx.addInputData(n8n_workflow_1.NodeConnectionTypes.AiTool, [[{ json: input }]], runIndex);
|
|
187
|
-
try {
|
|
188
|
-
console.log(`[AI Agent] Executing tool ${toolName} with args:`, JSON.stringify(args));
|
|
189
|
-
const result = await sessionManager.callTool(toolName, args);
|
|
190
|
-
const output = { tool: toolName, result };
|
|
191
|
-
// Ensure the output is an array of INodeExecutionData
|
|
192
|
-
const executionData = [[{ json: output }]];
|
|
193
|
-
ctx.addOutputData(n8n_workflow_1.NodeConnectionTypes.AiTool, index, executionData);
|
|
194
|
-
return formatMcpToolResult(result);
|
|
195
|
-
}
|
|
196
|
-
catch (e) {
|
|
197
|
-
const errorMsg = e.message || String(e);
|
|
198
|
-
console.error(`[AI Agent] Tool ${toolName} failed:`, errorMsg);
|
|
199
|
-
// Fix: properly format error for addOutputData
|
|
200
|
-
const errorOutput = [[{
|
|
201
|
-
json: {
|
|
202
|
-
tool: toolName,
|
|
203
|
-
error: errorMsg,
|
|
204
|
-
stack: e.stack
|
|
205
|
-
}
|
|
206
|
-
}]];
|
|
207
|
-
// Safely attempt to report error to UI
|
|
208
|
-
try {
|
|
209
|
-
ctx.addOutputData(n8n_workflow_1.NodeConnectionTypes.AiTool, index, errorOutput);
|
|
210
|
-
}
|
|
211
|
-
catch (addError) {
|
|
212
|
-
console.error('[AI Agent] Failed to add error output:', addError);
|
|
213
|
-
}
|
|
214
|
-
// Re-throw so the AI Agent knows it failed
|
|
215
|
-
throw e;
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
metadata: { isFromToolkit: true },
|
|
219
|
-
});
|
|
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
|
+
}));
|
|
220
130
|
}));
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
constructor(_tools) {
|
|
224
|
-
super();
|
|
225
|
-
this._tools = _tools;
|
|
226
|
-
}
|
|
227
|
-
getTools() {
|
|
228
|
-
return this._tools;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
131
|
+
const toolkit = new McpUtils_1.McpToolkit(dynamicTools);
|
|
132
|
+
// Return the toolkit and the cleanup function
|
|
231
133
|
return {
|
|
232
|
-
response:
|
|
134
|
+
response: toolkit,
|
|
233
135
|
closeFunction: async () => {
|
|
234
|
-
|
|
235
|
-
await
|
|
136
|
+
console.log('[Cleanup] Closing MCP Client');
|
|
137
|
+
await client.close();
|
|
236
138
|
},
|
|
237
139
|
};
|
|
238
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,23 @@
|
|
|
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
|
+
export declare class McpToolkit extends Toolkit {
|
|
21
|
+
tools: DynamicStructuredTool[];
|
|
22
|
+
constructor(tools: DynamicStructuredTool[]);
|
|
23
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
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
|
+
const tools_1 = require("@langchain/core/tools");
|
|
7
|
+
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
8
|
+
const agents_1 = require("@langchain/classic/agents");
|
|
9
|
+
const zod_1 = require("zod");
|
|
10
|
+
const jsonSchemaToZod_1 = require("../jsonSchemaToZod");
|
|
11
|
+
function getSelectedTools({ mode, includeTools, excludeTools, tools, }) {
|
|
12
|
+
switch (mode) {
|
|
13
|
+
case 'selected': {
|
|
14
|
+
if (!includeTools?.length)
|
|
15
|
+
return tools;
|
|
16
|
+
const include = new Set(includeTools);
|
|
17
|
+
return tools.filter((tool) => include.has(tool.name));
|
|
18
|
+
}
|
|
19
|
+
case 'except': {
|
|
20
|
+
const except = new Set(excludeTools ?? []);
|
|
21
|
+
return tools.filter((tool) => !except.has(tool.name));
|
|
22
|
+
}
|
|
23
|
+
case 'all':
|
|
24
|
+
default:
|
|
25
|
+
return tools;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const getErrorDescriptionFromToolCall = (result) => {
|
|
29
|
+
if (result && typeof result === 'object') {
|
|
30
|
+
if ('content' in result && Array.isArray(result.content)) {
|
|
31
|
+
const errorMessage = result.content.find((content) => content && typeof content === 'object' && typeof content.text === 'string')?.text;
|
|
32
|
+
return errorMessage;
|
|
33
|
+
}
|
|
34
|
+
else if ('toolResult' in result && typeof result.toolResult === 'string') {
|
|
35
|
+
return result.toolResult;
|
|
36
|
+
}
|
|
37
|
+
if ('message' in result && typeof result.message === 'string') {
|
|
38
|
+
return result.message;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
};
|
|
43
|
+
exports.getErrorDescriptionFromToolCall = getErrorDescriptionFromToolCall;
|
|
44
|
+
const createCallTool = (name, client, timeout, onError) => async (args) => {
|
|
45
|
+
let result;
|
|
46
|
+
function handleError(error) {
|
|
47
|
+
const errorDescription = (0, exports.getErrorDescriptionFromToolCall)(error) ?? `Failed to execute tool "${name}"`;
|
|
48
|
+
onError(errorDescription);
|
|
49
|
+
return errorDescription;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
result = await client.callTool({ name, arguments: args }, types_js_1.CompatibilityCallToolResultSchema, {
|
|
53
|
+
timeout,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return handleError(error);
|
|
58
|
+
}
|
|
59
|
+
if (result.isError) {
|
|
60
|
+
return handleError(result);
|
|
61
|
+
}
|
|
62
|
+
if (result.toolResult !== undefined) {
|
|
63
|
+
return result.toolResult;
|
|
64
|
+
}
|
|
65
|
+
if (result.content !== undefined) {
|
|
66
|
+
return result.content;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
70
|
+
exports.createCallTool = createCallTool;
|
|
71
|
+
async function mcpToolToDynamicTool(tool, onCallTool) {
|
|
72
|
+
// Use our existing jsonSchemaToZod implementation
|
|
73
|
+
const rawSchema = await (0, jsonSchemaToZod_1.jsonSchemaToZod)(tool.inputSchema);
|
|
74
|
+
// Ensure we always have an object schema for structured tools
|
|
75
|
+
const objectSchema = rawSchema instanceof zod_1.z.ZodObject ? rawSchema : zod_1.z.object({ value: rawSchema });
|
|
76
|
+
return new tools_1.DynamicStructuredTool({
|
|
77
|
+
name: tool.name,
|
|
78
|
+
description: tool.description ?? '',
|
|
79
|
+
schema: objectSchema,
|
|
80
|
+
func: onCallTool,
|
|
81
|
+
metadata: { isFromToolkit: true },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
class McpToolkit extends agents_1.Toolkit {
|
|
85
|
+
tools;
|
|
86
|
+
constructor(tools) {
|
|
87
|
+
super();
|
|
88
|
+
this.tools = tools;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.McpToolkit = McpToolkit;
|