mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
Fix: Environment variable expansion in headers for HTTP-based MCP transports (#380)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com>
This commit is contained in:
@@ -162,7 +162,7 @@ const createTransportFromConfig = (name: string, conf: ServerConfig): any => {
|
||||
const options: any = {};
|
||||
if (conf.headers && Object.keys(conf.headers).length > 0) {
|
||||
options.requestInit = {
|
||||
headers: conf.headers,
|
||||
headers: replaceEnvVars(conf.headers),
|
||||
};
|
||||
}
|
||||
transport = new StreamableHTTPClientTransport(new URL(conf.url || ''), options);
|
||||
@@ -171,10 +171,10 @@ const createTransportFromConfig = (name: string, conf: ServerConfig): any => {
|
||||
const options: any = {};
|
||||
if (conf.headers && Object.keys(conf.headers).length > 0) {
|
||||
options.eventSourceInit = {
|
||||
headers: conf.headers,
|
||||
headers: replaceEnvVars(conf.headers),
|
||||
};
|
||||
options.requestInit = {
|
||||
headers: conf.headers,
|
||||
headers: replaceEnvVars(conf.headers),
|
||||
};
|
||||
}
|
||||
transport = new SSEClientTransport(new URL(conf.url), options);
|
||||
|
||||
179
tests/services/mcpService-headers.test.ts
Normal file
179
tests/services/mcpService-headers.test.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { expandEnvVars, replaceEnvVars } from '../../src/config/index.js';
|
||||
|
||||
describe('MCP Service - Headers Environment Variable Expansion', () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset process.env before each test
|
||||
jest.resetModules();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Restore original environment
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe('expandEnvVars', () => {
|
||||
it('should expand environment variables in ${VAR} format', () => {
|
||||
process.env.CONTEXT7_API_KEY = 'ctx7sk-test123';
|
||||
const result = expandEnvVars('${CONTEXT7_API_KEY}');
|
||||
expect(result).toBe('ctx7sk-test123');
|
||||
});
|
||||
|
||||
it('should expand environment variables in $VAR format', () => {
|
||||
process.env.TEST_VAR = 'test-value';
|
||||
const result = expandEnvVars('$TEST_VAR');
|
||||
expect(result).toBe('test-value');
|
||||
});
|
||||
|
||||
it('should expand multiple environment variables', () => {
|
||||
process.env.VAR1 = 'value1';
|
||||
process.env.VAR2 = 'value2';
|
||||
const result = expandEnvVars('${VAR1}-and-${VAR2}');
|
||||
expect(result).toBe('value1-and-value2');
|
||||
});
|
||||
|
||||
it('should return empty string for undefined variables', () => {
|
||||
const result = expandEnvVars('${UNDEFINED_VAR}');
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should handle strings without variables', () => {
|
||||
const result = expandEnvVars('plain-string');
|
||||
expect(result).toBe('plain-string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceEnvVars - Object (Headers)', () => {
|
||||
it('should expand environment variables in header values', () => {
|
||||
process.env.CONTEXT7_API_KEY = 'ctx7sk-d16example123';
|
||||
const headers = {
|
||||
CONTEXT7_API_KEY: '${CONTEXT7_API_KEY}',
|
||||
};
|
||||
const result = replaceEnvVars(headers);
|
||||
expect(result).toEqual({
|
||||
CONTEXT7_API_KEY: 'ctx7sk-d16example123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should expand multiple headers with environment variables', () => {
|
||||
process.env.API_KEY = 'test-api-key';
|
||||
process.env.AUTH_TOKEN = 'test-auth-token';
|
||||
const headers = {
|
||||
'X-API-Key': '${API_KEY}',
|
||||
Authorization: 'Bearer ${AUTH_TOKEN}',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const result = replaceEnvVars(headers);
|
||||
expect(result).toEqual({
|
||||
'X-API-Key': 'test-api-key',
|
||||
Authorization: 'Bearer test-auth-token',
|
||||
'Content-Type': 'application/json',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle $VAR format in headers', () => {
|
||||
process.env.MY_KEY = 'my-value';
|
||||
const headers = {
|
||||
'X-Custom-Header': '$MY_KEY',
|
||||
};
|
||||
const result = replaceEnvVars(headers);
|
||||
expect(result).toEqual({
|
||||
'X-Custom-Header': 'my-value',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty string for undefined variables in headers', () => {
|
||||
const headers = {
|
||||
'X-Undefined': '${UNDEFINED_VAR}',
|
||||
};
|
||||
const result = replaceEnvVars(headers);
|
||||
expect(result).toEqual({
|
||||
'X-Undefined': '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mix of variables and static values', () => {
|
||||
process.env.TOKEN = 'secret123';
|
||||
const headers = {
|
||||
Authorization: 'Bearer ${TOKEN}',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Custom': 'static-value',
|
||||
};
|
||||
const result = replaceEnvVars(headers);
|
||||
expect(result).toEqual({
|
||||
Authorization: 'Bearer secret123',
|
||||
'Content-Type': 'application/json',
|
||||
'X-Custom': 'static-value',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty object', () => {
|
||||
const headers = {};
|
||||
const result = replaceEnvVars(headers);
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceEnvVars - Array (Args)', () => {
|
||||
it('should expand environment variables in array elements', () => {
|
||||
process.env.PORT = '3000';
|
||||
const args = ['--port', '${PORT}'];
|
||||
const result = replaceEnvVars(args);
|
||||
expect(result).toEqual(['--port', '3000']);
|
||||
});
|
||||
|
||||
it('should handle multiple variables in array', () => {
|
||||
process.env.HOST = 'localhost';
|
||||
process.env.PORT = '8080';
|
||||
const args = ['--host', '${HOST}', '--port', '${PORT}'];
|
||||
const result = replaceEnvVars(args);
|
||||
expect(result).toEqual(['--host', 'localhost', '--port', '8080']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world Context7 Scenario', () => {
|
||||
it('should correctly expand Context7 API key from environment', () => {
|
||||
// Simulate the environment variable being set in the container
|
||||
process.env.CONTEXT7_API_KEY = 'ctx7sk-d16examplekey123';
|
||||
|
||||
// Simulate the configuration from mcp_settings.json
|
||||
const serverConfig = {
|
||||
type: 'streamable-http',
|
||||
url: 'https://mcp.context7.com/mcp',
|
||||
headers: {
|
||||
CONTEXT7_API_KEY: '${CONTEXT7_API_KEY}',
|
||||
},
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// Simulate what happens in createTransportFromConfig
|
||||
const expandedHeaders = replaceEnvVars(serverConfig.headers);
|
||||
|
||||
// Verify that the environment variable was correctly expanded
|
||||
expect(expandedHeaders.CONTEXT7_API_KEY).toBe('ctx7sk-d16examplekey123');
|
||||
expect(expandedHeaders.CONTEXT7_API_KEY).not.toBe('${CONTEXT7_API_KEY}');
|
||||
expect(expandedHeaders.CONTEXT7_API_KEY).toMatch(/^ctx7sk-/);
|
||||
});
|
||||
|
||||
it('should handle case when environment variable is not set', () => {
|
||||
// Don't set the environment variable
|
||||
delete process.env.CONTEXT7_API_KEY;
|
||||
|
||||
const serverConfig = {
|
||||
type: 'streamable-http',
|
||||
url: 'https://mcp.context7.com/mcp',
|
||||
headers: {
|
||||
CONTEXT7_API_KEY: '${CONTEXT7_API_KEY}',
|
||||
},
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
const expandedHeaders = replaceEnvVars(serverConfig.headers);
|
||||
|
||||
// Should be empty string when env var is not set
|
||||
expect(expandedHeaders.CONTEXT7_API_KEY).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user