Skip to content

MCP tools

Hadron exposes several dozen hadron_* tools through the Model Context Protocol. Any MCP host (Claude Code, Cursor, the Claude desktop app, …) connected to a Hadron MCP server can call them to read and write memory, post to the team chat, and run actions.

This page is a categorized lookup of the tool surface — name, purpose, required inputs, and what the call returns. For walk-throughs that use these tools end-to-end, see Connecting an MCP host and Adding nodes to a memory.

Connecting

The MCP server is shipped as part of hadron-server and registered in your MCP host's config (.mcp.json, Claude Desktop's settings, etc.). The fastest way to wire it up in a coding-agent project is the install script, which configures .mcp.json and the Spec Kit extension in one step.

Once connected, every tool below is callable as mcp__hadron__<tool-name> (or under whatever namespace your host uses).

X-Hadron-Client-Id — keeping your selections across reconnects

MCP clients re-initialize transparently: a server restart replaces the session without telling the agent driving it. Everything held per-session goes with it — the active App, the active memory, the active scope and the active organization. The symptom is calls that work for a while and then start refusing, or searching somewhere else, for no visible reason.

A client that sends a stable instance id gets all four restored on the next connection:

X-Hadron-Client-Id: <stable opaque id>

What an integrator needs to know:

  • Stable across restarts. A per-process id defeats the purpose — it is a new client every time.
  • Per client instance, not per user. Two clients on one account keep separate selections, so changing App in one does not silently change it in the other. Sharing an id across instances works and is a deliberate choice, not the default.
  • Opaque, trimmed, and max 200 characters. Empty or over-length is treated as not sent rather than truncated — truncating would collide two instances onto one selection.
  • Entirely optional. Send nothing and you get exactly the previous behaviour: selections live for the connection and no further.
  • Restoration is re-authorized every time, so a restored selection can never hand back access the user has since lost. It also needs a user identity — a pure App-key caller has no per-user selection to restore.

Active memory

Most tree tools take an optional memoryUrn (or a loc that starts with one). When you don't pass one, they fall back to the active memory for the session. Bind it once with hadron_set_active_memory and subsequent calls operate on that memory by default. This is the recommended pattern — passing memoryUrn on every call is verbose and error-prone.

Search is the exception. hadron_find_nodes never falls back to the active memory: with no memory selector it runs under the search ladder — the active scope, the active App, the active organization, or failing those your own user-owned memories. If a search returns "No nodes found" across queries that should match, read the scope line at the top of the result before assuming the topic isn't covered: it names what was actually searched.

Memory selection

Tool Purpose Key inputs
hadron_list_memories List every memory accessible to the caller, with URN and access level (read-only or read-write). (none)
hadron_set_active_memory Bind a memory as the session default for subsequent calls. memoryUrn
hadron_sync_graph Trigger a one-shot GitHub source sync for a memory backed by a Git source. memoryUrn

The list returned by hadron_list_memories is the authoritative inventory of where you can read and write — agents are not memories, and apps are not memories. If a memory you expect isn't in the list, your identity is missing access to it.

Sessions

\"Session\" means the worker session, not your chat session

Throughout this page, a session is the Hadron Session row — the thing hadron_start_session opens and hadron_end_session closes. Your chat session (the Claude Desktop window, the Claude Code conversation) is a separate lifecycle, and closing it does not end the worker session: only hadron_end_session does that, and the worker keeps reading as taken until you call it or its idle window passes. See the two kinds of session.

Worker sessions are optional but strongly recommended for traceability. Every node created inside a session is attributed to it; ending the session records summary metrics.

