n8n-nodes-smart-browser-automation 1.6.23 → 1.6.25
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.
|
@@ -12,6 +12,7 @@ declare class BrowserSessionManager {
|
|
|
12
12
|
private tools;
|
|
13
13
|
private constructor();
|
|
14
14
|
static getInstance(): BrowserSessionManager;
|
|
15
|
+
private parseUrl;
|
|
15
16
|
initialize(mcpEndpoint: string, useCDP: boolean, cdpEndpoint?: string): Promise<MCPTool[]>;
|
|
16
17
|
private getAllTools;
|
|
17
18
|
callTool(toolName: string, toolArgs?: any): Promise<any>;
|
|
@@ -48,6 +48,17 @@ class BrowserSessionManager {
|
|
|
48
48
|
}
|
|
49
49
|
return BrowserSessionManager.instance;
|
|
50
50
|
}
|
|
51
|
+
/*
|
|
52
|
+
* Safe URL parser helper
|
|
53
|
+
*/
|
|
54
|
+
parseUrl(url, description) {
|
|
55
|
+
try {
|
|
56
|
+
return new URL(url);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
throw new Error(`Invalid ${description}: "${url}". Please ensure it is a valid URL (e.g. starting with http:// or https://).`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
51
62
|
async initialize(mcpEndpoint, useCDP, cdpEndpoint) {
|
|
52
63
|
// Validate endpoint inputs early to avoid confusing runtime errors
|
|
53
64
|
let trimmedMcpEndpoint = String(mcpEndpoint ?? '').trim();
|
|
@@ -55,10 +66,11 @@ class BrowserSessionManager {
|
|
|
55
66
|
throw new Error('MCP Endpoint is required');
|
|
56
67
|
}
|
|
57
68
|
// Be forgiving: if user passes host:port without scheme, assume http://
|
|
58
|
-
// Examples: localhost:3000, 127.0.0.1:3000, example.com:8080
|
|
59
69
|
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(trimmedMcpEndpoint)) {
|
|
60
70
|
trimmedMcpEndpoint = `http://${trimmedMcpEndpoint}`;
|
|
61
71
|
}
|
|
72
|
+
// Validate URL format immediately
|
|
73
|
+
const mcpUrlObj = this.parseUrl(trimmedMcpEndpoint, 'MCP Endpoint');
|
|
62
74
|
if (/^wss?:\/\//i.test(trimmedMcpEndpoint)) {
|
|
63
75
|
throw new Error(`Invalid MCP Endpoint: "${trimmedMcpEndpoint}". ` +
|
|
64
76
|
'MCP Endpoint must be an http(s) URL (for SSE or Streamable HTTP). ' +
|
|
@@ -73,69 +85,82 @@ class BrowserSessionManager {
|
|
|
73
85
|
throw new Error(`Invalid CDP Endpoint: "${trimmedCdp}". CDP Endpoint must be a ws(s) URL (e.g. ws://localhost:9222 or wss://.../devtools/...).`);
|
|
74
86
|
}
|
|
75
87
|
}
|
|
76
|
-
//
|
|
77
|
-
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 &&
|
|
78
92
|
this.config.mcpEndpoint === mcpEndpoint &&
|
|
79
93
|
this.config.useCDP === useCDP &&
|
|
80
|
-
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
|
|
81
97
|
return this.tools;
|
|
82
98
|
}
|
|
83
|
-
//
|
|
84
|
-
if (this.mcpClient) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
this.mcpClient = new index_js_1.Client({ name: 'n8n-browser-automation', version: '1.0.0' }, { capabilities: {} });
|
|
89
|
-
// Determine transport based on endpoint type
|
|
90
|
-
const isUrl = trimmedMcpEndpoint.startsWith('http://') || trimmedMcpEndpoint.startsWith('https://');
|
|
91
|
-
let urlIsSse = false;
|
|
92
|
-
if (isUrl) {
|
|
93
|
-
try {
|
|
94
|
-
urlIsSse = /(^|\/)sse\/?(\?|#|$)/i.test(new URL(trimmedMcpEndpoint).pathname);
|
|
95
|
-
}
|
|
96
|
-
catch (e) {
|
|
97
|
-
throw new Error(`Invalid MCP Endpoint: "${trimmedMcpEndpoint}". MCP Endpoint must be a valid http(s) URL (or a local file path for stdio).`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
if (isUrl) {
|
|
101
|
-
if (urlIsSse) {
|
|
102
|
-
// Connect via SSE
|
|
103
|
-
const { SSEClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/sse.js')));
|
|
104
|
-
this.transport = new SSEClientTransport(new URL(trimmedMcpEndpoint));
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
// Connect via Streamable HTTP
|
|
108
|
-
const { StreamableHTTPClientTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/client/streamableHttp.js')));
|
|
109
|
-
this.transport = new StreamableHTTPClientTransport(new URL(trimmedMcpEndpoint));
|
|
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();
|
|
110
104
|
}
|
|
105
|
+
this.isInitialized = false;
|
|
111
106
|
}
|
|
112
107
|
else {
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
this.transport = new StdioClientTransport({
|
|
116
|
-
command: 'node',
|
|
117
|
-
args: [mcpEndpoint],
|
|
118
|
-
env: {
|
|
119
|
-
...process.env,
|
|
120
|
-
...(useCDP && cdpEndpoint ? { CDP_URL: cdpEndpoint } : {}),
|
|
121
|
-
},
|
|
122
|
-
});
|
|
108
|
+
// MCP endpoint is same, just need to re-run tools logic (like CDP connect)
|
|
109
|
+
// We can skip client creation
|
|
123
110
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
//
|
|
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;
|
|
128
117
|
if (isUrl) {
|
|
129
|
-
|
|
130
|
-
|
|
118
|
+
try {
|
|
119
|
+
urlIsSse = /(^|\/)sse\/?(\?|#|$)/i.test(mcpUrlObj.pathname);
|
|
120
|
+
}
|
|
121
|
+
catch (e) {
|
|
122
|
+
throw new Error(`Invalid MCP Endpoint path: "${trimmedMcpEndpoint}".`);
|
|
131
123
|
}
|
|
132
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
133
124
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|
|
139
164
|
}
|
|
140
165
|
this.isInitialized = true;
|
|
141
166
|
this.config = { mcpEndpoint, useCDP, cdpEndpoint };
|
|
@@ -86,8 +86,8 @@ class SmartBrowserAutomationTools {
|
|
|
86
86
|
name: 'Browser Automation Tools',
|
|
87
87
|
},
|
|
88
88
|
inputs: [],
|
|
89
|
-
outputs: [n8n_workflow_1.NodeConnectionTypes.AiTool],
|
|
90
|
-
outputNames: ['Tools'],
|
|
89
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.AiTool, n8n_workflow_1.NodeConnectionTypes.Main],
|
|
90
|
+
outputNames: ['Tools', 'Debug'],
|
|
91
91
|
icon: 'file:smartBrowserAutomation.svg',
|
|
92
92
|
credentials: [
|
|
93
93
|
{
|
|
@@ -140,7 +140,7 @@ class SmartBrowserAutomationTools {
|
|
|
140
140
|
const e = error;
|
|
141
141
|
const mode = useCDP ? 'cdp' : 'launch';
|
|
142
142
|
const details = `mcp=${mcpEndpoint} | mode=${mode}${useCDP ? ` | cdp=${cdpUrl || 'unset'}` : ''}`;
|
|
143
|
-
throw new n8n_workflow_1.NodeOperationError(node, `Failed to connect to MCP
|
|
143
|
+
throw new n8n_workflow_1.NodeOperationError(node, `Failed to connect to MCP. ${details}. Original error: ${e.message}`, {
|
|
144
144
|
itemIndex,
|
|
145
145
|
description: details,
|
|
146
146
|
});
|
|
@@ -149,10 +149,26 @@ class SmartBrowserAutomationTools {
|
|
|
149
149
|
if (!mcpTools.length) {
|
|
150
150
|
throw new n8n_workflow_1.NodeOperationError(node, 'MCP Server returned no tools', { itemIndex });
|
|
151
151
|
}
|
|
152
|
+
// Filter out 'browser_connect_cdp' from the tools list if we are managing it automatically
|
|
153
|
+
// This prevents the AI from trying to connect manually and confusing the state
|
|
154
|
+
const exposedTools = mcpTools.filter((t) => t.name !== 'browser_connect_cdp');
|
|
155
|
+
// Emit a debug item on the Main output so users can see endpoints and tool list in the UI
|
|
156
|
+
const debugJson = {
|
|
157
|
+
mcpEndpoint,
|
|
158
|
+
browserMode: credentials.browserMode,
|
|
159
|
+
cdpEndpoint: useCDP ? cdpUrl : undefined,
|
|
160
|
+
initializationMode: 'strict_cdp_connect',
|
|
161
|
+
toolCount: exposedTools.length,
|
|
162
|
+
tools: exposedTools.map((t) => ({
|
|
163
|
+
name: t.name,
|
|
164
|
+
description: t.description ?? '',
|
|
165
|
+
})),
|
|
166
|
+
};
|
|
167
|
+
this.addOutputData(n8n_workflow_1.NodeConnectionTypes.Main, 0, [[{ json: debugJson }]]);
|
|
152
168
|
const { DynamicStructuredTool } = await importDynamicStructuredTool();
|
|
153
169
|
const { Toolkit } = await importToolkitBase();
|
|
154
170
|
const ctx = this;
|
|
155
|
-
const tools = await Promise.all(
|
|
171
|
+
const tools = await Promise.all(exposedTools.map(async (tool) => {
|
|
156
172
|
const schema = await (0, jsonSchemaToZod_1.jsonSchemaToZod)(tool.inputSchema);
|
|
157
173
|
const toolName = tool.name;
|
|
158
174
|
const toolDescription = (tool.description ?? '');
|
|
@@ -173,13 +189,33 @@ class SmartBrowserAutomationTools {
|
|
|
173
189
|
catch { }
|
|
174
190
|
const { index } = ctx.addInputData(n8n_workflow_1.NodeConnectionTypes.AiTool, [[{ json: input }]], runIndex);
|
|
175
191
|
try {
|
|
192
|
+
console.log(`[AI Agent] Executing tool ${toolName} with args:`, JSON.stringify(args));
|
|
176
193
|
const result = await sessionManager.callTool(toolName, args);
|
|
177
194
|
const output = { tool: toolName, result };
|
|
178
|
-
|
|
195
|
+
// Ensure the output is an array of INodeExecutionData
|
|
196
|
+
const executionData = [[{ json: output }]];
|
|
197
|
+
ctx.addOutputData(n8n_workflow_1.NodeConnectionTypes.AiTool, index, executionData);
|
|
179
198
|
return formatMcpToolResult(result);
|
|
180
199
|
}
|
|
181
200
|
catch (e) {
|
|
182
|
-
|
|
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
|
|
183
219
|
throw e;
|
|
184
220
|
}
|
|
185
221
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-smart-browser-automation",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.25",
|
|
4
4
|
"description": "n8n node for AI-driven browser automation using MCP",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -63,4 +63,4 @@
|
|
|
63
63
|
"@modelcontextprotocol/sdk": "1.17.0",
|
|
64
64
|
"zod": "3.25.76"
|
|
65
65
|
}
|
|
66
|
-
}
|
|
66
|
+
}
|