Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,14 @@ Register the MCP server with your MCP client:

### For NPX Usage (Recommended)

Add this configuration to your Claude Desktop config file:
Add this configuration to your Claude Desktop config file. The `--target-model gemini` flag is recommended to ensure compatibility with Gemini's stricter tool schema.

```json
{
"mcpServers": {
"gemini-cli": {
"command": "npx",
"args": ["-y", "gemini-mcp-tool"]
"args": ["-y", "gemini-mcp-tool", "--", "--target-model", "gemini"]
}
}
}
Expand All @@ -90,7 +90,8 @@ If you installed globally, use this configuration instead:
{
"mcpServers": {
"gemini-cli": {
"command": "gemini-mcp"
"command": "gemini-mcp",
"args": ["--target-model", "gemini"]
}
}
}
Expand All @@ -105,6 +106,32 @@ If you installed globally, use this configuration instead:

After updating the configuration, restart your terminal session.

## Model Compatibility

Different Large Language Models can have different requirements for how tool schemas are defined. This server can adapt its tool schemas to ensure compatibility with the target model you are using.

This is controlled by a startup flag or an environment variable.

### Command-Line Flag

The recommended way to enable compatibility mode is by passing the `--target-model` flag when starting the server.

```bash
# Example for Gemini
npx gemini-mcp-tool --target-model gemini
```

### Environment Variable

Alternatively, you can set the `MCP_TARGET_MODEL` environment variable.

```bash
export MCP_TARGET_MODEL=gemini
npx gemini-mcp-tool
```

Currently, the only special target is `gemini`. If the flag is omitted, it will use a `default` mode with more expressive schemas.

## Example Workflow

- **Natural language**: "use gemini to explain index.html", "understand the massive project using gemini", "ask gemini to search for latest news"
Expand Down
97 changes: 97 additions & 0 deletions docs/concepts/schema-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Schema Compatibility

## The Problem: Divergent Tool Schemas

Different Large Language Models can have different levels of strictness when it comes to validating the schemas of the tools they are provided. For example, Google Gemini's API enforces a stricter subset of the JSON Schema specification than other models might.

A common issue is the use of complex schema types like `anyOf` (which is what `zod` produces for a `z.union`). While valid JSON Schema, Gemini's API may reject it, causing `400 Bad Request` errors and preventing the tool from being called.

A simple solution would be to make all schemas conform to the strictest possible requirements, but this would reduce the expressiveness and validation power of our internal schemas for models that *do* support them.

## The Solution: A Strategy-Based Approach

To solve this, we've implemented a **strategy-based pattern** that allows the server to dynamically select the appropriate schema for a tool parameter based on a startup configuration. This gives us the best of both worlds: strict, compatible schemas for models that need them, and rich, expressive schemas for those that don't.

The system is composed of two parts:

### 1. Startup Configuration

The server's "mode" is determined once at startup by the `--target-model` command-line flag or the `MCP_TARGET_MODEL` environment variable. This is managed in `@/src/utils/config.ts`.

```bash
# Start the server in Gemini compatibility mode
npx gemini-mcp-tool --target-model gemini
```

If no target is specified, it defaults to `'default'`.

### 2. Schema Strategies

The core of the solution is in `@/src/utils/schema-strategies.ts`. This file contains functions (strategies) that are responsible for choosing which Zod schema to use based on the `config.target`.

This approach keeps the conditional logic centralized and allows our tool definitions to remain clean and declarative.

## Developer Guide: Adding a Compatible Parameter

If you are adding a new tool parameter that requires a different schema for a specific target (like Gemini), follow this pattern. We will use the `chunkIndex` parameter in the `ask-gemini` tool as our example.

### Step 1: Define Both Schema Variations

In your tool definition file (e.g., `@/src/tools/ask-gemini.tool.ts`), define both the standard and the model-specific schemas as named constants.

The `gemini` version should be the simplest possible schema that the API will accept. It's also a good practice to include a `z.preprocess` step to gracefully handle any data that might still come in the "standard" format (e.g., a number) before validation.

```typescript
// @/src/tools/ask-gemini.tool.ts

// The standard, expressive schema for most models
const standardChunkIndexSchema = z.union([z.number(), z.string()]).optional().describe("Which chunk to return (1-based)");

