MCP servers
MCP (Model Context Protocol) is a common way for a service to expose its tools to an assistant. Many services now host one. Nocturn can connect, so you gain those tools without writing any code.
Add a server
Section titled “Add a server”One folder per server under extensions/ in the workspace, each with an mcp.json:
{ "url": "https://api.example.com/mcp/", "auth": "token"}| Field | Meaning |
|---|---|
url |
Required, and must be https. A Bearer must never ride to a cleartext endpoint, so an http URL is rejected rather than warned about. |
name |
Optional — the folder name is the identity (extensions/cloudflare/ → cloudflare). It namespaces the tools the model sees (cloudflare_*) and the credential owner (ext:cloudflare), and must match ^[a-z0-9][a-z0-9_-]{0,31}$: no dots, and short enough that <server>_<tool> fits the 64 characters a tool name is allowed. A name that disagrees with its folder is warned about, because that is usually a slip. |
auth |
How the Bearer is obtained: "token" (you seed it into the vault yourself), "oauth" (the server discovers the flow, see below), or omit it for a public server. Any other value is an error. |
oauth |
Optional, and only for a server whose endpoints you must supply by hand. Mutually exclusive with auth — one credential, one source. |
An oauth block holds the endpoints the server would otherwise have discovered:
| Field | Meaning |
|---|---|
auth_url |
Required, https. Where you are sent to consent. |
token_url |
Required, https. Where the code is exchanged for a token, and where refreshes go. |
client_id |
Required. The client you registered with that provider. |
client_secret |
Optional — leave it empty for a public client. It is the one field here that is not checked, because “no secret” is a legitimate answer. |
scopes |
At least one. An empty list is refused rather than sent as a request for nothing. |
A block that could not complete a flow is refused when the workspace opens, not at the first request — the point being that you find out while looking at the file you just wrote.
Like a plugin manifest, mcp.json is parsed strictly: an unknown field is an error, not a
warning, so a typo’d auth never silently becomes “no credential”.
Delete the folder to remove the server — that also takes its credential shard with it, which is the
point rather than a side effect: the shard is encrypted under a key derived from that folder’s path,
so it is readable nowhere else, and leaving it behind would be a token for a server nobody declares.
Removing from the app does the same, and additionally revokes the remembered network permission
for that server’s host. A grant records what was allowed, never why, so once the server that prompted
the question is gone the answer would stand on its own — and the next server declared on that host
would inherit a “yes” nobody gave for it. The cost is that the same grant may be one you gave so
http_read could reach that host; you are asked once more, which is what the gate is for.
Declaring a server is an authority-relevant act: it hands the model that server’s tools and wires
your token to its host. That is why extensions/ lives in the control plane, outside mnt/ — the model
can neither read nor write these files. Presence is the authorization. There is no separate
review step; dropping the folder in is the decision.
Signing in
Section titled “Signing in”-
None — omit
authfor a public server. -
Token —
"auth": "token", then seed the bearer out of band:Terminal window printf %s "$TOKEN" | nocturn secret set mcp:<name>It lands encrypted in the server’s own shard. Nothing secret ever touches
mcp.jsonor the environment — see the vault for how that shard is keyed and why the model never sees the value. -
OAuth with discovery —
"auth": "oauth"and nothing else.nocturn auth <name>then discovers the server’s authorization endpoints, registers a client, prints a consent URL for you to open, and stores the token. No client id, no endpoints to copy. This follows the MCP authorization spec: protected-resource metadata → authorization-server metadata → dynamic client registration, PKCE, resource-bound tokens.{ "url": "https://mcp.cloudflare.com/mcp", "auth": "oauth" }Terminal window nocturn auth cloudflare # discovers, registers, prints the consent URLnocturn auth cloudflare -scope account:readScopes are not in
mcp.jsonhere — they belong to the sign-in, which is why they are a flag. Discovery registers a fresh OAuth client for you, and the scope list is part of that registration; asking for different scopes later means registering again, sonocturn authwith a different-scopeis the operation, not an edit to a file. Leave the flag off and an empty scope is requested, which lets the server apply its own default.Whether a given server supports discovery is worth checking before assuming — not every remote MCP server implements the chain, and one that does not needs the manual block below.
-
OAuth, endpoints by hand — if the server’s authorization server does not do dynamic registration, give an
oauthblock withauth_url,token_url,client_idandscopesinstead."auth": "oauth"and a manual block are mutually exclusive: one credential, one source, no silent tie-breaking.Here the scopes are in the file, and required. The client is one you registered yourself ahead of time, so its scopes describe the provider rather than a single sign-in, and they apply to every refresh that follows.
-scopeis ignored on this path.
Malformed declarations are rejected fail-closed — a non-HTTPS URL, an unknown auth mode, token
alongside an OAuth block. Either way the token stays host-side, bound to that server, and the model
never sees it — the vault is where all of that is explained once.
From the app, without a terminal
Section titled “From the app, without a terminal”You do not need the command line for this. Settings → Accounts lists every discover-mode server in the workspace and whether it already holds a token, and connecting one runs the identical flow — the server does the whole thing, the app only ferries two values.
The consent page opens in the system browser, not inside the app. That is a deliberate choice
(RFC 8252): your password manager works there, and
an embedded web view is a login form rendered by the app asking for the password. The provider
redirects to nocturn://oauth/callback, the OS routes that back into the app, and the app relays the
authorization code and state to the server.
The token never reaches the app. The server exchanges the code itself, and the PKCE verifier never leaves it — so an app that hijacked the URL scheme and captured the code still could not exchange it for anything.
Putting one in: Cloudflare, end to end
Section titled “Putting one in: Cloudflare, end to end”mkdir -p nocturn-data/workspaces/main/extensions/cloudflarecat > nocturn-data/workspaces/main/extensions/cloudflare/mcp.json <<'JSON'{ "url": "https://mcp.cloudflare.com/mcp", "auth": "oauth" }JSON
export NOCTURN_MASTER_PASSPHRASE=… # the vault has to be open to store a tokennocturn auth cloudflare # prints a URL; open it, consent, done
nocturn serve # start (or restart) — the server is connected hereThe order matters, and one step is easy to get wrong:
- The folder is the declaration. Nothing else registers a server.
nocturn authneeds no server. It reads the workspace folder directly and writes the token into that server’s own encrypted shard. It also needs the vault open — withoutNOCTURN_MASTER_PASSPHRASEthere is nowhere to put the token, and it says so.- Then start the server. On the command line, servers are discovered and connected when a workspace opens. The app can make it look again at any time.
Once it is up the server’s tools appear as cloudflare_*, and the first call asks about
mcp.cloudflare.com on the net axis, like any other request leaving the machine.
How MCP calls are gated
Section titled “How MCP calls are gated”Every MCP tool call is an HTTPS POST to the server. It is gated exactly like one:
kind net, target the server’s host. So you approve mcp.cloudflare.com once, and that covers
the server’s tools.
Two things follow, and the second is a real limitation worth stating plainly:
- What an MCP tool can reach is bounded regardless of what the server claims — it talks to its own host and nowhere else, and its responses go through the ingress leak scan.
- Nocturn does not distinguish a server’s read-only tools from its mutating ones. The protocol
has a
readOnlyHintannotation, and Nocturn parses it but does not act on it: an approval for the host covers whatever the server exposes. So a server you connect can act on your behalf within its own API surface without asking per tool.
That makes connecting an MCP server a trust decision of the same weight as installing a plugin. Both are structural: the reach is bounded by construction, but what happens inside the bound is the server’s to define.
What Nocturn refuses
Section titled “What Nocturn refuses”The parts of MCP that would let a remote server drive your assistant are left out: it does not accept server-sent prompts for your model to run, and it does not act on server-pushed content on its own. You get the tools without handing a stranger the wheel.