Files
gemini-cli/packages/cli/src/config/extensions/variables.ts
T
Taylor Mullen 42344c2666 feat(cli): support Open Plugins MCP servers
- Implement discovery of .mcp.json at the plugin root
- Support explicit mcpServers path or record in plugin.json
- Add variable expansion for PLUGIN_ROOT in command, args, env, and cwd
- Align MCP server naming with the pluginName:mcpServerName format
- Ensure MCP tool names use standard mcp_ prefix for API compatibility
- Enable settings and mcpServers for Open Plugins
- Refactor MCP server resolution into dedicated method
- Improve type safety in tests and configuration loading

Fixes https://github.com/google-gemini/maintainers-gemini-cli/issues/1595
2026-03-24 11:45:49 -07:00

104 lines
2.9 KiB
TypeScript

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import * as path from 'node:path';
import { type VariableSchema, VARIABLE_SCHEMA } from './variableSchema.js';
import { GEMINI_DIR } from '@google/gemini-cli-core';
/**
* Represents a set of keys that will be considered invalid while unmarshalling
* JSON in recursivelyHydrateStrings.
*/
const UNMARSHALL_KEY_IGNORE_LIST: Set<string> = new Set<string>([
'__proto__',
'constructor',
'prototype',
]);
export const EXTENSIONS_DIRECTORY_NAME = path.join(GEMINI_DIR, 'extensions');
export const EXTENSIONS_CONFIG_FILENAME = 'gemini-extension.json';
export const OPEN_PLUGIN_CONFIG_FILENAME = 'plugin.json';
export const HIDDEN_OPEN_PLUGIN_CONFIG_FILENAME = path.join(
'.plugin',
'plugin.json',
);
export const OPEN_PLUGIN_MCP_CONFIG_FILENAME = '.mcp.json';
export const HIDDEN_OPEN_PLUGIN_MCP_CONFIG_FILENAME = path.join(
'.plugin',
'.mcp.json',
);
export const INSTALL_METADATA_FILENAME = '.gemini-extension-install.json';
export const EXTENSION_SETTINGS_FILENAME = '.env';
export type JsonObject = { [key: string]: JsonValue };
export type JsonArray = JsonValue[];
export type JsonValue =
| string
| number
| boolean
| null
| JsonObject
| JsonArray;
export type VariableContext = {
[key: string]: string | undefined;
};
export function validateVariables(
variables: VariableContext,
schema: VariableSchema,
) {
for (const key in schema) {
const definition = schema[key];
if (definition.required && !variables[key]) {
throw new Error(`Missing required variable: ${key}`);
}
}
}
export function hydrateString(str: string, context: VariableContext): string {
validateVariables(context, VARIABLE_SCHEMA);
const regex = /\${(.*?)}/g;
return str.replace(regex, (match, key) =>
context[key] == null ? match : context[key],
);
}
export function recursivelyHydrateStrings<T>(
obj: T,
values: VariableContext,
): T {
if (typeof obj === 'string') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return hydrateString(obj, values) as unknown as T;
}
if (Array.isArray(obj)) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return obj.map((item) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
recursivelyHydrateStrings(item, values),
) as unknown as T;
}
if (typeof obj === 'object' && obj !== null) {
const newObj: Record<string, unknown> = {};
for (const key in obj) {
if (
!UNMARSHALL_KEY_IGNORE_LIST.has(key) &&
Object.prototype.hasOwnProperty.call(obj, key)
) {
newObj[key] = recursivelyHydrateStrings(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
(obj as Record<string, unknown>)[key],
values,
);
}
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return newObj as T;
}
return obj;
}