mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-22 11:20:15 +01:00
🔎 feat: Add Prompt and Agent Permissions Migration Checks (#9063)
* chore: fix mock typing in packages/api tests * chore: improve imports, type handling and method signatures for MCPServersRegistry * chore: use enum in migration scripts * chore: ParsedServerConfig type to enhance server configuration handling * feat: Implement agent permissions migration check and logging * feat: Integrate migration checks into server initialization process * feat: Add prompt permissions migration check and logging to server initialization * chore: move prompt formatting functions to dedicated prompts dir
This commit is contained in:
parent
e8ddd279fd
commit
e4e25aaf2b
17 changed files with 636 additions and 96 deletions
|
|
@ -1,23 +1,15 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
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 { type MCPConnection } from './connection';
|
||||
import { processMCPEnv } from '~/utils';
|
||||
import { CONSTANTS } from '~/mcp/enum';
|
||||
|
||||
type ParsedServerConfig = t.MCPOptions & {
|
||||
url?: string;
|
||||
requiresOAuth?: boolean;
|
||||
oauthMetadata?: Record<string, unknown> | null;
|
||||
capabilities?: string;
|
||||
tools?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Manages MCP server configurations and metadata discovery.
|
||||
* Fetches server capabilities, OAuth requirements, and tool definitions for registry.
|
||||
|
|
@ -29,7 +21,7 @@ export class MCPServersRegistry {
|
|||
private connections: ConnectionsRepository;
|
||||
|
||||
public readonly rawConfigs: t.MCPServers;
|
||||
public readonly parsedConfigs: Record<string, ParsedServerConfig>;
|
||||
public readonly parsedConfigs: Record<string, t.ParsedServerConfig>;
|
||||
|
||||
public oauthServers: Set<string> | null = null;
|
||||
public serverInstructions: Record<string, string> | null = null;
|
||||
|
|
@ -43,7 +35,7 @@ export class MCPServersRegistry {
|
|||
}
|
||||
|
||||
/** Initializes all startup-enabled servers by gathering their metadata asynchronously */
|
||||
public async initialize() {
|
||||
public async initialize(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
this.initialized = true;
|
||||
|
||||
|
|
@ -59,8 +51,8 @@ export class MCPServersRegistry {
|
|||
this.connections.disconnectAll();
|
||||
}
|
||||
|
||||
// Fetches all metadata for a single server in parallel
|
||||
private async gatherServerInfo(serverName: string) {
|
||||
/** 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];
|
||||
|
|
@ -82,8 +74,8 @@ export class MCPServersRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
// Sets app-level server configs (startup enabled, non-OAuth servers)
|
||||
private setAppServerConfigs() {
|
||||
/** Sets app-level server configs (startup enabled, non-OAuth servers) */
|
||||
private setAppServerConfigs(): void {
|
||||
const appServers = Object.keys(
|
||||
pickBy(
|
||||
this.parsedConfigs,
|
||||
|
|
@ -93,8 +85,8 @@ export class MCPServersRegistry {
|
|||
this.appServerConfigs = pick(this.rawConfigs, appServers);
|
||||
}
|
||||
|
||||
// Creates set of server names that require OAuth authentication
|
||||
private setOAuthServers() {
|
||||
/** 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)),
|
||||
|
|
@ -102,16 +94,16 @@ export class MCPServersRegistry {
|
|||
return this.oauthServers;
|
||||
}
|
||||
|
||||
// Collects server instructions from all configured servers
|
||||
private setServerInstructions() {
|
||||
/** 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() {
|
||||
/** 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) {
|
||||
|
|
@ -125,12 +117,12 @@ export class MCPServersRegistry {
|
|||
this.toolFunctions = allToolFunctions;
|
||||
}
|
||||
|
||||
// Converts server tools to LibreChat-compatible tool functions format
|
||||
/** Converts server tools to LibreChat-compatible tool functions format */
|
||||
private async getToolFunctions(
|
||||
serverName: string,
|
||||
conn: MCPConnection,
|
||||
): Promise<t.LCAvailableTools> {
|
||||
const { tools } = await conn.client.listTools();
|
||||
const { tools }: t.MCPToolListResponse = await conn.client.listTools();
|
||||
|
||||
const toolFunctions: t.LCAvailableTools = {};
|
||||
tools.forEach((tool) => {
|
||||
|
|
@ -148,7 +140,7 @@ export class MCPServersRegistry {
|
|||
return toolFunctions;
|
||||
}
|
||||
|
||||
// Determines if server requires OAuth if not already specified in the config
|
||||
/** 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;
|
||||
|
|
@ -161,8 +153,8 @@ export class MCPServersRegistry {
|
|||
return config.requiresOAuth;
|
||||
}
|
||||
|
||||
// Retrieves server instructions from MCP server if enabled in the config
|
||||
private async fetchServerInstructions(serverName: string) {
|
||||
/** 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;
|
||||
|
|
@ -174,8 +166,8 @@ export class MCPServersRegistry {
|
|||
}
|
||||
}
|
||||
|
||||
// Fetches server capabilities and available tools list
|
||||
private async fetchServerCapabilities(serverName: string) {
|
||||
/** 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();
|
||||
|
|
@ -187,7 +179,7 @@ export class MCPServersRegistry {
|
|||
}
|
||||
|
||||
// Logs server configuration summary after initialization
|
||||
private logUpdatedConfig(serverName: string) {
|
||||
private logUpdatedConfig(serverName: string): void {
|
||||
const prefix = this.prefix(serverName);
|
||||
const config = this.parsedConfigs[serverName];
|
||||
logger.info(`${prefix} -------------------------------------------------┐`);
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import { readFileSync } from 'fs';
|
||||
import { load as yamlLoad } from 'js-yaml';
|
||||
import { ConnectionsRepository } from '../ConnectionsRepository';
|
||||
import { MCPServersRegistry } from '../MCPServersRegistry';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import type { OAuthDetectionResult } from '~/mcp/oauth/detectOAuth';
|
||||
import type * as t from '~/mcp/types';
|
||||
import { ConnectionsRepository } from '~/mcp/ConnectionsRepository';
|
||||
import { MCPServersRegistry } from '~/mcp/MCPServersRegistry';
|
||||
import { detectOAuthRequirement } from '~/mcp/oauth';
|
||||
import { MCPConnection } from '../connection';
|
||||
import type * as t from '../types';
|
||||
import { MCPConnection } from '~/mcp/connection';
|
||||
|
||||
// Mock external dependencies
|
||||
jest.mock('../oauth/detectOAuth');
|
||||
|
|
@ -37,7 +38,7 @@ const mockLogger = logger as jest.Mocked<typeof logger>;
|
|||
|
||||
describe('MCPServersRegistry - Initialize Function', () => {
|
||||
let rawConfigs: t.MCPServers;
|
||||
let expectedParsedConfigs: Record<string, any>;
|
||||
let expectedParsedConfigs: Record<string, t.ParsedServerConfig>;
|
||||
let mockConnectionsRepo: jest.Mocked<ConnectionsRepository>;
|
||||
let mockConnections: Map<string, jest.Mocked<MCPConnection>>;
|
||||
|
||||
|
|
@ -49,7 +50,7 @@ describe('MCPServersRegistry - Initialize Function', () => {
|
|||
rawConfigs = yamlLoad(readFileSync(rawConfigsPath, 'utf8')) as t.MCPServers;
|
||||
expectedParsedConfigs = yamlLoad(readFileSync(parsedConfigsPath, 'utf8')) as Record<
|
||||
string,
|
||||
any
|
||||
t.ParsedServerConfig
|
||||
>;
|
||||
|
||||
// Setup mock connections
|
||||
|
|
@ -57,12 +58,13 @@ describe('MCPServersRegistry - Initialize Function', () => {
|
|||
const serverNames = Object.keys(rawConfigs);
|
||||
|
||||
serverNames.forEach((serverName) => {
|
||||
const mockClient = {
|
||||
listTools: jest.fn(),
|
||||
getInstructions: jest.fn(),
|
||||
getServerCapabilities: jest.fn(),
|
||||
};
|
||||
const mockConnection = {
|
||||
client: {
|
||||
listTools: jest.fn(),
|
||||
getInstructions: jest.fn(),
|
||||
getServerCapabilities: jest.fn(),
|
||||
},
|
||||
client: mockClient,
|
||||
} as unknown as jest.Mocked<MCPConnection>;
|
||||
|
||||
// Setup mock responses based on expected configs
|
||||
|
|
@ -75,30 +77,32 @@ describe('MCPServersRegistry - Initialize Function', () => {
|
|||
name,
|
||||
description: `Description for ${name}`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
type: 'object' as const,
|
||||
properties: {
|
||||
input: { type: 'string' },
|
||||
},
|
||||
},
|
||||
}));
|
||||
mockConnection.client.listTools.mockResolvedValue({ tools });
|
||||
(mockClient.listTools as jest.Mock).mockResolvedValue({ tools });
|
||||
} else {
|
||||
mockConnection.client.listTools.mockResolvedValue({ tools: [] });
|
||||
(mockClient.listTools as jest.Mock).mockResolvedValue({ tools: [] });
|
||||
}
|
||||
|
||||
// Mock getInstructions response
|
||||
if (expectedConfig.serverInstructions) {
|
||||
mockConnection.client.getInstructions.mockReturnValue(expectedConfig.serverInstructions);
|
||||
(mockClient.getInstructions as jest.Mock).mockReturnValue(
|
||||
expectedConfig.serverInstructions as string,
|
||||
);
|
||||
} else {
|
||||
mockConnection.client.getInstructions.mockReturnValue(null);
|
||||
(mockClient.getInstructions as jest.Mock).mockReturnValue(undefined);
|
||||
}
|
||||
|
||||
// Mock getServerCapabilities response
|
||||
if (expectedConfig.capabilities) {
|
||||
const capabilities = JSON.parse(expectedConfig.capabilities);
|
||||
mockConnection.client.getServerCapabilities.mockReturnValue(capabilities);
|
||||
const capabilities = JSON.parse(expectedConfig.capabilities) as Record<string, unknown>;
|
||||
(mockClient.getServerCapabilities as jest.Mock).mockReturnValue(capabilities);
|
||||
} else {
|
||||
mockConnection.client.getServerCapabilities.mockReturnValue(null);
|
||||
(mockClient.getServerCapabilities as jest.Mock).mockReturnValue(undefined);
|
||||
}
|
||||
|
||||
mockConnections.set(serverName, mockConnection);
|
||||
|
|
@ -111,9 +115,13 @@ describe('MCPServersRegistry - Initialize Function', () => {
|
|||
disconnectAll: jest.fn(),
|
||||
} as unknown as jest.Mocked<ConnectionsRepository>;
|
||||
|
||||
mockConnectionsRepo.get.mockImplementation((serverName: string) =>
|
||||
Promise.resolve(mockConnections.get(serverName)!),
|
||||
);
|
||||
mockConnectionsRepo.get.mockImplementation((serverName: string) => {
|
||||
const connection = mockConnections.get(serverName);
|
||||
if (!connection) {
|
||||
throw new Error(`Connection not found for server: ${serverName}`);
|
||||
}
|
||||
return Promise.resolve(connection);
|
||||
});
|
||||
|
||||
mockConnectionsRepo.getLoaded.mockResolvedValue(mockConnections);
|
||||
|
||||
|
|
@ -121,9 +129,10 @@ describe('MCPServersRegistry - Initialize Function', () => {
|
|||
|
||||
// Setup OAuth detection mock with deterministic results
|
||||
mockDetectOAuthRequirement.mockImplementation((url: string) => {
|
||||
const oauthResults: Record<string, any> = {
|
||||
const oauthResults: Record<string, OAuthDetectionResult> = {
|
||||
'https://api.github.com/mcp': {
|
||||
requiresOAuth: true,
|
||||
method: 'protected-resource-metadata',
|
||||
metadata: {
|
||||
authorization_url: 'https://github.com/login/oauth/authorize',
|
||||
token_url: 'https://github.com/login/oauth/access_token',
|
||||
|
|
@ -131,15 +140,19 @@ describe('MCPServersRegistry - Initialize Function', () => {
|
|||
},
|
||||
'https://api.disabled.com/mcp': {
|
||||
requiresOAuth: false,
|
||||
method: 'no-metadata-found',
|
||||
metadata: null,
|
||||
},
|
||||
'https://api.public.com/mcp': {
|
||||
requiresOAuth: false,
|
||||
method: 'no-metadata-found',
|
||||
metadata: null,
|
||||
},
|
||||
};
|
||||
|
||||
return Promise.resolve(oauthResults[url] || { requiresOAuth: false, metadata: null });
|
||||
return Promise.resolve(
|
||||
oauthResults[url] || { requiresOAuth: false, method: 'no-metadata-found', metadata: null },
|
||||
);
|
||||
});
|
||||
|
||||
// Clear all mocks
|
||||
|
|
|
|||
|
|
@ -105,3 +105,11 @@ export type FormattedToolResponse = [
|
|||
string | FormattedContent[],
|
||||
{ content: FormattedContent[] } | undefined,
|
||||
];
|
||||
|
||||
export type ParsedServerConfig = MCPOptions & {
|
||||
url?: string;
|
||||
requiresOAuth?: boolean;
|
||||
oauthMetadata?: Record<string, unknown> | null;
|
||||
capabilities?: string;
|
||||
tools?: string;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue