Skip to main content
Glama
quochuy
by quochuy
README.md
# ai-coord-mcp

A shared coordination layer between multiple AI coding assistants — not an orchestrator.

This MCP server never launches, manages, or talks to any AI process. It only exposes a
task queue, a per-task message thread, and progress reporting, all persisted as
human-readable JSON on disk. Claude Code and OpenCode (or any other MCP-capable client)
are launched manually, in separate terminals, and both point their MCP config at this
server. Claude typically plays coordinator (creates/reviews tasks); OpenCode plays
implementer (claims/updates/completes them) — but the server has no opinion about that,
it just stores state.

```
Claude Code (Sonnet)                    OpenCode (DeepSeek/Qwen/etc.)
        │                                        │
        │ create_task, list_tasks,                │ claim_task, update_progress,
        │ approve_task, request_revision           │ complete_task, post_message
        ▼                                        ▼
              ai-coord-mcp  (this server)
                       │
                       ▼
              .ai/coordination/*.json  (on disk)
```

## Design

- **Transport: stdio.** Each client spawns its own copy of this server as a subprocess.
  "The same server" means the same *storage directory* (`COORDINATION_DIR`), not one
  process. Every tool call re-reads the relevant JSON file from disk before mutating it;
  writes go through temp-file-then-rename (`writeJsonAtomic`) so a reader never sees a
  half-written file; a cross-process mutex (`withLock`, `src/storage/lock.ts`) built on
  `mkdir` guards concurrent writers, with stale-lock reclaim after 10s and an ownership
  token per holder.
- **IDs**: sequential `task-0001`, `task-0002`, … assigned under a short-lived lock.
  Validated against `/^task-\d{4,}$/` before being joined into any filesystem path.
- **Optimistic concurrency**: every task carries a `version` counter. Pass
  `expected_version` to any mutating tool to get a `ConflictError` instead of a silent
  overwrite.
- **Not implemented**: binary attachments (the `files` field is path references only,
  not stored content), archiving (the `archive/` directory exists but nothing writes to
  it), typed `branch`/`commit` fields (use the `metadata` convention below instead),
  structured Definition-of-Done / `verify_command` (use `expected_output` instead), task
  relationships / `parent_id`/`blocks` (use `tags` to group instead), MCP Resources /
  `resources/subscribe` (use `wait_for_task` instead).

**Status model:**

```
pending --claim_task--> claimed --update_progress--> in_progress
   ^                        |                              |
   |                        |     both claimed and in_progress can complete_task--> completed
   |                        |                                                            |
   |                        └──────────────────────cancel_task─────────────────┐  approve_task│request_revision
   |                                                                            |         |         |
   └───────────────reopen_task (from cancelled/completed/approved)◄────────────┘         ▼         ▼
                                                                                    approved   revision_requested
                                                                                               (loops back through
                                                                                                update_progress →
                                                                                                in_progress again)
```

`update_progress` auto-advances `claimed` or `revision_requested` to `in_progress` on its
first call in that cycle — there's no separate "start_task" tool.

## Storage layout

```
.ai/coordination/
  tasks/task-0001.json       one file per task (includes its own audit history[])
  messages/task-0001.json    array of {id, taskId, author, timestamp, content}
  progress/task-0001.json    latest snapshot + full history of updates
  archive/                   reserved, unused
  .locks/                    transient lock directories, safe to delete if empty
```

Everything is plain JSON — `cat .ai/coordination/tasks/task-0001.json` works fine while
the server is running.

## Setup

```bash
cd ai-coord-mcp
pnpm install
pnpm run build
```

Both Claude Code and OpenCode need to point at the same `COORDINATION_DIR` (an absolute
path) so they share state regardless of which directory each was launched from.

**Claude Code** — add to `.mcp.json` (project) or via `claude mcp add`:

```json
{
  "mcpServers": {
    "ai-coord": {
      "command": "node",
      "args": ["/absolute/path/to/ai-coord-mcp/dist/index.js"],
      "env": {
        "COORDINATION_DIR": "/absolute/path/to/shared/.ai/coordination"
      }
    }
  }
}
```

**OpenCode** — add the equivalent entry to its MCP config (`opencode.json` /
`~/.config/opencode/config.json`, depending on your OpenCode version):

```json
{
  "mcp": {
    "ai-coord": {
      "type": "local",
      "command": ["node", "/absolute/path/to/ai-coord-mcp/dist/index.js"],
      "environment": {
        "COORDINATION_DIR": "/absolute/path/to/shared/.ai/coordination"
      }
    }
  }
}
```

If `COORDINATION_DIR` is unset, the server defaults to `<cwd>/.ai/coordination` — only
safe if you know both clients will be launched from the same working directory.

## Getting started

1. Build the server and wire up `COORDINATION_DIR` for both clients (Setup above).
2. Launch Claude Code and OpenCode in separate terminals, both in the shared repo.
3. In Claude Code, create a task:
   > Create a task: title "Sort inventory list", description "Implement stable sort by
   > SKU for the inventory panel, keep the existing filter hooks working",
   > expected_output "inventory.ts updated, existing tests still pass, new test for
   > empty list".
   → calls `create_task`, returns `task-0001` (status `pending`).
