n8n-nodes-browser-smart-automation 0.1.2 → 0.1.5

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.
Files changed (59) hide show
  1. package/dist/McpClientTool/McpClientTool.node.js +2 -13
  2. package/dist/McpClientTool/McpClientTool.node.js.map +1 -1
  3. package/dist/McpClientTool/utils.js +1 -1
  4. package/dist/McpClientTool/utils.js.map +1 -1
  5. package/dist/McpTrigger/McpTrigger.node.js +1 -1
  6. package/dist/McpTrigger/McpTrigger.node.js.map +1 -1
  7. package/dist/shared/N8nBinaryLoader.js +203 -0
  8. package/dist/shared/N8nBinaryLoader.js.map +1 -0
  9. package/dist/shared/N8nJsonLoader.js +89 -0
  10. package/dist/shared/N8nJsonLoader.js.map +1 -0
  11. package/dist/shared/N8nTool.js +106 -0
  12. package/dist/shared/N8nTool.js.map +1 -0
  13. package/dist/shared/embeddingInputValidation.js +55 -0
  14. package/dist/shared/embeddingInputValidation.js.map +1 -0
  15. package/dist/shared/helpers.js +220 -13
  16. package/dist/shared/helpers.js.map +1 -1
  17. package/dist/shared/httpProxyAgent.js +40 -2
  18. package/dist/shared/httpProxyAgent.js.map +1 -1
  19. package/dist/shared/logWrapper.js +347 -2
  20. package/dist/shared/logWrapper.js.map +1 -1
  21. package/dist/shared/schemaParsing.js +47 -4
  22. package/dist/shared/schemaParsing.js.map +1 -1
  23. package/dist/shared/sharedFields.js +142 -7
  24. package/dist/shared/sharedFields.js.map +1 -1
  25. package/dist/shared/typesN8nTool.js +17 -0
  26. package/dist/shared/typesN8nTool.js.map +1 -0
  27. package/dist/shared/utils.js +1 -1
  28. package/dist/shared/utils.js.map +1 -1
  29. package/package.json +25 -7
  30. package/jest.config.js +0 -24
  31. package/nodes/McpClient/McpClient.node.ts +0 -327
  32. package/nodes/McpClient/__test__/McpClient.node.test.ts +0 -221
  33. package/nodes/McpClient/__test__/utils.test.ts +0 -302
  34. package/nodes/McpClient/listSearch.ts +0 -48
  35. package/nodes/McpClient/resourceMapping.ts +0 -48
  36. package/nodes/McpClient/utils.ts +0 -281
  37. package/nodes/McpClientTool/McpClientTool.node.ts +0 -468
  38. package/nodes/McpClientTool/__test__/McpClientTool.node.test.ts +0 -730
  39. package/nodes/McpClientTool/loadOptions.ts +0 -45
  40. package/nodes/McpClientTool/types.ts +0 -1
  41. package/nodes/McpClientTool/utils.ts +0 -116
  42. package/nodes/McpTrigger/FlushingTransport.ts +0 -61
  43. package/nodes/McpTrigger/McpServer.ts +0 -317
  44. package/nodes/McpTrigger/McpTrigger.node.ts +0 -204
  45. package/nodes/McpTrigger/__test__/FlushingTransport.test.ts +0 -102
  46. package/nodes/McpTrigger/__test__/McpServer.test.ts +0 -532
  47. package/nodes/McpTrigger/__test__/McpTrigger.node.test.ts +0 -171
  48. package/nodes/shared/__test__/utils.test.ts +0 -318
  49. package/nodes/shared/descriptions.ts +0 -65
  50. package/nodes/shared/helpers.ts +0 -31
  51. package/nodes/shared/httpProxyAgent.ts +0 -11
  52. package/nodes/shared/logWrapper.ts +0 -13
  53. package/nodes/shared/schemaParsing.ts +0 -9
  54. package/nodes/shared/sharedFields.ts +0 -20
  55. package/nodes/shared/types.ts +0 -12
  56. package/nodes/shared/utils.ts +0 -296
  57. package/officail/package.json +0 -255
  58. package/tsconfig.json +0 -32
  59. package/tsup.config.ts +0 -16