// The simplified schema for Gemini's stricter API
const geminiChunkIndexSchema = z.preprocess(
(val) => (val === undefined || val === null ? val : String(val)),
z.string().optional()
).describe("Which chunk to return (1-based)");
```

### Step 2: Create or Update a Strategy

In `@/src/utils/schema-strategies.ts`, ensure there is a strategy function that can select the correct schema. If you're adding a parameter with a new compatibility need, you may need to add a new strategy function.

Our existing strategy for `chunkIndex` looks like this:

```typescript
// @/src/utils/schema-strategies.ts

export const selectChunkIndexSchema = (schemas: {
standard: ZodTypeAny;
gemini: ZodTypeAny;
}): ZodTypeAny => {
if (config.target === 'gemini') {
return schemas.gemini;
}
return schemas.standard;
};
```

### Step 3: Use the Strategy in Your Tool Definition

Finally, in your tool's main Zod schema, import and use the strategy function. Pass your previously defined schemas to it. This makes your definition declarative and clean.

```typescript
// @/src/tools/ask-gemini.tool.ts
import { selectChunkIndexSchema } from '../utils/schema-strategies.js';

// ... (schemas defined above) ...

const askGeminiArgsSchema = z.object({
// ... other parameters
chunkIndex: selectChunkIndexSchema({
standard: standardChunkIndexSchema,
gemini: geminiChunkIndexSchema,
}),
// ... other parameters
});
```

By following this pattern, you can easily support multiple model targets without cluttering your tool definitions with conditional logic.
43 changes: 41 additions & 2 deletions docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ No installation needed - runs directly:
"mcpServers": {
"gemini-cli": {
"command": "npx",
"args": ["-y", "gemini-mcp-tool"]
"args": ["-y", "gemini-mcp-tool", "--", "--target-model", "gemini"]
}
}
}
Expand All @@ -30,16 +30,55 @@ claude mcp add gemini-cli -- npx -y gemini-mcp-tool
```

Then configure:

```json
{
"mcpServers": {
"gemini-cli": {
"command": "gemini-mcp"
"command": "gemini-mcp",
"args": ["--target-model", "gemini"]
}
}
}
```

## Server Configuration

You can configure the server's behavior using command-line arguments or environment variables.

### Command-Line Arguments

Arguments are passed after the main command. When using `npx`, you must add `--` before the arguments.

- `--target-model <name>`: Sets the compatibility mode for the tool schemas. This is crucial for ensuring tools work correctly with specific models.
- **Example**:
```bash
npx gemini-mcp-tool --target-model gemini
```
- **Default**: `default`

**Example `args` in `mcp-config.json`:**
```json
{
"args": ["-y", "gemini-mcp-tool", "--", "--target-model", "gemini"]
}
```

### Environment Variables

You can also use environment variables to configure the server.

- `MCP_TARGET_MODEL=<name>`: Same function as the `--target-model` flag.
- **Example**:
```bash
export MCP_TARGET_MODEL=gemini
npx gemini-mcp-tool
```

::: tip
Command-line arguments take precedence over environment variables.
:::

## Method 3: Local Project

