Skip to main content
Glama
README.md
<p align="center">
  <img src="assets/banner.svg" alt="polycode" width="100%" />
</p>

A production-grade MCP (Model Context Protocol) server exposing **13 tools** that let Claude Code (or any MCP client) control three AI coding agents — [opencode](https://opencode.ai), [Gemini CLI](https://github.com/google-gemini/gemini-cli), and [Qwen Code](https://github.com/QwenLM/qwen-code) — with full session continuity, auth checking, and structured error handling.

---

## How It Works

```
Claude Code  (or Gemini CLI / Qwen Code)
    │  calls tools (MCP stdio)
    ▼
polycode server  (this package — auto-started by the MCP client)
    │
    ├── spawns opencode serve  →  talks to any of 182 models via opencode
    ├── invokes gemini CLI     →  Gemini API with session continuity
    └── invokes qwen CLI       →  Qwen API with session continuity
```

The MCP client (Claude Code, Gemini CLI, Qwen Code) auto-starts this server when the session begins. You never start it manually.

---

## Requirements

1. **Python 3.11+**
   ```bash
   python --version
   ```

2. **opencode CLI** — for the `opencode_*` tools
   ```bash
   npm install -g opencode-ai
   opencode --version  # should print 1.x.x
   ```

3. **Gemini CLI** — for the `gemini_*` tools _(optional)_
   ```bash
   npm install -g @google/gemini-cli
   gemini --version  # should print 0.36.x or higher
   ```

4. **Qwen Code CLI** — for the `qwen_*` tools _(optional)_
   ```bash
   npm install -g @qwen-code/qwen-code
   qwen --version  # should print 0.14.x or higher
   ```

5. **A model provider for opencode** — for the default `ollama/qwen3.5:cloud`, Ollama must be running locally. See [Changing the Model](#changing-the-opencode-model) for alternatives.

---

## Installation

```bash
pip install polycode
```

Verify the CLI is accessible:
```bash
polycode --help
```

> **Windows note:** If `polycode` is not found after install, find the full path with `where polycode` in PowerShell and use it in the MCP config below.

---

## MCP Client Setup

All three supported MCP clients use the same config format — only the config file path differs.

**macOS / Linux:**
```json
{
  "mcpServers": {
    "polycode": {
      "command": "polycode",
      "env": {
        "OPENCODE_DEFAULT_MODEL": "ollama/qwen3.5:cloud"
      }
    }
  }
}
```

**Windows** — use the full path to the binary. Find it by running `where polycode` in PowerShell, then paste the result as the `command` value:
```json
{
  "mcpServers": {
    "polycode": {
      "command": "C:\\Users\\YourName\\AppData\\Local\\Programs\\Python\\Python313\\Scripts\\polycode.exe",
      "env": {
        "OPENCODE_DEFAULT_MODEL": "ollama/qwen3.5:cloud"
      }
    }
  }
}
```

| MCP Client | Config file |
|------------|-------------|
| **Claude Code** | `~/.claude.json` |
| **Gemini CLI** | `~/.gemini/settings.json` |
| **Qwen Code** | `~/.qwen/settings.json` |

Restart your MCP client after saving. All 13 tools appear automatically.

---

## Tools Reference

### opencode tools (8)

These tools control opencode — a multi-provider AI coding agent. Sessions are stateful — messages within a session share full context across any of the 180+ supported models.

---

#### `opencode_start_session`
Start a new opencode session. Must be called before `opencode_send_message`.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `project_dir` | string | No | Absolute path to the project. Defaults to current working directory. |
| `model` | string | No | Model in `provider/model` format. Defaults to `OPENCODE_DEFAULT_MODEL`. |

**Returns:**
```json
{
  "session_id": "ses_2a29...",
  "model": "ollama/qwen3.5:cloud",
  "project_dir": "/path/to/project"
}
```

---

#### `opencode_send_message`
Send a prompt to an active session. Blocks until opencode finishes responding.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `session_id` | string | Yes | From `opencode_start_session`. |
| `message` | string | Yes | Your prompt. |
| `timeout_seconds` | int | No | Default: `120`. |

**Returns:**
```json
{
  "response": "Here is the updated function...",
  "session_id": "ses_2a29...",
  "message_index": 1,
  "partial": false
}
```

---

#### `opencode_get_history`
Retrieve the full message history for a session (tracked in-process).

| Parameter | Type | Required |
|-----------|------|----------|
| `session_id` | string | Yes |

**Returns:**
```json
{
  "session_id": "ses_2a29...",
  "messages": [
    {"role": "user", "content": "...", "timestamp": "2026-04-05T19:00:00Z"},
    {"role": "assistant", "content": "...", "timestamp": "2026-04-05T19:00:05Z"}
  ]
}
```

---

#### `opencode_list_sessions`
List all active opencode sessions.

**Returns:**
```json
{
  "sessions": [
    {
      "session_id": "ses_2a29...",
      "model": "ollama/qwen3.5:cloud",
      "project_dir": "/path/to/project",
      "message_count": 4,
      "created_at": "2026-04-05T19:00:00Z"
    }
  ]
}
```

---

#### `opencode_end_session`
Close a session and free its resources.

| Parameter | Type | Required |
|-----------|------|----------|
| `session_id` | string | Yes |

**Returns:** `{"session_id": "ses_2a29...", "closed": true}`

---

#### `opencode_list_models`
List all models available in opencode across all authenticated providers, grouped by provider. Only providers you are authenticated/connected to will show models.

**Returns:**
```json
{
  "models": ["ollama/qwen3.5:cloud", "openai/gpt-4o", "google/gemini-2.5-flash", "..."],
  "by_provider": {
    "ollama": ["ollama/qwen3.5:cloud", "..."],
    "openai": ["openai/gpt-4o", "..."],
    "google": ["google/gemini-2.5-flash", "..."]
  },
  "total": 182,
  "default_model": "ollama/qwen3.5:cloud"
}
```

---

#### `opencode_set_model`
Change the default model for new sessions (takes effect immediately for all subsequent `opencode_start_session` calls).

| Parameter | Type | Required | Example |
|-----------|------|----------|---------|
| `model` | string | Yes | `ollama/qwen3.5:cloud` |

**Returns:** `{"previous_model": "ollama/...", "new_model": "openai/gpt-4o"}`

---

#### `opencode_shutdown`
Gracefully stop the opencode server and close all active sessions.

**Returns:** `{"stopped": true, "sessions_closed": 2}`

---

### Gemini CLI tools (4)

These tools invoke the `gemini` CLI directly. Sessions are persisted to disk by the CLI — pass `session_id` to continue a conversation across calls.

**Requires:** `gemini` CLI installed and authenticated (OAuth or `GEMINI_API_KEY`).

---

#### `gemini_check_auth`
Check whether the Gemini CLI is authenticated before making prompt calls.

| Parameter | Type | Required | Default |
|-----------|------|----------|---------|
| `timeout_seconds` | int | No | `15` |

**Returns:**
```json
{
  "authenticated": true,
  "method": "api_key_or_oauth",
  "detail": "OK — model: gemini-2.5-flash-lite",
  "suggestion": ""
}
```
If `authenticated` is `false`, `suggestion` tells you how to fix it.

---

#### `gemini_prompt`
Send a prompt to Gemini CLI. Returns the response and a `session_id` that can be passed back to continue the conversation.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `prompt` | string | Yes | The prompt to send. |
| `session_id` | string | No | Resume a previous session. Leave empty to start a new one. |
| `model` | string | No | E.g. `gemini-2.5-flash`. Defaults to the CLI's configured model. |
| `timeout_seconds` | int | No | Default: `120`. |
| `project_dir` | string | No | Working directory. Defaults to current directory. |

**Returns:**
```json
{
  "response": "The word you asked me to remember is BLUEBIRD.",
  "model": "gemini-2.5-flash-lite",
  "session_id": "69cfc177-319c-484c-9..."
}
```

**Multi-turn example:**
```
# Turn 1 — new session
gemini_prompt(prompt="Remember the word BLUEBIRD")
→ { session_id: "69cfc177-..." }

# Turn 2 — continue session
gemini_prompt(prompt="What word did I ask you to remember?", session_id="69cfc177-...")
→ { response: "BLUEBIRD" }
```

---

#### `gemini_list_sessions`
List saved Gemini CLI sessions for the current project.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `project_dir` | string | No | Defaults to current directory. |
| `timeout_seconds` | int | No | Default: `10`. |

**Returns:**
```json
{
  "sessions": [
    {"raw": "0: [2026-04-05] Remember the word BLUEBIRD"},
    {"raw": "1: [2026-04-05] Explain the polycode architecture"}
  ]
}
```

---

### Qwen Code CLI tools (3)

These tools invoke the `qwen` CLI directly. Sessions are persisted to disk by the CLI — pass `session_id` to continue a conversation across calls.

**Requires:** `qwen` CLI installed and authenticated (`qwen auth qwen-oauth` or `qwen auth coding-plan`).

---

#### `qwen_check_auth`
Check whether the Qwen Code CLI is authenticated before making prompt calls.

| Parameter | Type | Required | Default |
|-----------|------|----------|---------|
| `timeout_seconds` | int | No | `15` |

**Returns:**
```json
{
  "authenticated": true,
  "method": "qwen-oauth",
  "detail": "=== Authentication Status ===\n✓ Authentication Method: Qwen OAuth\n  Type: Free tier",
  "suggestion": ""
}
```
If `authenticated` is `false`, `suggestion` tells you the exact command to run.

---

#### `qwen_prompt`
Send a prompt to Qwen Code CLI. Returns the response and a `session_id` that can be passed back to continue the conversation.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `prompt` | string | Yes | The prompt to send. |
| `session_id` | string | No | Resume a previous session. Leave empty to start a new one. |
| `model` | string | No | E.g. `qwen-plus`. Defaults to the CLI's configured model. |
| `timeout_seconds` | int | No | Default: `120`. |
| `project_dir` | string | No | Working directory. Defaults to current directory. |

**Returns:**
```json
{
  "response": "The word you asked me to remember was REDPANDA.",
  "model": "coder-model",
  "session_id": "ead03e7a-afff-4ccd-a..."
}
```

**Multi-turn example:**
```
# Turn 1 — new session
qwen_prompt(prompt="Remember the word REDPANDA")
→ { session_id: "ead03e7a-..." }

# Turn 2 — continue session
qwen_prompt(prompt="What word did I ask you to remember?", session_id="ead03e7a-...")
→ { response: "REDPANDA" }
```

---

## Changing the opencode Model

The model format is `provider/model-name`. Set it via env var:

```json
"env": {
  "OPENCODE_DEFAULT_MODEL": "openai/gpt-4o"
}
```

Or call `opencode_set_model` at runtime. Call `opencode_list_models` to see all 182 available models across your connected providers.

**Common models:**

| Provider | Model string |
|----------|-------------|
| Ollama (local) | `ollama/qwen3.5:cloud` |
| OpenAI | `openai/gpt-4o` |
| Anthropic | `anthropic/claude-sonnet-4-5` |
| Google | `google/gemini-2.5-flash` |
| GitHub Copilot | `github-copilot/claude-sonnet-4.6` |

---

## Configuration

| Variable | Default | Description |
|----------|---------|-------------|
| `OPENCODE_DEFAULT_MODEL` | `ollama/qwen3.5:cloud` | Default model for new opencode sessions |
| `OPENCODE_PORT` | `0` (random) | Port for the opencode server |
| `OPENCODE_STARTUP_TIMEOUT` | `10` | Seconds to wait for opencode to start |
| `OPENCODE_REQUEST_TIMEOUT` | `120` | Seconds before a generation times out |
| `OPENCODE_LOG_LEVEL` | `INFO` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` |
| `OPENCODE_SERVER_PASSWORD` | _(unset)_ | Optional HTTP Basic Auth password for the opencode server |

---

## Error Handling

Every tool always returns a structured response — never a raw exception:

```json
{
  "error": "OpencodeBinaryNotFoundError",
  "message": "gemini CLI not found on PATH. Install: npm install -g @google/gemini-cli",
  "detail": {},
  "recoverable": false,
  "suggestion": "Install opencode via: npm install -g opencode-ai"
}
```

| Field | Description |
|-------|-------------|
| `error` | Exception class name |
| `message` | What went wrong |
| `detail` | Structured context (stderr, attempted values, etc.) |
| `recoverable` | Whether retrying makes sense |
| `suggestion` | Exact next step to fix it |

**Common errors:**

| Error | Cause | Fix |
|-------|-------|-----|
| `OpencodeBinaryNotFoundError` | CLI not on PATH | Install the CLI listed in `suggestion` |
| `OpencodeStartupError` | opencode failed to start | Increase `OPENCODE_STARTUP_TIMEOUT`; check `opencode serve` manually |
| `OpencodeTimeoutError` | Generation took too long | Increase `OPENCODE_REQUEST_TIMEOUT` or simplify the prompt |
| `OpencodeSessionError` | Session ID not found | Call `opencode_list_sessions` to see active sessions |
| `OpencodeValidationError` | Bad input or auth error | Read the `suggestion` field — includes exact CLI auth command if needed |
| `OpencodeProtocolError` | Unexpected CLI output shape | Update the CLI to the latest version |

---

## Troubleshooting

### "polycode not found" on Windows

Use the full path in your MCP config. Find it with:
```powershell
where polycode
```

### opencode server times out on startup

Cloud models do a network handshake on first use. Increase the timeout:
```json
"env": { "OPENCODE_STARTUP_TIMEOUT": "30" }
```

### Tools appear but calls hang

On Windows, subprocesses can inherit a blocked stdin from the MCP stdio pipe. This package sets `stdin=DEVNULL` on all subprocesses — ensure you are on `polycode >= 0.1.0`.

### gemini_prompt or qwen_prompt returns an auth error

Run the auth check first:
- `gemini_check_auth` → reads `suggestion` field for the fix
- `qwen_check_auth` → reads `suggestion` field for the fix

Then authenticate interactively (`gemini` or `qwen auth qwen-oauth`) and retry.

---

## Running Tests

```bash
git clone https://github.com/h19overflow/polycode
cd polycode
pip install -e ".[dev]"

# Unit tests — no CLIs required
pytest tests/ --ignore=tests/test_integration.py -v

# Integration tests — requires opencode + ollama
pytest tests/test_integration.py -m integration -v
```

---

## Contributing

1. Fork the repo
2. `pip install -e ".[dev]"`
3. Write tests first (TDD)
4. `pytest tests/ --ignore=tests/test_integration.py` must pass
5. `pyright .` must show 0 errors
6. Open a PR

---

## License

MIT — see [LICENSE](LICENSE)

TDQS

A4.2/5.0

Scored across 13 tools

Disambiguation5/5

Each tool is clearly prefixed by provider and uses a distinct verb_noun combination. opencode_start_session, opencode_send_message, gemini_prompt, qwen_prompt, etc. have unambiguous boundaries, and even the similar prompt tools are separated by provider.

Naming Consistency5/5

All 13 tools follow the exact snake_case pattern of provider_action_noun (e.g., opencode_list_sessions, gemini_check_auth, qwen_prompt). There are no deviations or mixed conventions.

Tool Count5/5

13 tools is well within the ideal range and appropriate for the server's scope of managing three separate coding CLIs. Each tool serves a distinct operational need, and none are redundant.

Completeness4/5

Opencode has full lifecycle coverage, and gemini/qwen include auth and prompting. However, qwen lacks a list_sessions equivalent, which is a minor gap compared to gemini and could hinder session resumption.

Maintenance

ActivityInactive
ResponsivenessNo issues