diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index 91a35bf..a451604 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -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); diff --git a/tests/services/mcpService-headers.test.ts b/tests/services/mcpService-headers.test.ts new file mode 100644 index 0000000..53db2bc --- /dev/null +++ b/tests/services/mcpService-headers.test.ts @@ -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(''); + }); + }); +});