granted
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@grantedCheck grant status for report 2026-09-30"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
granted
A time-bound, single-use grant that lets an agent carry out a decision a human already made — on a clock, with a receipt from the substrate, and never silently.
"you give your agent a task, then walk away and get a coffee, only to come back and find the agent got stuck on an approval on the first step and has made no progress. As a result, people often give in and set their agents to 'auto-approve', or
--dangerously-skip-permissions, which is, obviously, unsafe."— the cloudflare-os README
Cloudflare's answer is deferred approval: the agent proceeds against simulated results and the human approves later, in bulk. That fits a company where the human is back in minutes. granted is the answer for the owner-operator whose median time-to-keystroke is measured in days: approve once, in advance, with a deadline; the machine discharges it on the clock; the ledger — not the tool — confirms it; and silence is not a permitted outcome.
Not affiliated with Cloudflare. Python 3.11+, standard library only.
30-second setup
pip install git+https://github.com/wayneColt/granted
granted mint --type report --id 2026-09-30 --by "owner keystroke" \
--not-after 2026-10-01T00:00:00Z --allow send
granted status --type report --id 2026-09-30
granted discharge --type report --id 2026-09-30 --action send --exec ./send_report.shThe last line prints one of discharged, refused, uncertain, expired with a reason, and exits 0, 3, 4 or 5 to match. Run it again: refused: consumed. Grants are files in ~/.granted/grants/; the ledger is ~/.granted/ledger.jsonl. GRANTED_HOME, GRANTED_STORE, GRANTED_LEDGER move them.
Related MCP server: dingdawg-agent-wallet
Five clauses
A decision is a grant. One object, once. The human's decision becomes one file with the object's type and id in it. There is no grant for "things like this".
A grant carries its deadline.
not_afteris required;not_beforeis optional. A grant with no deadline is a standing permission, and the validator refuses it.The executor discharges inside the grant and never outside it. It may never mint. The library has no path from an executor, an MCP client, or a runner to a new grant. Minting is a CLI verb, run by a hand.
Every outcome is loud.
discharged | refused | uncertain | expired, each with a reason from a closed vocabulary, each written to the ledger as anoutcomerow, each returned — never raised — to the caller. A refusal leaves a row. An expiry leaves a row.Confirmation comes from the substrate. An
intentrow before the action, aresultrow the executor writes after it, and the grant's ownconsumedflag and receipt, re-read from disk. The executor's return value alone confirms nothing: success with no result row isuncertain.
What stays human
The primitive is for decisions already made. It does not make them, rank them, or infer them. These verbs never ride a grant by default, and nothing here will discharge them unless a human mints for that exact object, on purpose:
hire
negotiate
sign
pay
first contact with no prior thread
not_scope exists so the grant can say so in words the approver reads.
The grant
{
"kind": "granted/1",
"decision": "APPROVE_ONCE",
"object": {"type": "report", "id": "2026-09-30"},
"scope": "send the weekly report to the list it went to last week",
"not_scope": ["any other recipient", "any attachment not already in the draft"],
"allowed_actions": ["send"],
"not_before": null,
"not_after": "2026-10-01T00:00:00Z",
"minted_by": "owner keystroke",
"minted_at": "2026-09-20T12:00:00Z",
"consumed": false,
"consumed_at": null,
"receipt": null
}field | meaning |
|
|
|
|
| words for the human; the library does not parse them |
| if non-empty, a discharge must name one of them |
| ISO 8601 with a zone. Naive stamps are refused. |
| the hand. A string, not an identity system. |
| written by the store; the receipt names the discharge that spent it and its outcome |
The JSON Schema is at docs/grant.schema.json. One file per grant; the store finds the live one for an object and reports none | consumed | not_yet | expired | unreadable when there is none.
The state machine
find grant ──none/consumed/not_yet/unreadable──▶ refused
│ └──expired──────────────────────────▶ expired
▼
write intent row ──fails──▶ refused (ledger_unwritable; nothing runs)
▼
claim grant (receipt: pending) ──lost──▶ refused
▼
executor(grant)
├─ raises Uncertain ─────────────────────▶ uncertain (spent)
├─ raises ExecutorFailed / anything ────▶ refused (spent: effects unknown)
├─ returns falsy ───────────────────────▶ refused (executor_declined; grant stays live)
└─ returns truthy
▼
ledger.confirms(object)? no ─────────▶ uncertain (no_result_row; spent)
▼ yes, and the row says ok
settle receipt, re-read grant ──mismatch─▶ uncertain (spent)
▼
discharged (APPROVE_ONCE: spent)Once the executor has been called, the grant is spent whatever happens next — except when the executor positively declines, which means "nothing done, try me later". Retrying anything else is a human act: mint again. The full reason vocabulary is granted.discharge.REASONS.
The executor
The library never knows how to send an email or deploy anything. The host supplies executor(grant):
it | means |
returns truthy | claims success — and must have written a |
returns falsy | did nothing; the grant stays live |
raises | cannot say whether it happened |
raises | failed part-way; effects unknown |
raises anything else | a bug; treated like a failure |
from granted import Store, Ledger, discharge
store, ledger = Store("~/.granted/grants"), Ledger("~/.granted/ledger.jsonl")
def send_report(grant):
message_id = mailer.send(draft_for(grant.object_id)) # the host's code
ledger.result(grant.object, ok=True, receipt={"message_id": message_id})
return True
outcome = discharge(store, ledger, {"type": "report", "id": "2026-09-30"}, send_report, action="send")
print(outcome) # discharged: confirmed (report/2026-09-30) -- result row line 42The shell executor behind granted discharge --exec writes the result row itself (exit status, stdout and stderr tails). Exit 0 claims success; exit 75 (EX_TEMPFAIL) declines; any other exit is a failure; a --timeout is uncertain. The command sees GRANTED_OBJECT_TYPE, GRANTED_OBJECT_ID, GRANTED_DISCHARGE_ID, GRANTED_GRANT_PATH, GRANTED_LEDGER, GRANTED_NOT_AFTER in its environment.
CLI
granted mint --type T --id I --by "who" --not-after ISO [--not-before ISO] [--decision D] [--scope ..] [--not-scope ..] [--allow ..]
granted status --type T --id I
granted discharge --type T --id I --exec "cmd" [--action A] [--timeout S]
granted ledger [--tail N] [--id I]
granted lag [FILE|-]
granted mcp [--executor NAME=COMMAND ...]--json on any of them. python -m granted works from a clone without installing.
MCP server
granted mcp --executor send_report=./send_report.shstdio, JSON-RPC 2.0, no third-party code. Implements initialize, ping, tools/list, tools/call. Three tools:
tool | does |
|
|
| discharge through an executor the host registered by name; returns one of the four outcomes |
| the last N rows |
There is deliberately no mint. Minting is the human's act and lives in the CLI. An agent on this server can learn, discharge, and read back; it cannot widen its own permissions and cannot hand the executor a command of its own.
Client configuration, for a host that takes the usual shape:
{"mcpServers": {"granted": {"command": "granted", "args": ["mcp", "--executor", "send_report=./send_report.sh"]}}}The clock
adapters/systemd/ is a granted-discharge@.timer + .service pair and a runner. The timer ticks every 15 minutes; the grant's own window decides; the runner stops the timer once there is nothing left to ask. uncertain is left as a failed unit on purpose.
Measuring the lag
The motivation is a number. Keep a JSONL of {"raised": ISO, "answered": ISO} for every time an agent waited on a keystroke, then:
$ granted lag asks.jsonl
n=10 open=1 bad=1 p50=10.0h p80=72.0h max=240.0hNearest-rank percentiles. open is the count still waiting. If p50 is minutes, use deferred approval. If it is days, mint.
Status
Surface | Status |
Core: | yes; 63 tests under |
CLI: | yes |
MCP server (stdio; | yes; no |
systemd adapter | yes (unit, timer, runner) |
cloudflare-os Gatekeeper | design note + TypeScript skeleton; not compiled or deployed |
Door (signed admission endpoint) | |
Signed intents in the CLI / MCP path | not in 0.1.0; both trust the process boundary |
Store across hosts | not in 0.1.0; a directory on one disk, one lock file per grant |
Windows | untested |
Build
git clone https://github.com/wayneColt/granted
cd granted
python -m unittest -v
python -m granted --helpNo dependencies. License: Apache-2.0 OR MIT.
What this is not
Not an approval queue. Nothing waits here; a grant exists or it does not.
Not an identity system.
minted_byis a string the human types. The door contract indocs/DOOR.mdsays where signatures go; this release has none.Not a scheduler. The systemd timer is a tick; the deadline is the grant's.
Not a policy engine. It cannot tell a safe action from a dangerous one. That is what the human did when they minted, and what
not_scopeis for.
Release drafts (X, blog) live in RELEASE_DRAFTS.md and wait for an operator keystroke.
Available Tools
3 toolsgrant_dischargeA
Carry out the decision a human already recorded for this object, through an executor the host registered by name. Returns one of discharged | refused | uncertain | expired with a reason. Cannot mint; cannot run anything the host did not register.
Inputs: id (required), type (required), executor (required), action.
grant_statusB
Is there a live grant for this object? Returns live | none | consumed | not_yet | expired | unreadable, the grant on file, and the last ledger rows.
Inputs: id (required), type (required).
ledger_tailC
The last N ledger rows (intent, result, outcome), optionally for one object id.
Inputs: n, object_id.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
TDQS
Scored across 3 tools
Each tool has a clearly distinct purpose: grant_status checks the current state of a grant, grant_discharge executes a recorded human decision, and ledger_tail provides raw ledger history. Even though status and ledger_tail both touch ledger rows, their focus and output differ enough to avoid confusion.
All tool names use lowercase snake_case and follow a consistent 'domain_action/noun' style, such as grant_status, grant_discharge, and ledger_tail. They deviate from a strict verb_noun pattern but are still predictable and internally consistent.
With only three tools, the surface is tightly scoped to the server's apparent purpose: checking grant state, discharging grants, and auditing the ledger. Each tool covers a distinct operation and none feels redundant or unnecessary.
The server intentionally does not create or mint grants—those decisions are recorded by humans and executed via grant_discharge. Within that scope, status checking, execution, and ledger auditing form a complete workflow with no obvious dead ends.
Maintenance
Related MCP Connectors
Read-only XH Agents MCP connector: service catalogue, payment info, verification checks.
Managed MCP gateway with OAuth, per-agent permissions, shared accounts and activity logs.
Public MCP for agent verification, work discovery and governed interoperability.
Workflow diagnostics, capability routing, and x402 settlement for MCP-compatible agents.
Related MCP Servers
- FlicenseAqualityDmaintenanceProvides double-entry accounting ledger creation, transaction recording, and financial reporting capabilities via MCP.7-
- FlicenseNot gradedqualityDmaintenanceProvides MCP tools to enforce spend policies (allow, deny, step-up, allowlist) on agent wallets with an immutable audit trail.-
- FlicenseNot gradedqualityBmaintenanceProvides MCP tools that let an agent retrieve customer account, product usage, interaction, and support summaries, and create follow-up tasks after user approval.-
- AlicenseAqualityCmaintenanceProvides customer lookup, transaction history, knowledge-base search, and ticket management tools to any MCP client.5MIT