🪪 feat: Microsoft Graph Access Token Placeholder for MCP Servers (#10867)

* feat: MCP Graph Token env var

* Addressing copilot remarks

* Addressed Copilot review remarks

* Fixed graphtokenservice mock in MCP test suite

* fix: remove unnecessary type check and cast in resolveGraphTokensInRecord

* ci: add Graph Token integration tests in MCPManager

* refactor: update user type definitions to use Partial<IUser> in multiple functions

* test: enhance MCP tests for graph token processing and user placeholder resolution

- Added comprehensive tests to validate the interaction between preProcessGraphTokens and processMCPEnv.
- Ensured correct resolution of graph tokens and user placeholders in various configurations.
- Mocked OIDC utilities to facilitate testing of token extraction and validation.
- Verified that original options remain unchanged after processing.

* chore: import order

* chore: imports

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Max Sanna 2026-01-19 22:35:15 +01:00 committed by Danny Avila
parent ed61b7f967
commit dd4bbd38fc
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
10 changed files with 1411 additions and 15 deletions

View file

@ -3,6 +3,7 @@ import { logger } from '@librechat/data-schemas';
import { CallToolResultSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
import type { TokenMethods, IUser } from '@librechat/data-schemas';
import type { GraphTokenResolver } from '~/utils/graph';
import type { FlowStateManager } from '~/flow/manager';
import type { MCPOAuthTokens } from './oauth';
import type { RequestBody } from '~/types';
@ -12,6 +13,7 @@ import { ConnectionsRepository } from './ConnectionsRepository';
import { MCPServerInspector } from './registry/MCPServerInspector';
import { MCPServersInitializer } from './registry/MCPServersInitializer';
import { MCPServersRegistry } from './registry/MCPServersRegistry';
import { preProcessGraphTokens } from '~/utils/graph';
import { formatToolContent } from './parsers';
import { MCPConnection } from './connection';
import { processMCPEnv } from '~/utils/env';
@ -160,6 +162,10 @@ Please follow these instructions when using tools from the respective MCP server
* Calls a tool on an MCP server, using either a user-specific connection
* (if userId is provided) or an app-level connection. Updates the last activity timestamp
* for user-specific connections upon successful call initiation.
*
* @param graphTokenResolver - Optional function to resolve Graph API tokens via OBO flow.
* When provided and the server config contains `{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}` placeholders,
* they will be resolved to actual Graph API tokens before the tool call.
*/
async callTool({
user,
@ -174,6 +180,7 @@ Please follow these instructions when using tools from the respective MCP server
oauthStart,
oauthEnd,
customUserVars,
graphTokenResolver,
}: {
user?: IUser;
serverName: string;
@ -187,6 +194,7 @@ Please follow these instructions when using tools from the respective MCP server
flowManager: FlowStateManager<MCPOAuthTokens | null>;
oauthStart?: (authURL: string) => Promise<void>;
oauthEnd?: () => Promise<void>;
graphTokenResolver?: GraphTokenResolver;
}): Promise<t.FormattedToolResponse> {
/** User-specific connection */
let connection: MCPConnection | undefined;
@ -220,9 +228,16 @@ Please follow these instructions when using tools from the respective MCP server
serverName,
userId,
)) as t.MCPOptions;
// Pre-process Graph token placeholders (async) before sync processMCPEnv
const graphProcessedConfig = await preProcessGraphTokens(rawConfig, {
user,
graphTokenResolver,
scopes: process.env.GRAPH_API_SCOPES,
});
const currentOptions = processMCPEnv({
user,
options: rawConfig,
options: graphProcessedConfig,
customUserVars: customUserVars,
body: requestBody,
});

View file

@ -1,10 +1,13 @@
import { logger } from '@librechat/data-schemas';
import type { IUser } from '@librechat/data-schemas';
import type { GraphTokenResolver } from '~/utils/graph';
import type * as t from '~/mcp/types';
import { MCPManager } from '~/mcp/MCPManager';
import { MCPServersInitializer } from '~/mcp/registry/MCPServersInitializer';
import { MCPServerInspector } from '~/mcp/registry/MCPServerInspector';
import { ConnectionsRepository } from '~/mcp/ConnectionsRepository';
import { MCPConnection } from '../connection';
import { MCPConnection } from '~/mcp/connection';
import { MCPManager } from '~/mcp/MCPManager';
import * as graphUtils from '~/utils/graph';
// Mock external dependencies
jest.mock('@librechat/data-schemas', () => ({
@ -16,6 +19,15 @@ jest.mock('@librechat/data-schemas', () => ({
},
}));
jest.mock('~/utils/graph', () => ({
...jest.requireActual('~/utils/graph'),
preProcessGraphTokens: jest.fn(),
}));
jest.mock('~/utils/env', () => ({
processMCPEnv: jest.fn((params) => params.options),
}));
const mockRegistryInstance = {
getServerConfig: jest.fn(),
getAllServerConfigs: jest.fn(),
@ -389,4 +401,390 @@ describe('MCPManager', () => {
);
});
});
describe('callTool - Graph Token Integration', () => {
const mockUser: Partial<IUser> = {
id: 'user-123',
provider: 'openid',
openidId: 'oidc-sub-456',
};
const mockFlowManager = {
getState: jest.fn(),
setState: jest.fn(),
clearState: jest.fn(),
};
const mockConnection = {
isConnected: jest.fn().mockResolvedValue(true),
setRequestHeaders: jest.fn(),
timeout: 30000,
client: {
request: jest.fn().mockResolvedValue({
content: [{ type: 'text', text: 'Tool result' }],
isError: false,
}),
},
} as unknown as MCPConnection;
const mockGraphTokenResolver: GraphTokenResolver = jest.fn().mockResolvedValue({
access_token: 'resolved-graph-token',
token_type: 'Bearer',
expires_in: 3600,
scope: 'https://graph.microsoft.com/.default',
});
function createServerConfigWithGraphPlaceholder(): t.SSEOptions {
return {
type: 'sse',
url: 'https://api.example.com',
headers: {
Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}',
'Content-Type': 'application/json',
},
};
}
beforeEach(() => {
jest.clearAllMocks();
// Mock preProcessGraphTokens to simulate token resolution
(graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation(
async (options, graphOptions) => {
if (
options.headers?.Authorization?.includes('{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}') &&
graphOptions.graphTokenResolver
) {
return {
...options,
headers: {
...options.headers,
Authorization: 'Bearer resolved-graph-token',
},
};
}
return options;
},
);
});
it('should call preProcessGraphTokens with graphTokenResolver when provided', async () => {
const serverConfig = createServerConfigWithGraphPlaceholder();
mockAppConnections({
get: jest.fn().mockResolvedValue(mockConnection),
});
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig);
const manager = await MCPManager.createInstance(newMCPServersConfig());
await manager.callTool({
user: mockUser as IUser,
serverName,
toolName: 'test_tool',
provider: 'openai',
flowManager: mockFlowManager as unknown as Parameters<
typeof manager.callTool
>[0]['flowManager'],
graphTokenResolver: mockGraphTokenResolver,
});
expect(graphUtils.preProcessGraphTokens).toHaveBeenCalledWith(
serverConfig,
expect.objectContaining({
user: mockUser,
graphTokenResolver: mockGraphTokenResolver,
}),
);
});
it('should resolve graph token placeholders in headers before tool call', async () => {
const serverConfig = createServerConfigWithGraphPlaceholder();
mockAppConnections({
get: jest.fn().mockResolvedValue(mockConnection),
});
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig);
const manager = await MCPManager.createInstance(newMCPServersConfig());
await manager.callTool({
user: mockUser as IUser,
serverName,
toolName: 'test_tool',
provider: 'openai',
flowManager: mockFlowManager as unknown as Parameters<
typeof manager.callTool
>[0]['flowManager'],
graphTokenResolver: mockGraphTokenResolver,
});
// Verify the connection received the resolved headers
expect(mockConnection.setRequestHeaders).toHaveBeenCalledWith(
expect.objectContaining({
Authorization: 'Bearer resolved-graph-token',
}),
);
});
it('should pass options unchanged when no graphTokenResolver is provided', async () => {
const serverConfig: t.SSEOptions = {
type: 'sse',
url: 'https://api.example.com',
headers: {
Authorization: 'Bearer static-token',
},
};
// Reset mock to return options unchanged
(graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation(
async (options) => options,
);
mockAppConnections({
get: jest.fn().mockResolvedValue(mockConnection),
});
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig);
const manager = await MCPManager.createInstance(newMCPServersConfig());
await manager.callTool({
user: mockUser as IUser,
serverName,
toolName: 'test_tool',
provider: 'openai',
flowManager: mockFlowManager as unknown as Parameters<
typeof manager.callTool
>[0]['flowManager'],
// No graphTokenResolver provided
});
// Verify preProcessGraphTokens was still called (to check for placeholders)
expect(graphUtils.preProcessGraphTokens).toHaveBeenCalledWith(
serverConfig,
expect.objectContaining({
user: mockUser,
graphTokenResolver: undefined,
}),
);
});
it('should handle graph token resolution failure gracefully', async () => {
const serverConfig = createServerConfigWithGraphPlaceholder();
// Simulate resolution failure - returns original value unchanged
(graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation(
async (options) => options,
);
mockAppConnections({
get: jest.fn().mockResolvedValue(mockConnection),
});
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig);
const manager = await MCPManager.createInstance(newMCPServersConfig());
// Should not throw, even when token resolution fails
await expect(
manager.callTool({
user: mockUser as IUser,
serverName,
toolName: 'test_tool',
provider: 'openai',
flowManager: mockFlowManager as unknown as Parameters<
typeof manager.callTool
>[0]['flowManager'],
graphTokenResolver: mockGraphTokenResolver,
}),
).resolves.toBeDefined();
// Headers should contain the unresolved placeholder
expect(mockConnection.setRequestHeaders).toHaveBeenCalledWith(
expect.objectContaining({
Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}',
}),
);
});
it('should resolve graph tokens in env variables', async () => {
const serverConfig: t.StdioOptions = {
type: 'stdio',
command: 'node',
args: ['server.js'],
env: {
GRAPH_TOKEN: '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}',
OTHER_VAR: 'static-value',
},
};
// Mock resolution for env variables
(graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation(async (options) => {
if (options.env?.GRAPH_TOKEN?.includes('{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}')) {
return {
...options,
env: {
...options.env,
GRAPH_TOKEN: 'resolved-graph-token',
},
};
}
return options;
});
mockAppConnections({
get: jest.fn().mockResolvedValue(mockConnection),
});
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig);
const manager = await MCPManager.createInstance(newMCPServersConfig());
await manager.callTool({
user: mockUser as IUser,
serverName,
toolName: 'test_tool',
provider: 'openai',
flowManager: mockFlowManager as unknown as Parameters<
typeof manager.callTool
>[0]['flowManager'],
graphTokenResolver: mockGraphTokenResolver,
});
expect(graphUtils.preProcessGraphTokens).toHaveBeenCalledWith(
serverConfig,
expect.objectContaining({
graphTokenResolver: mockGraphTokenResolver,
}),
);
});
it('should resolve graph tokens in URL', async () => {
const serverConfig: t.SSEOptions = {
type: 'sse',
url: 'https://api.example.com?token={{LIBRECHAT_GRAPH_ACCESS_TOKEN}}',
};
// Mock resolution for URL
(graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation(async (options) => {
if (options.url?.includes('{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}')) {
return {
...options,
url: 'https://api.example.com?token=resolved-graph-token',
};
}
return options;
});
mockAppConnections({
get: jest.fn().mockResolvedValue(mockConnection),
});
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig);
const manager = await MCPManager.createInstance(newMCPServersConfig());
await manager.callTool({
user: mockUser as IUser,
serverName,
toolName: 'test_tool',
provider: 'openai',
flowManager: mockFlowManager as unknown as Parameters<
typeof manager.callTool
>[0]['flowManager'],
graphTokenResolver: mockGraphTokenResolver,
});
expect(graphUtils.preProcessGraphTokens).toHaveBeenCalledWith(
serverConfig,
expect.objectContaining({
graphTokenResolver: mockGraphTokenResolver,
}),
);
});
it('should pass scopes from environment variable to preProcessGraphTokens', async () => {
const originalEnv = process.env.GRAPH_API_SCOPES;
process.env.GRAPH_API_SCOPES = 'custom.scope.read custom.scope.write';
const serverConfig = createServerConfigWithGraphPlaceholder();
mockAppConnections({
get: jest.fn().mockResolvedValue(mockConnection),
});
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig);
const manager = await MCPManager.createInstance(newMCPServersConfig());
await manager.callTool({
user: mockUser as IUser,
serverName,
toolName: 'test_tool',
provider: 'openai',
flowManager: mockFlowManager as unknown as Parameters<
typeof manager.callTool
>[0]['flowManager'],
graphTokenResolver: mockGraphTokenResolver,
});
expect(graphUtils.preProcessGraphTokens).toHaveBeenCalledWith(
serverConfig,
expect.objectContaining({
scopes: 'custom.scope.read custom.scope.write',
}),
);
// Restore environment
if (originalEnv !== undefined) {
process.env.GRAPH_API_SCOPES = originalEnv;
} else {
delete process.env.GRAPH_API_SCOPES;
}
});
it('should work correctly when config has no graph token placeholders', async () => {
const serverConfig: t.SSEOptions = {
type: 'sse',
url: 'https://api.example.com',
headers: {
Authorization: 'Bearer static-token',
},
};
// Mock to return unchanged options when no placeholders
(graphUtils.preProcessGraphTokens as jest.Mock).mockImplementation(
async (options) => options,
);
mockAppConnections({
get: jest.fn().mockResolvedValue(mockConnection),
});
(mockRegistryInstance.getServerConfig as jest.Mock).mockResolvedValue(serverConfig);
const manager = await MCPManager.createInstance(newMCPServersConfig());
const result = await manager.callTool({
user: mockUser as IUser,
serverName,
toolName: 'test_tool',
provider: 'openai',
flowManager: mockFlowManager as unknown as Parameters<
typeof manager.callTool
>[0]['flowManager'],
graphTokenResolver: mockGraphTokenResolver,
});
expect(result).toBeDefined();
expect(mockConnection.setRequestHeaders).toHaveBeenCalledWith(
expect.objectContaining({
Authorization: 'Bearer static-token',
}),
);
});
});
});

