mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-27 02:01:00 -07:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62a4d375a8 | |||
| 4085b4f8e7 | |||
| 881822fabc | |||
| c56e58d7a2 | |||
| ec31811c6c |
Generated
+9
-9
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -16815,7 +16815,7 @@
|
||||
},
|
||||
"packages/a2a-server": {
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
"@google-cloud/storage": "^7.16.0",
|
||||
@@ -16930,7 +16930,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
@@ -17102,7 +17102,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "0.3.11",
|
||||
@@ -17358,7 +17358,7 @@
|
||||
},
|
||||
"packages/devtools": {
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.16.0"
|
||||
@@ -17373,7 +17373,7 @@
|
||||
},
|
||||
"packages/sdk": {
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -17390,7 +17390,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@google/gemini-cli-core": "file:../core",
|
||||
@@ -17407,7 +17407,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.23.0",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"url": "git+https://github.com/google-gemini/gemini-cli.git"
|
||||
},
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-nightly.20260310.4653b126f"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-preview.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env NODE_ENV=development node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-a2a-server",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"description": "Gemini CLI A2A Server",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"description": "Gemini CLI",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-nightly.20260310.4653b126f"
|
||||
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.34.0-preview.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.12.0",
|
||||
|
||||
@@ -12,12 +12,13 @@ import { ExtensionManager } from './extension-manager.js';
|
||||
import { createTestMergedSettings } from './settings.js';
|
||||
import { createExtension } from '../test-utils/createExtension.js';
|
||||
import { EXTENSIONS_DIRECTORY_NAME } from './extensions/variables.js';
|
||||
import { themeManager } from '../ui/themes/theme-manager.js';
|
||||
import {
|
||||
TrustLevel,
|
||||
loadTrustedFolders,
|
||||
isWorkspaceTrusted,
|
||||
} from './trustedFolders.js';
|
||||
import { getRealPath } from '@google/gemini-cli-core';
|
||||
import { getRealPath, type CustomTheme } from '@google/gemini-cli-core';
|
||||
import type { MergedSettings } from './settings.js';
|
||||
|
||||
const mockHomedir = vi.hoisted(() => vi.fn(() => '/tmp/mock-home'));
|
||||
@@ -39,6 +40,26 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
const testTheme: CustomTheme = {
|
||||
type: 'custom',
|
||||
name: 'MyTheme',
|
||||
background: {
|
||||
primary: '#282828',
|
||||
diff: { added: '#2b3312', removed: '#341212' },
|
||||
},
|
||||
text: {
|
||||
primary: '#ebdbb2',
|
||||
secondary: '#a89984',
|
||||
link: '#83a598',
|
||||
accent: '#d3869b',
|
||||
},
|
||||
status: {
|
||||
success: '#b8bb26',
|
||||
warning: '#fabd2f',
|
||||
error: '#fb4934',
|
||||
},
|
||||
};
|
||||
|
||||
describe('ExtensionManager', () => {
|
||||
let tempHomeDir: string;
|
||||
let tempWorkspaceDir: string;
|
||||
@@ -66,6 +87,7 @@ describe('ExtensionManager', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
themeManager.clearExtensionThemes();
|
||||
try {
|
||||
fs.rmSync(tempHomeDir, { recursive: true, force: true });
|
||||
} catch (_e) {
|
||||
@@ -485,4 +507,45 @@ describe('ExtensionManager', () => {
|
||||
).rejects.toThrow(/already installed/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('early theme registration', () => {
|
||||
it('should register themes with ThemeManager during loadExtensions for active extensions', async () => {
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'themed-ext',
|
||||
version: '1.0.0',
|
||||
themes: [testTheme],
|
||||
});
|
||||
|
||||
await extensionManager.loadExtensions();
|
||||
|
||||
expect(themeManager.getCustomThemeNames()).toContain(
|
||||
'MyTheme (themed-ext)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not register themes for inactive extensions', async () => {
|
||||
createExtension({
|
||||
extensionsDir: userExtensionsDir,
|
||||
name: 'disabled-ext',
|
||||
version: '1.0.0',
|
||||
themes: [testTheme],
|
||||
});
|
||||
|
||||
// Disable the extension by creating an enablement override
|
||||
const manager = new ExtensionManager({
|
||||
enabledExtensionOverrides: ['none'],
|
||||
settings: createTestMergedSettings(),
|
||||
workspaceDir: tempWorkspaceDir,
|
||||
requestConsent: vi.fn().mockResolvedValue(true),
|
||||
requestSetting: null,
|
||||
});
|
||||
|
||||
await manager.loadExtensions();
|
||||
|
||||
expect(themeManager.getCustomThemeNames()).not.toContain(
|
||||
'MyTheme (disabled-ext)',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -564,7 +564,7 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
protected override async startExtension(extension: GeminiCLIExtension) {
|
||||
await super.startExtension(extension);
|
||||
if (extension.themes) {
|
||||
if (extension.themes && !themeManager.hasExtensionThemes(extension.name)) {
|
||||
themeManager.registerExtensionThemes(extension.name, extension.themes);
|
||||
}
|
||||
}
|
||||
@@ -624,6 +624,13 @@ Would you like to attempt to install via "git clone" instead?`,
|
||||
|
||||
this.loadedExtensions = builtExtensions;
|
||||
|
||||
// Register extension themes early so they're available at startup.
|
||||
for (const ext of this.loadedExtensions) {
|
||||
if (ext.isActive && ext.themes) {
|
||||
themeManager.registerExtensionThemes(ext.name, ext.themes);
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
this.loadedExtensions.map((ext) => this.maybeStartExtension(ext)),
|
||||
);
|
||||
|
||||
@@ -241,6 +241,17 @@ class ThemeManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if themes for a given extension are already registered.
|
||||
* @param extensionName The name of the extension.
|
||||
* @returns True if any themes from the extension are registered.
|
||||
*/
|
||||
hasExtensionThemes(extensionName: string): boolean {
|
||||
return Array.from(this.extensionThemes.keys()).some((name) =>
|
||||
name.endsWith(`(${extensionName})`),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all registered extension themes.
|
||||
* This is primarily for testing purposes to reset state between tests.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-core",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"description": "Gemini CLI Core",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -107,9 +107,11 @@ const localAgentSchema = z
|
||||
display_name: z.string().optional(),
|
||||
tools: z
|
||||
.array(
|
||||
z.string().refine((val) => isValidToolName(val), {
|
||||
message: 'Invalid tool name',
|
||||
}),
|
||||
z
|
||||
.string()
|
||||
.refine((val) => isValidToolName(val, { allowWildcards: true }), {
|
||||
message: 'Invalid tool name',
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
model: z.string().optional(),
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
type PartListUnion,
|
||||
type Tool,
|
||||
type CallableTool,
|
||||
type FunctionDeclaration,
|
||||
} from '@google/genai';
|
||||
import type { Config } from '../config/config.js';
|
||||
import { MockTool } from '../test-utils/mock-tool.js';
|
||||
@@ -550,6 +551,34 @@ describe('LocalAgentExecutor', () => {
|
||||
|
||||
getToolSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not duplicate schemas when instantiated tools are provided in toolConfig', async () => {
|
||||
// Create an instantiated mock tool
|
||||
const instantiatedTool = new MockTool({ name: 'instantiated_tool' });
|
||||
|
||||
// Create an agent definition containing the instantiated tool
|
||||
const definition = createTestDefinition([instantiatedTool]);
|
||||
|
||||
// Create the executor
|
||||
const executor = await LocalAgentExecutor.create(
|
||||
definition,
|
||||
mockConfig,
|
||||
onActivity,
|
||||
);
|
||||
|
||||
// Extract the prepared tools list using the private method
|
||||
const toolsList = (
|
||||
executor as unknown as { prepareToolsList: () => FunctionDeclaration[] }
|
||||
).prepareToolsList();
|
||||
|
||||
// Filter for the specific tool schema
|
||||
const foundSchemas = (
|
||||
toolsList as unknown as FunctionDeclaration[]
|
||||
).filter((t: FunctionDeclaration) => t.name === 'instantiated_tool');
|
||||
|
||||
// Assert that there is exactly ONE schema for this tool
|
||||
expect(foundSchemas).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('run (Execution Loop and Logic)', () => {
|
||||
|
||||
@@ -16,7 +16,13 @@ import {
|
||||
type Schema,
|
||||
} from '@google/genai';
|
||||
import { ToolRegistry } from '../tools/tool-registry.js';
|
||||
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
|
||||
import { type AnyDeclarativeTool } from '../tools/tools.js';
|
||||
import {
|
||||
DiscoveredMCPTool,
|
||||
isMcpToolName,
|
||||
parseMcpToolName,
|
||||
MCP_TOOL_PREFIX,
|
||||
} from '../tools/mcp-tool.js';
|
||||
import { CompressionStatus } from '../core/turn.js';
|
||||
import { type ToolCallRequestInfo } from '../scheduler/types.js';
|
||||
import { type Message } from '../confirmation-bus/types.js';
|
||||
@@ -141,28 +147,55 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
runtimeContext.getAgentRegistry().getAllAgentNames(),
|
||||
);
|
||||
|
||||
const registerToolByName = (toolName: string) => {
|
||||
const registerToolInstance = (tool: AnyDeclarativeTool) => {
|
||||
// Check if the tool is a subagent to prevent recursion.
|
||||
// We do not allow agents to call other agents.
|
||||
if (allAgentNames.has(toolName)) {
|
||||
if (allAgentNames.has(tool.name)) {
|
||||
debugLogger.warn(
|
||||
`[LocalAgentExecutor] Skipping subagent tool '${toolName}' for agent '${definition.name}' to prevent recursion.`,
|
||||
`[LocalAgentExecutor] Skipping subagent tool '${tool.name}' for agent '${definition.name}' to prevent recursion.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
agentToolRegistry.registerTool(tool);
|
||||
};
|
||||
|
||||
const registerToolByName = (toolName: string) => {
|
||||
// Handle global wildcard
|
||||
if (toolName === '*') {
|
||||
for (const tool of parentToolRegistry.getAllTools()) {
|
||||
registerToolInstance(tool);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle MCP wildcards
|
||||
if (isMcpToolName(toolName)) {
|
||||
if (toolName === `${MCP_TOOL_PREFIX}*`) {
|
||||
for (const tool of parentToolRegistry.getAllTools()) {
|
||||
if (tool instanceof DiscoveredMCPTool) {
|
||||
registerToolInstance(tool);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = parseMcpToolName(toolName);
|
||||
if (parsed.serverName && parsed.toolName === '*') {
|
||||
for (const tool of parentToolRegistry.getToolsByServer(
|
||||
parsed.serverName,
|
||||
)) {
|
||||
registerToolInstance(tool);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the tool is referenced by name, retrieve it from the parent
|
||||
// registry and register it with the agent's isolated registry.
|
||||
const tool = parentToolRegistry.getTool(toolName);
|
||||
if (tool) {
|
||||
if (tool instanceof DiscoveredMCPTool) {
|
||||
// Subagents MUST use fully qualified names for MCP tools to ensure
|
||||
// unambiguous tool calls and to comply with policy requirements.
|
||||
// We automatically "upgrade" any MCP tool to its qualified version.
|
||||
agentToolRegistry.registerTool(tool.asFullyQualifiedTool());
|
||||
} else {
|
||||
agentToolRegistry.registerTool(tool);
|
||||
}
|
||||
registerToolInstance(tool);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1171,22 +1204,14 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
|
||||
const { toolConfig, outputConfig } = this.definition;
|
||||
|
||||
if (toolConfig) {
|
||||
const toolNamesToLoad: string[] = [];
|
||||
for (const toolRef of toolConfig.tools) {
|
||||
if (typeof toolRef === 'string') {
|
||||
toolNamesToLoad.push(toolRef);
|
||||
} else if (typeof toolRef === 'object' && 'schema' in toolRef) {
|
||||
// Tool instance with an explicit schema property.
|
||||
toolsList.push(toolRef.schema);
|
||||
} else {
|
||||
if (typeof toolRef === 'object' && !('schema' in toolRef)) {
|
||||
// Raw `FunctionDeclaration` object.
|
||||
toolsList.push(toolRef);
|
||||
}
|
||||
}
|
||||
// Add schemas from tools that were registered by name.
|
||||
toolsList.push(
|
||||
...this.toolRegistry.getFunctionDeclarationsFiltered(toolNamesToLoad),
|
||||
);
|
||||
// Add schemas from tools that were explicitly registered by name, wildcard, or instance.
|
||||
toolsList.push(...this.toolRegistry.getFunctionDeclarations());
|
||||
}
|
||||
|
||||
// Always inject complete_task.
|
||||
|
||||
@@ -58,6 +58,7 @@ export function parseMcpToolName(name: string): {
|
||||
// Remove the prefix
|
||||
const withoutPrefix = name.slice(MCP_TOOL_PREFIX.length);
|
||||
// The first segment is the server name, the rest is the tool name
|
||||
// Must be strictly `server_tool` where neither are empty
|
||||
const match = withoutPrefix.match(/^([^_]+)_(.+)$/);
|
||||
if (match) {
|
||||
return {
|
||||
@@ -390,25 +391,6 @@ export class DiscoveredMCPTool extends BaseDeclarativeTool<
|
||||
`${this.serverName}${MCP_QUALIFIED_NAME_SEPARATOR}${this.serverToolName}`,
|
||||
);
|
||||
}
|
||||
|
||||
asFullyQualifiedTool(): DiscoveredMCPTool {
|
||||
return new DiscoveredMCPTool(
|
||||
this.mcpTool,
|
||||
this.serverName,
|
||||
this.serverToolName,
|
||||
this.description,
|
||||
this.parameterSchema,
|
||||
this.messageBus,
|
||||
this.trust,
|
||||
this.isReadOnly,
|
||||
this.getFullyQualifiedName(),
|
||||
this.cliConfig,
|
||||
this.extensionName,
|
||||
this.extensionId,
|
||||
this._toolAnnotations,
|
||||
);
|
||||
}
|
||||
|
||||
protected createInvocation(
|
||||
params: ToolParams,
|
||||
messageBus: MessageBus,
|
||||
|
||||
@@ -25,7 +25,8 @@ vi.mock('./tool-names.js', async (importOriginal) => {
|
||||
...actual,
|
||||
TOOL_LEGACY_ALIASES: mockedAliases,
|
||||
isValidToolName: vi.fn().mockImplementation((name: string, options) => {
|
||||
if (mockedAliases[name]) return true;
|
||||
if (Object.prototype.hasOwnProperty.call(mockedAliases, name))
|
||||
return true;
|
||||
return actual.isValidToolName(name, options);
|
||||
}),
|
||||
getToolAliases: vi.fn().mockImplementation((name: string) => {
|
||||
@@ -55,11 +56,9 @@ describe('tool-names', () => {
|
||||
expect(isValidToolName(`${DISCOVERED_TOOL_PREFIX}my_tool`)).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate MCP tool names (server__tool)', () => {
|
||||
expect(isValidToolName('server__tool')).toBe(true);
|
||||
expect(isValidToolName('my-server__my-tool')).toBe(true);
|
||||
expect(isValidToolName('my.server__my:tool')).toBe(true);
|
||||
expect(isValidToolName('my-server...truncated__tool')).toBe(true);
|
||||
it('should validate modern MCP FQNs (mcp_server_tool)', () => {
|
||||
expect(isValidToolName('mcp_server_tool')).toBe(true);
|
||||
expect(isValidToolName('mcp_my-server_my-tool')).toBe(true);
|
||||
});
|
||||
|
||||
it('should validate legacy tool aliases', async () => {
|
||||
@@ -69,28 +68,33 @@ describe('tool-names', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid tool names', () => {
|
||||
expect(isValidToolName('')).toBe(false);
|
||||
expect(isValidToolName('invalid-name')).toBe(false);
|
||||
expect(isValidToolName('server__')).toBe(false);
|
||||
expect(isValidToolName('__tool')).toBe(false);
|
||||
expect(isValidToolName('server__tool__extra')).toBe(false);
|
||||
it('should return false for invalid tool names', () => {
|
||||
expect(isValidToolName('invalid-tool-name')).toBe(false);
|
||||
expect(isValidToolName('mcp_server')).toBe(false);
|
||||
expect(isValidToolName('mcp__tool')).toBe(false);
|
||||
expect(isValidToolName('mcp_invalid server_tool')).toBe(false);
|
||||
expect(isValidToolName('mcp_server_invalid tool')).toBe(false);
|
||||
expect(isValidToolName('mcp_server_')).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle wildcards when allowed', () => {
|
||||
// Default: not allowed
|
||||
expect(isValidToolName('*')).toBe(false);
|
||||
expect(isValidToolName('server__*')).toBe(false);
|
||||
expect(isValidToolName('mcp_*')).toBe(false);
|
||||
expect(isValidToolName('mcp_server_*')).toBe(false);
|
||||
|
||||
// Explicitly allowed
|
||||
expect(isValidToolName('*', { allowWildcards: true })).toBe(true);
|
||||
expect(isValidToolName('server__*', { allowWildcards: true })).toBe(true);
|
||||
expect(isValidToolName('mcp_*', { allowWildcards: true })).toBe(true);
|
||||
expect(isValidToolName('mcp_server_*', { allowWildcards: true })).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
// Invalid wildcards
|
||||
expect(isValidToolName('__*', { allowWildcards: true })).toBe(false);
|
||||
expect(isValidToolName('server__tool*', { allowWildcards: true })).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isValidToolName('mcp__*', { allowWildcards: true })).toBe(false);
|
||||
expect(
|
||||
isValidToolName('mcp_server_tool*', { allowWildcards: true }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -221,6 +221,12 @@ export const DISCOVERED_TOOL_PREFIX = 'discovered_tool_';
|
||||
/**
|
||||
* List of all built-in tool names.
|
||||
*/
|
||||
import {
|
||||
isMcpToolName,
|
||||
parseMcpToolName,
|
||||
MCP_TOOL_PREFIX,
|
||||
} from './mcp-tool.js';
|
||||
|
||||
export const ALL_BUILTIN_TOOL_NAMES = [
|
||||
GLOB_TOOL_NAME,
|
||||
WRITE_TODOS_TOOL_NAME,
|
||||
@@ -290,25 +296,44 @@ export function isValidToolName(
|
||||
return true;
|
||||
}
|
||||
|
||||
// MCP tools (format: server__tool)
|
||||
if (name.includes('__')) {
|
||||
const parts = name.split('__');
|
||||
if (parts.length !== 2 || parts[0].length === 0 || parts[1].length === 0) {
|
||||
// Handle standard MCP FQNs (mcp_server_tool or wildcards mcp_*, mcp_server_*)
|
||||
if (isMcpToolName(name)) {
|
||||
// Global wildcard: mcp_*
|
||||
if (name === `${MCP_TOOL_PREFIX}*` && options.allowWildcards) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Explicitly reject names with empty server component (e.g. mcp__tool)
|
||||
if (name.startsWith(`${MCP_TOOL_PREFIX}_`)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const server = parts[0];
|
||||
const tool = parts[1];
|
||||
const parsed = parseMcpToolName(name);
|
||||
// Ensure that both components are populated. parseMcpToolName splits at the second _,
|
||||
// so `mcp__tool` has serverName="", toolName="tool"
|
||||
if (parsed.serverName && parsed.toolName) {
|
||||
// Basic slug validation for server and tool names.
|
||||
// We allow dots (.) and colons (:) as they are valid in function names and
|
||||
// used for truncation markers.
|
||||
const slugRegex = /^[a-z0-9_.:-]+$/i;
|
||||
|
||||
if (tool === '*') {
|
||||
return !!options.allowWildcards;
|
||||
if (!slugRegex.test(parsed.serverName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parsed.toolName === '*') {
|
||||
return options.allowWildcards === true;
|
||||
}
|
||||
|
||||
// A tool name consisting only of underscores is invalid.
|
||||
if (/^_*$/.test(parsed.toolName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return slugRegex.test(parsed.toolName);
|
||||
}
|
||||
|
||||
// Basic slug validation for server and tool names.
|
||||
// We allow dots (.) and colons (:) as they are valid in function names and
|
||||
// used for truncation markers.
|
||||
const slugRegex = /^[a-z0-9_.:-]+$/i;
|
||||
return slugRegex.test(server) && slugRegex.test(tool);
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -310,13 +310,13 @@ describe('ToolRegistry', () => {
|
||||
excludedTools: ['tool-a'],
|
||||
},
|
||||
{
|
||||
name: 'should match simple MCP tool names, when qualified or unqualified',
|
||||
tools: [mcpTool, mcpTool.asFullyQualifiedTool()],
|
||||
name: 'should match simple MCP tool names',
|
||||
tools: [mcpTool],
|
||||
excludedTools: [mcpTool.name],
|
||||
},
|
||||
{
|
||||
name: 'should match qualified MCP tool names when qualified or unqualified',
|
||||
tools: [mcpTool, mcpTool.asFullyQualifiedTool()],
|
||||
name: 'should match qualified MCP tool names',
|
||||
tools: [mcpTool],
|
||||
excludedTools: [mcpTool.name],
|
||||
},
|
||||
{
|
||||
@@ -414,9 +414,9 @@ describe('ToolRegistry', () => {
|
||||
const toolName = 'my-tool';
|
||||
const mcpTool = createMCPTool(serverName, toolName, 'desc');
|
||||
|
||||
// Register same MCP tool twice (one as alias, one as qualified)
|
||||
// Register same MCP tool twice
|
||||
toolRegistry.registerTool(mcpTool);
|
||||
toolRegistry.registerTool(mcpTool);
|
||||
toolRegistry.registerTool(mcpTool.asFullyQualifiedTool());
|
||||
|
||||
const toolNames = toolRegistry.getAllToolNames();
|
||||
expect(toolNames).toEqual([`mcp_${serverName}_${toolName}`]);
|
||||
@@ -698,9 +698,8 @@ describe('ToolRegistry', () => {
|
||||
const toolName = 'my-tool';
|
||||
const mcpTool = createMCPTool(serverName, toolName, 'description');
|
||||
|
||||
// Register both alias and qualified
|
||||
toolRegistry.registerTool(mcpTool);
|
||||
toolRegistry.registerTool(mcpTool.asFullyQualifiedTool());
|
||||
toolRegistry.registerTool(mcpTool);
|
||||
|
||||
const declarations = toolRegistry.getFunctionDeclarations();
|
||||
expect(declarations).toHaveLength(1);
|
||||
|
||||
@@ -222,14 +222,10 @@ export class ToolRegistry {
|
||||
*/
|
||||
registerTool(tool: AnyDeclarativeTool): void {
|
||||
if (this.allKnownTools.has(tool.name)) {
|
||||
if (tool instanceof DiscoveredMCPTool) {
|
||||
tool = tool.asFullyQualifiedTool();
|
||||
} else {
|
||||
// Decide on behavior: throw error, log warning, or allow overwrite
|
||||
debugLogger.warn(
|
||||
`Tool with name "${tool.name}" is already registered. Overwriting.`,
|
||||
);
|
||||
}
|
||||
// Decide on behavior: throw error, log warning, or allow overwrite
|
||||
debugLogger.warn(
|
||||
`Tool with name "${tool.name}" is already registered. Overwriting.`,
|
||||
);
|
||||
}
|
||||
this.allKnownTools.set(tool.name, tool);
|
||||
}
|
||||
@@ -594,7 +590,17 @@ export class ToolRegistry {
|
||||
for (const name of toolNames) {
|
||||
const tool = this.getTool(name);
|
||||
if (tool) {
|
||||
declarations.push(tool.getSchema(modelId));
|
||||
let schema = tool.getSchema(modelId);
|
||||
|
||||
// Ensure the schema name matches the qualified name for MCP tools
|
||||
if (tool instanceof DiscoveredMCPTool) {
|
||||
schema = {
|
||||
...schema,
|
||||
name: tool.getFullyQualifiedName(),
|
||||
};
|
||||
}
|
||||
|
||||
declarations.push(schema);
|
||||
}
|
||||
}
|
||||
return declarations;
|
||||
@@ -670,17 +676,6 @@ export class ToolRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
if (!tool && name.includes('__')) {
|
||||
for (const t of this.allKnownTools.values()) {
|
||||
if (t instanceof DiscoveredMCPTool) {
|
||||
if (t.getFullyQualifiedName() === name) {
|
||||
tool = t;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tool && this.isActiveTool(tool)) {
|
||||
return tool;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-devtools",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"main": "dist/src/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-sdk",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"description": "Gemini CLI SDK",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@google/gemini-cli-test-utils",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "gemini-cli-vscode-ide-companion",
|
||||
"displayName": "Gemini CLI Companion",
|
||||
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
|
||||
"version": "0.34.0-nightly.20260310.4653b126f",
|
||||
"version": "0.34.0-preview.2",
|
||||
"publisher": "google",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user