Connect Claude and custom MCP clients
NomOS is a governed MCP server. Claude Desktop, Claude Code, or a custom client get the same evidenced, room-isolated answers as the interface, secured through OAuth.
What and for whom
Section titled “What and for whom”If you run an MCP client (Claude Desktop, Claude Code, or a custom integration), you can connect NomOS directly. NomOS exposes itself as an MCP server. Every answer carries its citation, owner, and validity, and is limited to the rooms the requesting person is a member of.
Access runs through the same governance as the web UI: per-room OPA admission, hard rule checks, and an evidence record for every question, every check, and every request. There is no side door around the rulebook.
OAuth flow
Section titled “OAuth flow”External clients authenticate via OAuth (authorization code with PKCE/S256) against the Keycloak realm «nimbus». The flow is standardised (RFC 9728) and works without a pre-created token:
- The client fetches discovery: GET on «/.well-known/oauth-protected-resource» (HTTP 200, JSON below).
- From «authorization_servers» the client follows to the Keycloak realm «nimbus».
- Login + PKCE (S256): the user signs in to Keycloak, the client exchanges the code for an access token.
- The client calls the MCP tools with the header «Authorization: Bearer …».
Discovery: GET https://<your-installation>/.well-known/oauth-protected-resource
{"resource":"https://<your-installation>/api/mcp", "authorization_servers":["https://<your-installation>/auth/realms/nimbus"], "scopes_supported":["openid","profile","email"], "bearer_methods_supported":["header"]}Without a token, the server responds with a 401 challenge and a «WWW-Authenticate» header. Its «resource_metadata» points to discovery. This way, a conformant client finds the authorization server on its own.
Registration alone does not confirm a working connection. Copy the displayed MCP address unchanged into the client and test it with a real request. If the public configuration is missing, the platform administrator must first correct the MCP address.
401: POST https://<your-installation>/api/mcp
www-authenticate: Bearer error="invalid_token", error_description="Authentication required", resource_metadata="https://<your-installation>/.well-known/oauth-protected-resource/api/mcp"Keycloak (Realm nimbus): https://<your-installation>/auth/realms/nimbus , PKCE S256, authorization_code.
Claude Desktop
Section titled “Claude Desktop”In Claude (web or desktop), add a «Custom Connector» under «Settings → Connectors» and enter the URL «https://<your-installation>/api/mcp». The OAuth client ID under «Advanced settings» is optional: without it, Claude registers itself, and your consent dialog decides on the first connection. With a fixed ID, Claude always uses the same client: either the pre-registered ID «claude-desktop» or (recommended, with audit trail and revocation) the client ID of a per-user agent created in the agents area. Leave the secret field empty, because the client is public and uses PKCE. The Keycloak login opens afterwards.
Note: on some desktop versions, the connector sign-in is not kept. If the tools are missing after a restart, reconnect the connector once.
Connection lifetime: a connection lasts up to 30 days without use, and every use extends it. Only after that do you need to reconnect once.
Claude Code
Section titled “Claude Code”For Claude Code, add the MCP URL with «claude mcp add –transport http». Claude Code signs in through OAuth: browser login, then the consent dialog. You don’t need a token or a script.
Terminal
claude mcp add --transport http ainomos "https://<your-installation>/api/mcp"Then run «/mcp» in the Claude Code session and start the sign-in for «ainomos». Once the server is connected, ask a first question, for example about your own rooms: Claude then calls «list_rooms».
Alternatively, if a per-user agent already exists in the agents area: its detail pane shows the ready-made block with the pinned client ID under «Configuration file». Claude Code writes the ID as «oauth.clientId». Cursor’s «auth.CLIENT_ID» has no effect there: the client then re-registers itself on every start and asks for consent again.
Hermes Agent
Section titled “Hermes Agent”Hermes Agent (Nous Research) runs OAuth 2.1 with PKCE itself, so it connects in the same three steps as Claude Code: no token to copy, no header to edit. Hermes 0.20.4 or later; the commands come from «hermes mcp –help».
-
Add the server: run the command in a terminal. Hermes writes the entry to «~/.hermes/config.yaml», registers itself with the authorization server (RFC 7591), and opens the browser login. Alternatively paste the configuration block, which pins the pre-registered client «hermes-agent» (ADR-038) instead of registering a new one.
hermes mcp add ainomos --url "https://<your-installation>/api/mcp" --auth oauth -
Sign in: the Keycloak login opens in the browser. If Hermes registered itself with the command from step 1, the consent dialog follows; with the pre-registered client «hermes-agent», there is no consent dialog. If no browser opens or the login times out, run «hermes mcp login ainomos» from a fresh terminal; it waits five minutes. Hermes stores the token in «~/.hermes/mcp-tokens/» and refreshes it on its own.
hermes mcp login ainomos -
Check: «hermes mcp test ainomos» lists the tools; in the chat they all carry the prefix «mcp_ainomos_», for example «mcp_ainomos_list_rooms».
hermes mcp test ainomos
The configuration block, if you prefer a file over the command:
~/.hermes/config.yaml
mcp_servers: ainomos: url: "https://<your-installation>/api/mcp" auth: oauth oauth: client_id: "hermes-agent"Verified against this installation: discovery («/.well-known/oauth-protected-resource»), the 401 challenge, the Keycloak metadata («registration_endpoint» present, no client ID metadata document, so Hermes registers via RFC 7591), and the pre-registered client «hermes-agent» with loopback redirects. To be confirmed by you: the browser round trip from a running Hermes (login, consent, «hermes mcp test»). Compared with Claude Code, Hermes lacks only a connector dialog, which is why step 1 is a command or a file.
Skill, hardening, and the tools Hermes must call first are on the Hermes help page:Connect the Hermes agent to NomOS
Devin for Terminal
Section titled “Devin for Terminal”Devin for Terminal (the devin command) connects with OAuth and PKCE, like Claude Code, but its configuration file uses its own field names: url, transport and oauthClientId. The Claude Code shape with type and an oauth block has no effect there. Pin the client ID of your agent from the agents area; no secret is needed. Live verified against this installation on 2026-09-24.
-
Create the file and replace the placeholder with your agent’s client ID:
~/.config/devin/mcp_config.json
{"mcpServers": {"ainomos": {"url": "https://<your-installation>/api/mcp","transport": "http","oauthClientId": "<client-id-of-your-agent>"}}} -
Sign in. The browser opens the login: choose the account whose rooms Devin should see.
devin mcp login ainomos -
Check that the server is connected and its tools are loaded. Restart a Devin session that was started before the login:
devin mcp list
Devin Desktop, the editor, reads ~/.codeium/windsurf/mcp_config.json and names the address serverUrl. Its documentation shows no field for a pinned OAuth client ID, so Devin would register itself and the connection would wait for approval in the agents area. This variant is not verified against this installation; please confirm it before relying on it.
An agent reaches this server only through the tools of this connection. It must not fetch a token itself, for example with curl and someone else’s credentials: that bypasses your sign-in, and the calls are recorded under the wrong account.
The seventeen tools
Section titled “The seventeen tools”The tools follow an agent’s lifecycle: orient and discover, read, check, contribute, and trace. The catalog tools only read. Contributing always means proposing; humans decide and accept.
Orient and read
Section titled “Orient and read”list_rooms()
Lists the rooms the requesting person is a member of, with a compact topic digest from current governed decisions. Drafts and superseded decisions do not count; rejected proposals remain visible as binding negative governance. «hidden» means that the decision digest is unavailable to this identity. It does not mean that the room is ungoverned.
search_brain_catalog(q?, room?, origin?, genre?, publisher?)
Searches visible Brain Store offers by free text, target room, origin, genre or publisher. With a target room, every offer carries its current relationship and next permitted action. Discovery is read-only.
get_brain_offer(offer_id, room?)
Reads a visible offer with publisher, version, data class, licence, typed content counts and representative titles. Private Brains remain undiscoverable; Decision content follows Decision visibility.
list_brain_relationships(room)
Shows direct, inherited, available and approval- or acquisition-related states for one of the caller’s rooms, including next_action. The tool does not subscribe, request or approve.
ask_brain(room, question)
Returns a governed answer with citations and a «run_id», on the same path as the web UI (retrieval, rule check, evidence). «room» accepts the slug or the display name of one of your member rooms. For follow-up questions, pass the «conversation_id» from the previous answer: the history flows in as context, and the answer returns the «conversation_id». Citations still apply per answer.
list_decisions(room, status?, type?, mine?)
Read the governing decisions structurally (key, title, type, status), deterministically and without a language model; with mine=true only your own proposals.
get_decision(room, key)
Full detail of a decision (body, relations, origin). Also answers the question “What became of my proposal?”.
get_decision_neighbours(room, key)
Shows a decision’s typed neighbours one step out: neighbour title and kind, relation type, direction, effect, edge status (active steers, proposed is inert), whether the retrieval priority is maintained, the answering room’s per-type weight, retrieval priority and provenance. Room-scoped, read-only.
get_passage(room, knowledge_object_id, version?)
Reads one knowledge entry’s passage through the same authorized reader the UI uses: title, passage text, source document, version, validity, owner, and whether it is knowledge (advisory) or a governing decision (binding). knowledge_object_id is the id an ask_brain citation carries; a GOV: key resolves to the decision’s record. Gated exactly like the UI reader: room membership, the knowledge surface, and for a decision record also the decisions surface; anything else is “not found” without a title. The reader keeps no version history: it serves the current passage and states version_served next to version_requested instead of reconstructing an older text.
validate_action(room, action, content="", tool="")
The runtime PDP for agents: submit a planned action and get a verdict («allow», «caution», «require_approval», or «block»), the applicable binding rules, an AI assessment marked as advisory, and an «evidence_id». If the action names a tool, pass «tool»: the verdict then also checks the room’s tool rules and, for «require_approval», names the approval path. «room» accepts the slug or the display name.
Contribute (draft, never binding)
Section titled “Contribute (draft, never binding)”request_brain_access(offer_id, room)
As the owner of the target room, requests access to a protected Corp Brain. The request writes an evidence record, approves nothing, and creates no subscription; a designated human publisher approver decides it in the inbox.
propose_decision(room, type, title, context_md, options_md, recommendation_md)
Captures a decision that comes up in the dialogue as a draft in the room (ADR, BDR, or SDR), with context, options, and recommendation. The draft never becomes binding automatically: the responsible decision maker reviews and approves it in the Library; the proposer is recorded as its origin. Each proposer can hold a limited number of open drafts per room; the room details show the cap and whether it comes from the room or the installation.
request_exception(room, policy_key, justification, scope?)
When an action runs into a guardrail, request a time-boxed exception. Only a decision maker of the room (room owner, governance owner, or tenant admin) can approve it, in the interface; the exception lasts 14 days by default. Hard rules have no exception path.
propose_knowledge(room, title, body_md, summary, source_note)
Submit produced knowledge as a draft into the import review. It enters the memory only after a human accepts it.
list_knowledge_proposals(room)
Shows your own knowledge submissions in the room across all statuses (draft, accepted, rejected), with the target room. This way, the agent sees what became of a «propose_knowledge» call. Read-only, without a language model.
get_evidence(run_id)
Fetch the full evidence bundle for a run_id, with the same proof that the UI shows.
get_answer_relations(run_id)
Reads an answer’s prerequisites and conflicts over its run_id: each depends_on prerequisite the governance block pulled in (and which decision pulled it in) and each conflicts_with pair, as data instead of prose. Read-only, no language model; same reach as get_evidence.
Deliberately absent: tools for publisher decisions, Brain subscriptions, administration, or deletion. Machines read or request; humans decide.
Why governed
Section titled “Why governed”Room access is decided through OPA based on membership. A call to another room fails with the same decision as in the UI. Questions, checks, and knowledge proposals go through hard checks: SEC-01 blocks secrets. DAT-03 withholds an answer containing personal data in rooms of the restricted data class and marks personal data in checked actions as «caution». Every question, every check, and every request creates an evidence record; pure read tools don’t write one. Budget gates cap consumption.
The approach is deliberately cooperative and evidenced: an agent learns which rule applies and why. This way, it can adjust its action before it acts.
Troubleshooting
Section titled “Troubleshooting”- 401 (Bearer, «invalid_token»): usually the token has expired. Repeat the OAuth login («/mcp» in Claude Code, «hermes mcp login ainomos» in Hermes), or fetch a new token for an NHI agent. If the error persists with a fresh token, the installation is more likely misconfigured (issuer/JWKS) than the token at fault → have the admin check it.
- 403 on a room: the OPA decision denies admission. Check your room membership first («list_rooms()» shows the allowed rooms). If the error reports «is closed to agents», the room is closed to agents and only reachable in the browser; this is a room setting. If the room is listed in «list_rooms()» and access is still denied without this notice, the server-side policy data does not add up (misconfiguration) → report it to the admin instead of searching your own permissions.
- Read the «WWW-Authenticate» header and «resource_metadata». A conformant client follows them automatically to the authorization server.
- Check the token claims («iss», «exp», «email»): a wrong issuer or an expired «exp» are the most common causes.
- «Client not found» on the Keycloak page: the OAuth client ID is unknown to the realm. Either let the app register itself (self-service: no client ID, the consent dialog is the gate) or set a client ID that exists: «claude-desktop», «cursor», «hermes-agent», or the ID of an agent from the agents area. A made-up or mistyped ID always fails this way.