View file

@ -4,12 +4,23 @@ import {
StreamableHTTPOptionsSchema,
} from 'librechat-data-provider';
import type { TUser } from 'librechat-data-provider';
import type { IUser } from '@librechat/data-schemas';
import type { GraphTokenResolver } from '~/utils/graph';
import { preProcessGraphTokens } from '~/utils/graph';
import { processMCPEnv } from '~/utils/env';
import * as oidcUtils from '~/utils/oidc';
// Mock oidc utilities for graph token tests
jest.mock('~/utils/oidc', () => ({
...jest.requireActual('~/utils/oidc'),
extractOpenIDTokenInfo: jest.fn(),
isOpenIDTokenValid: jest.fn(),
}));
// Helper function to create test user objects
function createTestUser(
overrides: Partial<TUser> & Record<string, unknown> = {},
): TUser & Record<string, unknown> {
): Omit<TUser, 'createdAt' | 'updatedAt'> | undefined {
return {
id: 'test-user-id',
username: 'testuser',
@ -18,8 +29,6 @@ function createTestUser(
avatar: 'https://example.com/avatar.png',
provider: 'email',
role: 'user',
createdAt: new Date('2021-01-01').toISOString(),
updatedAt: new Date('2021-01-01').toISOString(),
...overrides,
};
}
@ -858,5 +867,270 @@ describe('Environment Variable Extraction (MCP)', () => {
'Content-Type': 'application/json',
});
});
it('should leave {{LIBRECHAT_GRAPH_ACCESS_TOKEN}} unchanged (resolved by preProcessGraphTokens)', () => {
const user = createTestUser({ id: 'user-123' });
const options: MCPOptions = {
type: 'sse',
url: 'https://example.com',
headers: {
Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}',
'X-User-Id': '{{LIBRECHAT_USER_ID}}',
},
};
const result = processMCPEnv({ options, user });
expect('headers' in result && result.headers).toEqual({
// Graph token placeholder remains - it should be resolved by preProcessGraphTokens before processMCPEnv
Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}',
// User ID is resolved by processMCPEnv
'X-User-Id': 'user-123',
});
});
});
describe('preProcessGraphTokens and processMCPEnv working in tandem', () => {
const mockExtractOpenIDTokenInfo = oidcUtils.extractOpenIDTokenInfo as jest.Mock;
const mockIsOpenIDTokenValid = oidcUtils.isOpenIDTokenValid as jest.Mock;
const mockGraphTokenResolver: GraphTokenResolver = jest.fn().mockResolvedValue({
access_token: 'resolved-graph-api-token',
token_type: 'Bearer',
expires_in: 3600,
scope: 'https://graph.microsoft.com/.default',
});
beforeEach(() => {
// Set up mocks to simulate valid OpenID token
mockExtractOpenIDTokenInfo.mockReturnValue({ accessToken: 'test-access-token' });
mockIsOpenIDTokenValid.mockReturnValue(true);
(mockGraphTokenResolver as jest.Mock).mockClear();
});
afterEach(() => {
jest.clearAllMocks();
});
it('should resolve both graph tokens and user placeholders in sequence', async () => {
const user = createTestUser({
id: 'user-123',
email: 'test@example.com',
provider: 'openid',
openidId: 'oidc-sub-456',
}) as unknown as IUser;
const options: MCPOptions = {
type: 'sse',
url: 'https://graph.microsoft.com/v1.0/me',
headers: {
Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}',
'X-User-Id': '{{LIBRECHAT_USER_ID}}',
'X-User-Email': '{{LIBRECHAT_USER_EMAIL}}',
'Content-Type': 'application/json',
},
};
// Step 1: preProcessGraphTokens resolves graph token placeholders (async)
const graphProcessedConfig = await preProcessGraphTokens(options, {
user,
graphTokenResolver: mockGraphTokenResolver,
});
// Step 2: processMCPEnv resolves user and env placeholders (sync)
const finalConfig = processMCPEnv({
options: graphProcessedConfig,
user,
});
expect('headers' in finalConfig && finalConfig.headers).toEqual({
Authorization: 'Bearer resolved-graph-api-token',
'X-User-Id': 'user-123',
'X-User-Email': 'test@example.com',
'Content-Type': 'application/json',
});
});
it('should resolve graph tokens in env and user placeholders in headers', async () => {
const user = createTestUser({
id: 'user-456',
username: 'johndoe',
provider: 'openid',
}) as unknown as IUser;
const options: MCPOptions = {
command: 'node',
args: ['mcp-server.js', '--user', '{{LIBRECHAT_USER_USERNAME}}'],
env: {
GRAPH_ACCESS_TOKEN: '{{LIBRECHAT_GRAPH_ACCESS_TOKEN}}',
USER_ID: '{{LIBRECHAT_USER_ID}}',
API_KEY: '${TEST_API_KEY}',
},
};
// Step 1: preProcessGraphTokens
const graphProcessedConfig = await preProcessGraphTokens(options, {
user,
graphTokenResolver: mockGraphTokenResolver,
});
// Step 2: processMCPEnv
const finalConfig = processMCPEnv({
options: graphProcessedConfig,
user,
});
expect('env' in finalConfig && finalConfig.env).toEqual({
GRAPH_ACCESS_TOKEN: 'resolved-graph-api-token',
USER_ID: 'user-456',
API_KEY: 'test-api-key-value',
});
expect('args' in finalConfig && finalConfig.args).toEqual([
'mcp-server.js',
'--user',
'johndoe',
]);
});
it('should resolve graph tokens in URL alongside other placeholders', async () => {
const user = createTestUser({
id: 'user-789',
provider: 'openid',
}) as unknown as IUser;
const customUserVars = {
TENANT_ID: 'my-tenant-123',
};
const options: MCPOptions = {
type: 'sse',
url: 'https://{{TENANT_ID}}.example.com/api?token={{LIBRECHAT_GRAPH_ACCESS_TOKEN}}&user={{LIBRECHAT_USER_ID}}',
};
// Step 1: preProcessGraphTokens
const graphProcessedConfig = await preProcessGraphTokens(options, {
user,
graphTokenResolver: mockGraphTokenResolver,
});
// Step 2: processMCPEnv with customUserVars
const finalConfig = processMCPEnv({
options: graphProcessedConfig,
user,
customUserVars,
});
expect('url' in finalConfig && finalConfig.url).toBe(
'https://my-tenant-123.example.com/api?token=resolved-graph-api-token&user=user-789',
);
});
it('should handle config with no graph token placeholders (only user placeholders)', async () => {
const user = createTestUser({
id: 'user-abc',
email: 'user@company.com',
}) as unknown as IUser;
const options: MCPOptions = {
type: 'sse',
url: 'https://api.example.com',
headers: {
'X-User-Id': '{{LIBRECHAT_USER_ID}}',
'X-User-Email': '{{LIBRECHAT_USER_EMAIL}}',
},
};
// Step 1: preProcessGraphTokens (no-op since no graph placeholders)
const graphProcessedConfig = await preProcessGraphTokens(options, {
user,
graphTokenResolver: mockGraphTokenResolver,
});
// Step 2: processMCPEnv
const finalConfig = processMCPEnv({
options: graphProcessedConfig,
user,
});
expect('headers' in finalConfig && finalConfig.headers).toEqual({
'X-User-Id': 'user-abc',
'X-User-Email': 'user@company.com',
});
// graphTokenResolver should not have been called
expect(mockGraphTokenResolver).not.toHaveBeenCalled();
});
it('should handle config with only graph token placeholders (no user placeholders)', async () => {
const user = createTestUser({
id: 'user-xyz',
provider: 'openid',
}) as unknown as IUser;
const options: MCPOptions = {
type: 'sse',
url: 'https://graph.microsoft.com/v1.0/me',
headers: {
Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}',
'Content-Type': 'application/json',
},
};
// Reset mock call count
(mockGraphTokenResolver as jest.Mock).mockClear();
// Step 1: preProcessGraphTokens
const graphProcessedConfig = await preProcessGraphTokens(options, {
user,
graphTokenResolver: mockGraphTokenResolver,
});
// Step 2: processMCPEnv
const finalConfig = processMCPEnv({
options: graphProcessedConfig,
user,
});
expect('headers' in finalConfig && finalConfig.headers).toEqual({
Authorization: 'Bearer resolved-graph-api-token',
'Content-Type': 'application/json',
});
});
it('should not mutate original options through the tandem processing', async () => {
const user = createTestUser({
id: 'user-immutable',
provider: 'openid',
}) as unknown as IUser;
const originalOptions: MCPOptions = {
type: 'sse',
url: 'https://api.example.com',
headers: {
Authorization: 'Bearer {{LIBRECHAT_GRAPH_ACCESS_TOKEN}}',
'X-User-Id': '{{LIBRECHAT_USER_ID}}',
},
};
// Store original values
const originalAuth = originalOptions.headers?.Authorization;
const originalUserId = originalOptions.headers?.['X-User-Id'];
// Step 1 & 2: Process through both functions
const graphProcessedConfig = await preProcessGraphTokens(originalOptions, {
user,
graphTokenResolver: mockGraphTokenResolver,
});
processMCPEnv({
options: graphProcessedConfig,
user,
});
// Original should be unchanged
expect(originalOptions.headers?.Authorization).toBe(originalAuth);
expect(originalOptions.headers?.['X-User-Id']).toBe(originalUserId);
});
});
});