Skip to main content
Glama
workhorse-ai-mcp

workhorse-ai-mcp

Official
by actari

actari

npm skills.sh

A task journal for AI agents. The rules are yours.

An append-only event log over SQLite (tasks, reports, artifacts, incidents, full-text search) exposed as an MCP server. Zero dependencies — only Node.js >= 22.5 with the built-in node:sqlite.

The journal knows one thing for sure: DRAFT → DELEGATED → REPORTED → ACCEPTED | REWORK | FAILED, plus DRAFT → DROPPED through drop_task (drop a draft you no longer want, with a reason), where reported (the executor believes it is done) is never the same as accepted (confirmed by a check). Everything else — what a task must contain, what a report must show, what counts as acceptance — is a policy: data you set per project or per cloud workspace, not opinions baked into the package. A separate act, record_release, records what shipped to prod (Released) — not tied to any single task; pass notes (a human title and a one-line "what's new" per item) and the cloud builds the Telegram release message from them.

Quick start

What each part is for. The MCP server (actari-mcp) is the journal on your machine: the agent keeps tasks, reports and artifacts in it with tools, and it pushes them to the cloud. The skills teach the agent: actari the journal protocol and feature the working order for one feature (see First feature in 10 minutes). The protocol: register the project first, run a task from a human through the journal, publish a feature from a discussion with publish_intent, take work from the cloud with inbox / take. The policy holds the rules of drafting, reporting and accepting; it is set per project (set_policy) or in the cloud workspace and read with get_policy. The cloud adds the board, acceptance and the team on top of the journal.

1. Sign up and create a workspace in the cloud, then issue an MCP token on the MCP tokens page (a write token — a read-only one cannot connect).

2. Install the plugin.

Claude Code — two commands (installs the MCP server and the skill):

claude plugin marketplace add https://github.com/actari/actari-mcp
claude plugin install actari@actari

Any other agent — install the skill, then add the MCP server to your agent's MCP config:

npx skills add actari/actari-mcp
{ "mcpServers": { "actari": { "command": "npx", "args": ["-y", "actari-mcp"] } } }

3. Connect to the cloud. Tell the agent: "Connect actari: address <your cloud address>, token act_...". It calls connect { "token": "act_..." } (see Connect to the cloud).

4. Register the project. In the repository folder tell the agent: "Register this project in actari". Without a registered project the journal refuses every task.

5. Run the first feature. Follow First feature in 10 minutes: one sentence to the agent, one "ok", and you watch the feature go from the board to "accepted" and "shipped".

Optionally, pin the journal at the project level — copy this into your AGENTS.md / CLAUDE.md:

Delegated work is recorded in the `actari` MCP journal: `draft_task` →
`delegate` → `submit_report` → `accept`. The rules for each step come from
`get_policy { project }` and are echoed in the tool descriptions.

Related MCP server: Coordination Memory MCP

First feature in 10 minutes

Prerequisites: steps 1-4 of Quick start (cloud workspace and token, plugin installed, cloud connected, project registered). The feature skill drives everything below; in Claude Code it is actari:feature. Without the cloud the same cycle runs in the local journal.

  1. You say: "Please do it through actari: add CSV export of the report" (or in Russian, "Сделай через actari: …"; the agent answers in your language). The agent: registers the project if it is new, reads its policy, and shows you a short intent: what it will do, what it will not, and 3-5 criteria for "done". You: answer "ok" or correct it. This is the only stop before acceptance.

  2. The agent: records the intent (intent: <slug> in the journal), publishes it with publish_intent, and cuts the work into 1-3 tasks. In the cloud: the feature appears on the board with its intent and criteria.

  3. The agent: drafts, delegates and does each task, then submits a report with the commands it ran and their output. In the cloud: each task moves in progress then reported.

  4. The agent: hands every task to a separate reviewer that has none of its context. The reviewer reads the report and the diff, runs the checks itself and returns verdict: accept | rework | failed with evidence; the verdict is recorded as an acceptance: <task> artifact. In the cloud: reported then accepted (or back to rework). In a client without subagents the agent shows you the report and evidence and accepts only after you say ok. The agent never accepts its own work.

  5. The agent: writes a summary: what is done, what proved it, what is left, where to look. You: see the accepted tasks on the board.

  6. You ship it yourself, then say: "Shipped." The agent: records the release with record_release and syncs. In the cloud: the release appears, and the notification reaches Telegram.

Take it from actari. Created the feature in the cloud instead? Add tasks, press "Take into work", and tell the agent: "Take it from actari." It reads the inbox, takes the intent with take, and runs steps 3-6 on it without writing its own intent.

Nothing showed up?

  • The project is not registered: tell the agent "Register this project in actari". Without it the journal refuses every task.

  • Node.js is older than 22.5: the server does not start. Check node --version.

  • The token is read-only: connect and sync need a write token. Issue a new one on the MCP tokens page.

  • You belong to several workspaces: bind the project with sync_scope.

  • No cloud connected: the cycle still works in the local journal. Look there with list_tasks; nothing is pushed until you connect.

Install

Add the server to your .mcp.json:

{
  "mcpServers": {
    "actari": {
      "command": "npx",
      "args": ["-y", "actari-mcp"]
    }
  }
}

Or run it straight from a checkout:

{
  "mcpServers": {
    "actari": {
      "command": "node",
      "args": ["apps/mcp/server.mjs"]
    }
  }
}

Data lives in ~/.actari/journal.db (override with ACTARI_DB). On first start the server creates the directory and the database itself. sync.json always sits next to the database.

search_precedents { query, project?, limit?, full? } looks for similar tasks (brief and report), artifacts and incidents across all projects of the journal. Words are matched by stems (Snowball, Russian and English), so «расписание» finds «расписании»; the project of the directory the server was started in ranks higher, and project narrows the search to one project. An artifact shows up as one entry — the freshest version that matches (an older version carries version_label). Service notes (acceptance:, release:, released:, brief:, progress:) are not searched; Project baseline is.

The index lives in the same journal.db (search_chunks, search_fts, search_vectors, search_meta). When a new package version changes the word analysis, the index is rebuilt in place in one transaction on the first start (a line goes to stderr); the database file is never replaced. Restart running sessions after an upgrade — older server versions do not know the new index.

Vectors (opt-in)

By default search is words only and nothing leaves your machine. To add semantic search, set both:

ACTARI_SEARCH_EMBEDDINGS=on
OPENAI_EMBEDDING_BASE_URL=https://…/v1     # OpenAI-compatible /embeddings
OPENAI_EMBEDDING_MODEL=intfloat/multilingual-e5-large
# optional: OPENAI_EMBEDDING_API_KEY, OPENAI_EMBEDDING_API_KEY_HEADER (e.g. X-API-Key)

With the switch on, journal text (tasks, artifacts, incidents) is sent to that service to compute embeddings; the variables alone are not enough. Missing vectors are computed lazily (up to 64 per search, the rest in the background); if the service fails the answer falls back to words. connect prints the mode: поиск: слова or поиск: слова + векторы (host, model).

The search package is vendored into vendor/search/ (a copy of the @actari/search ES build); refresh it with pnpm --filter actari-mcp vendor:search.

Connect to the cloud (optional)

connect needs only a token — the managed Actari cloud is the default. The token is personal (a PAT, like on GitHub): one token covers every workspace you are a member of, and the journal is routed between them by sync_scope:

connect { "token": "act_..." }

Tokens come in two kinds: write (the default) and read-only. connect and sync require a write token; a read-only token gets 403 read-only token. For agents that only need to read the cloud journal, use the separate package actari-reader (npx -y actari-reader) together with a read-only token.

A human can cancel, close or take back an intent and change its acceptance criteria; the server then refuses the next act and says why. intent_status shows the intent; release_intent gives a taken intent back.

publish_intent publishes a brief to the cloud: it creates or updates a feature and its intent keyed by (project, slug); a card closed by a human is never recreated.

feature_questions { project, slug } lists the AI product owner's questions on a feature (id, status, text, link and the reply thread); answer_question { project, slug, question_id, text } writes an answer into the question's thread, signed with the token owner's name and marked "via MCP". The feature is addressed by the same (project, slug) key as publish_intent, in the project's cloud_workspace_id.

system_map { project, element?, level?, query? } reads the workspace's system map (C4 context, containers, components and domain entities) as text: element is a map key such as backend_api (description, technologies, members, relations both ways, sources), level is context | container | component | entity, query searches names, descriptions and technologies; without arguments it returns the context and containers. Elements marked "по истории — проверьте" are not confirmed by a human. Read-only: the map is built and edited on the "Система" page in the cloud. The project must have a cloud_workspace_id.

log_time { task_id, hours, date?, note? } logs time on a cloud task as the token's owner, exactly like "Log time" in the task card (minimum 15 minutes, hours: 1.5 = 1 h 30 min, date defaults to today). task_id is a cloud task id or a journal task id that has an intent_task_id. It writes only to the cloud — never to the local journal — and fails with a clear message when no cloud is connected. submit_report accepts an optional duration_minutes (integer, 1–1440); when given, the cloud uses it as the AI time of the task.

log_usage { project, task_id, role, source, session?, usage_file? } records the AI tokens a task cost in the local journal (UsageRecorded; status is not changed, any status except DRAFT). role is executor or acceptance; source is subagent (a Claude Code subagent or forked skill) or headless (an external executor). Tokens are found by the server, not copied by the agent. With session: { client, id } it looks the session up by the id the client returned: claude-code — the agentId of a subagent (transcript in <CLAUDE_CONFIG_DIR|~/.claude>/projects/…/subagents/agent-<id>.jsonl) or the session_id of claude -p; opencode — the ses_… id from the task tool result (read-only from <XDG_DATA_HOME|~/.local/share>/opencode/opencode.db); codex — a thread_id or the task_name that spawn_agent returned (rollouts in <CODEX_HOME|~/.codex>/sessions). With usage_file it counts a file: a subagent transcript, the output of claude -p --output-format json, opencode run --format json or codex exec --json. One source per call — session, usage_file or numbers (total_tokens, or the input/output/cache breakdown, only the sum for the whole work, never the context size of the last turn). The client files are internal formats, not a promised API: if one changes, the call is refused with a reason and nothing is recorded. The feature skill calls it after the executor and after the acceptor; the cloud shows the numbers next to the AI time of the task.

Self-hosted (on-premise)

Pass the base URL of your instance; endpoint paths are derived by the server, so a reverse-proxy prefix works as-is:

connect { "url": "https://actari.acme.internal", "token": "act_..." }
connect { "url": "https://tools.acme.com/actari", "token": "act_..." }

The resolved base is stored in sync.json next to the database. To point every run at your instance without passing a URL, set ACTARI_CLOUD_URL.

Configure from .mcp.json instead

If you would rather keep the credentials with the rest of your MCP config — no connect call, no sync.json — pass them as environment variables. They take precedence over the file:

{
  "mcpServers": {
    "actari": {
      "command": "npx",
      "args": ["-y", "actari-mcp"],
      "env": {
        "ACTARI_SYNC_URL": "https://actari.dev",
        "ACTARI_SYNC_TOKEN": "act_..."
      }
    }
  }
}

ACTARI_SYNC_URL takes the same base URL as connect. The journal id defaults to <user>-<host>; override it with ACTARI_SYNC_JOURNAL_ID when one machine feeds several journals.

Several workspaces at once

One machine, one journal — but projects may belong to different teams. List the targets in sync.json and the journal is pushed to every one of them, each with its own cursor and its own scope:

{
  "targets": [
    { "alias": "acme", "url": "https://wh.acme.internal", "token": "act_...", "journalId": "kv-mac" },
    { "alias": "lab",  "url": "https://actari.dev", "token": "act_...", "journalId": "kv-mac" }
  ]
}

connect { "alias": "lab", "token": "act_..." } adds a target instead of replacing the config. On a flat config without an alias it overwrites, exactly as before; once a targets list exists it replaces only its own entry — matched by alias, or by url plus journal id — and leaves the neighbours alone. The flat single-target form ({url, token, journalId}) keeps working untouched, and so do the ACTARI_SYNC_* variables — they describe one target, so when a targets list is present they are ignored with a line on stderr rather than silently adding a third destination.

A target that is down does not hold up the others: the push reports per target, and a failing one is a line on stderr, never a crash.

Which projects are pushed (sync scope)

