fix: extract instance context from HTTP headers for multi-tenant support

- Add header extraction logic in http-server-single-session.ts
- Extract X-N8n-Url, X-N8n-Key, X-Instance-Id, X-Session-Id headers
- Pass extracted context to handleRequest method
- Maintain full backward compatibility (falls back to env vars)
- Add comprehensive tests for header extraction scenarios
- Update documentation with HTTP header specifications

This fixes the bug where instance-specific configuration headers were not
being extracted and passed to the MCP server, preventing the multi-tenant
feature from working as designed in PR #209.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-09-20 00:25:40 +02:00
parent f0338ea5ce
commit 424f8ae1ff
3 changed files with 209 additions and 2 deletions

View File

@@ -99,6 +99,61 @@ if (client) {
}
```
### HTTP Headers for Multi-Tenant Support
When using the HTTP server mode, clients can pass instance-specific configuration via HTTP headers:
```bash
# Example curl request with instance headers
curl -X POST http://localhost:3000/mcp \
-H "Authorization: Bearer your-auth-token" \
-H "Content-Type: application/json" \
-H "X-N8n-Url: https://instance1.n8n.cloud" \
-H "X-N8n-Key: instance1-api-key" \
-H "X-Instance-Id: instance-1" \
-H "X-Session-Id: session-123" \
-d '{"method": "n8n_list_workflows", "params": {}, "id": 1}'
```
#### Supported Headers
- **X-N8n-Url**: The n8n instance URL (e.g., `https://instance.n8n.cloud`)
- **X-N8n-Key**: The API key for authentication with the n8n instance
- **X-Instance-Id**: A unique identifier for the instance (optional, for tracking)
- **X-Session-Id**: A session identifier (optional, for session tracking)
#### Header Extraction Logic
1. If either `X-N8n-Url` or `X-N8n-Key` header is present, an instance context is created
2. All headers are extracted and passed to the MCP server
3. The server uses the instance-specific configuration instead of environment variables
4. If no headers are present, the server falls back to environment variables (backward compatible)
#### Example: JavaScript Client
```javascript
const headers = {
'Authorization': 'Bearer your-auth-token',
'Content-Type': 'application/json',
'X-N8n-Url': 'https://customer1.n8n.cloud',
'X-N8n-Key': 'customer1-api-key',
'X-Instance-Id': 'customer-1',
'X-Session-Id': 'session-456'
};
const response = await fetch('http://localhost:3000/mcp', {
method: 'POST',
headers: headers,
body: JSON.stringify({
method: 'n8n_list_workflows',
params: {},
id: 1
})
});
const result = await response.json();
```
### HTTP Server Integration
```typescript

View File

@@ -1000,7 +1000,30 @@ export class SingleSessionHTTPServer {
sessionInitialized: this.session?.initialized
});
await this.handleRequest(req, res);
// Extract instance context from headers if present (for multi-tenant support)
const instanceContext: InstanceContext | undefined =
(req.headers['x-n8n-url'] || req.headers['x-n8n-key']) ? {
n8nApiUrl: req.headers['x-n8n-url'] as string,
n8nApiKey: req.headers['x-n8n-key'] as string,
instanceId: req.headers['x-instance-id'] as string,
sessionId: req.headers['x-session-id'] as string,
metadata: {
userAgent: req.headers['user-agent'],
ip: req.ip
}
} : undefined;
// Log context extraction for debugging (only if context exists)
if (instanceContext) {
logger.debug('Instance context extracted from headers', {
hasUrl: !!instanceContext.n8nApiUrl,
hasKey: !!instanceContext.n8nApiKey,
instanceId: instanceContext.instanceId,
sessionId: instanceContext.sessionId
});
}
await this.handleRequest(req, res, instanceContext);
logger.info('POST /mcp request completed - checking response status', {
responseHeadersSent: res.headersSent,

View File

@@ -208,4 +208,133 @@ describe('Flexible Instance Configuration', () => {
expect(isInstanceContext({ n8nApiUrl: 123 })).toBe(false);
});
});
describe('HTTP Header Extraction Logic', () => {
it('should create instance context from headers', () => {
// Test the logic that would extract context from headers
const headers = {
'x-n8n-url': 'https://instance1.n8n.cloud',
'x-n8n-key': 'test-api-key-123',
'x-instance-id': 'instance-test-1',
'x-session-id': 'session-test-123',
'user-agent': 'test-client/1.0'
};
// This simulates the logic in http-server-single-session.ts
const instanceContext: InstanceContext | undefined =
(headers['x-n8n-url'] || headers['x-n8n-key']) ? {
n8nApiUrl: headers['x-n8n-url'] as string,
n8nApiKey: headers['x-n8n-key'] as string,
instanceId: headers['x-instance-id'] as string,
sessionId: headers['x-session-id'] as string,
metadata: {
userAgent: headers['user-agent'],
ip: '127.0.0.1'
}
} : undefined;
expect(instanceContext).toBeDefined();
expect(instanceContext?.n8nApiUrl).toBe('https://instance1.n8n.cloud');
expect(instanceContext?.n8nApiKey).toBe('test-api-key-123');
expect(instanceContext?.instanceId).toBe('instance-test-1');
expect(instanceContext?.sessionId).toBe('session-test-123');
expect(instanceContext?.metadata?.userAgent).toBe('test-client/1.0');
});
it('should not create context when headers are missing', () => {
// Test when no relevant headers are present
const headers: Record<string, string | undefined> = {
'content-type': 'application/json',
'user-agent': 'test-client/1.0'
};
const instanceContext: InstanceContext | undefined =
(headers['x-n8n-url'] || headers['x-n8n-key']) ? {
n8nApiUrl: headers['x-n8n-url'] as string,
n8nApiKey: headers['x-n8n-key'] as string,
instanceId: headers['x-instance-id'] as string,
sessionId: headers['x-session-id'] as string,
metadata: {
userAgent: headers['user-agent'],
ip: '127.0.0.1'
}
} : undefined;
expect(instanceContext).toBeUndefined();
});
it('should create context with partial headers', () => {
// Test when only some headers are present
const headers: Record<string, string | undefined> = {
'x-n8n-url': 'https://partial.n8n.cloud',
'x-instance-id': 'partial-instance'
// Missing x-n8n-key and x-session-id
};
const instanceContext: InstanceContext | undefined =
(headers['x-n8n-url'] || headers['x-n8n-key']) ? {
n8nApiUrl: headers['x-n8n-url'] as string,
n8nApiKey: headers['x-n8n-key'] as string,
instanceId: headers['x-instance-id'] as string,
sessionId: headers['x-session-id'] as string,
metadata: undefined
} : undefined;
expect(instanceContext).toBeDefined();
expect(instanceContext?.n8nApiUrl).toBe('https://partial.n8n.cloud');
expect(instanceContext?.n8nApiKey).toBeUndefined();
expect(instanceContext?.instanceId).toBe('partial-instance');
expect(instanceContext?.sessionId).toBeUndefined();
});
it('should prioritize x-n8n-key for context creation', () => {
// Test when only API key is present
const headers: Record<string, string | undefined> = {
'x-n8n-key': 'key-only-test',
'x-instance-id': 'key-only-instance'
// Missing x-n8n-url
};
const instanceContext: InstanceContext | undefined =
(headers['x-n8n-url'] || headers['x-n8n-key']) ? {
n8nApiUrl: headers['x-n8n-url'] as string,
n8nApiKey: headers['x-n8n-key'] as string,
instanceId: headers['x-instance-id'] as string,
sessionId: headers['x-session-id'] as string,
metadata: undefined
} : undefined;
expect(instanceContext).toBeDefined();
expect(instanceContext?.n8nApiKey).toBe('key-only-test');
expect(instanceContext?.n8nApiUrl).toBeUndefined();
expect(instanceContext?.instanceId).toBe('key-only-instance');
});
it('should handle empty string headers', () => {
// Test with empty strings
const headers = {
'x-n8n-url': '',
'x-n8n-key': 'valid-key',
'x-instance-id': '',
'x-session-id': ''
};
// Empty string for URL should not trigger context creation
// But valid key should
const instanceContext: InstanceContext | undefined =
(headers['x-n8n-url'] || headers['x-n8n-key']) ? {
n8nApiUrl: headers['x-n8n-url'] as string,
n8nApiKey: headers['x-n8n-key'] as string,
instanceId: headers['x-instance-id'] as string,
sessionId: headers['x-session-id'] as string,
metadata: undefined
} : undefined;
expect(instanceContext).toBeDefined();
expect(instanceContext?.n8nApiUrl).toBe('');
expect(instanceContext?.n8nApiKey).toBe('valid-key');
expect(instanceContext?.instanceId).toBe('');
expect(instanceContext?.sessionId).toBe('');
});
});
});