@@ -1,45 +0,0 @@
1
- import { type ILoadOptionsFunctions, type INodePropertyOptions } from 'n8n-workflow';
2
-
3
- import type { McpAuthenticationOption, McpServerTransport } from '../shared/types';
4
- import {
5
- connectMcpClient,
6
- getAllTools,
7
- getAuthHeaders,
8
- mapToNodeOperationError,
9
- tryRefreshOAuth2Token,
10
- } from '../shared/utils';
11
-
12
- export async function getTools(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
13
- const authentication = this.getNodeParameter('authentication') as McpAuthenticationOption;
14
- const node = this.getNode();
15
- let serverTransport: McpServerTransport;
16
- let endpointUrl: string;
17
- if (node.typeVersion === 1) {
18
- serverTransport = 'sse';
19
- endpointUrl = this.getNodeParameter('sseEndpoint') as string;
20
- } else {
21
- serverTransport = this.getNodeParameter('serverTransport') as McpServerTransport;
22
- endpointUrl = this.getNodeParameter('endpointUrl') as string;
23
- }
24
- const { headers } = await getAuthHeaders(this, authentication);
25
- const client = await connectMcpClient({
26
- serverTransport,
27
- endpointUrl,
28
- headers,
29
- name: node.type,
30
- version: node.typeVersion,
31
- onUnauthorized: async (headers) => await tryRefreshOAuth2Token(this, authentication, headers),
32
- });
33
-
34
- if (!client.ok) {
35
- throw mapToNodeOperationError(node, client.error);
36
- }
37
-
38
- const tools = await getAllTools(client.result);
39
- return tools.map((tool) => ({
40
- name: tool.name,
41
- value: tool.name,
42
- description: tool.description,
43
- inputSchema: tool.inputSchema,
44
- }));
45
- }
@@ -1 +0,0 @@
1
- export type McpToolIncludeMode = 'all' | 'selected' | 'except';
@@ -1,116 +0,0 @@
1
- import { DynamicStructuredTool, type DynamicStructuredToolInput } from '@langchain/core/tools';
2
- import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
- import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
4
- import { Toolkit } from '@langchain/classic/agents';
5
- import { type IDataObject } from 'n8n-workflow';
6
- import { z } from 'zod';
7
-
8
- import { convertJsonSchemaToZod } from '@utils/schemaParsing';
9
-
10
- import type { McpToolIncludeMode } from './types';
11
- import type { McpTool } from '../shared/types';
12
-
13
- export function getSelectedTools({
14
- mode,
15
- includeTools,
16
- excludeTools,
17
- tools,
18
- }: {
19
- mode: McpToolIncludeMode;
20
- includeTools?: string[];
21
- excludeTools?: string[];
22
- tools: McpTool[];
23
- }) {
24
- switch (mode) {
25
- case 'selected': {
26
- if (!includeTools?.length) return tools;
27
- const include = new Set(includeTools);
28
- return tools.filter((tool) => include.has(tool.name));
29
- }
30
- case 'except': {
31
- const except = new Set(excludeTools ?? []);
32
- return tools.filter((tool) => !except.has(tool.name));
33
- }
34
- case 'all':
35
- default:
36
- return tools;
37
- }
38
- }
39
-
40
- export const getErrorDescriptionFromToolCall = (result: unknown): string | undefined => {
41
- if (result && typeof result === 'object') {
42
- if ('content' in result && Array.isArray(result.content)) {
43
- const errorMessage = (result.content as Array<{ type: 'text'; text: string }>).find(
44
- (content) => content && typeof content === 'object' && typeof content.text === 'string',
45
- )?.text;
46
- return errorMessage;
47
- } else if ('toolResult' in result && typeof result.toolResult === 'string') {
48
- return result.toolResult;
49
- }
50
- if ('message' in result && typeof result.message === 'string') {
51
- return result.message;
52
- }
53
- }
54
-
55
- return undefined;
56
- };
57
-
58
- export const createCallTool =
59
- (name: string, client: Client, timeout: number, onError: (error: string) => void) =>
60
- async (args: IDataObject) => {
61
- let result: Awaited<ReturnType<Client['callTool']>>;
62
-
63
- function handleError(error: unknown) {
64
- const errorDescription =
65
- getErrorDescriptionFromToolCall(error) ?? `Failed to execute tool "${name}"`;
66
- onError(errorDescription);
67
- return errorDescription;
68
- }
69
-
70
- try {
71
- result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema, {
72
- timeout,
73
- });
74
- } catch (error) {
75
- return handleError(error);
76
- }
77
-
78
- if (result.isError) {
79
- return handleError(result);
80
- }
81
-
82
- if (result.toolResult !== undefined) {
83
- return result.toolResult;
84
- }
85
-
86
- if (result.content !== undefined) {
87
- return result.content;
88
- }
89
-
90
- return result;
91
- };
92
-
93
- export function mcpToolToDynamicTool(
94
- tool: McpTool,
95
- onCallTool: DynamicStructuredToolInput['func'],
96
- ): DynamicStructuredTool {
97
- const rawSchema = convertJsonSchemaToZod(tool.inputSchema);
98
-
99
- // Ensure we always have an object schema for structured tools
100
- const objectSchema =
101
- rawSchema instanceof z.ZodObject ? rawSchema : z.object({ value: rawSchema });
102
-
103
- return new DynamicStructuredTool({
104
- name: tool.name,
105
- description: tool.description ?? '',
106
- schema: objectSchema,
107
- func: onCallTool,
108
- metadata: { isFromToolkit: true },
109
- });
110
- }
111
-
112
- export class McpToolkit extends Toolkit {
113
- constructor(public tools: DynamicStructuredTool[]) {
114
- super();
115
- }
116
- }
@@ -1,61 +0,0 @@
1
- import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
2
- import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
- import type { StreamableHTTPServerTransportOptions } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
- import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
5
- import type { Response } from 'express';
6
- import type { IncomingMessage, ServerResponse } from 'http';
7
-
8
- export type CompressionResponse = Response & {
9
- /**
10
- * `flush()` is defined in the compression middleware.
11
- * This is necessary because the compression middleware sometimes waits
12
- * for a certain amount of data before sending the data to the client
13
- */
14
- flush: () => void;
15
- };
16
-
17
- export class FlushingSSEServerTransport extends SSEServerTransport {
18
- constructor(
19
- _endpoint: string,
20
- private response: CompressionResponse,
21
- ) {
22
- super(_endpoint, response);
23
- }
24
-
25
- async send(message: JSONRPCMessage): Promise<void> {
26
- await super.send(message);
27
- this.response.flush();
28
- }
29
-
30
- async handleRequest(
31
- req: IncomingMessage,
32
- resp: ServerResponse,
33
- message: IncomingMessage,
34
- ): Promise<void> {
35
- await super.handlePostMessage(req, resp, message);
36
- this.response.flush();
37
- }
38
- }
39
-
40
- export class FlushingStreamableHTTPTransport extends StreamableHTTPServerTransport {
41
- private response: CompressionResponse;
42
-
43
- constructor(options: StreamableHTTPServerTransportOptions, response: CompressionResponse) {
44
- super(options);
45
- this.response = response;
46
- }
47
-
48
- async send(message: JSONRPCMessage): Promise<void> {
49
- await super.send(message);
50
- this.response.flush();
51
- }
52
-
53
- async handleRequest(
54
- req: IncomingMessage,
55
- resp: ServerResponse,
56
- parsedBody?: unknown,
57
- ): Promise<void> {
58
- await super.handleRequest(req, resp, parsedBody);
59
- this.response.flush();
60
- }
61
- }
@@ -1,317 +0,0 @@
1
- import type { Tool } from '@langchain/core/tools';
2
- import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
- import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
4
- import type {
5
- JSONRPCMessage,
6
- ServerRequest,
7
- ServerNotification,
8
- } from '@modelcontextprotocol/sdk/types.js';
9
- import {
10
- JSONRPCMessageSchema,
11
- ListToolsRequestSchema,
12
- CallToolRequestSchema,
13
- } from '@modelcontextprotocol/sdk/types.js';
14
- import { randomUUID } from 'crypto';
15
- import type * as express from 'express';
16
- import type { IncomingMessage } from 'http';
17
- import { jsonParse, OperationalError, type Logger } from 'n8n-workflow';
18
- import { zodToJsonSchema } from 'zod-to-json-schema';
19
-
20
- import { FlushingSSEServerTransport, FlushingStreamableHTTPTransport } from './FlushingTransport';
21
- import type { CompressionResponse } from './FlushingTransport';
22
-
23
- /**
24
- * Parses the JSONRPC message and checks whether the method used was a tool
25
- * call. This is necessary in order to not have executions for listing tools
26
- * and other commands sent by the MCP client
27
- */
28
- function wasToolCall(body: string) {
29
- try {
30
- const message: unknown = JSON.parse(body);
31
- const parsedMessage: JSONRPCMessage = JSONRPCMessageSchema.parse(message);
32
- return (
33
- 'method' in parsedMessage &&
34
- 'id' in parsedMessage &&
35
- parsedMessage?.method === CallToolRequestSchema.shape.method.value
36
- );
37
- } catch {
38
- return false;
39
- }
40
- }
41
-
42
- /**
43
- * Extracts the request ID from a JSONRPC message (for example for tool calls).
44
- * Returns undefined if the message doesn't have an ID (for example on a tool list request)
45
- *
46
- */
47
- function getRequestId(message: unknown): string | undefined {
48
- try {
49
- const parsedMessage: JSONRPCMessage = JSONRPCMessageSchema.parse(message);
50
- return 'id' in parsedMessage ? String(parsedMessage.id) : undefined;
51
- } catch {
52
- return undefined;
53
- }
54
- }
55
-
56
- /**
57
- * This singleton is shared across the instance, making sure it is the one
58
- * keeping account of MCP servers.
59
- * It needs to stay in memory to keep track of the long-lived connections.
60
- * It requires a logger at first creation to set everything up.
61
- */
62
- export class McpServerManager {
63
- static #instance: McpServerManager;
64
-
65
- servers: { [sessionId: string]: Server } = {};
66
-
67
- transports: {
68
- [sessionId: string]: FlushingSSEServerTransport | FlushingStreamableHTTPTransport;
69
- } = {};
70
-
71
- private tools: { [sessionId: string]: Tool[] } = {};
72
-
73
- private resolveFunctions: { [callId: string]: CallableFunction } = {};
74
-
75
- logger: Logger;
76
-
77
- private constructor(logger: Logger) {
78
- this.logger = logger;
79
- this.logger.debug('MCP Server created');
80
- }
81
-
82
- static instance(logger: Logger): McpServerManager {
83
- if (!McpServerManager.#instance) {
84
- McpServerManager.#instance = new McpServerManager(logger);
85
- logger.debug('Created singleton MCP manager');
86
- }
87
-
88
- return McpServerManager.#instance;
89
- }
90
-
91
- async createServerWithSSETransport(
92
- serverName: string,
93
- postUrl: string,
94
- resp: CompressionResponse,
95
- ): Promise<void> {
96
- const server = new Server(
97
- {
98
- name: serverName,
99
- version: '0.1.0',
100
- },
101
- {
102
- capabilities: {
103
- tools: {},
104
- },
105
- },
106
- );
107
-
108
- const transport = new FlushingSSEServerTransport(postUrl, resp);
109
-
110
- this.setUpHandlers(server);
111
-
112
- const sessionId = transport.sessionId;
113
- this.transports[sessionId] = transport;
114
- this.servers[sessionId] = server;
115
-
116
- resp.on('close', async () => {
117
- this.logger.debug(`Deleting transport for ${sessionId}`);
118
- delete this.tools[sessionId];
119
- delete this.transports[sessionId];
120
- delete this.servers[sessionId];
121
- });
122
- await server.connect(transport);
123
-
124
- // Make sure we flush the compression middleware, so that it's not waiting for more content to be added to the buffer
125
- if (resp.flush) {
126
- resp.flush();
127
- }
128
- }
129
-
130
- getSessionId(req: express.Request): string | undefined {
131
- // Session ID can be passed either as a query parameter (SSE transport)
132
- // or in the header (StreamableHTTP transport).
133
- return (req.query.sessionId ?? req.headers['mcp-session-id']) as string | undefined;
134
- }
135
-
136
- getTransport(
137
- sessionId: string,
138
- ): FlushingSSEServerTransport | FlushingStreamableHTTPTransport | undefined {
139
- return this.transports[sessionId];
140
- }
141
-
142
- async createServerWithStreamableHTTPTransport(
143
- serverName: string,
144
- resp: CompressionResponse,
145
- req?: express.Request,
146
- ): Promise<void> {
147
- const server = new Server(
148
- {
149
- name: serverName,
150
- version: '0.1.0',
151
- },
152
- {
153
- capabilities: {
154
- tools: {},
155
- },
156
- },
157
- );
158
-
159
- const transport = new FlushingStreamableHTTPTransport(
160
- {
161
- sessionIdGenerator: () => randomUUID(),
162
- onsessioninitialized: (sessionId) => {
163
- this.logger.debug(`New session initialized: ${sessionId}`);
164
- transport.onclose = () => {
165
- this.logger.debug(`Deleting transport for ${sessionId}`);
166
- delete this.tools[sessionId];
167
- delete this.transports[sessionId];
168
- delete this.servers[sessionId];
169
- };
170
- this.transports[sessionId] = transport;
171
- this.servers[sessionId] = server;
172
- },
173
- },
174
- resp,
175
- );
176
-
177
- this.setUpHandlers(server);
178
-
179
- await server.connect(transport);
180
-
181
- await transport.handleRequest(req as IncomingMessage, resp, req?.body);
182
- if (resp.flush) {
183
- resp.flush();
184
- }
185
- }
186
-
187
- async handlePostMessage(req: express.Request, resp: CompressionResponse, connectedTools: Tool[]) {
188
- // Session ID can be passed either as a query parameter (SSE transport)
189
- // or in the header (StreamableHTTP transport).
190
- const sessionId = this.getSessionId(req);
191
- const transport = this.getTransport(sessionId as string);
192
- if (sessionId && transport) {
193
- // We need to add a promise here because the `handlePostMessage` will send something to the
194
- // MCP Server, that will run in a different context. This means that the return will happen
195
- // almost immediately, and will lead to marking the sub-node as "running" in the final execution
196
- const message = jsonParse(req.rawBody.toString());
197
- const messageId = getRequestId(message);
198
- // Use session & message ID if available, otherwise fall back to sessionId
199
- const callId = messageId ? `${sessionId}_${messageId}` : sessionId;
200
- this.tools[sessionId] = connectedTools;
201
-
202
- try {
203
- await new Promise(async (resolve) => {
204
- this.resolveFunctions[callId] = resolve;
205
- await transport.handleRequest(req, resp, message as IncomingMessage);
206
- });
207
- } finally {
208
- delete this.resolveFunctions[callId];
209
- }
210
- } else {
211
- this.logger.warn(`No transport found for session ${sessionId}`);
212
- resp.status(401).send('No transport found for sessionId');
213
- }
214
-
215
- if (resp.flush) {
216
- resp.flush();
217
- }
218
-
219
- return wasToolCall(req.rawBody.toString());
220
- }
221
-
222
- async handleDeleteRequest(req: express.Request, resp: CompressionResponse) {
223
- const sessionId = this.getSessionId(req);
224
-
225
- if (!sessionId) {
226
- resp.status(400).send('No sessionId provided');
227
- return;
228
- }
229
-
230
- const transport = this.getTransport(sessionId);
231
-
232
- if (transport) {
233
- if (transport instanceof FlushingStreamableHTTPTransport) {
234
- await transport.handleRequest(req, resp);
235
- return;
236
- } else {
237
- // For SSE transport, we don't support DELETE requests
238
- resp.status(405).send('Method Not Allowed');
239
- return;
240
- }
241
- }
242
-
243
- resp.status(404).send('Session not found');
244
- }
245
-
246
- setUpHandlers(server: Server) {
247
- server.setRequestHandler(
248
- ListToolsRequestSchema,
249
- async (_, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
250
- if (!extra.sessionId) {
251
- throw new OperationalError('Require a sessionId for the listing of tools');
252
- }
253
-
254
- return {
255
- tools: this.tools[extra.sessionId].map((tool) => {
256
- return {
257
- name: tool.name,
258
- description: tool.description,
259
- // Allow additional properties on tool call input
260
- inputSchema: zodToJsonSchema(tool.schema, { removeAdditionalStrategy: 'strict' }),
261
- };
262
- }),
263
- };
264
- },
265
- );
266
-
267
- server.setRequestHandler(
268
- CallToolRequestSchema,
269
- async (request, extra: RequestHandlerExtra<ServerRequest, ServerNotification>) => {
270
- if (!request.params?.name || !request.params?.arguments) {
271
- throw new OperationalError('Require a name and arguments for the tool call');
272
- }
273
- if (!extra.sessionId) {
274
- throw new OperationalError('Require a sessionId for the tool call');
275
- }
276
-
277
- const callId = extra.requestId ? `${extra.sessionId}_${extra.requestId}` : extra.sessionId;
278
-
279
- const requestedTool: Tool | undefined = this.tools[extra.sessionId].find(
280
- (tool) => tool.name === request.params.name,
281
- );
282
- if (!requestedTool) {
283
- throw new OperationalError('Tool not found');
284
- }
285
-
286
- try {
287
- const result = await requestedTool.invoke(request.params.arguments);
288
- if (this.resolveFunctions[callId]) {
289
- this.resolveFunctions[callId]();
290
- } else {
291
- this.logger.warn(`No resolve function found for ${callId}`);
292
- }
293
-
294
- this.logger.debug(`Got request for ${requestedTool.name}, and executed it.`);
295
-
296
- if (typeof result === 'object') {
297
- return { content: [{ type: 'text', text: JSON.stringify(result) }] };
298
- }
299
- if (typeof result === 'string') {
300
- return { content: [{ type: 'text', text: result }] };
301
- }
302
- return { content: [{ type: 'text', text: String(result) }] };
303
- } catch (error) {
304
- this.logger.error(`Error while executing Tool ${requestedTool.name}: ${error}`);
305
- return { isError: true, content: [{ type: 'text', text: `Error: ${error.message}` }] };
306
- }
307
- },
308
- );
309
-
310
- server.onclose = () => {
311
- this.logger.debug('Closing MCP Server');
312
- };
313
- server.onerror = (error: unknown) => {
314
- this.logger.error(`MCP Error: ${error}`);
315
- };
316
- }
317
- }