The journal is one per machine and holds every project you work on, while a cloud workspace belongs to a team. The token is personal, so the cloud reports every workspace you are a member of, and each push fans the journal out across them by the project mapping. Bind projects to a workspace by its slug:

sync_scope {}                                                  # what would be pushed where, and why
sync_scope { "workspace": "acme", "projects": ["acme-web"] }   # bind these to that workspace

sync_scope asks the cloud for the workspace list itself and records the id as cloud_workspace_id in the project registry — no manual ids, no SQLite editing. With a single workspace the workspace argument may be omitted. With several targets (servers), binding also names the target by its alias: sync_scope { "target": "acme", "workspace": "team", "projects": ["acme-web"] }. ACTARI_SYNC_PROJECTS="acme-web,acme-api" overrides the registry for one process, but only while you have a single workspace — with several it names the projects yet not the destination, so it is ignored with a warning. With no mapping anywhere: a single workspace receives everything (with a warning, as before); with several workspaces nothing is pushed until you bind projects — privacy over convenience.

While a scope is active, events that belong to no project (journal-level incidents, _general) and ProjectRegistered stay local. Widening the scope re-pushes from seq 0 so previously filtered events catch up; the cloud drops duplicates by seq. The last scope is remembered per target in sync-state.json next to the database (keyed by workspace id), so widening the scope of one target does not re-push everything to the others.

If your instance uses a certificate from an internal CA, give Node the root certificate — otherwise the TLS handshake fails and connect refuses to write the config:

NODE_EXTRA_CA_CERTS=/etc/ssl/certs/acme-root.pem

Sync is one-way: the journal is pushed up, the cloud never rewrites it. The cursor request is sent with Cache-Control: no-store, so a caching proxy in front of an on-premise instance cannot serve a stale cursor.

Policies

The server records; a policy decides the rules. A policy is a small JSON document:

{
  "schemaVersion": 1,
  "name": "Evidence first",
  "description": "Accepted means confirmed by something you can check.",
  "enforce": { "accept_requires_evidence": true, "draft_requires_artifact": null },
  "guidance": {
    "draft": "State what must be true when the task is done and how it is proven.",
    "delegate": "",
    "report": "Say what was verified and how — commands and output, not a summary.",
    "accept": "Accept only with evidence you produced or inspected yourself."
  }
}
  • enforce — the two switches the server checks: accept without evidence is rejected; draft_task is rejected until an artifact with the templated title exists ({project} is replaced by the project name).

  • guidance — free text per act, embedded into the descriptions of draft_task, delegate, submit_report, accept. Keep each under ~1500 characters: it lands in every session's context.

Where it lives: ~/.actari/policy.json, next to the database. set_policy { policy } sets the default for local projects, set_policy { project, policy } overrides one project; get_policy { project } shows the effective policy and its source. A project bound to a cloud workspace (sync_scope) takes the workspace's policy only — it is pulled on start and on every sync, and edited in the workspace settings. Without any policy the server is lenient: it records and does not judge.

Built-in presets (Lenient, Evidence first, Strict) are published in the cloud as public policies — attach or fork one there, or export its JSON and use it locally with set_policy.

Skills

The package ships two skills:

  • actari - the journal protocol and the pointer to get_policy.

  • feature - the working order for one feature: intent with one confirmation, 1-3 tasks, acceptance by someone other than the author, summary, release record. In Claude Code it is actari:feature.

Install via the Claude Code plugin (both skills come with it), or:

npx skills add actari/actari-mcp --skill actari
npx skills add actari/actari-mcp --skill feature

License

MIT

Maintenance

ActivityActive
ResponsivenessSyncing

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Persistent activity journal for AI agents - enables logging and querying decisions, changes, errors, and observations across sessions.
    6 npm
    1
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    An append-only coordination memory for multi-agent and human work, backed by SQLite, with a local dashboard and acceptance contracts that enforce integrator review before work is considered accepted.
    43
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables AI agents to exchange structured work items with an auditable lifecycle, supporting send, acknowledge, block, complete, and cancel operations via a shared SQLite-backed inbox.
    91 npm
    2
    Apache 2.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables agents to manage a local SQLite task backlog over stdio, with tools to create, search, list, get, update, and stat tasks while preserving audit history and idempotency.
    MIT