Skip to content

Commit d2da643

Browse files
feat: headers override for MCP operations
1 parent 71c3d5b commit d2da643

3 files changed

Lines changed: 210 additions & 32 deletions

File tree

nodes/McpClient/McpClient.node.ts

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol';
1212
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
1313
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
1414
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
15+
import { parseHeaders, mergeHeaders } from './utils';
1516

1617
// Add Node.js process type declaration
1718
declare const process: {
@@ -100,6 +101,19 @@ export class McpClient implements INodeType {
100101
default: '',
101102
description: 'Override the URL from credentials with a custom URL',
102103
},
104+
{
105+
displayName: 'Headers Override',
106+
name: 'headersOverride',
107+
type: 'string',
108+
displayOptions: {
109+
show: {
110+
connectionType: ['sse', 'http'],
111+
},
112+
},
113+
default: '',
114+
description:
115+
'Additional headers to send in the request in NAME=VALUE format, separated by newlines (e.g., Authorization=Bearer token). These will be merged with headers from credentials, with override headers taking precedence.',
116+
},
103117
{
104118
displayName: 'Operation',
105119
name: 'operation',
@@ -248,23 +262,20 @@ export class McpClient implements INodeType {
248262
const messagesPostEndpoint = (httpCredentials.messagesPostEndpoint as string) || '';
249263
timeout = httpCredentials.httpTimeout as number || 60000;
250264

251-
// Parse headers
252-
let headers: Record<string, string> = {};
253-
if (httpCredentials.headers) {
254-
const headerLines = (httpCredentials.headers as string).split('\n');
255-
for (const line of headerLines) {
256-
const equalsIndex = line.indexOf('=');
257-
// Ensure '=' is present and not the first character of the line
258-
if (equalsIndex > 0) {
259-
const name = line.substring(0, equalsIndex).trim();
260-
const value = line.substring(equalsIndex + 1).trim();
261-
// Add to headers object if key is not empty and value is defined
262-
if (name && value !== undefined) {
263-
headers[name] = value;
264-
}
265-
}
266-
}
265+
// Parse headers from credentials
266+
const credentialHeaders = parseHeaders((httpCredentials.headers as string) || '');
267+
268+
// Get headers override
269+
let headersOverrideStr = '';
270+
try {
271+
headersOverrideStr = this.getNodeParameter('headersOverride', 0, '') as string;
272+
} catch (error) {
273+
// Parameter doesn't exist, ignore
267274
}
275+
const overrideHeaders = parseHeaders(headersOverrideStr);
276+
277+
// Merge headers with override headers taking precedence
278+
const headers = mergeHeaders(credentialHeaders, overrideHeaders);
268279

269280
const requestInit: RequestInit = { headers };
270281
if (messagesPostEndpoint) {
@@ -299,23 +310,20 @@ export class McpClient implements INodeType {
299310
const messagesPostEndpoint = (sseCredentials.messagesPostEndpoint as string) || '';
300311
timeout = sseCredentials.sseTimeout as number || 60000;
301312

302-
// Parse headers
303-
let headers: Record<string, string> = {};
304-
if (sseCredentials.headers) {
305-
const headerLines = (sseCredentials.headers as string).split('\n');
306-
for (const line of headerLines) {
307-
const equalsIndex = line.indexOf('=');
308-
// Ensure '=' is present and not the first character of the line
309-
if (equalsIndex > 0) {
310-
const name = line.substring(0, equalsIndex).trim();
311-
const value = line.substring(equalsIndex + 1).trim();
312-
// Add to headers object if key is not empty and value is defined
313-
if (name && value !== undefined) {
314-
headers[name] = value;
315-
}
316-
}
317-
}
313+
// Parse headers from credentials
314+
const credentialHeaders = parseHeaders((sseCredentials.headers as string) || '');
315+
316+
// Get headers override
317+
let headersOverrideStr = '';
318+
try {
319+
headersOverrideStr = this.getNodeParameter('headersOverride', 0, '') as string;
320+
} catch (error) {
321+
// Parameter doesn't exist, ignore
318322
}
323+
const overrideHeaders = parseHeaders(headersOverrideStr);
324+
325+
// Merge headers with override headers taking precedence
326+
const headers = mergeHeaders(credentialHeaders, overrideHeaders);
319327

320328
// Create SSE transport with dynamic import to avoid TypeScript errors
321329
transport = new SSEClientTransport(
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { parseHeaders, mergeHeaders } from '../utils';
2+
3+
describe('MCP Client Utils', () => {
4+
describe('parseHeaders', () => {
5+
it('should parse newline-separated NAME=VALUE headers', () => {
6+
const input = 'Authorization=Bearer token123\nContent-Type=application/json';
7+
const result = parseHeaders(input);
8+
expect(result).toEqual({
9+
Authorization: 'Bearer token123',
10+
'Content-Type': 'application/json',
11+
});
12+
});
13+
14+
it('should handle headers with equals signs in values', () => {
15+
const input = 'Api-Key=abc=def=ghi';
16+
const result = parseHeaders(input);
17+
expect(result).toEqual({
18+
'Api-Key': 'abc=def=ghi',
19+
});
20+
});
21+
22+
it('should ignore empty lines and malformed entries', () => {
23+
const input = 'Valid-Header=value\n\n=NoKey\nNoEquals\nAnother-Header=value2';
24+
const result = parseHeaders(input);
25+
expect(result).toEqual({
26+
'Valid-Header': 'value',
27+
'Another-Header': 'value2',
28+
});
29+
});
30+
31+
it('should trim whitespace from keys and values', () => {
32+
const input = ' Authorization = Bearer token \n Content-Type = application/json ';
33+
const result = parseHeaders(input);
34+
expect(result).toEqual({
35+
Authorization: 'Bearer token',
36+
'Content-Type': 'application/json',
37+
});
38+
});
39+
40+
it('should handle empty string input', () => {
41+
const result = parseHeaders('');
42+
expect(result).toEqual({});
43+
});
44+
45+
it('should handle multiple headers with various formats', () => {
46+
const input =
47+
'X-Api-Key=secret123\nAuthorization=Bearer xyz\nX-Custom-Header=value=with=equals';
48+
const result = parseHeaders(input);
49+
expect(result).toEqual({
50+
'X-Api-Key': 'secret123',
51+
Authorization: 'Bearer xyz',
52+
'X-Custom-Header': 'value=with=equals',
53+
});
54+
});
55+
});
56+
57+
describe('mergeHeaders', () => {
58+
it('should merge credential and override headers', () => {
59+
const credentialHeaders = {
60+
'X-Api-Key': 'from-credentials',
61+
'Content-Type': 'application/json',
62+
};
63+
const overrideHeaders = {
64+
Authorization: 'Bearer override-token',
65+
};
66+
const result = mergeHeaders(credentialHeaders, overrideHeaders);
67+
expect(result).toEqual({
68+
'X-Api-Key': 'from-credentials',
69+
'Content-Type': 'application/json',
70+
Authorization: 'Bearer override-token',
71+
});
72+
});
73+
74+
it('should allow override headers to take precedence over credential headers', () => {
75+
const credentialHeaders = {
76+
Authorization: 'Bearer credential-token',
77+
'X-Custom': 'credential-value',
78+
};
79+
const overrideHeaders = {
80+
Authorization: 'Bearer override-token',
81+
};
82+
const result = mergeHeaders(credentialHeaders, overrideHeaders);
83+
expect(result).toEqual({
84+
Authorization: 'Bearer override-token',
85+
'X-Custom': 'credential-value',
86+
});
87+
});
88+
89+
it('should handle empty override headers', () => {
90+
const credentialHeaders = {
91+
Authorization: 'Bearer token',
92+
};
93+
const overrideHeaders = {};
94+
const result = mergeHeaders(credentialHeaders, overrideHeaders);
95+
expect(result).toEqual({
96+
Authorization: 'Bearer token',
97+
});
98+
});
99+
100+
it('should handle empty credential headers', () => {
101+
const credentialHeaders = {};
102+
const overrideHeaders = {
103+
Authorization: 'Bearer token',
104+
};
105+
const result = mergeHeaders(credentialHeaders, overrideHeaders);
106+
expect(result).toEqual({
107+
Authorization: 'Bearer token',
108+
});
109+
});
110+
111+
it('should handle both empty headers objects', () => {
112+
const credentialHeaders = {};
113+
const overrideHeaders = {};
114+
const result = mergeHeaders(credentialHeaders, overrideHeaders);
115+
expect(result).toEqual({});
116+
});
117+
118+
it('should merge multiple headers correctly', () => {
119+
const credentialHeaders = {
120+
'X-Api-Key': 'cred-key',
121+
'Content-Type': 'application/json',
122+
Accept: 'application/json',
123+
};
124+
const overrideHeaders = {
125+
Authorization: 'Bearer new-token',
126+
'Content-Type': 'text/plain',
127+
};
128+
const result = mergeHeaders(credentialHeaders, overrideHeaders);
129+
expect(result).toEqual({
130+
'X-Api-Key': 'cred-key',
131+
'Content-Type': 'text/plain',
132+
Accept: 'application/json',
133+
Authorization: 'Bearer new-token',
134+
});
135+
});
136+
});
137+
});

nodes/McpClient/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Parse headers from newline-separated NAME=VALUE format
3+
*/
4+
export function parseHeaders(headerString: string): Record<string, string> {
5+
const headers: Record<string, string> = {};
6+
if (headerString) {
7+
const headerLines = headerString.split('\n');
8+
for (const line of headerLines) {
9+
const equalsIndex = line.indexOf('=');
10+
// Ensure '=' is present and not the first character of the line
11+
if (equalsIndex > 0) {
12+
const name = line.substring(0, equalsIndex).trim();
13+
const value = line.substring(equalsIndex + 1).trim();
14+
// Add to headers object if key is not empty and value is defined
15+
if (name && value !== undefined) {
16+
headers[name] = value;
17+
}
18+
}
19+
}
20+
}
21+
return headers;
22+
}
23+
24+
/**
25+
* Merge headers from credentials with dynamic headers
26+
* Dynamic headers take precedence over credential headers
27+
*/
28+
export function mergeHeaders(
29+
credentialHeaders: Record<string, string>,
30+
dynamicHeaders: Record<string, string>,
31+
): Record<string, string> {
32+
return { ...credentialHeaders, ...dynamicHeaders };
33+
}

0 commit comments

Comments
 (0)