Tool Purpose Key inputs
hadron_start_session Open a work session and get a session ID for attribution. description (required); optional type (DEVELOPER/CHATBOT/AUTOMATION/EDGE), repo, branch, prNumber, language, llmModel, parentSessionId, prevSessionId, workerRef (work AS a named Worker), force (take over a WORKER_TAKEN worker — only on the user's explicit override)
hadron_end_session Close the worker session, and for a worker-bound one write its closing handoff — the record the next stint inherits at bind (hadron-server#1029). Closing your chat session does not do this. id (required); optional handoff (the thing the next driver reads; a failed write refuses the end, so retrying is safe), summary (a display-only label the successor never sees), errorCount, turnCount, inputTokens, outputTokens
hadron_whoami Answer "what am I driving right now?" — your own open worker sessions, each with the worker it is bound to, its App, and when it started. The way to recover a session id you no longer have. Optional limit (default 10, cap 50), includeEnded (also list recently ended sessions)

Returns: hadron_start_session returns a session ID and a 6-phase protocol hint. The protocol's middle phases ("save plan", "update memory") are bookkeeping markers under Spec Kit — plan.md is the saved plan; memory updates flow through hadron_create_node / hadron_update_node directly. Always close with hadron_end_session.

Both tools are strict: they need App context and fail with [no_active_app] without it. See MCP App selection.

Attribution and the end gate

hadron_start_session stamps the caller's user id on the session. For an OAuth or PAT caller that is you; for a pure App-key caller it is null. That stamp is what makes the session yours — it drives the attributed-user self-read, and it decides who may close the session.

hadron_end_session is gated. Exactly two principals may end a session through MCP:

  • a pure App-key principal, for its own App's sessions, or
  • the authenticated user the session is attributed to.

Anyone else gets Forbidden: session "<id>" belongs to a different App or user. The App branch requires the absence of a user id: when you call as a user with an active App selected, the selection is a scope, not a credential — so co-members of one App cannot end each other's sessions. (The GraphQL endSession applies the same gate, plus a platform-admin branch that MCP does not expose.)

Ending a session you didn't start is therefore refused rather than silently succeeding. The full read/write table is in the Authorization reference.

Worker binding is on this surface; the rest of coding provenance is not

hadron_start_session accepts workerRef (a worker id from hadron_cast_worker) to work as that named casting, and force to take over a worker someone is already driving — binding a taken worker refuses with WORKER_TAKEN, carrying who last drove it and when (informed takeover, enforced server-side; retry with force: true only on the user's explicit override).

force reaches taken and never held. A name is held by the person who binds it, and nothing about a session frees a hold — not an end, an idle window, an expiry; only a release does. Binding a name held by somebody else refuses WORKER_HELD and force: true does not get past it: the hold is checked before the flag is consulted. The remedy is to cast your own worker, or to ask the holder or an App/org admin to release the name. It refuses every principal but the holder — including a pure App key, which is not a person and holds nothing. There is no MCP release tool yet (hadron-server#1060), so releasing goes through GraphQL releaseWorker or the CLI's hadron team worker release. The remaining coding-session fields — transcriptPath, host, agentRef — are set through GraphQL startSession and the hadron team session CLI commands. There is no MCP equivalent of updateSession either; hadron_end_session is the only session write MCP offers.

Recovering after a context compaction

On the MCP surface your working identity lives entirely in your context window. hadron_start_session hands back the session id and the worker's boot briefing once, and nothing on the wire carries either — there is no equivalent of the CLI's .git/hadron-team-session.json. A compaction can therefore leave an agent still named Iris but no longer working as her.

Two reads put it back, and neither costs a second session or a force: true takeover of yourself:

  1. hadron_whoami — your open sessions, with the bound worker and App. This is where the session id comes back.
  2. hadron_get_worker — the worker's boot briefing, re-rendered live.

Do this as soon as you are unsure, because the worst failure here is silent: hadron_team_chat_post takes session as an optional argument, so posting without it does not error — it records the message as authored by you, the human, instead of by the worker.

Walked through end to end, with sample output, in Recover after a context compaction.

Workers — named team members

Cast a Worker — a named AI team member ("Iris"): the casting of an agent already installed in the team App under a name. The agent carries the persona dressing (personaRole) and the systemPrompt template with {{name}} / {{role}} placeholders; the worker is the local named identity that does attributable work. A worker's name is required and permanent — casting reads no register and touches no system memory (server#1050). The roles:<role> nodes in the Team Agent's system memory are role definitions, readable and writable through the GraphQL teamRoles / createTeamRole / updateTeamRole / deleteTeamRole surface, and a role that has no definition is not a refusal: cast lists are ergonomics, never a gate.

Tool Purpose Key inputs
hadron_cast_worker Cast a worker and return its id, name, and resolved boot briefing (print the briefing; pass the id as workerRef when starting sessions). name (required in effect — omitting it refuses WORKER_NAME_REQUIRED; optional in the schema only so the refusal carries its reason); optional role (picks the installed agent by personaRole, and becomes the casting's role label), agent (Agent URN/ID — must be installed in the App), app (defaults to the active App), promptOverride (per-worker individuality)
hadron_list_workers List the App's staff — name, URN, role, who holds each name, whether a worker session is live on it, when it was last driven, and whether it is retired. The roster read: use it when you need a teammate's name and don't know it. Optional includeRetired, limit, offset, app, session
hadron_get_worker Read one worker, including its resolved boot briefing and its raw promptOverride. A pure read — it reserves nothing and mints nothing, so it is the way to recover a briefing you lost. workerRef (name, URN, or id — a name needs App context); optional app, session
hadron_update_worker Amend a worker's promptOverride — the per-worker individuality layered over the role agent's shared template. Reaches the worker at its next bind. workerRef; promptOverride (replaces the whole override — omit to change nothing, empty string to clear); optional app, session

All four delegate to the matching GraphQL resolvers, so their authorization is exactly the GraphQL surface's. The reads and the writes are gated differently, and the difference is deliberate: following the roster is not the same act as changing it.

Reading (hadron_list_workers, hadron_get_worker) — an AppMember of the team App at any role, reader included; an org member with CONTRIBUTOR+ on the App's org; the owner of a user-owned App; or the App's own key, which may read its own staff so that team automation can follow the roster.

Writing (hadron_cast_worker, hadron_update_worker) — narrower: an org CONTRIBUTOR+, a non-reader AppMember, or the owner of a user-owned App. A pure App-key principal may not — staffing is a human act.

The typed refusals differ with the gate. Only the write path can produce the naming refusals: a cast with no name is refused with WORKER_NAME_REQUIRED, and names are unique per App, case-insensitively, forever, so a taken name (including a retired worker's) is refused with WORKER_NAME_TAKEN — one attempt, no fallback. hadron_update_worker additionally refuses a retired worker. A read never returns any of these; a worker you may not read is reported as not found.

To rehearse a cast without burning a name, use the GraphQL castWorkerPreview query: the same resolution and refusals with no writes (and no reservation — a previewed name may be gone at cast time, by design). Note it takes the mint gate, not the read gate — a preview reveals a name the caller could mint anyway.

What the roster reports — three facts, not one. Every line carries the hold (whose name it is) and whether a worker session is live on it. A line with no live session also carries when the worker was last driven:

- Iris (backend-engineer) — held by YOU, session live
- Dara (server-engineer) — held by sam, no live session, last driven 3d ago
- Pia (android-engineer) — unheld — binding it holds it, no live session, never driven

Held and live answer different questions and neither implies the other (taken and held are different states). Both can block a bind, and only one of them can be argued with. A name held by somebody else refuses WORKER_HELD however idle it looks, and force does not reach that gate at all — the remedy is to cast your own worker, never to adjudicate whether the holder is really gone. A live session refuses WORKER_TAKEN, which force can take after an informed prompt. The hold is checked first, so liveness only ever changes the outcome on a name that is not somebody else's — but it is reported on every row, and an unheld row is not automatically bindable for that reason.

What an unheld name is not is abandoned: casting does not hold, so a roster staffed for other people stays unheld until each of them binds.

never driven is the state worth reading for. A casting nobody has picked up otherwise renders exactly like one worked yesterday, so work can be dispatched to a name no one reads with no signal that it went nowhere. The age is shown only when no session is live — beside a live one it would invite being read as presence, which is the chat-session/worker-session confusion in a new place.

Where the age can be wrong, it is wrong in one direction: it overstates idleness, never understates it. Two exclusions produce that, both deliberate.

Writes that land after a stint ends — a merge attributing a PR back days later, a work record for an ended session — are not somebody driving the worker, so the activity instant is capped at the session's end.

And on an auto-expired session the last genuine heartbeat has been overwritten by the cleanup write itself, so the server falls back to the session's start and its usage events rather than count the cleanup as a drive. Since hadron-server#1114 that case is narrow: only a session with a hard expiresAt is ever auto-expired, and hadron_start_session sets none — so a worker session you opened is not subject to it.

Reading a worker by name. hadron_get_worker and hadron_update_worker accept a worker's name ("Iris"), its URN, or its id. A name is unique only within an App, so the name form needs App context — the active App, an explicit app, or your session, which names its own App and so still works right after a reconnect. An id or a URN needs none of that, since each names exactly one worker and therefore exactly one App. Whichever form you use, a worker you may not read is reported as not found, the same answer an absent name gets: the roster is not something to probe by guessing.

Amending a worker. hadron_update_worker changes promptOverride and nothing else. A worker's name is permanent, and its role and agent are the casting — a different role is a different casting, not an edit. The override replaces, so to extend one, read it with hadron_get_worker and amend the text shown under Prompt override — not the composed boot briefing printed below it, which already includes the role agent's shared template. A retired worker is refused: an override is delivered at bind time, and a retired worker takes no new bindings.

Team chat

Post to and read the team App's group chat — one well-known chat per team App, living at chats:team in the Team Agent's shared app memory and bootstrapped on the first post. Message ordering is a server-assigned seq (atomic, unique per chat), so readers poll with a watermark instead of comparing timestamps.

Tool Purpose Key inputs
hadron_team_chat_post Post a message. Mention teammates as @worker-name / @handle (a multiword name by its slug, e.g. @mary-jane) — mentions are extracted server-side into the message envelope. Returns the posted message's seq. body (required, max 65 536 chars); optional replyToSeq (reply to an earlier message — wires a replies-to edge), session (a session id — the message is authored by that session's bound Worker; the session must be active and yours, and its Worker this App's — a Worker of another App may post only when the session's user can write this chat's host memory, see below), app (App URN/ID — defaults to the active App)
hadron_team_chat_read Read the chat, oldest first by seq, with the total count. Optional sinceSeq (only messages with seq strictly greater — pass the last seq you have seen), mentions (filter to messages mentioning a worker or user; the ref must be on the App's roster), limit / offset, app, session (resolves which team App to read when app is omitted — and keeps that session alive; see below)

Names written as Markdown code — for example, inline @ada, in a fenced code block, or in an indented code block — are literal text and do not notify anyone. A mention in ordinary prose still does. The server stores the extracted mentions when the message is posted; existing messages are not reprocessed.

Both delegate to the GraphQL operations (createTeamChatMessage / teamChatMessages), so authorization and typed refusals are identical: any AppMember of the team App (every member of the host is a participant), an org CONTRIBUTOR+, or a user-owned App's owner may post and read; a pure App-key principal cannot post but may read its own App's chat. Posting through a session records the driving session in the envelope, so an agent message is attributable to the human behind it.

A session you pass to hadron_team_chat_post must be worker-bound. Authoring as a worker needs a worker to author as, so a session started without workerRef refuses SESSION_NOT_WORKER_BOUND rather than quietly posting under your own name. The sibling refusals are SESSION_ENDED (the worker session is closed — posting is the one team surface that requires a live one), WORKER_RETIRED, and CHANNEL_HOST_NOT_WRITABLE.

A worker can post into another App's chat, but only when the session's own user — the human driving it, never the caller — may write that chat's host memory. A cross-App post records the author's App. If that user may not, the post is refused with CHANNEL_HOST_NOT_WRITABLE, whose extensions carry workerAppId (the worker's App) and appId (the chat's host App, absent when the chat has none). A pure App-key session is never admitted across Apps. To clear it, post with a session bound to a worker of the chat's own App, post as yourself, or get write access to the host memory. The code replaces SESSION_NOT_IN_APP and SESSION_WORKER_NOT_IN_APP on this surface. Both are still raised by hadron_record_work, whose records never cross Apps.

None of that applies to hadron_team_chat_read, where session only resolves the App and keeps the session alive — an ordinary unbound session reads the chat fine. The binding requirement is about authorship, so it attaches to the write.

The same code guards the handoff at hadron_end_session: a handoff belongs to a worker's continuity sequence, so writing one on an unbound session is refused, and the session stays open so you can retry or end without one. Omitting session on a post is not refused at all — that is the quiet failure described above, and it is why these refusals are worth knowing: everything except the omission tells you.

Passing session also keeps your session reading as live

Since hadron-server#1081, handing a session id to any team tool — including a read — counts as driving that session, so it does not read as idle. This is the heartbeat the desktop track runs on, and it is why a stint spent mostly reading the chat does not start looking abandoned mid-work.

That matters more since server#1114 than it did before: a session that falls outside the idle window is no longer ended, but it does stop reading as live — and a worker that is not live no longer raises WORKER_TAKEN. That gate is what stops you displacing your own stint from another host; it is not what keeps the name from a colleague, which is the hold, and the hold is untouched. Pass session even when you have already selected an App and do not need it for scoping.

Team worklog

Record externally visible work milestones — a PR opened, a branch pushed, an issue closed — against the session that did the work, and query them back. The worklog is the authoritative join between a work artifact and the sessions that produced it, which is what turns "which session wrote this PR?" into one lookup. It lives in the Team Agent's shared app memory alongside the chat, and is bootstrapped on the first record.

Tool Purpose Key inputs
hadron_record_work Record one milestone. Echoes back the stored record — including the canonical ref it normalized to, which is worth reading if you passed a URL. session (required — the session id the work is attributed to, which must belong to this App), tool (required, e.g. github), kind (required: pr | issue | commit | branch | repo), ref (required — any accepted spelling, see below), action (required — a free verb: opened, merged, reviewed, closed, pushed, …); optional detail (a JSON bag of display extras such as a title or URL — stored, never filtered on), app (App URN/ID — defaults to the active App)
hadron_team_work_items The provenance query. Newest first, with the total count. Each item carries its sessionId — read that session for its transcriptPath. All optional: ref (in any accepted spelling — it is normalized before matching), session, tool, kind, limit (default 50, cap 200), offset, app

Both delegate to the GraphQL operations (recordTeamWork / teamWorkItems), so authorization and typed refusals are identical. Recording passes two independent gates, and it's worth keeping them apart:

Gate What it decides
Team membership Whether you may touch this App's worklog at all. Any AppMember of the team App (at any role), an org CONTRIBUTOR+, or a user-owned App's owner qualifies.
Session write access Which sessions you may record against — the session must be one you are attributed to (or you are a platform admin).

The second gate is the one that surprises people: being on the team does not let you record work against a teammate's session. Reading is team-wide, but a record is a claim about who did what, so it stays with the person the session belongs to. Attempting it is FORBIDDEN — "You cannot record work against a session you are not attributed to."

A pure App-key principal may read its own App's worklog but may not record — worker work has a human driver by construction, and App-key automation's platform activity is already covered by usage events. An impersonation session may read (bounded to the support org) and never records.

Four behaviours are worth knowing before you write a client:

  • Ended sessions are accepted. Late attribution is the whole point: a merge lands days after the session that opened the PR has ended. (This differs from team chat, which requires a live session to author a message.)
  • Records are append-only. There is no update and no delete — correct a wrong record by recording a newer one. That ledger posture is what provenance rests on.
  • kind: pr also refreshes the session's displayed PR number. That field is a latest-wins display convenience; the worklog record is the actual provenance.
  • Only external milestones belong here. The litmus is "would the platform otherwise have no record?" Platform calls are already usage events, and an App's declared capabilities live on the App.

Typed refusals: WORK_REF_INVALID, SESSION_NOT_FOUND, SESSION_NOT_IN_APP, SESSION_WORKER_NOT_IN_APP (the session's bound Worker belongs to a different App — a record is never attributed across the team boundary), WORKER_RETIRED (a retired worker takes no new records; its history stands, its authorship ended), TEAM_AGENT_NOT_FOUND / TEAM_AGENT_AMBIGUOUS (the first-write bootstrap couldn't identify the Team Agent), APP_UNINSTALLED, FORBIDDEN, BAD_USER_INPUT. A session with no Worker bound records under the attributed user's handle.

Work refs — one spelling per artifact

The worklog answers its question by equality lookup on ref, so every writer and reader has to agree on one spelling per artifact. The server accepts the spellings people actually paste and stores exactly one canonical form. This grammar is a stable contract — CLI help text and portal placeholders restate it.

Canonical spellings (tool github), one per kind:

Kind Canonical form Notes
pr, issue owner/repo#371 PRs and issues share GitHub's number space, so they share a spelling — a lookup by o/r#371 finds both.
commit owner/repo@a1b2c3d SHA lowercased, 7–40 hex chars, stored at the length you gave.
branch owner/repo:feat/x Branch case is preserved — git refnames are case-sensitive.
repo owner/repo

owner and repo are always lowercased, since GitHub treats them case-insensitively.

Accepted inputs, all normalized to the above:

  • Any canonical form (normalization is idempotent).
  • Web URLs, with or without scheme or www., query and fragment stripped: https://github.com/o/r/pull/371, .../issues/42, .../commit/<sha>, .../tree/feat/x. Trailing segments after a number or SHA (/files, /checks) are dropped. /pull/N and /issues/N are each accepted for both pr and issue — GitHub redirects between them and the canonical spelling is identical, so refusing one would manufacture a failure for nothing. Everything after /tree/ is the branch, because branch names contain slashes.
  • SSH remotes — git@github.com:o/r.git → o/r. A .git suffix on a repo path is stripped.

A bare number is refused

371 and #371 fail with WORK_REF_INVALID. Inferring the repo from a git remote is client-inherent context — the server never guesses a repo. This is a deliberate split, not an omission: the hadron CLI does accept a bare number and qualifies it from the session's recorded repo before sending a canonical ref.

Other tools. The grammar above is github's. Any other tool has no defined grammar yet: its refs are stored verbatim (trimmed) and matched byte-for-byte. Reads honour the split — a query with tool: 'github' matches canonical spellings, another tool matches that tool's stored spelling, and a query with no tool matches either. A read never fails on an unrecognised spelling; it just returns an empty page.

Browse nodes

Read-only node lookups. None of these mutate anything.

Tool Purpose Key inputs
hadron_find_nodes Relevance-ranked node search. mode: keyword (default, stemmed full-text), vector/hybrid (semantic, needs the memory's vector index), or regex. Also filters structurally by objectType and a where predicate. query (required — use "*" or "" for survey/list-all mode); optional mode, scope (see Search scopes), parentRel, memoryUrn, memoryUrns, allMemories, objectType, where, isRunnable, brief, limit, minScore, expand, granularity
hadron_search Find a known entity by id, URN or name across kinds — organizations, memories, nodes, agents, apps, AI service configs, users — in one ranked list. Matching is case-insensitive substring — except pk, which is exact — and never stemmed; for concept search over node text use hadron_find_nodes. query (required); optional entityTypes, fields (urn, name, description, pk, or content — node bodies, opt-in, skips encrypted memories), scope (applies to the memory and node slices), limit
hadron_get_node Read a single node by loc path or full URN. loc
hadron_get_next_node Read the next sibling under a parent, ordered by seq. Used to walk a conversation flow stage by stage. parent; optional current
hadron_list_nodes Browse the tree under a parent — names + descriptions only, optionally to a depth. optional parentRel, memoryUrn, depth

hadron_find_nodes takes a brief: true flag that returns name, URN, description, and edges only — useful for triage before deciding which nodes to read in full. hadron_get_node returns the full content including frontmatter, edges, properties, and data.

On a memory with a structured schema, narrow to a collection with objectType and filter by exact field values with the where predicate — where composes with every mode (it filters, then the mode ranks the survivors). Ordering by a property (sortProperty) is not exposed on this tool — use GraphQL findNodes or the CLI's --sort-property to sort by a property; from MCP, sort client-side. See Query nodes by their properties.

Search scopes

A scope — a "lens" — is a named, ordered list of memories owned by exactly one organization, Agent, or App. It narrows what a search looks at and never grants: a scope's results are its memories ∩ what you may read, and memories you cannot read are reported as a count, never by name. Names are 1–64 characters of [a-z0-9_-], unique per owner; app and global are reserved for the built-in views below.

Tool Purpose Key inputs
hadron_list_scopes List the scopes you may read. With no filter, lists the active App's context — its own scopes, its installed Agents', and its organization's: what a bare scope name resolves against there. optional app, ownerType + ownerRef (one owner's scopes instead), name, limit, offset
hadron_create_scope Create a scope. With no owner ref, the active App owns it. Every memory must be one you can read. name, memoryUrns (ordered, at least one); optional one of organizationRef / agentRef / appRef, description
hadron_update_scope Rename, re-describe, or replace the memory list — memoryUrns replaces, never merges. scope (id); optional name, description, memoryUrns
hadron_delete_scope Soft-delete a scope; its name is free again at once. Attaches or detaches nothing. scope (id)
hadron_scope_explain What a scope resolves to for you: readable memories in order, how many were dropped for access, which owner tier won, the scopes it shadowed, and — with loc — which memory would win for that address. scope (id) or name; optional app, loc
hadron_set_active_scope Set the scope hadron_find_nodes and hadron_search run under when a call names no scope and no memory — or drop it with clear: true. Persisted per client instance, like the active App. scope (id or name), or clear: true

Creating or changing a scope needs CONTRIBUTOR or above on an owning organization, or — for an App or Agent owner — ADMIN of its organization, or ownership of a user-owned one.

A name resolves in an App's context: App › Agent › organization, never unioned. Two installed Agents that each own the name make it ambiguous; pass the id instead.

Which memories a search covers when you don't list them. A search runs under the first of these that applies:

  1. a scope named in the call;
  2. the active scope (hadron_set_active_scope);
  3. the active App — app, its attached memories;
  4. the active organization — global, see Organization selection;
  5. your own user-owned memories.

The active memory (hadron_set_active_memory) is not a rung: search never falls back to it.

Every result says which scope it ran under. An explicit memoryUrn, memoryUrns, allMemories or parentRel on hadron_find_nodes outranks the ambient rungs (2–5), but still intersects with a scope named in the same call. So on MCP, omitting every memory selector does not search everything you can read once an App is active: it searches that App's attached memories. Pass allMemories: true for the everything-you-can-read probe. (The GraphQL findNodes and the CLI's hadron search apply no ambient ladder: with no scope they search your accessible set.) hadron_list_nodes takes no scope: it browses one memory's tree.

Mutate nodes

All require write access to the target memory.

Tool Purpose Key inputs
hadron_create_node Create or upsert a node. The node type defaults to info; pass parent (a parent-type container) for sections. loc, name; optional nodeType, objectType, description, content, seq, tags, properties, data, edges, memoryId, llmModel, aiAgent
hadron_create_parent_node Create a section/category container node. Convenience wrapper that pins nodeType: parent. loc, name; optional description, seq, tags
hadron_update_node Partially update an existing node — only the fields you pass change. The data argument replaces the whole data bag; use hadron_update_node_data to merge instead. loc; any subset of name, nodeType, objectType, description, content, seq, tags, properties, data, edges, llmModel, aiAgent, plus reason (stored in revision history)
hadron_update_node_data Merge a JSON object into a node's data bag, preserving keys you don't mention. The patch's top-level keys overwrite existing ones; other keys stay; a node with no data takes the patch as-is. Shallow — a nested object value is replaced wholesale, not deep-merged. Patch must be an object (arrays, scalars, and null are rejected). nodeUrn (URN or ID), data (the object to merge)
hadron_create_edge Add a single outgoing edge from one node to another. Non-destructive (other edges on the source are preserved), idempotent on the edge loc. Cross-memory edges allowed if the caller can read the target memory. sourceUrn (fully-qualified node URN), targetUrn (URN or ID); optional name, loc, description, isRunnable
hadron_move_node Move a node with its whole subtree, within a memory or across memories. The node keeps its stable id, so every edge reference stays valid, and it keeps its name — a move changes the address, not the display title. urn (current), targetUrn (destination; a different memory in the target URN performs a cross-memory move); optional overwrite, renameToLeaf (default false — pass true to also overwrite name with the target loc's last atom)
hadron_delete_node Delete a node. Soft-delete by default (sets deletedAt); pass hard: true for a row delete. Refuses with descendants unless recursive: true. Hard-delete refuses cross-memory edges unless cascadeCrossMemoryEdges: true. urn (fully-qualified); optional hard, recursive, cascadeCrossMemoryEdges

Replacing vs. merging data. hadron_create_node and hadron_update_node write the data bag wholesale — the object you pass becomes the node's entire data. hadron_update_node_data merges instead: it overlays your patch's top-level keys onto the existing data (patch wins on collision) and leaves the rest intact. Reach for it when you want to set one or two keys without re-sending the whole bag. The replace and merge tools cross-reference each other so an agent picks the right one. The merge honors the same write-permission, encryption, and revision-history invariants as a normal write.

edges on hadron_create_node / hadron_update_node is an array of edge objects. Each edge object can include targetId (or loc/URN), plus optional name, description, loc, and isRunnable. Passing edges replaces the source node's full edge set. Use hadron_create_edge when you want to append a single edge without touching the rest. Targets can be loc paths or full URNs. Adding an edge to a missing target does not fail at write time; hadron_validate will flag it later.

objectType on hadron_create_node / hadron_update_node tags a node as a member of a schema collection (e.g. competitor), orthogonal to nodeType. On a memory that has a schema, the write is validated against the declared collection — a violation is returned as a Schema violation: error. On an unschema'd memory objectType is free-form.

What a move does to name. Nothing, by default. A node's name is its human-authored display title and survives a move, whether you arrive through hadron_move_node or the GraphQL moveNode mutation. Pass renameToLeaf: true to also re-derive the name from the destination loc's last atom (services:db-helper → db-helper); that overwrites the existing title, so pass it only when the name is meant to track the loc. Descendants' names are never rewritten either way — only their loc prefix changes.

Changed in hadron-server #1017

Before that fix the MCP tool rewrote name on every move, silently and with no way to decline, while moveNode did not. If you carry a habit of re-setting the name after each move, you can drop it — doing so now overwrites a title the platform just preserved.

Moving a subtree across memories. hadron_move_node relocates a node together with its entire loc-subtree — every descendant whose loc is prefixed by <loc>: moves too, and their URNs follow the parent. When targetUrn names a different memory, the whole subtree relocates there: the moved nodes carry their embeddings (re-indexed under the destination memory's policy), and outgoing edges whose source moved are rewritten into the destination. An edge to a node left behind becomes a cross-memory edge, whose far endpoint is access-gated on read (so a reader who can't see the other memory gets null for that edge's source/target). Cross-memory moves are refused (a BAD_USER_INPUT-class error) for a documented safe subset:

  • either the source or the destination memory is encrypted,
  • the subtree contains a chat-root node (its data.scope ties it to the memory class), or
  • a moved node would violate the destination memory's schema.

Git-backed memories are not on that list. A move touching one used to be refused, on the grounds that it reached only the database and the next repo-wins sync would re-mint the moved rows from the stale source files. The move is mirrored instead: the moved nodes' files are removed from the source repository and written to the destination's. Git is one integration of a memory, not a lock on it. Like every git mirror this is best-effort — a repository that is unreachable leaves the database authoritative, exactly as it would for an ordinary edit.

A collision at the target loc (or any descendant slot) refuses the move with URN_ALREADY_EXISTS unless you pass overwrite: true (which soft-deletes the explicitly-named target first). A same-memory move leaves existing edge locs untouched.

Object store

The object-store tools are the record-oriented surface over structured storage: an object is a node with an objectType, presented as a flat record { id, type, ...fields }. id and type are the reserved envelope; loc/name are auto-derived and hidden. This is the friendly path for typed records — reach for it instead of the node tools when you're doing CRUD on collection members. See Object store API for the full contract.

Tool Purpose Key inputs
hadron_create_object Create a record in a collection. Fields are validated against the memory's schema when the collection is declared. memoryUrn, type, fields; optional key (natural id), name
hadron_get_object Read one object by id or node URN as { id, type, ...fields }. ref
hadron_update_object Shallow-merge fields into an object (atomic; patch wins, unmentioned fields kept), then re-validate. ref, fields; optional reason
hadron_delete_object Delete an object — soft by default; hard: true removes the row. Non-recursive. ref; optional hard
hadron_find_objects Query one collection → { objects, total }. memoryUrn, type; optional match, where, sort, limit, offset

hadron_update_object merges (unlike hadron_update_node, which replaces the properties bag). hadron_find_objects takes match (equality shorthand {field: value}, cast inferred from the schema), where (the full hadron_find_nodes predicate grammar, ANDed with match), and sort ({field: "asc"|"desc"}); it returns { objects, total }, where total is not a reliable count today (usually null, 0 for some empty results), so page with limit/offset until a short page. id and type are reserved and can't be field names.

Validate & run

Tool Purpose Key inputs
hadron_validate Walk a memory and report broken edges, missing content, stale abstracts, embedding failures, and property-schema conformance violations. memoryUrn (required, fully-qualified)
hadron_run_action Load an action-type node, resolve its dependencies, and execute it. loc; optional args (key-value object)

hadron_validate is safe to run any time — it's a read-only sweep. It takes the memory to check as memoryUrn (fully-qualified). On a memory with a structured schema it also reports nodes whose objectType/properties violate the schema — the non-retroactive backstop for records written before the schema existed. hadron_run_action is what makes action nodes executable from any MCP host: write the action once, run it from Claude Code or any other host the same way.

Chat

There are no MCP tools for driving a chatbot agent's chat. The eleven hadron_chatbot_* tools that once did — start, send, process, the routing map, route history, the goal stack, training entries and test personas — were removed in hadron-server#1143. The chat runtime itself is unchanged: drive it through the Chat API (GraphQL) or the portal.

The team chat is a different feature and keeps its MCP tools — see Team chat above and Channels below.

Channels

Channels are durable, ordered message streams. Every App has a default one named team at chats:team — the Channel hadron_team_chat_post writes to. These tools are the same contract addressed by Channel rather than by App.

Tool What it does Key arguments
hadron_list_channels The Channels whose host memory you may read. Defaults to the active App's. app, memory
hadron_create_channel Create a named Channel with its chat root, in an app-class memory you may write. memory, loc, name
hadron_channel_read Read a Channel, oldest first by seq. channel, sinceSeq, beforeSeq, mentions
hadron_channel_post Post into a Channel. channel, body, session, replyToSeq
hadron_register_list Who is declared to take part in what. owner, channel, attendee
hadron_register_add Declare that an attendee takes part. channel, attendee, role, mentionOnly
hadron_register_remove Withdraw a declaration. the entry

channel takes the id or the address — the chat-root URN that hadron_list_channels prints. Copy one of those: a ref naming nothing reads as a Channel you may not see, which is [channel_not_found], the same answer you get for one you genuinely may not read.

session on hadron_channel_post is optional, and that is the quiet failure

Exactly as on hadron_team_chat_post: omit it and the message is attributed to you, not to the worker, with no error. See attribution and the end gate.

The register grants nothing

hadron_register_add declares intent, never permission. What an attendee may actually read or post is the Channel's host memory's decision, so a row naming someone who cannot read the Channel is a legitimate statement whose precondition is unmet — not something to clean up.

To change who can reach a Channel, change access to the memory hosting it.

Omitting attendee means every attendee in the owner's context. That is how an install declares a default Channel without enumerating staff who do not exist yet — and it is why the CLI makes you ask for the wide form by name.

Writing to a Channel's address

A Channel reserves and protects its address. A generic node write landing there is refused with [loc_protected], and the refusal is by overlap — a recursive delete containing the address, or a move into or out of it, are all refused too.

That is not an access decision. It says which door a write comes through, not who may write; the host memory still decides that.

App selection

For OAuth callers who can see more than one Hadron App, two tools pin an App as the active scope for the rest of the MCP session:

Tool Purpose Key inputs
hadron_list_apps List the Hadron Apps the caller has access to, each with its canonical URN and the caller's role on it. (none)
hadron_set_active_app Pin an App as the active scope. Subsequent App-scoped calls (hadron_list_memories, hadron_find_nodes, etc.) resolve to this App until the session ends. app (canonical App URN or unique display name)

See MCP App selection for the full walk-through — typed-error vocabulary (app_identifier_ambiguous, app_not_found, …), short-form resolution rules, and the session-lifecycle behavior.

Organization selection

Tool Purpose Key inputs
hadron_set_active_org Set — or with clear: true drop — the active organization: the tenancy an org-scoped read, such as a global search, runs against. Persisted per client instance, like the active App. org (hrn:org:<root> or id — one you are a member of), or clear: true

You need it only when you belong to several organizations. The organization is taken from the first of: an org named in the call › the active App's organization, when that App is org-owned (a personal App has none) › this selection › your single membership › with no membership at all, your own user-owned memories. With two or more memberships and none of those, an org-scoped read refuses with ORG_REQUIRED and lists your memberships. A foreign or nonexistent org reads the same (org_not_found). App-key callers carry their organization in the credential and cannot use this tool. hadron_server_info reports the organization currently resolved, and which rung chose it.

Selecting an organization is not a search scope and attaches nothing: the organization picks the tenancy, a scope picks the view.

Server identity

Tool Purpose Key inputs
hadron_server_info Return the deployment URL, the version, the capability tokens this build carries, and whether this connection was re-established. Useful when a caller has multiple MCP namespaces wired up and needs to know which one a tool call landed on — and to check a feature is present before relying on it. (none)

Does not require an active App or any memory access — safe to call before authentication is established.

server: https://srv.example.com
version: 0.15.0
capabilities: app-selection, worker-model, worker-boot-briefing, team-chat,
              team-worklog, session-app-recovery, reconnect-notice,
              client-session-state
connection: re-established (the previous MCP session ended; any active App or
            active memory selection was reset)

Test for a token; never compare version numbers (cor:api:220:01). Tokens are named per feature, so they answer the question a client actually has — "can this deployment bind a worker?" — which a release number only answers if you also know which release introduced it.

What you see What it means
The token you need The feature is on this build. Use it.
No such token Fall back — take another route, or decline and say why. Do not proceed hopefully.
No capabilities: line at all A build predating the mechanism. Assume nothing it has not otherwise confirmed.

Tokens are additive and permanent while the behaviour is honoured, so they are safe to depend on. The vocabulary is not frozen — the list above is a snapshot of one deployment, and new tokens appear as features ship.

Why this is not paranoia. MCP silently drops arguments a build does not recognise. A deployment behind the docs accepted hadron_start_session without workerRef, returned a healthy-looking session that was never bound to a worker, and quietly broke provenance for everything that session produced. The attempt succeeded either way, which is why probing cannot substitute for asking.

A connection: re-established line means this session replaced an earlier one and your selections were reset — unless you send X-Hadron-Client-Id, in which case they are restored instead. The notice names only the active App and the active memory, but the active scope and active organization reset too, so after a reconnect a search can run on a different rung of the search ladder. Check the scope line on the next result.

Stability and changes

The MCP tool surface is stable — the names and core signatures have not churned. Recent activity adds capabilities (personas, goal stack tools) and fixes edge-case bugs. New tools are additive; removals would be breaking and have not occurred.

If a tool you call returns an unexpected error, call hadron_server_info before assuming a regression: check that the deployment carries the capability token for the feature, and whether the connection: line says this session replaced an earlier one. Both are commoner causes than a broken tool, and both are visible in one call.

Release notes tell you what changed in a release; the tokens tell you what this deployment can do, which is the question you actually have.