mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-23 18:29:21 -05:00
feat: add passthrough headers support for OpenAPI client and MCP protocol (#345)
This commit is contained in:
@@ -121,6 +121,66 @@ See the `examples/openapi-schema-config.json` file for complete configuration ex
|
||||
- **Validation**: Enhanced validation logic in server controllers
|
||||
- **Type Safety**: Updated TypeScript interfaces for both input modes
|
||||
|
||||
## Header Passthrough Support
|
||||
|
||||
MCPHub supports passing through specific headers from tool call requests to upstream OpenAPI endpoints. This is useful for authentication tokens, API keys, and other request-specific headers.
|
||||
|
||||
### Configuration
|
||||
|
||||
Add `passthroughHeaders` to your OpenAPI configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "openapi",
|
||||
"openapi": {
|
||||
"url": "https://api.example.com/openapi.json",
|
||||
"version": "3.1.0",
|
||||
"passthroughHeaders": ["Authorization", "X-API-Key", "X-Custom-Header"],
|
||||
"security": {
|
||||
"type": "apiKey",
|
||||
"apiKey": {
|
||||
"name": "X-API-Key",
|
||||
"in": "header",
|
||||
"value": "your-api-key"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Configuration**: List header names in the `passthroughHeaders` array
|
||||
2. **Tool Calls**: When calling tools via HTTP API, include headers in the request
|
||||
3. **Passthrough**: Only configured headers are forwarded to the upstream API
|
||||
4. **Case Insensitive**: Header matching is case-insensitive for flexibility
|
||||
|
||||
### Example Usage
|
||||
|
||||
```bash
|
||||
# Call an OpenAPI tool with passthrough headers
|
||||
curl -X POST "http://localhost:3000/api/tools/myapi/createUser" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer your-token" \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-H "X-Custom-Header: custom-value" \
|
||||
-d '{"name": "John Doe", "email": "john@example.com"}'
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
- If `passthroughHeaders` includes `["Authorization", "X-API-Key"]`
|
||||
- Only `Authorization` and `X-API-Key` headers will be forwarded
|
||||
- `X-Custom-Header` will be ignored (not in passthrough list)
|
||||
- `Content-Type` is handled by the OpenAPI operation definition
|
||||
|
||||
### Security Considerations
|
||||
|
||||
- **Whitelist Only**: Only explicitly configured headers are passed through
|
||||
- **Sensitive Data**: Be careful with headers containing sensitive information
|
||||
- **Validation**: Upstream APIs should validate all received headers
|
||||
- **Logging**: Headers may appear in logs - consider this for sensitive data
|
||||
|
||||
## Security Considerations
|
||||
|
||||
When using JSON schemas:
|
||||
|
||||
@@ -65,13 +65,16 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
oauth2Token: initialData.config.openapi.security?.oauth2?.token || '',
|
||||
// OpenID Connect initialization
|
||||
openIdConnectUrl: initialData.config.openapi.security?.openIdConnect?.url || '',
|
||||
openIdConnectToken: initialData.config.openapi.security?.openIdConnect?.token || ''
|
||||
openIdConnectToken: initialData.config.openapi.security?.openIdConnect?.token || '',
|
||||
// Passthrough headers initialization
|
||||
passthroughHeaders: initialData.config.openapi.passthroughHeaders ? initialData.config.openapi.passthroughHeaders.join(', ') : '',
|
||||
} : {
|
||||
inputMode: 'url',
|
||||
url: '',
|
||||
schema: '',
|
||||
version: '3.1.0',
|
||||
securityType: 'none'
|
||||
securityType: 'none',
|
||||
passthroughHeaders: '',
|
||||
}
|
||||
})
|
||||
|
||||
@@ -235,6 +238,14 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
};
|
||||
}
|
||||
|
||||
// Add passthrough headers if provided
|
||||
if (formData.openapi?.passthroughHeaders && formData.openapi.passthroughHeaders.trim()) {
|
||||
openapi.passthroughHeaders = formData.openapi.passthroughHeaders
|
||||
.split(',')
|
||||
.map(header => header.trim())
|
||||
.filter(header => header.length > 0);
|
||||
}
|
||||
|
||||
return openapi;
|
||||
})(),
|
||||
...(Object.keys(headers).length > 0 ? { headers } : {})
|
||||
@@ -616,6 +627,24 @@ const ServerForm = ({ onSubmit, onCancel, initialData = null, modalTitle, formEr
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Passthrough Headers Configuration */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
||||
{t('server.openapi.passthroughHeaders')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.openapi?.passthroughHeaders || ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
openapi: { ...prev.openapi, passthroughHeaders: e.target.value, url: prev.openapi?.url || '' }
|
||||
}))}
|
||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline form-input"
|
||||
placeholder="Authorization, X-API-Key, X-Custom-Header"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">{t('server.openapi.passthroughHeadersHelp')}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="block text-gray-700 text-sm font-bold">
|
||||
|
||||
@@ -127,6 +127,7 @@ export interface ServerConfig {
|
||||
schema?: Record<string, any>; // Complete OpenAPI JSON schema
|
||||
version?: string; // OpenAPI version (default: '3.1.0')
|
||||
security?: OpenAPISecurityConfig; // Security configuration for API calls
|
||||
passthroughHeaders?: string[]; // Header names to pass through from tool call requests to upstream OpenAPI endpoints
|
||||
};
|
||||
}
|
||||
|
||||
@@ -232,6 +233,8 @@ export interface ServerFormData {
|
||||
openIdConnectClientId?: string;
|
||||
openIdConnectClientSecret?: string;
|
||||
openIdConnectToken?: string;
|
||||
// Passthrough headers
|
||||
passthroughHeaders?: string; // Comma-separated list of header names
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,9 @@
|
||||
"openIdConnectToken": "ID Token",
|
||||
"apiKeyInHeader": "Header",
|
||||
"apiKeyInQuery": "Query",
|
||||
"apiKeyInCookie": "Cookie"
|
||||
"apiKeyInCookie": "Cookie",
|
||||
"passthroughHeaders": "Passthrough Headers",
|
||||
"passthroughHeadersHelp": "Comma-separated list of header names to pass through from tool call requests to upstream OpenAPI endpoints (e.g., Authorization, X-API-Key)"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -159,7 +159,9 @@
|
||||
"openIdConnectToken": "Jeton d'identification",
|
||||
"apiKeyInHeader": "En-tête",
|
||||
"apiKeyInQuery": "Requête",
|
||||
"apiKeyInCookie": "Cookie"
|
||||
"apiKeyInCookie": "Cookie",
|
||||
"passthroughHeaders": "En-têtes de transmission",
|
||||
"passthroughHeadersHelp": "Liste séparée par des virgules des noms d'en-têtes à transmettre des requêtes d'appel d'outils vers les points de terminaison OpenAPI en amont (par ex. : Authorization, X-API-Key)"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
@@ -618,4 +620,4 @@
|
||||
"serverToolsUpdated": "Outils du serveur mis à jour avec succès"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,9 @@
|
||||
"openIdConnectToken": "ID 令牌",
|
||||
"apiKeyInHeader": "请求头",
|
||||
"apiKeyInQuery": "查询",
|
||||
"apiKeyInCookie": "Cookie"
|
||||
"apiKeyInCookie": "Cookie",
|
||||
"passthroughHeaders": "透传请求头",
|
||||
"passthroughHeadersHelp": "要从工具调用请求透传到上游OpenAPI接口的请求头名称列表,用逗号分隔(如:Authorization, X-API-Key)"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -299,7 +299,11 @@ export class OpenAPIClient {
|
||||
return schema;
|
||||
}
|
||||
|
||||
async callTool(toolName: string, args: Record<string, unknown>): Promise<unknown> {
|
||||
async callTool(
|
||||
toolName: string,
|
||||
args: Record<string, unknown>,
|
||||
passthroughHeaders?: Record<string, string>,
|
||||
): Promise<unknown> {
|
||||
const tool = this.tools.find((t) => t.name === toolName);
|
||||
if (!tool) {
|
||||
throw new Error(`Tool '${toolName}' not found`);
|
||||
@@ -340,18 +344,32 @@ export class OpenAPIClient {
|
||||
requestConfig.data = args.body;
|
||||
}
|
||||
|
||||
// Collect all headers to be sent
|
||||
const allHeaders: Record<string, string> = {};
|
||||
|
||||
// Add headers if any header parameters are defined
|
||||
const headerParams = tool.parameters?.filter((p) => p.in === 'header') || [];
|
||||
if (headerParams.length > 0) {
|
||||
requestConfig.headers = {};
|
||||
for (const param of headerParams) {
|
||||
const value = args[param.name];
|
||||
if (value !== undefined) {
|
||||
requestConfig.headers[param.name] = String(value);
|
||||
for (const param of headerParams) {
|
||||
const value = args[param.name];
|
||||
if (value !== undefined) {
|
||||
allHeaders[param.name] = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Add passthrough headers based on configuration
|
||||
if (passthroughHeaders && this.config.openapi?.passthroughHeaders) {
|
||||
for (const headerName of this.config.openapi.passthroughHeaders) {
|
||||
if (passthroughHeaders[headerName]) {
|
||||
allHeaders[headerName] = passthroughHeaders[headerName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set headers if any were collected
|
||||
if (Object.keys(allHeaders).length > 0) {
|
||||
requestConfig.headers = allHeaders;
|
||||
}
|
||||
|
||||
const response = await this.httpClient.request(requestConfig);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
|
||||
@@ -201,6 +201,7 @@ export const executeToolViaOpenAPI = async (req: Request, res: Response): Promis
|
||||
const extra = {
|
||||
sessionId: (req.headers['x-session-id'] as string) || 'openapi-session',
|
||||
server: serverName,
|
||||
headers: req.headers, // Pass all request headers for potential passthrough
|
||||
};
|
||||
|
||||
const result = await handleCallToolRequest(mockRequest, extra);
|
||||
|
||||
@@ -61,6 +61,7 @@ export const callTool = async (req: Request, res: Response): Promise<void> => {
|
||||
const extra = {
|
||||
sessionId: req.headers['x-session-id'] || 'api-session',
|
||||
server: server || undefined,
|
||||
headers: req.headers, // Include request headers for passthrough
|
||||
};
|
||||
|
||||
const result = (await handleCallToolRequest(mockRequest, extra)) as ToolCallResult;
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getGroup } from './sseService.js';
|
||||
import { getServersInGroup, getServerConfigInGroup } from './groupService.js';
|
||||
import { saveToolsAsVectorEmbeddings, searchToolsByVector } from './vectorSearchService.js';
|
||||
import { OpenAPIClient } from '../clients/openapi.js';
|
||||
import { RequestContextService } from './requestContextService.js';
|
||||
import { getDataService } from './services.js';
|
||||
import { getServerDao, ServerConfigWithName } from '../dao/index.js';
|
||||
|
||||
@@ -403,6 +404,7 @@ export const initializeClientsFromSettings = async (
|
||||
prompts: [],
|
||||
createTime: Date.now(),
|
||||
enabled: conf.enabled === undefined ? true : conf.enabled,
|
||||
config: conf, // Store reference to original config for OpenAPI passthrough headers
|
||||
};
|
||||
serverInfos.push(serverInfo);
|
||||
|
||||
@@ -487,6 +489,7 @@ export const initializeClientsFromSettings = async (
|
||||
transport,
|
||||
options: requestOptions,
|
||||
createTime: Date.now(),
|
||||
config: conf, // Store reference to original config
|
||||
};
|
||||
serverInfos.push(serverInfo);
|
||||
|
||||
@@ -1036,7 +1039,34 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
? toolName.replace(`${targetServerInfo.name}-`, '')
|
||||
: toolName;
|
||||
|
||||
const result = await openApiClient.callTool(cleanToolName, finalArgs);
|
||||
// Extract passthrough headers from extra or request context
|
||||
let passthroughHeaders: Record<string, string> | undefined;
|
||||
let requestHeaders: Record<string, string | string[] | undefined> | null = null;
|
||||
|
||||
// Try to get headers from extra parameter first (if available)
|
||||
if (extra?.headers) {
|
||||
requestHeaders = extra.headers;
|
||||
} else {
|
||||
// Fallback to request context service
|
||||
const requestContextService = RequestContextService.getInstance();
|
||||
requestHeaders = requestContextService.getHeaders();
|
||||
}
|
||||
|
||||
if (requestHeaders && targetServerInfo.config?.openapi?.passthroughHeaders) {
|
||||
passthroughHeaders = {};
|
||||
for (const headerName of targetServerInfo.config.openapi.passthroughHeaders) {
|
||||
// Handle different header name cases (Express normalizes headers to lowercase)
|
||||
const headerValue =
|
||||
requestHeaders[headerName] || requestHeaders[headerName.toLowerCase()];
|
||||
if (headerValue) {
|
||||
passthroughHeaders[headerName] = Array.isArray(headerValue)
|
||||
? headerValue[0]
|
||||
: String(headerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await openApiClient.callTool(cleanToolName, finalArgs, passthroughHeaders);
|
||||
|
||||
console.log(`OpenAPI tool invocation result: ${JSON.stringify(result)}`);
|
||||
return {
|
||||
@@ -1099,7 +1129,38 @@ export const handleCallToolRequest = async (request: any, extra: any) => {
|
||||
`Invoking OpenAPI tool '${cleanToolName}' on server '${serverInfo.name}' with arguments: ${JSON.stringify(request.params.arguments)}`,
|
||||
);
|
||||
|
||||
const result = await openApiClient.callTool(cleanToolName, request.params.arguments || {});
|
||||
// Extract passthrough headers from extra or request context
|
||||
let passthroughHeaders: Record<string, string> | undefined;
|
||||
let requestHeaders: Record<string, string | string[] | undefined> | null = null;
|
||||
|
||||
// Try to get headers from extra parameter first (if available)
|
||||
if (extra?.headers) {
|
||||
requestHeaders = extra.headers;
|
||||
} else {
|
||||
// Fallback to request context service
|
||||
const requestContextService = RequestContextService.getInstance();
|
||||
requestHeaders = requestContextService.getHeaders();
|
||||
}
|
||||
|
||||
if (requestHeaders && serverInfo.config?.openapi?.passthroughHeaders) {
|
||||
passthroughHeaders = {};
|
||||
for (const headerName of serverInfo.config.openapi.passthroughHeaders) {
|
||||
// Handle different header name cases (Express normalizes headers to lowercase)
|
||||
const headerValue =
|
||||
requestHeaders[headerName] || requestHeaders[headerName.toLowerCase()];
|
||||
if (headerValue) {
|
||||
passthroughHeaders[headerName] = Array.isArray(headerValue)
|
||||
? headerValue[0]
|
||||
: String(headerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = await openApiClient.callTool(
|
||||
cleanToolName,
|
||||
request.params.arguments || {},
|
||||
passthroughHeaders,
|
||||
);
|
||||
|
||||
console.log(`OpenAPI tool invocation result: ${JSON.stringify(result)}`);
|
||||
return {
|
||||
|
||||
105
src/services/requestContextService.ts
Normal file
105
src/services/requestContextService.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
/**
|
||||
* Request context interface for MCP request handling
|
||||
*/
|
||||
export interface RequestContext {
|
||||
headers: Record<string, string | string[] | undefined>;
|
||||
sessionId?: string;
|
||||
userAgent?: string;
|
||||
remoteAddress?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for managing request context during MCP request processing
|
||||
* This allows MCP request handlers to access HTTP headers and other request metadata
|
||||
*/
|
||||
export class RequestContextService {
|
||||
private static instance: RequestContextService;
|
||||
private requestContext: RequestContext | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
public static getInstance(): RequestContextService {
|
||||
if (!RequestContextService.instance) {
|
||||
RequestContextService.instance = new RequestContextService();
|
||||
}
|
||||
return RequestContextService.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current request context from Express request
|
||||
*/
|
||||
public setRequestContext(req: Request): void {
|
||||
this.requestContext = {
|
||||
headers: req.headers,
|
||||
sessionId: (req.headers['mcp-session-id'] as string) || undefined,
|
||||
userAgent: req.headers['user-agent'] as string,
|
||||
remoteAddress: req.ip || req.socket?.remoteAddress,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set request context from custom data
|
||||
*/
|
||||
public setCustomRequestContext(context: RequestContext): void {
|
||||
this.requestContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current request context
|
||||
*/
|
||||
public getRequestContext(): RequestContext | null {
|
||||
return this.requestContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get headers from the current request context
|
||||
*/
|
||||
public getHeaders(): Record<string, string | string[] | undefined> | null {
|
||||
return this.requestContext?.headers || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific header value (case-insensitive)
|
||||
*/
|
||||
public getHeader(name: string): string | string[] | undefined {
|
||||
if (!this.requestContext?.headers) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Try exact match first
|
||||
if (this.requestContext.headers[name]) {
|
||||
return this.requestContext.headers[name];
|
||||
}
|
||||
|
||||
// Try lowercase match (Express normalizes headers to lowercase)
|
||||
const lowerName = name.toLowerCase();
|
||||
if (this.requestContext.headers[lowerName]) {
|
||||
return this.requestContext.headers[lowerName];
|
||||
}
|
||||
|
||||
// Try case-insensitive search
|
||||
for (const [key, value] of Object.entries(this.requestContext.headers)) {
|
||||
if (key.toLowerCase() === lowerName) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the current request context
|
||||
*/
|
||||
public clearRequestContext(): void {
|
||||
this.requestContext = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session ID from current request context
|
||||
*/
|
||||
public getSessionId(): string | undefined {
|
||||
return this.requestContext?.sessionId;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { deleteMcpServer, getMcpServer } from './mcpService.js';
|
||||
import { loadSettings } from '../config/index.js';
|
||||
import config from '../config/index.js';
|
||||
import { UserContextService } from './userContextService.js';
|
||||
import { RequestContextService } from './requestContextService.js';
|
||||
|
||||
const transports: { [sessionId: string]: { transport: Transport; group: string } } = {};
|
||||
|
||||
@@ -131,7 +132,16 @@ export const handleSseMessage = async (req: Request, res: Response): Promise<voi
|
||||
`Received message for sessionId: ${sessionId} in group: ${group}${username ? ` for user: ${username}` : ''}`,
|
||||
);
|
||||
|
||||
await (transport as SSEServerTransport).handlePostMessage(req, res);
|
||||
// Set request context for MCP handlers to access HTTP headers
|
||||
const requestContextService = RequestContextService.getInstance();
|
||||
requestContextService.setRequestContext(req);
|
||||
|
||||
try {
|
||||
await (transport as SSEServerTransport).handlePostMessage(req, res);
|
||||
} finally {
|
||||
// Clean up request context after handling
|
||||
requestContextService.clearRequestContext();
|
||||
}
|
||||
};
|
||||
|
||||
export const handleMcpPostRequest = async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -202,7 +212,17 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise
|
||||
}
|
||||
|
||||
console.log(`Handling request using transport with type ${transport.constructor.name}`);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
|
||||
// Set request context for MCP handlers to access HTTP headers
|
||||
const requestContextService = RequestContextService.getInstance();
|
||||
requestContextService.setRequestContext(req);
|
||||
|
||||
try {
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} finally {
|
||||
// Clean up request context after handling
|
||||
requestContextService.clearRequestContext();
|
||||
}
|
||||
};
|
||||
|
||||
export const handleMcpOtherRequest = async (req: Request, res: Response) => {
|
||||
|
||||
@@ -186,6 +186,7 @@ export interface ServerConfig {
|
||||
schema?: Record<string, any>; // Complete OpenAPI JSON schema
|
||||
version?: string; // OpenAPI version (default: '3.1.0')
|
||||
security?: OpenAPISecurityConfig; // Security configuration for API calls
|
||||
passthroughHeaders?: string[]; // Header names to pass through from tool call requests to upstream OpenAPI endpoints
|
||||
};
|
||||
}
|
||||
|
||||
@@ -236,6 +237,7 @@ export interface ServerInfo {
|
||||
createTime: number; // Timestamp of when the server was created
|
||||
enabled?: boolean; // Flag to indicate if the server is enabled
|
||||
keepAliveIntervalId?: NodeJS.Timeout; // Timer ID for keep-alive ping interval
|
||||
config?: ServerConfig; // Reference to the original server configuration for OpenAPI passthrough headers
|
||||
}
|
||||
|
||||
// Details about a tool available on the server
|
||||
|
||||
141
tests/services/requestContextService.test.ts
Normal file
141
tests/services/requestContextService.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { RequestContextService } from '../../src/services/requestContextService.js';
|
||||
import { Request } from 'express';
|
||||
|
||||
describe('RequestContextService', () => {
|
||||
let service: RequestContextService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = RequestContextService.getInstance();
|
||||
service.clearRequestContext();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
service.clearRequestContext();
|
||||
});
|
||||
|
||||
it('should be a singleton', () => {
|
||||
const service1 = RequestContextService.getInstance();
|
||||
const service2 = RequestContextService.getInstance();
|
||||
expect(service1).toBe(service2);
|
||||
});
|
||||
|
||||
it('should set and get request context from Express request', () => {
|
||||
const mockRequest = {
|
||||
headers: {
|
||||
authorization: 'Bearer test-token',
|
||||
'x-api-key': 'test-api-key',
|
||||
'user-agent': 'test-agent',
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
connection: { remoteAddress: '127.0.0.1' },
|
||||
} as unknown as Request;
|
||||
|
||||
service.setRequestContext(mockRequest);
|
||||
const context = service.getRequestContext();
|
||||
|
||||
expect(context).toBeTruthy();
|
||||
expect(context?.headers).toEqual(mockRequest.headers);
|
||||
expect(context?.userAgent).toBe('test-agent');
|
||||
expect(context?.remoteAddress).toBe('127.0.0.1');
|
||||
});
|
||||
|
||||
it('should get specific headers case-insensitively', () => {
|
||||
const mockRequest = {
|
||||
headers: {
|
||||
authorization: 'Bearer test-token',
|
||||
'X-API-Key': 'test-api-key',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
connection: { remoteAddress: '127.0.0.1' },
|
||||
} as unknown as Request;
|
||||
|
||||
service.setRequestContext(mockRequest);
|
||||
|
||||
// Test exact match
|
||||
expect(service.getHeader('authorization')).toBe('Bearer test-token');
|
||||
expect(service.getHeader('X-API-Key')).toBe('test-api-key');
|
||||
|
||||
// Test case-insensitive match
|
||||
expect(service.getHeader('Authorization')).toBe('Bearer test-token');
|
||||
expect(service.getHeader('x-api-key')).toBe('test-api-key');
|
||||
expect(service.getHeader('CONTENT-TYPE')).toBe('application/json');
|
||||
|
||||
// Test non-existent header
|
||||
expect(service.getHeader('non-existent')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle array header values', () => {
|
||||
const mockRequest = {
|
||||
headers: {
|
||||
accept: ['application/json', 'text/html'],
|
||||
authorization: 'Bearer test-token',
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
connection: { remoteAddress: '127.0.0.1' },
|
||||
} as unknown as Request;
|
||||
|
||||
service.setRequestContext(mockRequest);
|
||||
|
||||
const acceptHeader = service.getHeader('accept');
|
||||
expect(acceptHeader).toEqual(['application/json', 'text/html']);
|
||||
|
||||
const authHeader = service.getHeader('authorization');
|
||||
expect(authHeader).toBe('Bearer test-token');
|
||||
});
|
||||
|
||||
it('should extract session ID from mcp-session-id header', () => {
|
||||
const mockRequest = {
|
||||
headers: {
|
||||
'mcp-session-id': 'test-session-123',
|
||||
authorization: 'Bearer test-token',
|
||||
},
|
||||
ip: '127.0.0.1',
|
||||
connection: { remoteAddress: '127.0.0.1' },
|
||||
} as unknown as Request;
|
||||
|
||||
service.setRequestContext(mockRequest);
|
||||
|
||||
expect(service.getSessionId()).toBe('test-session-123');
|
||||
});
|
||||
|
||||
it('should handle custom request context', () => {
|
||||
const customContext = {
|
||||
headers: {
|
||||
'custom-header': 'custom-value',
|
||||
authorization: 'Bearer custom-token',
|
||||
},
|
||||
sessionId: 'custom-session',
|
||||
userAgent: 'custom-agent',
|
||||
remoteAddress: '192.168.1.1',
|
||||
};
|
||||
|
||||
service.setCustomRequestContext(customContext);
|
||||
const context = service.getRequestContext();
|
||||
|
||||
expect(context).toEqual(customContext);
|
||||
expect(service.getHeader('custom-header')).toBe('custom-value');
|
||||
expect(service.getSessionId()).toBe('custom-session');
|
||||
});
|
||||
|
||||
it('should return null when no context is set', () => {
|
||||
expect(service.getRequestContext()).toBeNull();
|
||||
expect(service.getHeaders()).toBeNull();
|
||||
expect(service.getHeader('any-header')).toBeUndefined();
|
||||
expect(service.getSessionId()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should clear request context', () => {
|
||||
const mockRequest = {
|
||||
headers: { authorization: 'Bearer test-token' },
|
||||
ip: '127.0.0.1',
|
||||
connection: { remoteAddress: '127.0.0.1' },
|
||||
} as unknown as Request;
|
||||
|
||||
service.setRequestContext(mockRequest);
|
||||
expect(service.getRequestContext()).toBeTruthy();
|
||||
|
||||
service.clearRequestContext();
|
||||
expect(service.getRequestContext()).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user