Summarize extensions and MCP servers on startup (#3977)

This commit is contained in:
Billy Biggs
2025-07-18 20:45:00 +02:00
committed by GitHub
parent 9dadf22958
commit 18c3bf3a42
11 changed files with 218 additions and 64 deletions

View File

@@ -22,7 +22,7 @@ import {
} from '@google/gemini-cli-core';
import { Settings } from './settings.js';
import { Extension, filterActiveExtensions } from './extension.js';
import { Extension, annotateActiveExtensions } from './extension.js';
import { getCliVersion } from '../utils/version.js';
import { loadSandboxConfig } from './sandboxConfig.js';
@@ -252,11 +252,15 @@ export async function loadCliConfig(
process.env.TERM_PROGRAM === 'vscode' &&
!process.env.SANDBOX;
const activeExtensions = filterActiveExtensions(
const allExtensions = annotateActiveExtensions(
extensions,
argv.extensions || [],
);
const activeExtensions = extensions.filter(
(_, i) => allExtensions[i].isActive,
);
// Set the context filename in the server's memoryTool module BEFORE loading memory
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
// directly to the Config constructor in core, and have core handle setGeminiMdFilename.
@@ -283,6 +287,7 @@ export async function loadCliConfig(
let mcpServers = mergeMcpServers(settings, activeExtensions);
const excludeTools = mergeExcludeTools(settings, activeExtensions);
const blockedMcpServers: Array<{ name: string; extensionName: string }> = [];
if (!argv.allowedMcpServerNames) {
if (settings.allowMCPServers) {
@@ -308,9 +313,24 @@ export async function loadCliConfig(
const allowedNames = new Set(argv.allowedMcpServerNames.filter(Boolean));
if (allowedNames.size > 0) {
mcpServers = Object.fromEntries(
Object.entries(mcpServers).filter(([key]) => allowedNames.has(key)),
Object.entries(mcpServers).filter(([key, server]) => {
const isAllowed = allowedNames.has(key);
if (!isAllowed) {
blockedMcpServers.push({
name: key,
extensionName: server.extensionName || '',
});
}
return isAllowed;
}),
);
} else {
blockedMcpServers.push(
...Object.entries(mcpServers).map(([key, server]) => ({
name: key,
extensionName: server.extensionName || '',
})),
);
mcpServers = {};
}
}
@@ -403,10 +423,8 @@ export async function loadCliConfig(
maxSessionTurns: settings.maxSessionTurns ?? -1,
experimentalAcp: argv.experimentalAcp || false,
listExtensions: argv.listExtensions || false,
activeExtensions: activeExtensions.map((e) => ({
name: e.config.name,
version: e.config.version,
})),
extensions: allExtensions,
blockedMcpServers,
noBrowser: !!process.env.NO_BROWSER,
summarizeToolOutput: settings.summarizeToolOutput,
ideMode,
@@ -424,7 +442,10 @@ function mergeMcpServers(settings: Settings, extensions: Extension[]) {
);
return;
}
mcpServers[key] = server;
mcpServers[key] = {
...server,
extensionName: extension.config.name,
};
},
);
}

View File

@@ -11,7 +11,7 @@ import * as path from 'path';
import {
EXTENSIONS_CONFIG_FILENAME,
EXTENSIONS_DIRECTORY_NAME,
filterActiveExtensions,
annotateActiveExtensions,
loadExtensions,
} from './extension.js';
@@ -86,42 +86,52 @@ describe('loadExtensions', () => {
});
});
describe('filterActiveExtensions', () => {
describe('annotateActiveExtensions', () => {
const extensions = [
{ config: { name: 'ext1', version: '1.0.0' }, contextFiles: [] },
{ config: { name: 'ext2', version: '1.0.0' }, contextFiles: [] },
{ config: { name: 'ext3', version: '1.0.0' }, contextFiles: [] },
];
it('should return all extensions if no enabled extensions are provided', () => {
const activeExtensions = filterActiveExtensions(extensions, []);
it('should mark all extensions as active if no enabled extensions are provided', () => {
const activeExtensions = annotateActiveExtensions(extensions, []);
expect(activeExtensions).toHaveLength(3);
expect(activeExtensions.every((e) => e.isActive)).toBe(true);
});
it('should return only the enabled extensions', () => {
const activeExtensions = filterActiveExtensions(extensions, [
it('should mark only the enabled extensions as active', () => {
const activeExtensions = annotateActiveExtensions(extensions, [
'ext1',
'ext3',
]);
expect(activeExtensions).toHaveLength(2);
expect(activeExtensions.some((e) => e.config.name === 'ext1')).toBe(true);
expect(activeExtensions.some((e) => e.config.name === 'ext3')).toBe(true);
expect(activeExtensions).toHaveLength(3);
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(
true,
);
expect(activeExtensions.find((e) => e.name === 'ext2')?.isActive).toBe(
false,
);
expect(activeExtensions.find((e) => e.name === 'ext3')?.isActive).toBe(
true,
);
});
it('should return no extensions when "none" is provided', () => {
const activeExtensions = filterActiveExtensions(extensions, ['none']);
expect(activeExtensions).toHaveLength(0);
it('should mark all extensions as inactive when "none" is provided', () => {
const activeExtensions = annotateActiveExtensions(extensions, ['none']);
expect(activeExtensions).toHaveLength(3);
expect(activeExtensions.every((e) => !e.isActive)).toBe(true);
});
it('should handle case-insensitivity', () => {
const activeExtensions = filterActiveExtensions(extensions, ['EXT1']);
expect(activeExtensions).toHaveLength(1);
expect(activeExtensions[0].config.name).toBe('ext1');
const activeExtensions = annotateActiveExtensions(extensions, ['EXT1']);
expect(activeExtensions.find((e) => e.name === 'ext1')?.isActive).toBe(
true,
);
});
it('should log an error for unknown extensions', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
filterActiveExtensions(extensions, ['ext4']);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
annotateActiveExtensions(extensions, ['ext4']);
expect(consoleSpy).toHaveBeenCalledWith('Extension not found: ext4');
consoleSpy.mockRestore();
});

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { MCPServerConfig } from '@google/gemini-cli-core';
import { MCPServerConfig, GeminiCLIExtension } from '@google/gemini-cli-core';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -34,9 +34,6 @@ export function loadExtensions(workspaceDir: string): Extension[] {
const uniqueExtensions = new Map<string, Extension>();
for (const extension of allExtensions) {
if (!uniqueExtensions.has(extension.config.name)) {
console.log(
`Loading extension: ${extension.config.name} (version: ${extension.config.version})`,
);
uniqueExtensions.set(extension.config.name, extension);
}
}
@@ -113,12 +110,18 @@ function getContextFileNames(config: ExtensionConfig): string[] {
return config.contextFileName;
}
export function filterActiveExtensions(
export function annotateActiveExtensions(
extensions: Extension[],
enabledExtensionNames: string[],
): Extension[] {
): GeminiCLIExtension[] {
const annotatedExtensions: GeminiCLIExtension[] = [];
if (enabledExtensionNames.length === 0) {
return extensions;
return extensions.map((extension) => ({
name: extension.config.name,
version: extension.config.version,
isActive: true,
}));
}
const lowerCaseEnabledExtensions = new Set(
@@ -129,31 +132,33 @@ export function filterActiveExtensions(
lowerCaseEnabledExtensions.size === 1 &&
lowerCaseEnabledExtensions.has('none')
) {
if (extensions.length > 0) {
console.log('All extensions are disabled.');
}
return [];
return extensions.map((extension) => ({
name: extension.config.name,
version: extension.config.version,
isActive: false,
}));
}
const activeExtensions: Extension[] = [];
const notFoundNames = new Set(lowerCaseEnabledExtensions);
for (const extension of extensions) {
const lowerCaseName = extension.config.name.toLowerCase();
if (lowerCaseEnabledExtensions.has(lowerCaseName)) {
console.log(
`Activated extension: ${extension.config.name} (version: ${extension.config.version})`,
);
activeExtensions.push(extension);
const isActive = lowerCaseEnabledExtensions.has(lowerCaseName);
if (isActive) {
notFoundNames.delete(lowerCaseName);
} else {
console.log(`Disabled extension: ${extension.config.name}`);
}
annotatedExtensions.push({
name: extension.config.name,
version: extension.config.version,
isActive,
});
}
for (const requestedName of notFoundNames) {
console.log(`Extension not found: ${requestedName}`);
console.error(`Extension not found: ${requestedName}`);
}
return activeExtensions;
return annotatedExtensions;
}