LibreChat/packages/api/src/mcp/__tests__/customUserVars.integration.test.ts
crossagent 290984c514
Some checks failed
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled
🔑 fix: Type-Safe User Context Forwarding for Non-OAuth Tool Discovery (#12348)
* fix(mcp): pass missing customUserVars and user during unauthenticated tool discovery

* fix(mcp): type-safe user context forwarding for non-OAuth tool discovery

Extract UserConnectionContext from OAuthConnectionOptions to properly
model the non-OAuth case where user/customUserVars/requestBody need
placeholder resolution without requiring OAuth-specific fields.

- Remove prohibited `as unknown as` double-cast
- Forward requestBody and connectionTimeout (previously omitted)
- Add unit tests for argument forwarding at Manager and Factory layers
- Add integration test exercising real processMCPEnv substitution

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
2026-03-21 12:46:23 -04:00

74 lines
2.5 KiB
TypeScript

/**
* Integration test exercising real processMCPEnv for the non-OAuth
* customUserVars scenario: a streamable-http server whose URL contains
* a {{PLACEHOLDER}} that must be resolved from per-user custom variables.
*
* This is the exact bug scenario from PR #12348 — without the fix,
* the literal string `{{MY_CUSTOM_KEY}}` would be sent to the MCP
* server endpoint instead of the substituted value.
*/
import type { IUser } from '@librechat/data-schemas';
import type * as t from '~/mcp/types';
import { processMCPEnv } from '~/utils/env';
describe('processMCPEnv — customUserVars placeholder resolution', () => {
const mockUser = { id: 'user-abc', email: 'test@example.com' } as IUser;
it('should resolve {{CUSTOM_VAR}} in a streamable-http URL', () => {
const serverConfig: t.MCPOptions = {
type: 'streamable-http',
url: 'https://my-mcp.server.com/server?key={{MY_CUSTOM_KEY}}',
} as t.MCPOptions;
const result = processMCPEnv({
options: serverConfig,
user: mockUser,
customUserVars: { MY_CUSTOM_KEY: 'c527bd0abc123' },
});
expect((result as t.StreamableHTTPOptions).url).toBe(
'https://my-mcp.server.com/server?key=c527bd0abc123',
);
});
it('should resolve multiple placeholders in URL and headers simultaneously', () => {
const serverConfig: t.MCPOptions = {
type: 'streamable-http',
url: 'https://my-mcp.server.com/server?key={{API_KEY}}&project={{PROJECT_ID}}',
headers: {
Authorization: 'Bearer {{AUTH_TOKEN}}',
'X-Project': '{{PROJECT_ID}}',
},
} as t.MCPOptions;
const result = processMCPEnv({
options: serverConfig,
user: mockUser,
customUserVars: {
API_KEY: 'key-123',
PROJECT_ID: 'proj-456',
AUTH_TOKEN: 'tok-789',
},
});
const typed = result as t.StreamableHTTPOptions;
expect(typed.url).toBe('https://my-mcp.server.com/server?key=key-123&project=proj-456');
expect(typed.headers).toEqual({
Authorization: 'Bearer tok-789',
'X-Project': 'proj-456',
});
});
it('should leave unmatched placeholders as literal strings when customUserVars is undefined', () => {
const serverConfig: t.MCPOptions = {
type: 'streamable-http',
url: 'https://my-mcp.server.com/server?key={{MY_CUSTOM_KEY}}',
} as t.MCPOptions;
const result = processMCPEnv({
options: serverConfig,
});
expect((result as t.StreamableHTTPOptions).url).toContain('{{MY_CUSTOM_KEY}}');
});
});