actari-mcp
v0.16.0
Published
Task journal for AI agents: event sourcing over SQLite with an MCP interface. Policies set the rules. Zero dependencies.
Maintainers
Readme
actari
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.
Quick start
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@actariAny 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"] } } }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.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.
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_..." }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.
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 workspacesync_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.pemSync 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:acceptwithoutevidenceis rejected;draft_taskis 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 ofdraft_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.
Skill
The package ships one skill, actari — the journal protocol and the
pointer to get_policy. Install via the Claude Code plugin, or:
npx skills add actari/actari-mcp --skill actariLicense
MIT
