# tix MCP server
`tix mcp` exposes the tix issue tracker to AI coding agents over the [Model
Context Protocol](https://modelcontextprotocol.io) (MCP). It is a **stdio
subprocess server**: the harness spawns `tix mcp`, exchanges newline-delimited
JSON-RPC 2.0 messages over `stdin`/`stdout`, and the agent gains 12 tools to
create, read, update, move, delete, search, comment on, export/import and lint
issues — the same operations as the CLI, backed by the same command cores.
- Transport : MCP stdio, JSON-RPC 2.0, one message per line (UTF-8)
- Feature : `tools/*` (tools/list, tools/call), `ping`, `initialize`
- Repo scope : the working directory of the spawned process (see
[Repository resolution](#repository-resolution))
- Current protocol versions: `2025-06-18` (newest), `2025-03-26`, `2024-11-05`
---
## How it works
### Lifecycle
1. The harness spawns `tix mcp` from the repository root.
2. The server performs a strict bootstrap: it walks **upward** from the cwd to
find a `tix.toml` (the same discovery as `tix` with no subcommand). No
`tix.toml` ⇒ the process writes `no tix.toml found — run "tix init" first`
to **stderr** and exits `1` **before reading any protocol bytes**.
3. The client sends `initialize` with its `protocolVersion`. The server echoes
the version if it is supported, otherwise negotiates down to `2025-06-18`.
The response advertises `capabilities.tools.listChanged = false` and
`serverInfo = { name: "tix", version: "<tix version>" }`.
4. The client sends `notifications/initialized` (no response is produced).
5. Normal operation: `tools/list` and `tools/call`.
6. The server runs until `stdin` closes (EOF), then exits `0`.
### Channel discipline
- **stdout**: only protocol messages, one JSON line each. Nothing else is ever
written to stdout (no notices, no logs, no prompts).
- **stderr**: human diagnostics only (e.g. the no-`tix.toml` bootstrap error).
- **stdin**: only protocol messages. The server never prompts on stdin; every
tool call must supply everything it needs as arguments.
### Result shapes
A successful `tools/call` returns:
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [{ "type": "text", "text": "<human/agent-readable rendering>" }],
"structuredContent": { "...": "machine-readable data" },
"isError": false
}
}
```
The `content` text mirrors the corresponding CLI command's output exactly
(bit-parity with `tix list`/`tix view`/`tix status`/`tix search`/`tix lint`),
so a human and an agent read identical facts.
**Domain failures** (issue not found, ambiguous selector, unknown column/bad
priority, missing `glab`/`linear-cli`, delete without `confirm`) are **not**
JSON-RPC errors. They return a tool result with `isError: true`, the CLI's exact
error wording in `content`, and no `structuredContent`. The connection stays
alive — a failing tool never kills the server.
### JSON-RPC errors
The five frozen error codes are reserved for protocol-level failures:
| -32700 | Parse error | Undecodable JSON / bad framing line (`id: null`) |
| -32600 | Invalid request | Valid JSON but not a JSON-RPC message/method shape |
| -32601 | Method not found | Unknown method or unknown tool name in `tools/call` |
| -32602 | Invalid params | `tools/call` missing `name`, or type-invalid arguments |
| -32603 | Internal error | Unexpected internal failure (panic guard) |
### Concurrency
The server is single-threaded and sequential: one request → one response, in
order. Mutation results are reported only after the underlying write succeeds
(no "side-effect-then-error" lie). Clients may still pipeline.
---
## The 12 tools
| `create_issue` | `tix create` | Create an issue (`title` required, no prompt) |
| `list_issues` | `tix list` | Board table, optional `status` filter |
| `get_issue` | `tix view` | Full issue (frontmatter + body) for a selector |
| `board_status` | `tix status` | Column counts + total |
| `update_issue` | `tix edit` (flags) | Merge fields; at least one field required |
| `move_issue` | `tix move` | Move to a configured column; never prompts |
| `delete_issue` | `tix delete --yes` | Delete; **requires `confirm: true`** |
| `search_issues` | `tix search` | Case-insensitive substring over full markdown |
| `comment_on_issue`| (model writer) | Append a comment to an issue |
| `export_issues` | `tix export` | Mirror to GitLab/Linear via `glab`/`linear-cli` |
| `import_issues` | `tix import` | Pull open remote issues (idempotent by remote id) |
| `lint_issues` | `tix lint` | Validate frontmatter in every issue file |
Conventions:
- Every `inputSchema` is JSON-Schema draft-07, `additionalProperties: false`.
- Issue selectors accept an integer (`#123`) or a filename/title fragment,
matching the CLI's precedence: number → unique filename fragment → unique
title fragment.
- `labels` is a comma-separated string (parsed like the CLI).
- `priority` accepts only `low | medium | high`.
- `move_issue` records a commit only when supply `commit`; moving out of the
done column clears it. There is **no TTY prompt** anywhere.
- `update_issue`/`delete_issue`/`create_issue` never open `$EDITOR`, the TUI,
or read stdin.
---
## Prerequisites
- The `tix` binary. Build it (`cargo build --release`) and either install it on
`PATH` or reference the absolute binary path in the harness config.
- A repository with a `tix.toml` (create one with `tix init`). The server fails
fast if the cwd is not inside a repository.
- Map to the **right repository**: the server operates on the worktree it is
launched in. With harnesses that spawn from the project root (Claude Code,
Codex, opencode with `cwd` set), the server manages that project's issues.
## Smoke test (no harness)
```bash
cd /path/to/repo # must contain tix.toml
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' |
tix mcp
```
---
## Integration by harness
### Claude Code
Add to the project's `.mcp.json` (checked into the repo) or run
`claude mcp add tix -- tix mcp` for a personal, machine-scoped entry. Claude
Code spawns server processes with the project directory as cwd, so the server
picks up the repo automatically.
```json
{
"mcpServers": {
"tix": {
"command": "/path/to/tix",
"args": ["mcp"]
}
}
}
```
Restart (or `/mcp` reconnect) after adding. The agent can then call the 12 tools;
`create_issue`, `list_issues`, `get_issue` and `board_status` are the natural
first four an agent will use when asked to "track this as an issue".
### OpenAI Codex CLI
Two equivalent options:
```bash
# 1. CLI (writes the table for you)
codex mcp add tix -- /path/to/tix mcp
```
```toml
# 2. ~/.codex/config.toml (or project-scoped .codex/config.toml)
[mcp_servers.tix]
command = "/path/to/tix"
args = ["mcp"]
```
Notes specific to Codex:
- The table key is `mcp_servers` (underscore) — TOML, not JSON. A wrong key is
silently ignored.
- The server launches from Codex's working directory. If you want a different
repo, set `cwd = "/path/to/repo"`. For trusted project configs, add
`cwd = "${workspace}"` semantics are not built in; pin the absolute path.
- Raise `startup_timeout_sec` only if needed — `tix` is a native binary, so the
default 10 s is ample.
- Check with `codex mcp list`; inside the TUI, `/mcp` shows connected tools.
### opencode
Add a **local** MCP server to `opencode.json(c)` (project) or the global
`~/.config/opencode/opencode.json`:
```jsonc
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"tix": {
"type": "local",
"command": ["/path/to/tix", "mcp"],
"enabled": true
}
}
}
```
Optional keys: `cwd` (working directory for the server process; relative paths
resolve from the workspace) and `environment` (extra env vars). If `tix` is on
`PATH`, `["tix", "mcp"]` suffices. Use it in prompts by referencing the server
name, e.g. "use the tix tool to file this as an issue". MCP tools are prefixed
`tix_*`; disable/enable via the `tools` config if desired.
### Pi (pi.dev / pi agent)
Pi reads the standard MCP shapes in addition to its own override files, so the
universal `.mcp.json` also works:
```json
{
"mcpServers": {
"tix": {
"command": "/path/to/tix",
"args": ["mcp"]
}
}
}
```
Where to put it (highest precedence first):
- **Project**: `.pi/mcp.json` — Pi-specific project override.
- **Project shared**: `.mcp.json` at the repo root — used immediately.
- **User global**: `~/.pi/agent/mcp.json`, or `~/.config/mcp/mcp.json`.
A stdio server entry may set `"transport": "stdio"`, `"lifecycle": "eager"`
(auto-start) or `"lazy"` (start via `/mcp:start`), and `"env"` for extra
environment variables. Prefer `"lifecycle": "eager"` if you always want the
tools available. Check connectivity with `/mcp`.
### Generic / spec-compliant clients
`tix mcp` is a standard MCP stdio server (newline-delimited JSON-RPC 2.0), so
any conforming client works. Use the same registration pattern:
```
server name : tix
command : <tix binary> mcp
```
---
## Repository resolution
`tix mcp` resolves the repository **once**, at startup, using upward
`tix.toml` discovery from the process cwd (`cli::repo_config` → `Config::project_root`):
- A `tix.toml` is found in a parent directory of cwd → that is the issue store.
- No `tix.toml` is reachable → exit `1` with `no tix.toml found — run "tix init"
first` on stderr, and **no** bytes on stdout.
Implication: the server manages the repo it is spawned in. Harnesses that run
from the project root give each project its own issue store automatically; a
single global server config only makes sense for one repo at a time.
---
## Troubleshooting
| Server exits `1`, stderr says `no tix.toml found` | cwd is not inside a repo, or the harness set a different `cwd`. Run `tix init` (or point `cwd` at the repo root). |
| No tools appear in the agent | Wrong config file/shape — e.g. Codex wants TOML `[mcp_servers.tix]`, not JSON `mcpServers`; opencode wants `"type": "local"` under its own `mcp` key. |
| `-32602` error on a call | Argument type mismatch (e.g. a boolean where a string is expected) or a missing `name` in `tools/call`. Consult the tool's `inputSchema`. |
| `isError: true` with CLI-like text | A domain failure (unknown selector/column/priority, missing external CLI, delete without `confirm`). The text tells you exactly what is wrong. |
| stdout looks polluted / non-JSON | Some other process is writing to the server's stdout. tix itself never does. |
## Reference
- Wire protocol: `kitty-specs/tix-mcp-server-01M2G3FX/contracts/mcp-protocol.md`
- Tool definitions: `kitty-specs/tix-mcp-server-01M2G3FX/contracts/mcp-tools.md`
- Source: `src/mcp/{mod,protocol,tools}.rs`, bootstrap in `src/cli.rs` (`run_mcp`)
- Integration tests: `tests/mcp.rs`