This page gets an agent talking to a running Noon instance: which transport to use, how to get a token, and how to check the connection works.
Pick a transport
Noon exposes two MCP transports. Use whichever matches where your agent runs.
- Streamable HTTP at
POST /mcp, with anAuthorization: Bearer <token>header on every request. Use this when the agent is a separate process, possibly on another machine, reaching Noon over a network (a tailnet, or a public address behind a TLS-terminating proxy). The bind host and port come fromNOON_MCP_HOST(default127.0.0.1) andNOON_MCP_PORT(default3001). - stdio, for a client that launches Noon's MCP server as a local child process on the same machine. The token comes from the
NOON_TOKENenvironment variable of that process, not a header. The server checks the token at startup and refuses to start if it is missing, malformed, expired, or revoked. It also re-checks the token on every tool call, so revoking it stops an already-running stdio session on its next call.
Get a token
Bootstrap, once. On a fresh install, run the bootstrap CLI with ALLOW_BOOTSTRAP set:
ALLOW_BOOTSTRAP=true pnpm --filter @noon/core noon:bootstrap
This prints a root token to the console exactly once. Copy it now. It is never shown again, and running the command a second time does not mint a second one: the underlying claim is single-use even if you forget to unset ALLOW_BOOTSTRAP afterward.
Exchange it for a scoped session token. Do not hand the root token to a routine agent. Instead call:
curl -s -X POST http://localhost:3000/api/v1/tokens \
-H "Authorization: Bearer $ROOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"scopes": ["content:write", "content:read"], "expiresInDays": 14}'
This route requires a root-scoped bearer token. The body is {scopes, expiresInDays}: scopes is a non-empty list and may never include root, and expiresInDays defaults to 14 with a maximum of 365. The response is:
{
"id": "tok_...",
"token": "<plaintext, shown once>",
"scopes": ["content:write", "content:read"],
"expiresAt": "2026-08-24T00:00:00.000Z"
}
Store token, not id. id is a non-secret reference used later to revoke it.
Revoke a token when it should stop working immediately:
curl -s -X POST http://localhost:3000/api/v1/tokens/<id>/revoke \
-H "Authorization: Bearer $ROOT_TOKEN"
This also requires root and returns {"id": "<id>", "revoked": true}. An already-revoked or unknown id still returns revoked: false rather than an error. An agent holding a root token can do the same over MCP with the revoke_token tool:
{"tool": "revoke_token", "arguments": {"tokenId": "<id>"}}
which returns {tokenId, revoked}.
Scopes
| Scope | Unlocks |
|---|---|
root | Everything, including minting and revoking tokens, and the root-only audit_log collection. Keep this out of routine agents. |
content:read | Read pages, posts, templates, and other content collections through the query tool. Also enough to call describe_schema. |
content:write | Create, update, rename, publish, unpublish, discard, and roll back pages, posts, templates, and site settings. Also enough to call describe_schema. Does not by itself unlock reading through query. |
query:read | The same read access as content:read, through the query tool. No write access. |
secrets:write | The only scope that unlocks manage_connection (creating, updating, rotating, and revoking stored connections and credentials). Not implied by content:write. |
For a routine authoring agent, issue content:write plus content:read (or query:read): it can write content and read back what it wrote, and nothing else. Add secrets:write only if the agent also manages connections.
Recovery, if the root token is lost
Recovery is a server-side path, not an API call. No HTTP route reads CMS_ROOT_OVERRIDE_KEY; only the recovery CLI does.
- Set
CMS_ROOT_OVERRIDE_KEYin the server's environment. It must be a real 32-byte (256-bit) high-entropy secret; the CLI refuses to run against a weak value. - Run the recovery CLI on the server:
pnpm --filter @noon/core noon:recover
It prompts for the key with input hidden if run at an interactive terminal, or reads it from piped stdin otherwise. It is never accepted as a command-line argument, so it cannot land in shell history. On a correct key it mints and prints a fresh root token, once, the same way bootstrap does.
Connect Claude Code, or another MCP client
Add this to your client's MCP server configuration, over Streamable HTTP:
{
"mcpServers": {
"noon": {
"type": "http",
"url": "http://<host>:3001/mcp",
"headers": { "Authorization": "Bearer <your-token>" }
}
}
}
Put this in a user-level config file, not one checked into a repository. The token is a secret.
Check it works
List the tools, then ask the server to describe its content schemas:
curl -s -X POST http://<host>:3001/mcp \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
curl -s -X POST http://<host>:3001/mcp \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"describe_schema","arguments":{}}}'
A missing, malformed, or revoked token gets a structured auth error before either call reaches the tool layer. If both calls return real data, the agent is connected.