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
21 changes: 13 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,27 @@ jobs:

strategy:
matrix:
node-version: [16.x, 18.x, 20.x]

# node:test (used by the suite) requires Node >= 18.
node-version: [18.x, 20.x, 22.x]

steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build


- name: Type-check (source + tests)
run: npm run lint

# Hermetic suite only (unit + integration). The e2e suite needs an
# authenticated gemini CLI and is run on demand via `npm run test:e2e`.
- name: Run tests
run: npm test
continue-on-error: true
run: npm test
31 changes: 30 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,36 @@
# Changelog

## [1.2.0] - 2026-05-30
First feature release after the 1.1.6 security patch. Hardens cross-platform execution, adds an opt-in safety control and native multi-turn sessions, makes the CLI backend pluggable (ahead of Gemini CLI's retirement), and adds a real test suite. **With no environment variables set, behaviour is identical to 1.1.6** — every new knob (backend, model, approval mode, timeout, executable path) is off/unset by default and only changes behaviour when you opt in.

### Added
- **Approval mode** — optional `approvalMode` argument on `ask-gemini`/`brainstorm` (and `GEMINI_MCP_APPROVAL_MODE` env), forwarding Gemini's `--approval-mode` (`default` / `auto_edit` / `yolo` / `plan`). Opt-in: when unset, behaviour is unchanged. Use `yolo` / `auto_edit` with `sandbox` to let Gemini run or edit; `plan` runs Gemini as an autonomous read-only planner.
- **Native multi-turn sessions** — `sessionId` and `resume` arguments forward Gemini's `--session-id` / `--resume`; the active session id is surfaced in the response so a follow-up call can continue the conversation. Builds on #50; uses the CLI's own sessions rather than local transcript storage.
- **Pluggable backends** — the executor is now backend-agnostic. The Gemini CLI stays the default; set `GEMINI_MCP_BACKEND=agy` to use the **experimental** Antigravity CLI (`agy`) backend, ahead of Gemini CLI's 2026-06-18 retirement for free/Pro/Ultra tiers. (agy print-mode is Flash-only, and its reply is recovered from agy's transcript files to work around the upstream `agy -p` empty-stdout bug.)
- **Per-command timeout (opt-in)** — set `GEMINI_MCP_TIMEOUT_MS` to a positive number of milliseconds to terminate a hung CLI call (SIGTERM → SIGKILL). **Disabled by default** to match 1.1.6, which waited indefinitely; unset or `0` keeps that behaviour.
- **Windows executable resolution** — honours `GEMINI_CLI_PATH`, otherwise resolves the real `gemini` shim via `where` (preferring `.cmd`), fixing "command not found" when the MCP server doesn't inherit your shell's PATH.
- **Configurable default model** — `GEMINI_MODEL` sets the model used when a call doesn't pass one, so the assistant can't silently fall back to an older model (#49); `GEMINI_FLASH_MODEL` overrides the quota-fallback target. Precedence: per-call `model` arg → `GEMINI_MODEL` → Gemini CLI default.
- **`.env` support** — the server loads recognised `GEMINI_*` keys from a `.env` file (package root, then cwd) at startup as global per-install defaults. Opt-in: only known keys are read, an already-set shell export or MCP-client env always wins, and no `.env` means no change (1.1.6 parity).
- **Test suite** — `node:test` coverage for the `@file` security guard, Windows quoting/resolution, approval-mode and session argument building, backend selection, and timeout parsing (`npm test`).

### Changed
- `engines.node` raised to `>=18`.
- The server version and the documentation navbar badge are now read from `package.json` dynamically, instead of hardcoded strings that had drifted to `1.1.4`.
- Installing from a Git checkout now builds automatically via a `prepare` script.

### Fixed
- The `Help` tool now invokes `gemini --help` instead of `-help`, which yargs mis-parsed as `-h -e -l -p`.
- Clearer, platform-aware guidance when the executable is not found (ENOENT), including the `GEMINI_CLI_PATH` hint.
- Windows robustness: complex prompts (`changeMode` / `@file`) are sent to the Gemini CLI on **stdin** instead of the `-p` flag, sidestepping cmd.exe argument parsing and the OS command-line length limit; added `windowsHide` to suppress the popup console window. (#27, #77)

### Internal
- **Per-call timeout default flipped to off** — `GEMINI_MCP_TIMEOUT_MS` now defaults to disabled (waits forever, exactly like 1.1.6) instead of 30 minutes; the timeout is strictly opt-in. `resolveTimeoutMs` returns `0` when unset/blank.
- **Setup doctor kept as an unpublished dev tool** — `scripts/doctor.mjs` (run via `npm run doctor`) prints the live system state relevant to the MCP server — active backend, detected `gemini`/`agy` installs (path + version), effective model/approval/timeout config, and every related env var — for debugging and at-a-glance awareness. Intentionally removed from the npm `bin` and published `files`, so it ships with the repo but **not** the package; may be released publicly later.
- Reports the source of each setting: a **global** value (this shell's env or the loaded `.env`, shown in gold as `(set globally)`) affects every client, vs a **per-client** value read from each Claude Code MCP server's `env` block in `~/.claude.json`.
- Adds `npm run doctor setup` — an interactive wizard that walks each option (showing the current value + recommended default; skip / set / pick-from-list, with a curated model list per backend and model selection skipped for `agy`) and applies the result to the `.env` file and/or a chosen Claude Code server (backing up `~/.claude.json` first).

## [1.1.6] - 2026-05-30
_Emergency security patch — the CVE-2026-0755 fix only, ahead of the larger 1.2.0 release._
_Emergency security patch — the CVE-2026-0755 fix only, ahead of this 1.2.0 release._
- Security fix: OS command-injection / `@file` exfiltration via prompt quoting in `geminiExecutor.ts` (CVE-2026-0755, CWE-78). Fixes #73 (and the literal-quote corruption in #66).
- Removed the broken double-quote wrapping from both the primary and fallback paths. With `spawn` running `shell: false`, those quotes were passed as literal characters — they provided no protection and corrupted `@file` references. Windows `.cmd` argument quoting is hardened separately (see below).
- Added `assertSafeFileReferences()`, which rejects any `@file` reference that resolves outside the project working directory (absolute paths, `~` home references, and `../` traversal), closing the arbitrary-file-read exfiltration vector while preserving legitimate in-project `@file` usage.
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,33 @@ If you installed globally, use this configuration instead:

After updating the configuration, restart your terminal session.

### Environment Variables (1.2.0)

All optional — set them in your MCP client's `env` block. See the [Configuration docs](docs/concepts/configuration.md) for full detail.

| Variable | Default | Purpose |
|----------|---------|---------|
| `GEMINI_MODEL` | *(CLI default)* | Default model when a call doesn't specify one (e.g. `gemini-3-pro-preview`) |
| `GEMINI_MCP_APPROVAL_MODE` | *(unset)* | `default` / `auto_edit` / `yolo` / `plan` → forwarded to `gemini --approval-mode` |
| `GEMINI_MCP_BACKEND` | `gemini` | CLI backend: `gemini` or `agy` (experimental) |
| `GEMINI_MCP_TIMEOUT_MS` | *(disabled)* | Opt-in per-call timeout in ms; unset/`0` waits forever (e.g. `1800000` = 30 min) |
| `GEMINI_CLI_PATH` | *(auto)* | Full path to the `gemini` executable (Windows PATH issues) |
| `GEMINI_FLASH_MODEL` | `gemini-2.5-flash` | Model used for the automatic quota fallback |

Example — pin a default model so the assistant can't fall back to an older one ([#49](https://github.com/jamubc/gemini-mcp-tool/issues/49)):

```json
{
"mcpServers": {
"gemini-cli": {
"command": "npx",
"args": ["-y", "gemini-mcp-tool"],
"env": { "GEMINI_MODEL": "gemini-3-pro-preview" }
}
}
}
```

## 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
92 changes: 92 additions & 0 deletions SECURITY-REPORT-2026-05-28.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Security Report — gemini-mcp-tool

- **Date:** 2026-05-28
- **Repository:** `jamubc/gemini-mcp-tool`
- **Branch reviewed:** `security/cve-2026-0755` (PR #75)
- **Scope:** All hand-written source under `src/`, plus declared npm dependencies.
- **Method:** Manual code review + sink analysis (`child_process` / `fs` / network / `eval`), `npm audit` with runtime-vs-dev tree attribution, and a cross-check of open GitHub issues.

> No security issue was filed today (2026-05-28). The most recent security report is **#73 (CVE-2026-0755)**, which is fixed on this branch (PR #75).

---

## Executive summary

| Area | Critical | High | Moderate | Low / Info |
|--------------|:--------:|:----:|:--------:|:----------:|
| Code | 1 (fixed)| 0 | 0 | 4 |
| Dependencies | 0 | 8* | 15 | 2 |

\* Only **2 of the 8 dependency HIGHs reach the published/runtime tree** (`@modelcontextprotocol/sdk`, and `tmp` via the unused `inquirer` dep). The other 6 HIGHs live exclusively in the docs/build toolchain (`vitepress`, `mermaid`, `archiver`) and are never installed for end users.

---

## Code findings

### C1 — CVE-2026-0755: OS command-injection / `@file` exfiltration — **Critical — FIXED (PR #75)**
`geminiExecutor.ts` wrapped any prompt containing `@` in literal `"` before passing it to `spawn` (`shell: false`), which injected literal quote characters and corrupted `@file` references, while leaving an arbitrary-file-read vector through the Gemini CLI's `@file` parser.

**Fix (this branch):** removed the broken quoting from the primary and fallback paths; added `assertSafeFileReferences()` which rejects `@file` references that resolve outside the project working directory (absolute, `~`, and `../` traversal). The guard runs on the fully-processed prompt, so it also protects the `brainstorm` and `changeMode` code paths.

### C2 — Windows `cmd.exe` variable expansion in prompts — **Low (Windows-only)**
`commandExecutor.ts` uses `shell: true` on Windows and wraps whitespace/quote args in `"..."` (escaping `"`→`""`). `cmd.exe` still expands `%VAR%` **inside** double quotes, so a prompt containing e.g. `%USERNAME%` / `%PATH%` is substituted before reaching `gemini`. This is not a command-execution break-out, but it is a correctness + minor information-substitution issue. Unix is unaffected (`shell: false`).
**Recommendation:** adopt the issue #62 approach — spawn `process.execPath` with the resolved `gemini.js` path and `shell: false` on Windows too — eliminating the shell (and the quoting fragility) entirely.

### C3 — Verbose logging of full tool arguments / prompts — **Low / Informational**
`logger.ts` logs raw args via `JSON.stringify` on every invocation (`Logger.toolInvocation`), and `Logger.debug` is wired to `console.warn`, so prompt bodies are written to stderr **regardless of any debug flag**. Prompts may contain pasted file contents or secrets; on shared hosts or captured MCP logs this is a disclosure risk.
**Recommendation:** gate full-argument logging behind an explicit debug env var; avoid logging full prompt bodies at the default level.

### C4 — Raw `error.message` returned to client — **Informational**
`index.ts` returns `Error executing ${tool}: ${error.message}`. CLI/`fs` errors may embed absolute local paths. Low impact for a local stdio server; noted for completeness.

### C5 — Unbounded lazy regex over model output — **Informational**
`changeModeParser.ts` uses `[\s\S]*?` groups. Input is Gemini's *response* (model-controlled, not direct attacker network input), so ReDoS exposure is low. Acceptable today; revisit if these inputs ever become untrusted.

### Positives observed
- `commandExecutor.ts` uses `spawn` with `shell: false` on Unix and an args array — no shell injection.
- #72 path-traversal hardening on `cacheKey` is solid: format regex (`/^[a-f0-9]{8}$/`) + `path.resolve` containment + removal of the silent `unlink` primitive.
- All tool arguments are validated through `zod` before execution.
- The server is **stdio-only** — there is no network listener by default.

---

## Dependency findings

`npm audit`: **25 vulnerabilities (8 high, 15 moderate, 2 low)**. The published package ships only `dist/`, but its `dependencies` are installed transitively for every end user, so the runtime-vs-dev split below is what actually matters.

### D1 — `@modelcontextprotocol/sdk@0.5.0` — **High — runtime, USED**
- Advisories: ReDoS (high); "DNS-rebinding protection not enabled by default" (high).
- **DNS rebinding does not apply** here: this server uses `StdioServerTransport`, not the Streamable-HTTP transport the advisory concerns.
- ReDoS applies to SDK message handling; with a trusted local stdio client, exposure is limited but real.
- `0.5.0` is far behind the current `1.x` line. **Upgrading is recommended but is a breaking API change** and will require edits to `index.ts`.

### D2 — `inquirer@9.3.7` → `external-editor` → `tmp@0.0.33` — **High path traversal — runtime, UNUSED**
- `inquirer`, `ai`, `chalk`, `d3-shape`, and `prismjs` are declared as runtime `dependencies` but are **not imported anywhere in `src/`**. They are still installed for every user, and `inquirer` drags in the HIGH `tmp` path-traversal advisory.
- **Recommendation (high value, low effort):** remove these unused runtime deps. This eliminates the only runtime-tree HIGH besides the SDK and significantly shrinks install/attack surface. (Note: `package.json` references a `contribute` script at `src/contribute.ts` which does not exist in the tree — confirm nothing relies on these before removal.)

### D3 — Docs/build toolchain HIGHs — **Not shipped, lower priority**
All remaining HIGHs are confined to `devDependencies` and are not installed for end users or used by the running server:
- `archiver` → `glob`, `minimatch`, `lodash`
- `vitepress` → `rollup`, `vite`, `esbuild`, `preact`
- `mermaid` → `dompurify`

Patch opportunistically with `npm audit fix`, but these do not affect deployed MCP servers.

---

## Additional observations (full source-tree read)

These do **not** affect the published npm package or the running MCP server (the docs site is built/deployed separately to GitHub Pages), but are noted for completeness:

- **Docs site loads a third-party ad script.** `docs/.vitepress/theme/components/AdBanner.vue` injects `//cdn.carbonads.com/carbon.js` into the page `<head>`. It is currently an inert placeholder (`serve=YOUR_CARBON_ID`), but any third-party script on the docs origin is a supply-chain/privacy consideration. *(Informational — docs site only.)*
- **`v-html` in `CodeBlock.vue`.** Renders Prism-highlighted output via `v-html`. Input is build-time-authored doc content and Prism escapes HTML, so this is not an exploitable XSS today. *(Informational — docs site only.)*
- **Dead / duplicate files.** `src/utils/timeoutManager.ts` is effectively empty (1 line) and imported nowhere; `src/scripts/deploy-wiki.sh` is a byte-for-byte duplicate of `scripts/deploy-wiki.sh`. Housekeeping, not security — safe to remove.

## Prioritized recommendations

1. **Merge PR #75** — CVE-2026-0755 fix. *(Critical — done, pending merge.)*
2. **Remove unused runtime deps** (`ai`, `chalk`, `d3-shape`, `inquirer`, `prismjs`) — removes the `tmp` HIGH from the shipped tree. *(High, low effort.)*
3. **Plan `@modelcontextprotocol/sdk` 0.5 → 1.x upgrade.** *(High, breaking — needs code changes.)*
4. **Gate verbose prompt/argument logging** behind a debug flag. *(Low.)*
5. **Windows:** drop `shell: true` in favor of the node + `gemini.js` approach (issue #62) to remove `%VAR%` expansion and quoting fragility. *(Low.)*
6. **`npm audit fix`** for the docs/build toolchain. *(Low.)*
1 change: 1 addition & 0 deletions docs/.vitepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default withMermaid(
collapsed: false,
items: [
{ text: 'How It Works', link: '/concepts/how-it-works' },
{ text: 'Configuration', link: '/concepts/configuration' },
{ text: 'File Analysis (@)', link: '/concepts/file-analysis' },
{ text: 'Model Selection', link: '/concepts/models' },
{ text: 'Sandbox Mode', link: '/concepts/sandbox' }
Expand Down
5 changes: 4 additions & 1 deletion docs/.vitepress/theme/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</template>
<template #nav-bar-content-before>
<div class="nav-warning">
🏷️ <span>1.1.4</span>
🏷️ <span>{{ version }}</span>
</div>
</template>
<template #sidebar-nav-after>
Expand Down Expand Up @@ -36,6 +36,9 @@ import FundingHero from './components/FundingHero.vue'
import FundingEffects from './components/FundingEffects.vue'
import FundingLayout from './FundingLayout.vue'

// Import version dynamically from package.json
import { version } from '../../../package.json'

const { Layout } = DefaultTheme
const route = useRoute()
const { frontmatter } = useData()
Expand Down
105 changes: 103 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,104 @@
# API
# API Reference

Stay tuned.
## Tools

The MCP server exposes the following tools over stdio transport.

### ask-gemini

The primary tool for sending prompts to Gemini.

**Arguments:**

```typescript
{
prompt: string; // Required. Use @ to include files.
model?: string; // e.g. "gemini-2.5-flash"
sandbox?: boolean; // default false
changeMode?: boolean; // default false — structured edits
approvalMode?: "default" | "auto_edit" | "yolo" | "plan";
sessionId?: string; // tag a session
resume?: string; // resume by id or "latest"
chunkIndex?: number; // 1-based chunk (changeMode)
chunkCacheKey?: string; // hex cache key (changeMode)
}
```

### brainstorm

Structured ideation with methodology frameworks.

**Arguments:**

```typescript
{
prompt: string; // Required. The challenge to brainstorm.
model?: string;
approvalMode?: "default" | "auto_edit" | "yolo" | "plan";
methodology?: "divergent" | "convergent" | "scamper"
| "design-thinking" | "lateral" | "auto";
domain?: string; // e.g. "software", "business"
constraints?: string;
existingContext?: string;
ideaCount?: number; // default 12
includeAnalysis?: boolean; // default true
}
```

### ping

Echo test. Returns the input message.

```typescript
{ prompt?: string; } // defaults to "Pong!"
```

### Help

Returns `gemini --help` output.

```typescript
{} // no arguments
```

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `GEMINI_MODEL` | *(CLI default)* | Default model when a call omits `model` |
| `GEMINI_FLASH_MODEL` | `gemini-2.5-flash` | Model used for the quota fallback |
| `GEMINI_MCP_BACKEND` | `gemini` | Backend: `gemini` or `agy` (experimental) |
| `GEMINI_MCP_APPROVAL_MODE` | *(unset)* | Default approval mode for all calls |
| `GEMINI_MCP_TIMEOUT_MS` | `1800000` | Per-call timeout in ms; `0` disables |
| `GEMINI_CLI_PATH` | *(auto)* | Full path to the gemini executable (Windows) |

## Transport

The server uses **stdio** transport (MCP standard). It reads JSON-RPC from stdin and writes responses to stdout. No HTTP server, no ports.

```json
{
"mcpServers": {
"gemini-cli": {
"command": "npx",
"args": ["-y", "gemini-mcp-tool"]
}
}
}
```

## Backends

The `BackendProvider` interface is:

```typescript
interface Backend {
readonly name: string;
readonly supportsModelSelection: boolean;
run(prompt: string, options: BackendRunOptions): Promise<string>;
}
```

Two implementations ship:
- **`geminiBackend`** — default, full feature support
- **`agyBackend`** — experimental, Flash-only, transcript-file recovery
Loading