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.
@@ -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
- // Only initialize if not already done or config changed
89
- if (this.isInitialized &&
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
- // Close existing session if config changed
96
- if (this.mcpClient) {
97
- await this.close();
98
- }
99
- // Initialize MCP client
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
- // Connect via Stdio (lazy import so environments that only use SSE don't need stdio deps)
127
- const { StdioClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/stdio.js')));
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
- try {
138
- await this.mcpClient.connect(this.transport);
139
- // Stability delay for HTTP connections to ensure session is fully open
140
- // Remote servers often need a moment to register the session before accepting POST calls
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
- if (process.env.NODE_ENV !== 'production') {
143
- console.log(`[MCP] Connected to ${trimmedMcpEndpoint}. Waiting for session stabilization...`);
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
- catch (error) {
149
- this.isInitialized = false;
150
- const transportType = isUrl ? (urlIsSse ? 'SSE' : 'Streamable HTTP') : 'Stdio';
151
- throw new Error(`Failed to connect to MCP server via ${transportType} at ${trimmedMcpEndpoint}. Error: ${error.message}`);
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
- const sessionManager = BrowserSessionManager_1.default.getInstance();
101
- await sessionManager.initialize(credentials.mcpEndpoint, false, '');
102
- const tools = await sessionManager.listTools();
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
- await sessionManager.initialize(credentials.mcpEndpoint, false, '');
127
- const tools = await sessionManager.listTools();
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
- if (!sessionManager.isReady()) {
195
- const cdpUrl = cdpOverride || credentials.cdpEndpoint || '';
196
- await sessionManager.initialize(credentials.mcpEndpoint, !!cdpUrl, cdpUrl);
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 sessionManager.close();
212
- await sessionManager.initialize(credentials.mcpEndpoint, true, endpoint);
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 = await sessionManager.listTools();
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 sessionManager.callTool(toolName, toolParams);
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 BrowserSessionManager_1 = __importDefault(require("./BrowserSessionManager"));
44
- const jsonSchemaToZod_1 = require("./jsonSchemaToZod");
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 useCDP = credentials.browserMode === 'cdp';
124
- const cdpUrl = (cdpOverride || credentials.cdpEndpoint || '').trim();
125
- const mcpEndpoint = (mcpOverride || credentials.mcpEndpoint || '').trim();
126
- if (!mcpEndpoint) {
127
- throw new n8n_workflow_1.NodeOperationError(node, 'MCP Endpoint is required in credentials', {
128
- itemIndex,
129
- description: 'Configure MCP Endpoint in Smart Browser Automation credentials (e.g. http://localhost:3000 or https://host/sse)',
130
- });
131
- }
132
- // Basic pre-validation so users see a clear error in the UI
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
- catch (error) {
140
- const e = error;
141
- const mode = useCDP ? 'cdp' : 'launch';
142
- const details = `mcp=${mcpEndpoint} | mode=${mode}${useCDP ? ` | cdp=${cdpUrl || 'unset'}` : ''}`;
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
- const mcpTools = await sessionManager.listTools();
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
- // Emit a debug item on the Main output so users can see endpoints and tool list in the UI
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: credentials.browserMode,
156
- cdpEndpoint: useCDP ? cdpUrl : undefined,
157
- toolCount: mcpTools.length,
158
- tools: mcpTools.map((t) => ({
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
- const { DynamicStructuredTool } = await importDynamicStructuredTool();
165
- const { Toolkit } = await importToolkitBase();
166
- const ctx = this;
167
- const tools = await Promise.all(mcpTools.map(async (tool) => {
168
- const schema = await (0, jsonSchemaToZod_1.jsonSchemaToZod)(tool.inputSchema);
169
- const toolName = tool.name;
170
- const toolDescription = (tool.description ?? '');
171
- return new DynamicStructuredTool({
172
- name: toolName,
173
- description: toolDescription,
174
- schema,
175
- func: async (args) => {
176
- // Make tool calls visible in n8n execution UI
177
- const input = {
178
- tool: toolName,
179
- arguments: args,
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
- class SmartBrowserAutomationToolkit extends Toolkit {
222
- _tools;
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: new SmartBrowserAutomationToolkit(tools),
134
+ response: toolkit,
233
135
  closeFunction: async () => {
234
- // Called by n8n when Agent execution finishes
235
- await sessionManager.close();
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-smart-browser-automation",
3
- "version": "1.6.24",
3
+ "version": "1.6.26",
4
4
  "description": "n8n node for AI-driven browser automation using MCP",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",