```bash
Expand Down
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#!/usr/bin/env node

// Initialize the application configuration at startup.
// This must be one of the first imports to ensure the configuration is available to all other modules.
import "./utils/config.js";

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
Expand Down
53 changes: 37 additions & 16 deletions src/tools/ask-gemini.tool.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { z } from 'zod';
import { UnifiedTool } from './registry.js';
import { executeGeminiCLI, processChangeModeOutput } from '../utils/geminiExecutor.js';
import {
ERROR_MESSAGES,
import {
ERROR_MESSAGES,
STATUS_MESSAGES
} from '../constants.js';
import { selectChunkIndexSchema } from '../utils/schema-strategies.js';

const askGeminiArgsSchema = z.object({
prompt: z.string().min(1).describe("Analysis request. Use @ syntax to include files (e.g., '@largefile.js explain what this does') or ask general questions"),
model: z.string().optional().describe("Optional model to use (e.g., 'gemini-2.5-flash'). If not specified, uses the default model (gemini-2.5-pro)."),
sandbox: z.boolean().default(false).describe("Use sandbox mode (-s flag) to safely test code changes, execute scripts, or run potentially risky operations in an isolated environment"),
changeMode: z.boolean().default(false).describe("Enable structured change mode - formats prompts to prevent tool errors and returns structured edit suggestions that Claude can apply directly"),
chunkIndex: z.union([z.number(), z.string()]).optional().describe("Which chunk to return (1-based)"),
chunkIndex: selectChunkIndexSchema({
standard: z.union([z.number(), z.string()]).optional().describe("Which chunk to return (1-based)"),
gemini: z.preprocess(
(val) => (val === undefined || val === null ? val : String(val)),
z.string().optional()
).describe("Which chunk to return (1-based)"),
}),
chunkCacheKey: z.string().optional().describe("Optional cache key for continuation"),
});

Expand All @@ -24,33 +31,47 @@ export const askGeminiTool: UnifiedTool = {
},
category: 'gemini',
execute: async (args, onProgress) => {
const { prompt, model, sandbox, changeMode, chunkIndex, chunkCacheKey } = args; if (!prompt?.trim()) { throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED); }

if (changeMode && chunkIndex && chunkCacheKey) {
const { prompt, model, sandbox, changeMode, chunkIndex, chunkCacheKey } = args as z.infer<typeof askGeminiArgsSchema>;
if (!prompt.trim()) {
throw new Error(ERROR_MESSAGES.NO_PROMPT_PROVIDED);
}

// Helper function to safely parse chunkIndex to a number.
// This is necessary because the model may provide a string or number,
// and internal logic requires a number.
const parseChunkIndex = (index: unknown): number | undefined => {
if (index === undefined || index === null) return undefined;
const num = parseInt(String(index), 10);
return isNaN(num) ? undefined : num;
};

const chunkIndexNum = parseChunkIndex(chunkIndex);

if (changeMode && chunkIndexNum !== undefined && chunkCacheKey) {
return processChangeModeOutput(
'', // empty for cache...
chunkIndex as number,
chunkCacheKey as string,
prompt as string
chunkIndexNum,
chunkCacheKey,
prompt
);
}

const result = await executeGeminiCLI(
prompt as string,
model as string | undefined,
prompt,
model,
!!sandbox,
!!changeMode,
onProgress
);

if (changeMode) {
return processChangeModeOutput(
result,
args.chunkIndex as number | undefined,
chunkIndexNum,
undefined,
prompt as string
prompt
);
}
return `${STATUS_MESSAGES.GEMINI_RESPONSE}\n${result}`; // changeMode false
}
};
};
69 changes: 69 additions & 0 deletions src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @module config
* Manages the server's runtime configuration, primarily for determining the
* target LLM environment. This allows the application to adapt its behavior,
* such as tool schema generation, based on the specific model it's serving.
*
* The configuration is determined once at startup by checking command-line
* arguments and environment variables, then exported as a read-only object.
*/
import { Logger } from './logger.js';

/**
* An immutable, frozen object containing the server's runtime configuration.
*/
export interface AppConfig {
/** The target model environment, used for feature switching like schema generation. */
readonly target: string;
}

/**
* Initializes the application configuration by reading from the environment.
* This function should only be executed once when the application starts.
*
* The configuration is resolved with the following precedence:
* 1. Command-line argument: `--target-model <value>`
* 2. Environment variable: `MCP_TARGET_MODEL=<value>`
* 3. Default value (`'default'`)
*
* @returns A frozen `AppConfig` object.
*/
function initializeConfig(): AppConfig {
let target = 'default';
let detectedVia = '';

// 1. Check for the command-line argument
const argIndex = process.argv.indexOf('--target-model');
if (argIndex > -1 && process.argv.length > argIndex + 1) {
target = process.argv[argIndex + 1].toLowerCase();
detectedVia = 'command-line argument';
}
// 2. Check for the environment variable if not already set by arg
else if (process.env.MCP_TARGET_MODEL) {
target = process.env.MCP_TARGET_MODEL.toLowerCase();
detectedVia = 'environment variable';
}

if (target !== 'default') {
Logger.debug(`Target model set to "${target}" via ${detectedVia}.`);
}

// Return a frozen, read-only configuration object
return Object.freeze({ target });
}

/**
* The single, application-wide configuration instance.
* Initialized once at startup.
*/
export const config: AppConfig = initializeConfig();

/**
* A utility function to quickly check if the Gemini compatibility mode is active.
* This is the preferred way to check for the target in application logic.
*
* @returns `true` if the target model is Gemini, otherwise `false`.
*/
export function isGeminiTarget(): boolean {
return config.target === 'gemini';
}
Loading