About decisionscope
decisionscope is a read-only CLI and stdio MCP server for querying an organization’s hosted decision record and polling for changes. It returns authoritative answers or an explicit no-answer result, without taking action in other systems.
Key features
- 01Natural-language decision queries
- 02Decision filters by type, owner, area, and surface
- 03Cursor-based change polling
- 04Organization-scoped token authentication
Use cases
- 01Check the current answer before an automated action
- 02Poll for decision changes since a saved cursor
MCP support
Runs as a stdio MCP server and forwards tool calls to a hosted endpoint. It exposes query_decision, list_decision_changes, and whoami.
Configuration
Local server config is the same for every client. Copy it into your client’s MCP config file.
Tools
3Saved tool list · updated 1w ago
| Parameter | Type | Required | Description |
|---|---|---|---|
| area | string | No | Restrict to records in this area. |
| type | enum | No | Restrict to records of this type: action item, follow-up, or decision.action_item | follow_up | decision |
| owner | string | No | Restrict to records owned by this person. |
| surface | string | No | Restrict to records from this surface. |
| question | string | Yes | The natural-language question to look up a decision for. |
Details
From the project READMEdecisionscope
decisionscope gives a tool read-only access to a hosted decision record: a system of record for what was actually decided. Before acting, a tool can ask the record a natural-language question — "Are we still shipping the new onboarding flow?" — and get back the current authoritative answer, or an explicit hold when the record has no confident answer. A polling caller can also drain decisions whose state changed since a cursor. The record only answers; it never takes actions in your systems.
decisionscope is a forwarding client. It runs locally as a CLI
and a stdio MCP server, and forwards each call to the hosted
endpoint as a JSON-RPC tools/call request. The endpoint is named
by RITHMO_MCP_URL and defaults to the hosted service at
https://app.rithmo.ai/api/mcp; a self-hosted deployment exposes
the same tools and contract at its own address.
Setup
Every request carries a per-organization service token as a
Bearer token. The token resolves to exactly one organization,
must carry the mcp:read scope, and every result is scoped to
that organization. There is no anonymous access.
export RITHMO_MCP_TOKEN="<your service token>"
# Optional; only needed for a self-hosted endpoint:
export RITHMO_MCP_URL="https://app.rithmo.ai/api/mcp"
Keep the token in a secret store, never in code. With no token
set, every call fails before any request is sent, with an error
naming RITHMO_MCP_TOKEN.
Install
npx @reneturcios/decisionscope whoami
Or run the container image:
docker run -i ghcr.io/cloudcorprecords/decisionscope
The container defaults to the MCP server (see below). Pass CLI
arguments to use it as a CLI instead, and pass the two environment
variables above with -e so the container can reach your record.
CLI
decisionscope query_decision "Are we still shipping the new onboarding flow?"
decisionscope query_decision "What did we decide about GitHub integration?" --type decision
decisionscope query_decision "Who owns the pricing change?" --owner "Priya" --surface jira
decisionscope list_decision_changes
decisionscope list_decision_changes --since "0_" --limit 200
decisionscope whoami
decisionscope --version
Results print as JSON. Exit codes: 0 on success, 1 on an
operation error (including a missing token), 2 for usage
errors. Arguments are validated before the token is checked, so a
malformed call is always a usage error.
MCP server
One bin serves both faces. The mcp subcommand starts a stdio
MCP server:
npx -y @reneturcios/decisionscope mcp
MCP client configuration:
{
"mcpServers": {
"decisionscope": {
"command": "npx",
"args": ["-y", "@reneturcios/decisionscope", "mcp"],
"env": {
"RITHMO_MCP_TOKEN": "<your service token>"
}
}
}
}
Object results are returned as a JSON text block and as
structuredContent, so clients can read
res.structuredContent directly.
Tools
| Tool | Input | What it returns |
|---|---|---|
query_decision | question (required); optional type (action_item | follow_up | decision), owner, area, surface | A result discriminated on result: found, found_record, or no_record — see below. |
list_decision_changes | optional since (opaque cursor), optional limit (integer 1–200, default 50) | { changes, cursor, has_more } — the changes since the cursor, in (changed_at, decision_id) order. |
whoami | (none) | { org_id, scopes } for the configured token. |
Reading a query_decision result
The result field has three variants, and the distinction
matters:
found— a live answer from the commitment ledger. The answer text is atanswer, alongsidestatus,lifecycle_state,ownerandsource(both discriminated states, never bare nulls),decided_at,due_at,rationale,conditions,director,context, asupersedeschain (newest-first, cycle-guarded, capped at 12 hops), an optionalspine, and aconfidencescore. Apart fromconfidence, afoundresult for the same decision is byte-identical on every call.found_record— a live answer from the subject record. This is an answer, not a refusal. The answer text is atsubject.currentAnswer, notanswer— and it may benull, meaning the subject is on record but there is no answer text to act on.no_record— no confident answer. The bare form is just{ "result": "no_record" }; a typed form adds arecordexplaining why the answer is held (closed,no_record, ornot_observedwhen the asked-about surface is one the record does not observe). Apremise_check, when present, always hassafe_to_proceed: false. Never proceed on ano_record.
Gate on a usable answer, not on the variant name:
const r = res.structuredContent;
const answer =
r.result === 'found' ? r.answer :
r.result === 'found_record' ? r.subject.currentAnswer :
null;
if (answer != null && String(answer).trim() !== '') { /* act */ }
else { /* hold — no answer to act on */ }
One special case: a no_record whose record.reason starts
with record_unavailable: means the record itself could not be
read (a transient failure). That is not "we decided no" — hold
and retry.
Draining changes
list_decision_changes is built for a polling trigger:
- First call (no
since): returns no changes and the current head cursor. You start from now; history is not replayed. Passsince: "0_"to backfill from the start instead. has_moremeans the page was full (changes.length === limit). It can be true on an exactly-full final page, so the next call may legitimately return zero changes — that is how the drain terminates, not an error.- When a page returns no rows, the cursor is echoed back unchanged, so re-polling with it is safe and idempotent.
- Each change carries the same record core as a
foundresult, pluschanged_at— but neversupersedes,spine, orconfidence. Itsstatusis wider: reversals appear asdroppedorsuperseded, values afoundresult never returns.
Development
npm install
npm run build # tsc -> dist/
npm test # node test runner over test/*.test.ts, no network (fake fetch only)
npm run dev # run the CLI from source via tsx
License
MIT — see LICENSE.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.