Skip to content

Commit 82f816b

Browse files
committed
feat(mcp-client): Add httpCustomAuth support
1 parent f620f57 commit 82f816b

4 files changed

Lines changed: 81 additions & 2 deletions

File tree

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,57 @@ describe('McpClientTool', () => {
188188
expect(tools[0].name).toBe('MyTool1');
189189
});
190190

191+
it('should support custom auth', async () => {
192+
jest.spyOn(Client.prototype, 'connect').mockResolvedValue();
193+
jest.spyOn(Client.prototype, 'listTools').mockResolvedValue({
194+
tools: [
195+
{
196+
name: 'MyTool1',
197+
description: 'MyTool1 does something',
198+
inputSchema: { type: 'object', properties: { input: { type: 'string' } } },
199+
},
200+
],
201+
});
202+
203+
const supplyDataResult = await new McpClientTool().supplyData.call(
204+
mock<ISupplyDataFunctions>({
205+
getNode: jest.fn(() => mock<INode>({ typeVersion: 1 })),
206+
getNodeParameter: jest.fn((key, _index) => {
207+
const parameters: Record<string, any> = {
208+
include: 'except',
209+
excludeTools: ['MyTool2'],
210+
authentication: 'customAuth',
211+
sseEndpoint: 'https://my-mcp-endpoint.ai/sse',
212+
};
213+
return parameters[key];
214+
}),
215+
logger: { debug: jest.fn(), error: jest.fn() },
216+
addInputData: jest.fn(() => ({ index: 0 })),
217+
getCredentials: jest
218+
.fn()
219+
.mockResolvedValue({ name: 'my-header', json: '{"headers":{"h1": "v1", "h2": "v2"}}' }),
220+
}),
221+
0,
222+
);
223+
224+
expect(supplyDataResult.closeFunction).toBeInstanceOf(Function);
225+
expect(supplyDataResult.response).toBeInstanceOf(McpToolkit);
226+
227+
const fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue(mock());
228+
const url = new URL('https://my-mcp-endpoint.ai/sse');
229+
expect(SSEClientTransport).toHaveBeenCalledTimes(1);
230+
expect(SSEClientTransport).toHaveBeenCalledWith(url, {
231+
eventSourceInit: { fetch: expect.any(Function) },
232+
requestInit: { headers: { h1: 'v1', h2: 'v2' } },
233+
});
234+
235+
const customFetch = jest.mocked(SSEClientTransport).mock.calls[0][1]?.eventSourceInit?.fetch;
236+
await customFetch?.(url);
237+
expect(fetchSpy).toHaveBeenCalledWith(url, {
238+
headers: { Accept: 'text/event-stream', h1: 'v1', h2: 'v2' },
239+
});
240+
});
241+
191242
it('should support header auth', async () => {
192243
jest.spyOn(Client.prototype, 'connect').mockResolvedValue();
193244
jest.spyOn(Client.prototype, 'listTools').mockResolvedValue({

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ export class McpClientTool implements INodeType {
7171
},
7272
},
7373
},
74+
{
75+
name: 'httpCustomAuth',
76+
required: true,
77+
displayOptions: {
78+
show: {
79+
authentication: ['customAuth'],
80+
},
81+
},
82+
},
7483
],
7584
properties: [
7685
getConnectionHintNoticeField([NodeConnectionTypes.AiAgent]),
@@ -137,6 +146,10 @@ export class McpClientTool implements INodeType {
137146
name: 'Header Auth',
138147
value: 'headerAuth',
139148
},
149+
{
150+
name: 'Custom Auth',
151+
value: 'customAuth',
152+
},
140153
{
141154
name: 'None',
142155
value: 'none',
@@ -152,7 +165,7 @@ export class McpClientTool implements INodeType {
152165
default: '',
153166
displayOptions: {
154167
show: {
155-
authentication: ['headerAuth', 'bearerAuth'],
168+
authentication: ['headerAuth', 'bearerAuth', 'customAuth'],
156169
},
157170
},
158171
},

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ export type McpServerTransport = 'sse' | 'httpStreamable';
66

77
export type McpToolIncludeMode = 'all' | 'selected' | 'except';
88

9-
export type McpAuthenticationOption = 'none' | 'headerAuth' | 'bearerAuth';
9+
export type McpAuthenticationOption = 'none' | 'headerAuth' | 'bearerAuth' | 'customAuth';

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { Toolkit } from 'langchain/agents';
88
import {
99
createResultError,
1010
createResultOk,
11+
jsonParse,
1112
type IDataObject,
1213
type IExecuteFunctions,
14+
type IRequestOptionsSimplified,
1315
type Result,
1416
} from 'n8n-workflow';
1517
import { z } from 'zod';
@@ -232,6 +234,19 @@ export async function getAuthHeaders(
232234

233235
return { headers: { Authorization: `Bearer ${result.token}` } };
234236
}
237+
case 'customAuth': {
238+
const result = await ctx
239+
.getCredentials<{ token: string }>('httpCustomAuth')
240+
.catch(() => null);
241+
242+
if (!result) return {};
243+
244+
const customAuth = jsonParse<IRequestOptionsSimplified>((result.json as string) || '{}', {
245+
errorMessage: 'Invalid Custom Auth JSON',
246+
});
247+
248+
return { headers: customAuth.headers };
249+
}
235250
case 'none':
236251
default: {
237252
return {};

0 commit comments

Comments
 (0)