Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 40 additions & 32 deletions nodes/McpClient/McpClient.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
import { parseHeaders, mergeHeaders } from './utils';

// Add Node.js process type declaration
declare const process: {
Expand Down Expand Up @@ -100,6 +101,19 @@ export class McpClient implements INodeType {
default: '',
description: 'Override the URL from credentials with a custom URL',
},
{
displayName: 'Headers Override',
name: 'headersOverride',
type: 'string',
displayOptions: {
show: {
connectionType: ['sse', 'http'],
},
},
default: '',
description:
'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.',
},
{
displayName: 'Operation',
name: 'operation',
Expand Down Expand Up @@ -248,23 +262,20 @@ export class McpClient implements INodeType {
const messagesPostEndpoint = (httpCredentials.messagesPostEndpoint as string) || '';
timeout = httpCredentials.httpTimeout as number || 60000;

// Parse headers
let headers: Record<string, string> = {};
if (httpCredentials.headers) {
const headerLines = (httpCredentials.headers as string).split('\n');
for (const line of headerLines) {
const equalsIndex = line.indexOf('=');
// Ensure '=' is present and not the first character of the line
if (equalsIndex > 0) {
const name = line.substring(0, equalsIndex).trim();
const value = line.substring(equalsIndex + 1).trim();
// Add to headers object if key is not empty and value is defined
if (name && value !== undefined) {
headers[name] = value;
}
}
}
// Parse headers from credentials
const credentialHeaders = parseHeaders((httpCredentials.headers as string) || '');

// Get headers override
let headersOverrideStr = '';
try {
headersOverrideStr = this.getNodeParameter('headersOverride', 0, '') as string;
} catch (error) {
// Parameter doesn't exist, ignore
}
const overrideHeaders = parseHeaders(headersOverrideStr);

// Merge headers with override headers taking precedence
const headers = mergeHeaders(credentialHeaders, overrideHeaders);

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

// Parse headers
let headers: Record<string, string> = {};
if (sseCredentials.headers) {
const headerLines = (sseCredentials.headers as string).split('\n');
for (const line of headerLines) {
const equalsIndex = line.indexOf('=');
// Ensure '=' is present and not the first character of the line
if (equalsIndex > 0) {
const name = line.substring(0, equalsIndex).trim();
const value = line.substring(equalsIndex + 1).trim();
// Add to headers object if key is not empty and value is defined
if (name && value !== undefined) {
headers[name] = value;
}
}
}
// Parse headers from credentials
const credentialHeaders = parseHeaders((sseCredentials.headers as string) || '');

// Get headers override
let headersOverrideStr = '';
try {
headersOverrideStr = this.getNodeParameter('headersOverride', 0, '') as string;
} catch (error) {
// Parameter doesn't exist, ignore
}
const overrideHeaders = parseHeaders(headersOverrideStr);

// Merge headers with override headers taking precedence
const headers = mergeHeaders(credentialHeaders, overrideHeaders);

// Create SSE transport with dynamic import to avoid TypeScript errors
transport = new SSEClientTransport(
Expand Down
137 changes: 137 additions & 0 deletions nodes/McpClient/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { parseHeaders, mergeHeaders } from '../utils';

describe('MCP Client Utils', () => {
describe('parseHeaders', () => {
it('should parse newline-separated NAME=VALUE headers', () => {
const input = 'Authorization=Bearer token123\nContent-Type=application/json';
const result = parseHeaders(input);
expect(result).toEqual({
Authorization: 'Bearer token123',
'Content-Type': 'application/json',
});
});

it('should handle headers with equals signs in values', () => {
const input = 'Api-Key=abc=def=ghi';
const result = parseHeaders(input);
expect(result).toEqual({
'Api-Key': 'abc=def=ghi',
});
});

it('should ignore empty lines and malformed entries', () => {
const input = 'Valid-Header=value\n\n=NoKey\nNoEquals\nAnother-Header=value2';
const result = parseHeaders(input);
expect(result).toEqual({
'Valid-Header': 'value',
'Another-Header': 'value2',
});
});

it('should trim whitespace from keys and values', () => {
const input = ' Authorization = Bearer token \n Content-Type = application/json ';
const result = parseHeaders(input);
expect(result).toEqual({
Authorization: 'Bearer token',
'Content-Type': 'application/json',
});
});

it('should handle empty string input', () => {
const result = parseHeaders('');
expect(result).toEqual({});
});

it('should handle multiple headers with various formats', () => {
const input =
'X-Api-Key=secret123\nAuthorization=Bearer xyz\nX-Custom-Header=value=with=equals';
const result = parseHeaders(input);
expect(result).toEqual({
'X-Api-Key': 'secret123',
Authorization: 'Bearer xyz',
'X-Custom-Header': 'value=with=equals',
});
});
});

describe('mergeHeaders', () => {
it('should merge credential and override headers', () => {
const credentialHeaders = {
'X-Api-Key': 'from-credentials',
'Content-Type': 'application/json',
};
const overrideHeaders = {
Authorization: 'Bearer override-token',
};
const result = mergeHeaders(credentialHeaders, overrideHeaders);
expect(result).toEqual({
'X-Api-Key': 'from-credentials',
'Content-Type': 'application/json',
Authorization: 'Bearer override-token',
});
});

it('should allow override headers to take precedence over credential headers', () => {
const credentialHeaders = {
Authorization: 'Bearer credential-token',
'X-Custom': 'credential-value',
};
const overrideHeaders = {
Authorization: 'Bearer override-token',
};
const result = mergeHeaders(credentialHeaders, overrideHeaders);
expect(result).toEqual({
Authorization: 'Bearer override-token',
'X-Custom': 'credential-value',
});
});

it('should handle empty override headers', () => {
const credentialHeaders = {
Authorization: 'Bearer token',
};
const overrideHeaders = {};
const result = mergeHeaders(credentialHeaders, overrideHeaders);
expect(result).toEqual({
Authorization: 'Bearer token',
});
});

it('should handle empty credential headers', () => {
const credentialHeaders = {};
const overrideHeaders = {
Authorization: 'Bearer token',
};
const result = mergeHeaders(credentialHeaders, overrideHeaders);
expect(result).toEqual({
Authorization: 'Bearer token',
});
});

it('should handle both empty headers objects', () => {
const credentialHeaders = {};
const overrideHeaders = {};
const result = mergeHeaders(credentialHeaders, overrideHeaders);
expect(result).toEqual({});
});

it('should merge multiple headers correctly', () => {
const credentialHeaders = {
'X-Api-Key': 'cred-key',
'Content-Type': 'application/json',
Accept: 'application/json',
};
const overrideHeaders = {
Authorization: 'Bearer new-token',
'Content-Type': 'text/plain',
};
const result = mergeHeaders(credentialHeaders, overrideHeaders);
expect(result).toEqual({
'X-Api-Key': 'cred-key',
'Content-Type': 'text/plain',
Accept: 'application/json',
Authorization: 'Bearer new-token',
});
});
});
});
33 changes: 33 additions & 0 deletions nodes/McpClient/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Parse headers from newline-separated NAME=VALUE format
*/
export function parseHeaders(headerString: string): Record<string, string> {
const headers: Record<string, string> = {};
if (headerString) {
const headerLines = headerString.split('\n');
for (const line of headerLines) {
const equalsIndex = line.indexOf('=');
// Ensure '=' is present and not the first character of the line
if (equalsIndex > 0) {
const name = line.substring(0, equalsIndex).trim();
const value = line.substring(equalsIndex + 1).trim();
// Add to headers object if key is not empty and value is defined
if (name && value !== undefined) {
headers[name] = value;
}
}
}
}
return headers;
}

/**
* Merge headers from credentials with dynamic headers
* Dynamic headers take precedence over credential headers
*/
export function mergeHeaders(
credentialHeaders: Record<string, string>,
dynamicHeaders: Record<string, string>,
): Record<string, string> {
return { ...credentialHeaders, ...dynamicHeaders };
}