Skip to content

Commit 6ef66e1

Browse files
authored
Merge branch 'n8n-io:master' into master
2 parents 48a7986 + 424c79c commit 6ef66e1

155 files changed

Lines changed: 6573 additions & 1144 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cypress/e2e/42-nps-survey.cy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ describe('NpsSurvey', () => {
3131
config: {
3232
key: 'test',
3333
url: 'https://telemetry-test.n8n.io',
34+
proxy: 'http://localhost:5678/rest/telemetry/proxy',
35+
sourceConfig: 'http://localhost:5678/rest/telemetry/rudderstack',
3436
},
3537
};
3638
}
@@ -77,6 +79,8 @@ describe('NpsSurvey', () => {
7779
config: {
7880
key: 'test',
7981
url: 'https://telemetry-test.n8n.io',
82+
proxy: 'http://localhost:5678/rest/telemetry/proxy',
83+
sourceConfig: 'http://localhost:5678/rest/telemetry/rudderstack',
8084
},
8185
};
8286
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
"ics": "patches/ics.patch",
124124
"minifaker": "patches/minifaker.patch",
125125
"z-vue-scan": "patches/z-vue-scan.patch",
126+
"@lezer/highlight": "patches/@lezer__highlight.patch",
126127
"v-code-diff": "patches/v-code-diff.patch"
127128
}
128129
}

packages/@n8n/api-types/src/frontend-settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface IVersionNotificationSettings {
1313
export interface ITelemetryClientConfig {
1414
url: string;
1515
key: string;
16+
proxy: string;
17+
sourceConfig: string;
1618
}
1719

1820
export interface ITelemetrySettings {

packages/@n8n/backend-common/src/modules/module-registry.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ export class ModuleRegistry {
5858
modulesDir = path.join(n8nRoot, dir, 'modules');
5959
} catch {
6060
// local dev
61-
modulesDir = path.resolve(__dirname, '../../../../cli/dist/modules');
61+
// n8n binary is inside the bin folder, so we need to go up two levels
62+
modulesDir = path.resolve(process.argv[1], '../../dist/modules');
6263
}
6364

6465
for (const moduleName of modules ?? this.eligibleModules) {

packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
22
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
33
import { mock } from 'jest-mock-extended';
44
import {
5+
NodeConnectionTypes,
56
NodeOperationError,
67
type ILoadOptionsFunctions,
78
type INode,
@@ -284,5 +285,78 @@ describe('McpClientTool', () => {
284285
headers: { Accept: 'text/event-stream', Authorization: 'Bearer my-token' },
285286
});
286287
});
288+
289+
it('should successfully execute a tool', async () => {
290+
jest.spyOn(Client.prototype, 'connect').mockResolvedValue();
291+
jest.spyOn(Client.prototype, 'callTool').mockResolvedValue({ content: 'Sunny' });
292+
jest.spyOn(Client.prototype, 'listTools').mockResolvedValue({
293+
tools: [
294+
{
295+
name: 'Weather Tool',
296+
description: 'Gets the current weather',
297+
inputSchema: { type: 'object', properties: { location: { type: 'string' } } },
298+
},
299+
],
300+
});
301+
302+
const supplyDataResult = await new McpClientTool().supplyData.call(
303+
mock<ISupplyDataFunctions>({
304+
getNode: jest.fn(() =>
305+
mock<INode>({
306+
typeVersion: 1,
307+
}),
308+
),
309+
logger: { debug: jest.fn(), error: jest.fn() },
310+
addInputData: jest.fn(() => ({ index: 0 })),
311+
}),
312+
0,
313+
);
314+
315+
expect(supplyDataResult.closeFunction).toBeInstanceOf(Function);
316+
expect(supplyDataResult.response).toBeInstanceOf(McpToolkit);
317+
318+
const tools = (supplyDataResult.response as McpToolkit).getTools();
319+
const toolResult = await tools[0].invoke({ location: 'Berlin' });
320+
expect(toolResult).toEqual('Sunny');
321+
});
322+
323+
it('should handle tool errors', async () => {
324+
jest.spyOn(Client.prototype, 'connect').mockResolvedValue();
325+
jest
326+
.spyOn(Client.prototype, 'callTool')
327+
.mockResolvedValue({ isError: true, content: [{ text: 'Weather unknown at location' }] });
328+
jest.spyOn(Client.prototype, 'listTools').mockResolvedValue({
329+
tools: [
330+
{
331+
name: 'Weather Tool',
332+
description: 'Gets the current weather',
333+
inputSchema: { type: 'object', properties: { location: { type: 'string' } } },
334+
},
335+
],
336+
});
337+
338+
const supplyDataFunctions = mock<ISupplyDataFunctions>({
339+
getNode: jest.fn(() =>
340+
mock<INode>({
341+
typeVersion: 1,
342+
}),
343+
),
344+
logger: { debug: jest.fn(), error: jest.fn() },
345+
addInputData: jest.fn(() => ({ index: 0 })),
346+
});
347+
const supplyDataResult = await new McpClientTool().supplyData.call(supplyDataFunctions, 0);
348+
349+
expect(supplyDataResult.closeFunction).toBeInstanceOf(Function);
350+
expect(supplyDataResult.response).toBeInstanceOf(McpToolkit);
351+
352+
const tools = (supplyDataResult.response as McpToolkit).getTools();
353+
const toolResult = await tools[0].invoke({ location: 'Berlin' });
354+
expect(toolResult).toEqual('Weather unknown at location');
355+
expect(supplyDataFunctions.addOutputData).toHaveBeenCalledWith(
356+
NodeConnectionTypes.AiTool,
357+
0,
358+
new NodeOperationError(supplyDataFunctions.getNode(), 'Weather unknown at location'),
359+
);
360+
});
287361
});
288362
});

packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/McpClientTool.node.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { logWrapper } from '@utils/logWrapper';
2+
import { getConnectionHintNoticeField } from '@utils/sharedFields';
13
import {
24
NodeConnectionTypes,
35
NodeOperationError,
@@ -7,9 +9,6 @@ import {
79
type SupplyData,
810
} from 'n8n-workflow';
911

10-
import { logWrapper } from '@utils/logWrapper';
11-
import { getConnectionHintNoticeField } from '@utils/sharedFields';
12-
1312
import { getTools } from './loadOptions';
1413
import type { McpServerTransport, McpAuthenticationOption, McpToolIncludeMode } from './types';
1514
import {
@@ -294,11 +293,10 @@ export class McpClientTool implements INodeType {
294293
logWrapper(
295294
mcpToolToDynamicTool(
296295
tool,
297-
createCallTool(tool.name, client.result, (error) => {
296+
createCallTool(tool.name, client.result, (errorMessage) => {
297+
const error = new NodeOperationError(node, errorMessage, { itemIndex });
298+
void this.addOutputData(NodeConnectionTypes.AiTool, itemIndex, error);
298299
this.logger.error(`McpClientTool: Tool "${tool.name}" failed to execute`, { error });
299-
throw new NodeOperationError(node, `Failed to execute tool "${tool.name}"`, {
300-
description: error,
301-
});
302300
}),
303301
),
304302
this,

packages/@n8n/nodes-langchain/nodes/mcp/McpClientTool/utils.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
33
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
44
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
55
import { CompatibilityCallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
6+
import { convertJsonSchemaToZod } from '@utils/schemaParsing';
67
import { Toolkit } from 'langchain/agents';
78
import {
89
createResultError,
@@ -13,12 +14,10 @@ import {
1314
} from 'n8n-workflow';
1415
import { z } from 'zod';
1516

16-
import { convertJsonSchemaToZod } from '@utils/schemaParsing';
17-
1817
import type {
1918
McpAuthenticationOption,
20-
McpTool,
2119
McpServerTransport,
20+
McpTool,
2221
McpToolIncludeMode,
2322
} from './types';
2423

@@ -78,17 +77,24 @@ export const getErrorDescriptionFromToolCall = (result: unknown): string | undef
7877
};
7978

8079
export const createCallTool =
81-
(name: string, client: Client, onError: (error: string | undefined) => void) =>
82-
async (args: IDataObject) => {
80+
(name: string, client: Client, onError: (error: string) => void) => async (args: IDataObject) => {
8381
let result: Awaited<ReturnType<Client['callTool']>>;
82+
83+
function handleError(error: unknown) {
84+
const errorDescription =
85+
getErrorDescriptionFromToolCall(error) ?? `Failed to execute tool "${name}"`;
86+
onError(errorDescription);
87+
return errorDescription;
88+
}
89+
8490
try {
8591
result = await client.callTool({ name, arguments: args }, CompatibilityCallToolResultSchema);
8692
} catch (error) {
87-
return onError(getErrorDescriptionFromToolCall(error));
93+
return handleError(error);
8894
}
8995

9096
if (result.isError) {
91-
return onError(getErrorDescriptionFromToolCall(result));
97+
return handleError(result);
9298
}
9399

94100
if (result.toolResult !== undefined) {
@@ -105,7 +111,7 @@ export const createCallTool =
105111
export function mcpToolToDynamicTool(
106112
tool: McpTool,
107113
onCallTool: DynamicStructuredToolInput['func'],
108-
): DynamicStructuredTool<z.ZodObject<any, any, any, any>> {
114+
): DynamicStructuredTool {
109115
const rawSchema = convertJsonSchemaToZod(tool.inputSchema);
110116

111117
// Ensure we always have an object schema for structured tools
@@ -122,7 +128,7 @@ export function mcpToolToDynamicTool(
122128
}
123129

124130
export class McpToolkit extends Toolkit {
125-
constructor(public tools: Array<DynamicStructuredTool<z.ZodObject<any, any, any, any>>>) {
131+
constructor(public tools: DynamicStructuredTool[]) {
126132
super();
127133
}
128134
}

0 commit comments

Comments
 (0)