mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
* 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>
198 lines
7.6 KiB
TypeScript
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}]`;
|
|
}
|
|
}
|