tix-tracker 0.2.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
# 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:

| Code   | Meaning               | When                                                   |
|--------|-----------------------|--------------------------------------------------------|
| -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

| Tool             | CLI counterpart      | Purpose                                            |
|------------------|----------------------|----------------------------------------------------|
| `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

| Symptom                            | Cause / fix                                                    |
|------------------------------------|----------------------------------------------------------------|
| 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`