4. In OpenCode:
   > Check ai-coord for any pending tasks.
   → calls `claim_next_task`, works the task, calls `update_progress` a few times, then
   `complete_task`.
5. Back in Claude Code:
   > Review task-0001 and approve or request revision.
   → reviews the diff, calls `approve_task` or `request_revision` (the latter posts
   feedback to the thread; OpenCode reads it via `get_messages` and repeats from
   `update_progress`).

## Tools

| Tool | Purpose |
|---|---|
| `create_task` | Create a task (status `pending`) |
| `list_tasks` | List task summaries (no `history[]`, includes computed `idleSeconds`), filter by `status` / `assignee` / `creator` / `tag` |
| `get_task` | Fetch one task by id, full record including `history[]` |
| `search_tasks` | Substring search over title/description/tags, same summary shape as `list_tasks` |
| `claim_task` | `pending` → `claimed` |
| `claim_next_task` | Atomically claim the first eligible pending task (optionally by `tag`) instead of a `list_tasks` + `claim_task` round trip; returns `null` if none available |
| `wait_for_task` | Block (polling, lock-free) until a task's status changes or `timeout_seconds` elapses, instead of looping `get_task` yourself |
| `complete_task` | `claimed`/`in_progress`/`revision_requested` → `completed` |
| `approve_task` | `completed` → `approved` |
| `request_revision` | `completed` → `revision_requested` (also posts the feedback as a message) |
| `cancel_task` | any open status → `cancelled` |
| `reopen_task` | `cancelled`/`completed`/`approved` → `pending`, clears assignee |
| `post_message` | Append to a task's conversation thread |
| `get_messages` | Read a task's thread, optionally only messages `since` an ISO timestamp |
| `update_progress` | Report `percent` (0-100) / `current_step` / `notes`; auto-advances status to `in_progress` |
| `get_progress` | Latest progress snapshot + history |

All mutating tools accept an optional `expected_version` — pass the task's current
`version` to get a `ConflictError` instead of clobbering a concurrent change.

Every status-changing tool above (`claim_task` through `reopen_task`) returns the same
summary shape as `list_tasks` — no `history[]` — so an implementer with a small context
window (a local model, say) isn't handed the whole growing transition/note log back on
every call. Same for `update_progress`: it returns the current snapshot, not the growing
`history[]`. `get_task`/`get_progress` remain the only tools that return full history.

## Workflow conventions

- **Waiting for work**: use `wait_for_task` over polling `get_task`/`list_tasks` — one
  call instead of N, blocks server-side without holding a lock, returns `timed_out: true`
  as a normal result.
- **Picking up work**: use `claim_next_task` over `list_tasks({status:"pending"})` +
  `claim_task` — same two steps, atomic, retries automatically if another claimer wins
  the race.
- **Git state**: no typed `branch`/`commit` field. Convention: `create_task`'s `metadata`
  carries `{ baseCommit, branch }`; both agents read/write those same keys.
  `expected_output` plus this README is the Definition-of-Done contract.
- **Implementer loop**: `claim_next_task` (or `claim_task` on a specific id) → work → a
  few `update_progress` calls → `complete_task`. If revision is requested, `get_messages`
  for the feedback, then repeat from `update_progress`.
- **Coordinator loop**: `create_task` → `wait_for_task` (or check back later) → on
  `completed`, review the diff at `working_directory` → `approve_task` or
  `request_revision`.

## Tests

```bash
pnpm test
```

Runs `node:test` (via `tsx`) over every `src/**/*.test.ts` file: the storage layer
(atomic writes, cross-process lock semantics including the stale-lock/ownership-token
edge case, id generation, message/progress persistence) and the full tool surface
(state-machine transitions, `TASK_ID` schema rejecting malformed/traversal ids, version
conflicts) driven through the real `registerXxx()` functions against a fresh temp
`COORDINATION_DIR` per test file.

TDQS

A4/5.0

Scored across 16 tools

Disambiguation5/5

Each tool targets a distinct resource/action: task CRUD, claiming, lifecycle transitions (complete/cancel/reopen/approve/revision), messaging, progress, and waiting. Even similar tools like list_tasks/search_tasks and claim_task/claim_next_task are clearly differentiated by purpose and described behavior.

Naming Consistency5/5

All tools follow a consistent verb_noun pattern in snake_case (create_task, get_task, claim_task, update_progress, wait_for_task). Verb choices are semantically aligned with actions (list/get/search for reads; claim/complete/cancel for state changes), making the set predictable and easy to navigate.

Tool Count4/5

At 16 tools, the set sits at the upper boundary of typical scope, but every tool serves a distinct purpose in the coordination workflow. The slightly elevated count is justified by the inclusion of specialized tools like claim_next_task and wait_for_task that optimize common operations.

Completeness4/5

The surface covers the full task lifecycle (create, read, claim, complete, cancel, reopen, approve, revision) plus messaging and progress tracking. Minor gaps exist: there's no tool to edit task metadata (e.g., title, description, assignee) after creation, but this can be worked around via cancel/recreate.

Maintenance

ActivitySlowing
ResponsivenessNo issues