LibreChat/packages/api/src/mcp/MCPServersRegistry.ts
Danny Avila d7d02766ea
🏷️ feat: Request Placeholders for Custom Endpoint & MCP Headers (#9095)
* feat: Add conversation ID support to custom endpoint headers

- Add LIBRECHAT_CONVERSATION_ID to customUserVars when provided
- Pass conversation ID to header resolution for dynamic headers
- Add comprehensive test coverage

Enables custom endpoints to access conversation context using {{LIBRECHAT_CONVERSATION_ID}} placeholder.

* fix: filter out unresolved placeholders from headers (thanks @MrunmayS)

* feat: add support for request body placeholders in custom endpoint headers

- Add {{LIBRECHAT_BODY_*}} placeholders for conversationId, parentMessageId, messageId
- Update tests to reflect new body placeholder functionality

* refactor resolveHeaders

* style: minor styling cleanup

* fix: type error in unit test

* feat: add body to other endpoints

* feat: add body for mcp tool calls

* chore: remove changes that unnecessarily increase scope after clarification of requirements

* refactor: move http.ts to packages/api and have RequestBody intersect with Express request body

* refactor: processMCPEnv now uses single object argument pattern

* refactor: update processMCPEnv to use 'options' parameter and align types across MCP connection classes

* feat: enhance MCP connection handling with dynamic request headers to pass request body fields

---------

Co-authored-by: Gopal Sharma <gopalsharma@gopal.sharma1>
Co-authored-by: s10gopal <36487439+s10gopal@users.noreply.github.com>
Co-authored-by: Dustin Healy <dustinhealy1@gmail.com>
2025-08-16 20:45:55 -04:00

198 lines
7.6 KiB
TypeScript

import pick from 'lodash/pick';
import pickBy from 'lodash/pickBy';
import mapValues from 'lodash/mapValues';
import { logger } from '@librechat/data-schemas';
import type { MCPConnection } from '~/mcp/connection';
import type { JsonSchemaType } from '~/types';
import type * as t from '~/mcp/types';
import { ConnectionsRepository } from '~/mcp/ConnectionsRepository';
import { detectOAuthRequirement } from '~/mcp/oauth';
import { processMCPEnv } from '~/utils';
import { CONSTANTS } from '~/mcp/enum';
/**
* Manages MCP server configurations and metadata discovery.
* Fetches server capabilities, OAuth requirements, and tool definitions for registry.
* Determines which servers are for app-level connections.
* Has its own connections repository. All connections are disconnected after initialization.
*/
export class MCPServersRegistry {
private initialized: boolean = false;
private connections: ConnectionsRepository;
public readonly rawConfigs: t.MCPServers;
public readonly parsedConfigs: Record<string, t.ParsedServerConfig>;
public oauthServers: Set<string> | null = null;
public serverInstructions: Record<string, string> | null = null;
public toolFunctions: t.LCAvailableTools | null = null;
public appServerConfigs: t.MCPServers | null = null;
constructor(configs: t.MCPServers) {
this.rawConfigs = configs;
this.parsedConfigs = mapValues(configs, (con) => processMCPEnv({ options: con }));
this.connections = new ConnectionsRepository(configs);
}
/** Initializes all startup-enabled servers by gathering their metadata asynchronously */
public async initialize(): Promise<void> {
if (this.initialized) return;
this.initialized = true;
const serverNames = Object.keys(this.parsedConfigs);
await Promise.allSettled(serverNames.map((serverName) => this.gatherServerInfo(serverName)));
this.setOAuthServers();
this.setServerInstructions();
this.setAppServerConfigs();
await this.setAppToolFunctions();
this.connections.disconnectAll();
}
/** Fetches all metadata for a single server in parallel */
private async gatherServerInfo(serverName: string): Promise<void> {
try {
await this.fetchOAuthRequirement(serverName);
const config = this.parsedConfigs[serverName];
if (config.startup !== false && !config.requiresOAuth) {
await Promise.allSettled([
this.fetchServerInstructions(serverName).catch((error) =>
logger.warn(`${this.prefix(serverName)} Failed to fetch server instructions:`, error),
),
this.fetchServerCapabilities(serverName).catch((error) =>
logger.warn(`${this.prefix(serverName)} Failed to fetch server capabilities:`, error),
),
]);
}
this.logUpdatedConfig(serverName);
} catch (error) {
logger.warn(`${this.prefix(serverName)} Failed to initialize server:`, error);
}
}
/** Sets app-level server configs (startup enabled, non-OAuth servers) */
private setAppServerConfigs(): void {
const appServers = Object.keys(
pickBy(
this.parsedConfigs,
(config) => config.startup !== false && config.requiresOAuth === false,
),
);
this.appServerConfigs = pick(this.rawConfigs, appServers);
}
/** Creates set of server names that require OAuth authentication */
private setOAuthServers(): Set<string> {
if (this.oauthServers) return this.oauthServers;
this.oauthServers = new Set(
Object.keys(pickBy(this.parsedConfigs, (config) => config.requiresOAuth)),
);
return this.oauthServers;
}
/** Collects server instructions from all configured servers */
private setServerInstructions(): void {
this.serverInstructions = mapValues(
pickBy(this.parsedConfigs, (config) => config.serverInstructions),
(config) => config.serverInstructions as string,
);
}
/** Builds registry of all available tool functions from loaded connections */
private async setAppToolFunctions(): Promise<void> {
const connections = (await this.connections.getLoaded()).entries();
const allToolFunctions: t.LCAvailableTools = {};
for (const [serverName, conn] of connections) {
try {
const toolFunctions = await this.getToolFunctions(serverName, conn);
Object.assign(allToolFunctions, toolFunctions);
} catch (error) {
logger.warn(`${this.prefix(serverName)} Error fetching tool functions:`, error);
}
}
this.toolFunctions = allToolFunctions;
}
/** Converts server tools to LibreChat-compatible tool functions format */
private async getToolFunctions(
serverName: string,
conn: MCPConnection,
): Promise<t.LCAvailableTools> {
const { tools }: t.MCPToolListResponse = await conn.client.listTools();
const toolFunctions: t.LCAvailableTools = {};
tools.forEach((tool) => {
const name = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`;
toolFunctions[name] = {
type: 'function',
['function']: {
name,
description: tool.description,
parameters: tool.inputSchema as JsonSchemaType,
},
};
});
return toolFunctions;
}
/** Determines if server requires OAuth if not already specified in the config */
private async fetchOAuthRequirement(serverName: string): Promise<boolean> {
const config = this.parsedConfigs[serverName];
if (config.requiresOAuth != null) return config.requiresOAuth;
if (config.url == null) return (config.requiresOAuth = false);
if (config.startup === false) return (config.requiresOAuth = false);
const result = await detectOAuthRequirement(config.url);
config.requiresOAuth = result.requiresOAuth;
config.oauthMetadata = result.metadata;
return config.requiresOAuth;
}
/** Retrieves server instructions from MCP server if enabled in the config */
private async fetchServerInstructions(serverName: string): Promise<void> {
const config = this.parsedConfigs[serverName];
if (!config.serverInstructions) return;
if (typeof config.serverInstructions === 'string') return;
const conn = await this.connections.get(serverName);
config.serverInstructions = conn.client.getInstructions();
if (!config.serverInstructions) {
logger.warn(`${this.prefix(serverName)} No server instructions available`);
}
}
/** Fetches server capabilities and available tools list */
private async fetchServerCapabilities(serverName: string): Promise<void> {
const config = this.parsedConfigs[serverName];
const conn = await this.connections.get(serverName);
const capabilities = conn.client.getServerCapabilities();
if (!capabilities) return;
config.capabilities = JSON.stringify(capabilities);
if (!capabilities.tools) return;
const tools = await conn.client.listTools();
config.tools = tools.tools.map((tool) => tool.name).join(', ');
}
// Logs server configuration summary after initialization
private logUpdatedConfig(serverName: string): void {
const prefix = this.prefix(serverName);
const config = this.parsedConfigs[serverName];
logger.info(`${prefix} -------------------------------------------------┐`);
logger.info(`${prefix} URL: ${config.url}`);
logger.info(`${prefix} OAuth Required: ${config.requiresOAuth}`);
logger.info(`${prefix} Capabilities: ${config.capabilities}`);
logger.info(`${prefix} Tools: ${config.tools}`);
logger.info(`${prefix} Server Instructions: ${config.serverInstructions}`);
logger.info(`${prefix} -------------------------------------------------┘`);
}
// Returns formatted log prefix for server messages
private prefix(serverName: string): string {
return `[MCP][${serverName}]`;
}
}