Lesson 42 — AI clients and API tokens: the graph over /mcp
Goal: by the end of this lesson an AI coding client (Claude Code, Cursor, anything that speaks MCP) can read, extend and run your graph on a branch of its own, and you know how to hand it exactly the rights it needs and take them back.
Concepts introduced: the /mcp endpoint (JSON-RPC over HTTP),
the ten tools and the one resource an agent gets, branch
scoping for every call, the agent cycle (branch → upsert →
execute → test → diff → your review), API tokens with scopes
and an expiry, and what a token can never do.
Everything an agent does through /mcp is an ordinary graph write or
run under your rights — the same rights a browser session has, or a
narrower set if the token says so. There is no second permission
system to learn.
The interactive tour covers the account token loop on deployments with the
tenancy addon and a signed-in account (api-tokens): select a readable function,
create tutorial-token with only Edit graph & branches and a 7 days
lifetime, verify that the token authenticates but Execute returns token-scope,
then Revoke it and verify that it no longer authenticates. The masked
listing supplies the token's UUID, scopes and expiry; the bearer appears only
in the normal one-time reveal and is never saved in Lessons. Leave the reveal
open until revocation so the final check can use that same bearer. After a
reload, cleanup can still revoke the saved exact UUID; repeat the lesson for
the full authentication check. A lost creation reply retains the proposed
create-only UUID for cleanup, without adopting an existing token by label.
External MCP client setup remains the written exercise below.
The endpoint
One route: POST /mcp, JSON-RPC 2.0, stateless — every call carries an
Authorization: Bearer <token> header and stands alone. On a
self-hosted instance the bearer is the instance's AUTH_TOKEN; on the
cloud it is an API token you mint in Settings (below). The mcp
package is optional: an instance that does not load it answers 404.
Ask it what it offers:
curl -s https://<your-org>.graphden.dev/mcp \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Ten tools come back — list-namespaces, search-fns, read-fn,
describe-fn, execute-fn, create-branch, upsert-fn-defs,
run-tests, list-branches, diff-branch — and one resource,
graphden://ai-context: the authoring guide for AI clients (the entity
model, composition patterns, naming rules). Tell your agent to read it
before it writes anything.
Connecting a client
Claude Code, per machine:
claude mcp add --transport http graphden https://<your-org>.graphden.dev/mcp \
--header "Authorization: Bearer $TOKEN"
Cursor and other MCP clients: the same URL, the same header, transport
"streamable HTTP", no session state. A self-hosted instance is the
same with http://localhost:9002/mcp.
Branch scoping — every call names its branch
Reads (read-fn, describe-fn, search-fns, execute-fn,
list-namespaces) run against the branch the request rides:
the X-Graphden-Branch: <name> header, ?branch=, or — per call — the
optional branch argument, which sends that one call to the named
branch. Mutations name their branch explicitly: upsert-fn-defs takes
branch and never falls back to main. That is the safety property:
an agent cannot write to your trunk unless you ask it to.
The agent cycle
The loop an agent runs, and what each turn gives you back:
create-branchai/<feature>— its sandbox, an ordinary branch (Lesson 23).upsert-fn-defswithfn-defsas EDN text in exactly thefns.ednshape (Lesson 31) — one string inside the JSON call, because only EDN keeps:other-fn(a reference) and"other-fn"(a string) apart. A refused def comes back as a-32602naming what was wrong.execute-fn/run-testson that branch — the same pipeline as the editor's Run pane and the tests runner (Lesson 15, Lesson 17).execute-fnwaits up to 10 s inline; a still-running call returnspendingwith its execution ID. Effectful runs persist automatically, andtrace: truerecords the call tree and persists the run. The tool does not expose intermediate-value capture.diff-branch— the proposal, as data.- You open the editor on
ai/<feature>, press Δ against main, and land it through the review cycle you already know — propose, approve, merge (Lesson 24).
read-fn returns a subtree as fns.edn text — the same syntax
upsert-fn-defs takes, so an agent can read, edit and write back
without translating. describe-fn is the computed contract on the
branch: free args, effects, return type, whether the fn could be a
service.
What the agent cannot do through this door. upsert-fn-defs
refuses fn-defs whose identity is package-owned — the platform's own
fns, restored on every boot (Lesson 32 § Fork
tells the same story for packages) — unless allow-platform-overwrite
is passed on purpose. Base-fn implementations (impls.clj) are code,
not graph: they need a rebuild, so no MCP tool touches them.
API tokens — the rights you hand out
On the cloud (any deployment with the tenancy addon) tokens live in Settings → Account → API tokens. Create token… unfolds the form:
- a label (
laptop MCP) — what the row will be called; - scopes — tick what this token may do: Edit graph & branches, Execute functions (both on by default), Merge branches, Manage services, Write secrets, Publish packages;
- a lifetime — 7, 30 or 90 days (the default), a year, or never.
Create shows the value once; copy it into the client's configuration and it is gone from the screen. Each row lists its scopes and expiry and carries Revoke — revoking is immediate, and the client's next call answers 401.
Scopes are a ceiling, not a grant: a token's rights are your account's grants (Lesson 28) intersected with its scopes, so a token can narrow what you may do, never widen it. Two rules hold regardless of scopes: an API token never reaches organization management (members, invites, grants, roles) and never mints or revokes tokens — a leaked key cannot make itself another key.
On a self-hosted instance without the addon there are no per-token scopes: the bearer is the instance token, as powerful as the account.
Try it
You need a token (cloud: mint one with the default scopes; self-host:
AUTH_TOKEN) and curl. Replace the host with yours.
-
Create the sandbox branch:
curl -s $BASE/mcp -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call", "params":{"name":"create-branch","arguments":{"name":"ai/tutorial-42"}}}' # → {"ok":true,"branch":{"name":"ai/tutorial-42",…}} -
Write one fn-def onto it — EDN text inside the JSON string:
curl -s $BASE/mcp -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call", "params":{"name":"upsert-fn-defs","arguments":{"branch":"ai/tutorial-42", "fn-defs":"[{:name :shout :namespace \"ai-demo\" :parent :str-upper :args {:string {:value \"hello from mcp\"}}}]"}}}' # → {"ok":true,"branch":"ai/tutorial-42","fn-ids":["…"]} -
Run it on that branch:
curl -s $BASE/mcp -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -H "X-Graphden-Branch: ai/tutorial-42" \ -d '{"jsonrpc":"2.0","id":3,"method":"tools/call", "params":{"name":"execute-fn","arguments":{"name":"shout","args":{}}}}' # → {"status":"succeeded","result":"HELLO FROM MCP",…} -
Read it back as
fns.edn(read-fnwith"branch":"ai/tutorial-42") — the text you get is the text you sent, plus its parent's row. -
Open the editor, switch to
ai/tutorial-42in the branch chip:ai-demo.shoutsits in the Explorer like anything you clicked together. Press Δ onmain— one added fn. That is the review surface; merging or deleting the branch is your call. -
Clean up: delete
ai/tutorial-42from the branch popover (it was never merged, so it goes), and — on the cloud — Revoke the token if it was only for this lesson.
What we glossed over
- The full workflow contract for people developing graphden itself (worktree stacks, what the MCP cycle verifies vs. what only a real boot-sync catches): docs/MCP_CLIENTS.md.
- Traces over MCP —
execute-fnwithtrace: truereturns the call tree (one row per fn invoked), the same data the canvas draws in Lesson 18. - Never point an agent at a shared demo — branches isolate writes, not attention; give each agent its own instance or branch.
Next
This is the last lesson. The written docs continue where the tutorial stops: docs/MCP_CLIENTS.md for the agent workflow, docs/SECURITY_MODEL.md for what each principal may do.