2025-03-28 15:21:10 -04:00
|
|
|
import { CallToolResultSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
2025-03-20 22:56:57 -04:00
|
|
|
import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js';
|
2025-03-18 23:16:45 -04:00
|
|
|
import type { JsonSchemaType, MCPOptions } from 'librechat-data-provider';
|
2024-12-17 13:12:57 -05:00
|
|
|
import type { Logger } from 'winston';
|
🧠 feat: User Memories for Conversational Context (#7760)
* 🧠 feat: User Memories for Conversational Context
chore: mcp typing, use `t`
WIP: first pass, Memories UI
- Added MemoryViewer component for displaying, editing, and deleting user memories.
- Integrated data provider hooks for fetching, updating, and deleting memories.
- Implemented pagination and loading states for better user experience.
- Created unit tests for MemoryViewer to ensure functionality and interaction with data provider.
- Updated translation files to include new UI strings related to memories.
chore: move mcp-related files to own directory
chore: rename librechat-mcp to librechat-api
WIP: first pass, memory processing and data schemas
chore: linting in fileSearch.js query description
chore: rename librechat-api to @librechat/api across the project
WIP: first pass, functional memory agent
feat: add MemoryEditDialog and MemoryViewer components for managing user memories
- Introduced MemoryEditDialog for editing memory entries with validation and toast notifications.
- Updated MemoryViewer to support editing and deleting memories, including pagination and loading states.
- Enhanced data provider to handle memory updates with optional original key for better management.
- Added new localization strings for memory-related UI elements.
feat: add memory permissions management
- Implemented memory permissions in the backend, allowing roles to have specific permissions for using, creating, updating, and reading memories.
- Added new API endpoints for updating memory permissions associated with roles.
- Created a new AdminSettings component for managing memory permissions in the frontend.
- Integrated memory permissions into the existing roles and permissions schemas.
- Updated the interface to include memory settings and permissions.
- Enhanced the MemoryViewer component to conditionally render admin settings based on user roles.
- Added localization support for memory permissions in the translation files.
feat: move AdminSettings component to a new position in MemoryViewer for better visibility
refactor: clean up commented code in MemoryViewer component
feat: enhance MemoryViewer with search functionality and improve MemoryEditDialog integration
- Added a search input to filter memories in the MemoryViewer component.
- Refactored MemoryEditDialog to accept children for better customization.
- Updated MemoryViewer to utilize the new EditMemoryButton and DeleteMemoryButton components for editing and deleting memories.
- Improved localization support by adding new strings for memory filtering and deletion confirmation.
refactor: optimize memory filtering in MemoryViewer using match-sorter
- Replaced manual filtering logic with match-sorter for improved search functionality.
- Enhanced performance and readability of the filteredMemories computation.
feat: enhance MemoryEditDialog with triggerRef and improve updateMemory mutation handling
feat: implement access control for MemoryEditDialog and MemoryViewer components
refactor: remove commented out code and create runMemory method
refactor: rename role based files
feat: implement access control for memory usage in AgentClient
refactor: simplify checkVisionRequest method in AgentClient by removing commented-out code
refactor: make `agents` dir in api package
refactor: migrate Azure utilities to TypeScript and consolidate imports
refactor: move sanitizeFilename function to a new file and update imports, add related tests
refactor: update LLM configuration types and consolidate Azure options in the API package
chore: linting
chore: import order
refactor: replace getLLMConfig with getOpenAIConfig and remove unused LLM configuration file
chore: update winston-daily-rotate-file to version 5.0.0 and add object-hash dependency in package-lock.json
refactor: move primeResources and optionalChainWithEmptyCheck functions to resources.ts and update imports
refactor: move createRun function to a new run.ts file and update related imports
fix: ensure safeAttachments is correctly typed as an array of TFile
chore: add node-fetch dependency and refactor fetch-related functions into packages/api/utils, removing the old generators file
refactor: enhance TEndpointOption type by using Pick to streamline endpoint fields and add new properties for model parameters and client options
feat: implement initializeOpenAIOptions function and update OpenAI types for enhanced configuration handling
fix: update types due to new TEndpointOption typing
fix: ensure safe access to group parameters in initializeOpenAIOptions function
fix: remove redundant API key validation comment in initializeOpenAIOptions function
refactor: rename initializeOpenAIOptions to initializeOpenAI for consistency and update related documentation
refactor: decouple req.body fields and tool loading from initializeAgentOptions
chore: linting
refactor: adjust column widths in MemoryViewer for improved layout
refactor: simplify agent initialization by creating loadAgent function and removing unused code
feat: add memory configuration loading and validation functions
WIP: first pass, memory processing with config
feat: implement memory callback and artifact handling
feat: implement memory artifacts display and processing updates
feat: add memory configuration options and schema validation for validKeys
fix: update MemoryEditDialog and MemoryViewer to handle memory state and display improvements
refactor: remove padding from BookmarkTable and MemoryViewer headers for consistent styling
WIP: initial tokenLimit config and move Tokenizer to @librechat/api
refactor: update mongoMeili plugin methods to use callback for better error handling
feat: enhance memory management with token tracking and usage metrics
- Added token counting for memory entries to enforce limits and provide usage statistics.
- Updated memory retrieval and update routes to include total token usage and limit.
- Enhanced MemoryEditDialog and MemoryViewer components to display memory usage and token information.
- Refactored memory processing functions to handle token limits and provide feedback on memory capacity.
feat: implement memory artifact handling in attachment handler
- Enhanced useAttachmentHandler to process memory artifacts when receiving updates.
- Introduced handleMemoryArtifact utility to manage memory updates and deletions.
- Updated query client to reflect changes in memory state based on incoming data.
refactor: restructure web search key extraction logic
- Moved the logic for extracting API keys from the webSearchAuth configuration into a dedicated function, getWebSearchKeys.
- Updated webSearchKeys to utilize the new function for improved clarity and maintainability.
- Prevents build time errors
feat: add personalization settings and memory preferences management
- Introduced a new Personalization tab in settings to manage user memory preferences.
- Implemented API endpoints and client-side logic for updating memory preferences.
- Enhanced user interface components to reflect personalization options and memory usage.
- Updated permissions to allow users to opt out of memory features.
- Added localization support for new settings and messages related to personalization.
style: personalization switch class
feat: add PersonalizationIcon and align Side Panel UI
feat: implement memory creation functionality
- Added a new API endpoint for creating memory entries, including validation for key and value.
- Introduced MemoryCreateDialog component for user interface to facilitate memory creation.
- Integrated token limit checks to prevent exceeding user memory capacity.
- Updated MemoryViewer to include a button for opening the memory creation dialog.
- Enhanced localization support for new messages related to memory creation.
feat: enhance message processing with configurable window size
- Updated AgentClient to use a configurable message window size for processing messages.
- Introduced messageWindowSize option in memory configuration schema with a default value of 5.
- Improved logic for selecting messages to process based on the configured window size.
chore: update librechat-data-provider version to 0.7.87 in package.json and package-lock.json
chore: remove OpenAPIPlugin and its associated tests
chore: remove MIGRATION_README.md as migration tasks are completed
ci: fix backend tests
chore: remove unused translation keys from localization file
chore: remove problematic test file and unused var in AgentClient
chore: remove unused import and import directly for JSDoc
* feat: add api package build stage in Dockerfile for improved modularity
* docs: reorder build steps in contributing guide for clarity
2025-06-07 18:52:22 -04:00
|
|
|
import type * as t from './types';
|
2024-12-17 13:12:57 -05:00
|
|
|
import { formatToolContent } from './parsers';
|
|
|
|
|
import { MCPConnection } from './connection';
|
|
|
|
|
import { CONSTANTS } from './enum';
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
export interface CallToolOptions extends RequestOptions {
|
|
|
|
|
userId?: string;
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-17 13:12:57 -05:00
|
|
|
export class MCPManager {
|
|
|
|
|
private static instance: MCPManager | null = null;
|
2025-03-28 15:21:10 -04:00
|
|
|
/** App-level connections initialized at startup */
|
2024-12-17 13:12:57 -05:00
|
|
|
private connections: Map<string, MCPConnection> = new Map();
|
2025-03-28 15:21:10 -04:00
|
|
|
/** User-specific connections initialized on demand */
|
|
|
|
|
private userConnections: Map<string, Map<string, MCPConnection>> = new Map();
|
|
|
|
|
/** Last activity timestamp for users (not per server) */
|
|
|
|
|
private userLastActivity: Map<string, number> = new Map();
|
|
|
|
|
private readonly USER_CONNECTION_IDLE_TIMEOUT = 15 * 60 * 1000; // 15 minutes (TODO: make configurable)
|
|
|
|
|
private mcpConfigs: t.MCPServers = {};
|
|
|
|
|
private processMCPEnv?: (obj: MCPOptions, userId?: string) => MCPOptions; // Store the processing function
|
2025-06-06 23:29:17 +02:00
|
|
|
/** Store MCP server instructions */
|
|
|
|
|
private serverInstructions: Map<string, string> = new Map();
|
2024-12-17 13:12:57 -05:00
|
|
|
private logger: Logger;
|
|
|
|
|
|
|
|
|
|
private static getDefaultLogger(): Logger {
|
|
|
|
|
return {
|
|
|
|
|
error: console.error,
|
|
|
|
|
warn: console.warn,
|
|
|
|
|
info: console.info,
|
|
|
|
|
debug: console.debug,
|
|
|
|
|
} as Logger;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private constructor(logger?: Logger) {
|
|
|
|
|
this.logger = logger || MCPManager.getDefaultLogger();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static getInstance(logger?: Logger): MCPManager {
|
|
|
|
|
if (!MCPManager.instance) {
|
|
|
|
|
MCPManager.instance = new MCPManager(logger);
|
|
|
|
|
}
|
2025-03-28 15:21:10 -04:00
|
|
|
// Check for idle connections when getInstance is called
|
|
|
|
|
MCPManager.instance.checkIdleConnections();
|
2024-12-17 13:12:57 -05:00
|
|
|
return MCPManager.instance;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
/** Stores configs and initializes app-level connections */
|
2025-03-18 23:16:45 -04:00
|
|
|
public async initializeMCP(
|
|
|
|
|
mcpServers: t.MCPServers,
|
|
|
|
|
processMCPEnv?: (obj: MCPOptions) => MCPOptions,
|
|
|
|
|
): Promise<void> {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.info('[MCP] Initializing app-level servers');
|
|
|
|
|
this.processMCPEnv = processMCPEnv; // Store the function
|
|
|
|
|
this.mcpConfigs = mcpServers;
|
2024-12-17 13:12:57 -05:00
|
|
|
|
|
|
|
|
const entries = Object.entries(mcpServers);
|
|
|
|
|
const initializedServers = new Set();
|
|
|
|
|
const connectionResults = await Promise.allSettled(
|
2025-03-18 23:16:45 -04:00
|
|
|
entries.map(async ([serverName, _config], i) => {
|
2025-03-28 15:21:10 -04:00
|
|
|
/** Process env for app-level connections */
|
|
|
|
|
const config = this.processMCPEnv ? this.processMCPEnv(_config) : _config;
|
2024-12-17 13:12:57 -05:00
|
|
|
const connection = new MCPConnection(serverName, config, this.logger);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const connectionTimeout = new Promise<void>((_, reject) =>
|
|
|
|
|
setTimeout(() => reject(new Error('Connection timeout')), 30000),
|
|
|
|
|
);
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
const connectionAttempt = this.initializeServer(connection, `[MCP][${serverName}]`);
|
2024-12-17 13:12:57 -05:00
|
|
|
await Promise.race([connectionAttempt, connectionTimeout]);
|
|
|
|
|
|
2025-05-15 12:17:17 -04:00
|
|
|
if (await connection.isConnected()) {
|
2024-12-17 13:12:57 -05:00
|
|
|
initializedServers.add(i);
|
2025-03-28 15:21:10 -04:00
|
|
|
this.connections.set(serverName, connection); // Store in app-level map
|
2024-12-17 13:12:57 -05:00
|
|
|
|
2025-06-06 23:29:17 +02:00
|
|
|
// Handle unified serverInstructions configuration
|
|
|
|
|
const configInstructions = config.serverInstructions;
|
|
|
|
|
|
|
|
|
|
if (configInstructions !== undefined) {
|
|
|
|
|
if (typeof configInstructions === 'string') {
|
|
|
|
|
// Custom instructions provided
|
|
|
|
|
this.serverInstructions.set(serverName, configInstructions);
|
|
|
|
|
this.logger.info(
|
|
|
|
|
`[MCP][${serverName}] Custom instructions stored for context inclusion: ${configInstructions}`,
|
|
|
|
|
);
|
|
|
|
|
} else if (configInstructions === true) {
|
|
|
|
|
// Use server-provided instructions
|
|
|
|
|
const serverInstructions = connection.client.getInstructions();
|
|
|
|
|
|
|
|
|
|
if (serverInstructions) {
|
|
|
|
|
this.serverInstructions.set(serverName, serverInstructions);
|
|
|
|
|
this.logger.info(
|
|
|
|
|
`[MCP][${serverName}] Server instructions stored for context inclusion: ${serverInstructions}`,
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
this.logger.info(
|
|
|
|
|
`[MCP][${serverName}] serverInstructions=true but no server instructions available`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// configInstructions is false - explicitly disabled
|
|
|
|
|
this.logger.info(
|
|
|
|
|
`[MCP][${serverName}] Instructions explicitly disabled (serverInstructions=false)`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.logger.info(
|
|
|
|
|
`[MCP][${serverName}] Instructions not included (serverInstructions not configured)`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2024-12-17 13:12:57 -05:00
|
|
|
const serverCapabilities = connection.client.getServerCapabilities();
|
|
|
|
|
this.logger.info(
|
|
|
|
|
`[MCP][${serverName}] Capabilities: ${JSON.stringify(serverCapabilities)}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (serverCapabilities?.tools) {
|
|
|
|
|
const tools = await connection.client.listTools();
|
|
|
|
|
if (tools.tools.length) {
|
|
|
|
|
this.logger.info(
|
|
|
|
|
`[MCP][${serverName}] Available tools: ${tools.tools
|
|
|
|
|
.map((tool) => tool.name)
|
|
|
|
|
.join(', ')}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.error(`[MCP][${serverName}] Initialization failed`, error);
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const failedConnections = connectionResults.filter(
|
|
|
|
|
(result): result is PromiseRejectedResult => result.status === 'rejected',
|
|
|
|
|
);
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.info(
|
|
|
|
|
`[MCP] Initialized ${initializedServers.size}/${entries.length} app-level server(s)`,
|
|
|
|
|
);
|
2024-12-17 13:12:57 -05:00
|
|
|
|
|
|
|
|
if (failedConnections.length > 0) {
|
|
|
|
|
this.logger.warn(
|
2025-03-28 15:21:10 -04:00
|
|
|
`[MCP] ${failedConnections.length}/${entries.length} app-level server(s) failed to initialize`,
|
2024-12-17 13:12:57 -05:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
entries.forEach(([serverName], index) => {
|
|
|
|
|
if (initializedServers.has(index)) {
|
|
|
|
|
this.logger.info(`[MCP][${serverName}] ✓ Initialized`);
|
|
|
|
|
} else {
|
|
|
|
|
this.logger.info(`[MCP][${serverName}] ✗ Failed`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (initializedServers.size === entries.length) {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.info('[MCP] All app-level servers initialized successfully');
|
2024-12-17 13:12:57 -05:00
|
|
|
} else if (initializedServers.size === 0) {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.warn('[MCP] No app-level servers initialized');
|
2024-12-17 13:12:57 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
/** Generic server initialization logic */
|
|
|
|
|
private async initializeServer(connection: MCPConnection, logPrefix: string): Promise<void> {
|
2024-12-17 13:12:57 -05:00
|
|
|
const maxAttempts = 3;
|
|
|
|
|
let attempts = 0;
|
|
|
|
|
|
|
|
|
|
while (attempts < maxAttempts) {
|
|
|
|
|
try {
|
|
|
|
|
await connection.connect();
|
2025-05-15 12:17:17 -04:00
|
|
|
if (await connection.isConnected()) {
|
2024-12-17 13:12:57 -05:00
|
|
|
return;
|
|
|
|
|
}
|
2025-03-28 15:21:10 -04:00
|
|
|
throw new Error('Connection attempt succeeded but status is not connected');
|
2024-12-17 13:12:57 -05:00
|
|
|
} catch (error) {
|
|
|
|
|
attempts++;
|
|
|
|
|
if (attempts === maxAttempts) {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.error(`${logPrefix} Failed to connect after ${maxAttempts} attempts`, error);
|
|
|
|
|
throw error; // Re-throw the last error
|
2024-12-17 13:12:57 -05:00
|
|
|
}
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 2000 * attempts));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
/** Check for and disconnect idle connections */
|
2025-04-12 18:46:36 -04:00
|
|
|
private checkIdleConnections(currentUserId?: string): void {
|
2025-03-28 15:21:10 -04:00
|
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
|
|
// Iterate through all users to check for idle ones
|
|
|
|
|
for (const [userId, lastActivity] of this.userLastActivity.entries()) {
|
2025-04-12 18:46:36 -04:00
|
|
|
if (currentUserId && currentUserId === userId) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-03-28 15:21:10 -04:00
|
|
|
if (now - lastActivity > this.USER_CONNECTION_IDLE_TIMEOUT) {
|
|
|
|
|
this.logger.info(
|
|
|
|
|
`[MCP][User: ${userId}] User idle for too long. Disconnecting all connections...`,
|
|
|
|
|
);
|
|
|
|
|
// Disconnect all user connections asynchronously (fire and forget)
|
|
|
|
|
this.disconnectUserConnections(userId).catch((err) =>
|
|
|
|
|
this.logger.error(`[MCP][User: ${userId}] Error disconnecting idle connections:`, err),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Updates the last activity timestamp for a user */
|
|
|
|
|
private updateUserLastActivity(userId: string): void {
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
this.userLastActivity.set(userId, now);
|
|
|
|
|
this.logger.debug(
|
|
|
|
|
`[MCP][User: ${userId}] Updated last activity timestamp: ${new Date(now).toISOString()}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Gets or creates a connection for a specific user */
|
|
|
|
|
public async getUserConnection(userId: string, serverName: string): Promise<MCPConnection> {
|
|
|
|
|
const userServerMap = this.userConnections.get(userId);
|
|
|
|
|
let connection = userServerMap?.get(serverName);
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
|
|
|
|
|
// Check if user is idle
|
|
|
|
|
const lastActivity = this.userLastActivity.get(userId);
|
|
|
|
|
if (lastActivity && now - lastActivity > this.USER_CONNECTION_IDLE_TIMEOUT) {
|
|
|
|
|
this.logger.info(
|
|
|
|
|
`[MCP][User: ${userId}] User idle for too long. Disconnecting all connections.`,
|
|
|
|
|
);
|
|
|
|
|
// Disconnect all user connections
|
2025-04-23 18:56:06 -04:00
|
|
|
try {
|
|
|
|
|
await this.disconnectUserConnections(userId);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
this.logger.error(`[MCP][User: ${userId}] Error disconnecting idle connections:`, err);
|
|
|
|
|
}
|
2025-03-28 15:21:10 -04:00
|
|
|
connection = undefined; // Force creation of a new connection
|
|
|
|
|
} else if (connection) {
|
2025-05-15 12:17:17 -04:00
|
|
|
if (await connection.isConnected()) {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.debug(`[MCP][User: ${userId}][${serverName}] Reusing active connection`);
|
|
|
|
|
// Update timestamp on reuse
|
|
|
|
|
this.updateUserLastActivity(userId);
|
|
|
|
|
return connection;
|
|
|
|
|
} else {
|
|
|
|
|
// Connection exists but is not connected, attempt to remove potentially stale entry
|
|
|
|
|
this.logger.warn(
|
|
|
|
|
`[MCP][User: ${userId}][${serverName}] Found existing but disconnected connection object. Cleaning up.`,
|
|
|
|
|
);
|
|
|
|
|
this.removeUserConnection(userId, serverName); // Clean up maps
|
|
|
|
|
connection = undefined;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If no valid connection exists, create a new one
|
|
|
|
|
if (!connection) {
|
|
|
|
|
this.logger.info(`[MCP][User: ${userId}][${serverName}] Establishing new connection`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let config = this.mcpConfigs[serverName];
|
|
|
|
|
if (!config) {
|
|
|
|
|
throw new McpError(
|
|
|
|
|
ErrorCode.InvalidRequest,
|
|
|
|
|
`[MCP][User: ${userId}] Configuration for server "${serverName}" not found.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.processMCPEnv) {
|
|
|
|
|
config = { ...(this.processMCPEnv(config, userId) ?? {}) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connection = new MCPConnection(serverName, config, this.logger, userId);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const connectionTimeout = new Promise<void>((_, reject) =>
|
|
|
|
|
setTimeout(() => reject(new Error('Connection timeout')), 30000),
|
|
|
|
|
);
|
|
|
|
|
const connectionAttempt = this.initializeServer(
|
|
|
|
|
connection,
|
|
|
|
|
`[MCP][User: ${userId}][${serverName}]`,
|
|
|
|
|
);
|
|
|
|
|
await Promise.race([connectionAttempt, connectionTimeout]);
|
|
|
|
|
|
2025-05-15 12:17:17 -04:00
|
|
|
if (!(await connection.isConnected())) {
|
2025-03-28 15:21:10 -04:00
|
|
|
throw new Error('Failed to establish connection after initialization attempt.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!this.userConnections.has(userId)) {
|
|
|
|
|
this.userConnections.set(userId, new Map());
|
|
|
|
|
}
|
|
|
|
|
this.userConnections.get(userId)?.set(serverName, connection);
|
|
|
|
|
this.logger.info(`[MCP][User: ${userId}][${serverName}] Connection successfully established`);
|
|
|
|
|
// Update timestamp on creation
|
|
|
|
|
this.updateUserLastActivity(userId);
|
|
|
|
|
return connection;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this.logger.error(
|
|
|
|
|
`[MCP][User: ${userId}][${serverName}] Failed to establish connection`,
|
|
|
|
|
error,
|
|
|
|
|
);
|
|
|
|
|
// Ensure partial connection state is cleaned up if initialization fails
|
|
|
|
|
await connection.disconnect().catch((disconnectError) => {
|
|
|
|
|
this.logger.error(
|
|
|
|
|
`[MCP][User: ${userId}][${serverName}] Error during cleanup after failed connection`,
|
|
|
|
|
disconnectError,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
// Ensure cleanup even if connection attempt fails
|
|
|
|
|
this.removeUserConnection(userId, serverName);
|
|
|
|
|
throw error; // Re-throw the error to the caller
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Removes a specific user connection entry */
|
|
|
|
|
private removeUserConnection(userId: string, serverName: string): void {
|
|
|
|
|
// Remove connection object
|
|
|
|
|
const userMap = this.userConnections.get(userId);
|
|
|
|
|
if (userMap) {
|
|
|
|
|
userMap.delete(serverName);
|
|
|
|
|
if (userMap.size === 0) {
|
|
|
|
|
this.userConnections.delete(userId);
|
|
|
|
|
// Only remove user activity timestamp if all connections are gone
|
|
|
|
|
this.userLastActivity.delete(userId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.logger.debug(`[MCP][User: ${userId}][${serverName}] Removed connection entry.`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Disconnects and removes a specific user connection */
|
|
|
|
|
public async disconnectUserConnection(userId: string, serverName: string): Promise<void> {
|
|
|
|
|
const userMap = this.userConnections.get(userId);
|
|
|
|
|
const connection = userMap?.get(serverName);
|
|
|
|
|
if (connection) {
|
|
|
|
|
this.logger.info(`[MCP][User: ${userId}][${serverName}] Disconnecting...`);
|
|
|
|
|
await connection.disconnect();
|
|
|
|
|
this.removeUserConnection(userId, serverName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Disconnects and removes all connections for a specific user */
|
|
|
|
|
public async disconnectUserConnections(userId: string): Promise<void> {
|
|
|
|
|
const userMap = this.userConnections.get(userId);
|
2025-04-23 18:56:06 -04:00
|
|
|
const disconnectPromises: Promise<void>[] = [];
|
2025-03-28 15:21:10 -04:00
|
|
|
if (userMap) {
|
|
|
|
|
this.logger.info(`[MCP][User: ${userId}] Disconnecting all servers...`);
|
2025-04-23 18:56:06 -04:00
|
|
|
const userServers = Array.from(userMap.keys());
|
|
|
|
|
for (const serverName of userServers) {
|
|
|
|
|
disconnectPromises.push(
|
|
|
|
|
this.disconnectUserConnection(userId, serverName).catch((error) => {
|
|
|
|
|
this.logger.error(
|
|
|
|
|
`[MCP][User: ${userId}][${serverName}] Error during disconnection:`,
|
|
|
|
|
error,
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-03-28 15:21:10 -04:00
|
|
|
await Promise.allSettled(disconnectPromises);
|
|
|
|
|
// Ensure user activity timestamp is removed
|
|
|
|
|
this.userLastActivity.delete(userId);
|
|
|
|
|
this.logger.info(`[MCP][User: ${userId}] All connections processed for disconnection.`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Returns the app-level connection (used for mapping tools, etc.) */
|
2024-12-17 13:12:57 -05:00
|
|
|
public getConnection(serverName: string): MCPConnection | undefined {
|
|
|
|
|
return this.connections.get(serverName);
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
/** Returns all app-level connections */
|
2024-12-17 13:12:57 -05:00
|
|
|
public getAllConnections(): Map<string, MCPConnection> {
|
|
|
|
|
return this.connections;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
/**
|
|
|
|
|
* Maps available tools from all app-level connections into the provided object.
|
|
|
|
|
* The object is modified in place.
|
|
|
|
|
*/
|
2024-12-17 13:12:57 -05:00
|
|
|
public async mapAvailableTools(availableTools: t.LCAvailableTools): Promise<void> {
|
|
|
|
|
for (const [serverName, connection] of this.connections.entries()) {
|
|
|
|
|
try {
|
2025-05-15 12:17:17 -04:00
|
|
|
if ((await connection.isConnected()) !== true) {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.warn(
|
|
|
|
|
`[MCP][${serverName}] Connection not established. Skipping tool mapping.`,
|
|
|
|
|
);
|
2024-12-17 13:12:57 -05:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tools = await connection.fetchTools();
|
|
|
|
|
for (const tool of tools) {
|
|
|
|
|
const name = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`;
|
|
|
|
|
availableTools[name] = {
|
|
|
|
|
type: 'function',
|
|
|
|
|
['function']: {
|
|
|
|
|
name,
|
|
|
|
|
description: tool.description,
|
|
|
|
|
parameters: tool.inputSchema as JsonSchemaType,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.warn(`[MCP][${serverName}] Error fetching tools for mapping:`, error);
|
2024-12-17 13:12:57 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
/**
|
|
|
|
|
* Loads tools from all app-level connections into the manifest.
|
|
|
|
|
*/
|
2025-05-08 12:12:36 -04:00
|
|
|
public async loadManifestTools(manifestTools: t.LCToolManifest): Promise<t.LCToolManifest> {
|
|
|
|
|
const mcpTools: t.LCManifestTool[] = [];
|
|
|
|
|
|
2024-12-17 13:12:57 -05:00
|
|
|
for (const [serverName, connection] of this.connections.entries()) {
|
|
|
|
|
try {
|
2025-05-15 12:17:17 -04:00
|
|
|
if ((await connection.isConnected()) !== true) {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.warn(
|
|
|
|
|
`[MCP][${serverName}] Connection not established. Skipping manifest loading.`,
|
|
|
|
|
);
|
2024-12-17 13:12:57 -05:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tools = await connection.fetchTools();
|
|
|
|
|
for (const tool of tools) {
|
|
|
|
|
const pluginKey = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`;
|
2025-05-08 12:12:36 -04:00
|
|
|
const manifestTool: t.LCManifestTool = {
|
2024-12-17 13:12:57 -05:00
|
|
|
name: tool.name,
|
|
|
|
|
pluginKey,
|
|
|
|
|
description: tool.description ?? '',
|
|
|
|
|
icon: connection.iconPath,
|
2025-05-08 12:12:36 -04:00
|
|
|
};
|
|
|
|
|
const config = this.mcpConfigs[serverName];
|
|
|
|
|
if (config?.chatMenu === false) {
|
|
|
|
|
manifestTool.chatMenu = false;
|
|
|
|
|
}
|
|
|
|
|
mcpTools.push(manifestTool);
|
2024-12-17 13:12:57 -05:00
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.error(`[MCP][${serverName}] Error fetching tools for manifest:`, error);
|
2024-12-17 13:12:57 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-05-08 12:12:36 -04:00
|
|
|
|
|
|
|
|
return [...mcpTools, ...manifestTools];
|
2024-12-17 13:12:57 -05:00
|
|
|
}
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2025-03-20 22:56:57 -04:00
|
|
|
async callTool({
|
|
|
|
|
serverName,
|
|
|
|
|
toolName,
|
|
|
|
|
provider,
|
|
|
|
|
toolArguments,
|
|
|
|
|
options,
|
|
|
|
|
}: {
|
|
|
|
|
serverName: string;
|
|
|
|
|
toolName: string;
|
|
|
|
|
provider: t.Provider;
|
|
|
|
|
toolArguments?: Record<string, unknown>;
|
2025-03-28 15:21:10 -04:00
|
|
|
options?: CallToolOptions;
|
2025-03-20 22:56:57 -04:00
|
|
|
}): Promise<t.FormattedToolResponse> {
|
2025-03-28 15:21:10 -04:00
|
|
|
let connection: MCPConnection | undefined;
|
|
|
|
|
const { userId, ...callOptions } = options ?? {};
|
|
|
|
|
const logPrefix = userId ? `[MCP][User: ${userId}][${serverName}]` : `[MCP][${serverName}]`;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (userId) {
|
|
|
|
|
this.updateUserLastActivity(userId);
|
|
|
|
|
// Get or create user-specific connection
|
|
|
|
|
connection = await this.getUserConnection(userId, serverName);
|
|
|
|
|
} else {
|
|
|
|
|
// Use app-level connection
|
|
|
|
|
connection = this.connections.get(serverName);
|
|
|
|
|
if (!connection) {
|
|
|
|
|
throw new McpError(
|
|
|
|
|
ErrorCode.InvalidRequest,
|
|
|
|
|
`${logPrefix} No app-level connection found. Cannot execute tool ${toolName}.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-15 12:17:17 -04:00
|
|
|
if (!(await connection.isConnected())) {
|
2025-03-28 15:21:10 -04:00
|
|
|
// This might happen if getUserConnection failed silently or app connection dropped
|
|
|
|
|
throw new McpError(
|
|
|
|
|
ErrorCode.InternalError, // Use InternalError for connection issues
|
|
|
|
|
`${logPrefix} Connection is not active. Cannot execute tool ${toolName}.`,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = await connection.client.request(
|
|
|
|
|
{
|
|
|
|
|
method: 'tools/call',
|
|
|
|
|
params: {
|
|
|
|
|
name: toolName,
|
|
|
|
|
arguments: toolArguments,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
CallToolResultSchema,
|
|
|
|
|
{
|
|
|
|
|
timeout: connection.timeout,
|
|
|
|
|
...callOptions,
|
|
|
|
|
},
|
2024-12-17 13:12:57 -05:00
|
|
|
);
|
2025-03-28 15:21:10 -04:00
|
|
|
if (userId) {
|
|
|
|
|
this.updateUserLastActivity(userId);
|
|
|
|
|
}
|
|
|
|
|
this.checkIdleConnections();
|
|
|
|
|
return formatToolContent(result, provider);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Log with context and re-throw or handle as needed
|
|
|
|
|
this.logger.error(`${logPrefix}[${toolName}] Tool call failed`, error);
|
|
|
|
|
// Rethrowing allows the caller (createMCPTool) to handle the final user message
|
|
|
|
|
throw error;
|
2024-12-17 13:12:57 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
/** Disconnects a specific app-level server */
|
2024-12-17 13:12:57 -05:00
|
|
|
public async disconnectServer(serverName: string): Promise<void> {
|
|
|
|
|
const connection = this.connections.get(serverName);
|
|
|
|
|
if (connection) {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.info(`[MCP][${serverName}] Disconnecting...`);
|
2024-12-17 13:12:57 -05:00
|
|
|
await connection.disconnect();
|
|
|
|
|
this.connections.delete(serverName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
/** Disconnects all app-level and user-level connections */
|
2024-12-17 13:12:57 -05:00
|
|
|
public async disconnectAll(): Promise<void> {
|
2025-03-28 15:21:10 -04:00
|
|
|
this.logger.info('[MCP] Disconnecting all app-level and user-level connections...');
|
|
|
|
|
|
|
|
|
|
const userDisconnectPromises = Array.from(this.userConnections.keys()).map((userId) =>
|
|
|
|
|
this.disconnectUserConnections(userId),
|
2024-12-17 13:12:57 -05:00
|
|
|
);
|
2025-03-28 15:21:10 -04:00
|
|
|
await Promise.allSettled(userDisconnectPromises);
|
|
|
|
|
this.userLastActivity.clear();
|
|
|
|
|
|
|
|
|
|
// Disconnect all app-level connections
|
|
|
|
|
const appDisconnectPromises = Array.from(this.connections.values()).map((connection) =>
|
|
|
|
|
connection.disconnect().catch((error) => {
|
|
|
|
|
this.logger.error(`[MCP][${connection.serverName}] Error during disconnectAll:`, error);
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
await Promise.allSettled(appDisconnectPromises);
|
2024-12-17 13:12:57 -05:00
|
|
|
this.connections.clear();
|
2025-03-28 15:21:10 -04:00
|
|
|
|
|
|
|
|
this.logger.info('[MCP] All connections processed for disconnection.');
|
2024-12-17 13:12:57 -05:00
|
|
|
}
|
|
|
|
|
|
2025-03-28 15:21:10 -04:00
|
|
|
/** Destroys the singleton instance and disconnects all connections */
|
2024-12-17 13:12:57 -05:00
|
|
|
public static async destroyInstance(): Promise<void> {
|
|
|
|
|
if (MCPManager.instance) {
|
|
|
|
|
await MCPManager.instance.disconnectAll();
|
|
|
|
|
MCPManager.instance = null;
|
2025-03-28 15:21:10 -04:00
|
|
|
const logger = MCPManager.getDefaultLogger();
|
|
|
|
|
logger.info('[MCP] Manager instance destroyed.');
|
2024-12-17 13:12:57 -05:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-06 23:29:17 +02:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get instructions for MCP servers
|
|
|
|
|
* @param serverNames Optional array of server names. If not provided or empty, returns all servers.
|
|
|
|
|
* @returns Object mapping server names to their instructions
|
|
|
|
|
*/
|
|
|
|
|
public getInstructions(serverNames?: string[]): Record<string, string> {
|
|
|
|
|
const instructions: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
if (!serverNames || serverNames.length === 0) {
|
|
|
|
|
// Return all instructions if no specific servers requested
|
|
|
|
|
for (const [serverName, serverInstructions] of this.serverInstructions.entries()) {
|
|
|
|
|
instructions[serverName] = serverInstructions;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Return instructions for specific servers
|
|
|
|
|
for (const serverName of serverNames) {
|
|
|
|
|
const serverInstructions = this.serverInstructions.get(serverName);
|
|
|
|
|
if (serverInstructions) {
|
|
|
|
|
instructions[serverName] = serverInstructions;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return instructions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Format MCP server instructions for injection into context
|
|
|
|
|
* @param serverNames Optional array of server names to include. If not provided, includes all servers.
|
|
|
|
|
* @returns Formatted instructions string ready for context injection
|
|
|
|
|
*/
|
|
|
|
|
public formatInstructionsForContext(serverNames?: string[]): string {
|
|
|
|
|
/** Instructions for specified servers or all stored instructions */
|
|
|
|
|
const instructionsToInclude = this.getInstructions(serverNames);
|
|
|
|
|
|
|
|
|
|
if (Object.keys(instructionsToInclude).length === 0) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Format instructions for context injection
|
|
|
|
|
const formattedInstructions = Object.entries(instructionsToInclude)
|
|
|
|
|
.map(([serverName, instructions]) => {
|
|
|
|
|
return `## ${serverName} MCP Server Instructions
|
|
|
|
|
|
|
|
|
|
${instructions}`;
|
|
|
|
|
})
|
|
|
|
|
.join('\n\n');
|
|
|
|
|
|
|
|
|
|
return `# MCP Server Instructions
|
|
|
|
|
|
|
|
|
|
The following MCP servers are available with their specific instructions:
|
|
|
|
|
|
|
|
|
|
${formattedInstructions}
|
|
|
|
|
|
|
|
|
|
Please follow these instructions when using tools from the respective MCP servers.`;
|
|
|
|
|
}
|
2024-12-17 13:12:57 -05:00
|
|
|
}
|