MCP tools reference

hal0 mounts three FastMCP (Streamable-HTTP) servers
on the main FastAPI app: hal0-admin at /mcp/admin, hal0-memory at
/mcp/memory and hal0-agents at /mcp/agents. hal0-agents is for remote coding agents
admitted with hal0 bridge;
it is mounted whenever [bridge].enabled is on (the default), and is 404 while it is off.

Every Hermes profile is wired to both servers. hal0 writes hal0-admin and hal0-memory into each named profile’s config.yaml as hermes__<profile>, with shared memory by default (hal0: {memory_private: true} in a profile’s config.yaml makes its memory facts private; mental models and directives are bank-level in the unified bank and are not covered) and the box bearer, at provisioning and on every hal0-api reconcile sweep (120 s). A profile opts out with hal0: {wire_mcp: false}. The default profile is not converged periodically: after adding the opt-in to its config.yaml by hand, run sudo hal0 agent reprovision hermes --repair (or the next hal0 update does it). hal0 doctor reports the state on the hermes_profiles_mcp row, and on hermes_default_memory and hermes_brain_memory for the default and hal0-brain profiles.

Per-slot tune through MCP

slot_edit accepts a sparse tune object: ctx_size, enable_thinking,
reasoning_effort, reasoning_budget, vision, speculation. Null removes
an override; null for the whole table clears it. REST guards also apply here.
slot_list retains tune_effective (including tune_unavailable) in its compact roster. Profile create/edit
tools also accept reasoning effort and integer budget.

hal0-admin (/mcp/admin)

Every admin tool is registered through one generic factory, so every tool’s call
signature is {"args": {...per-tool schema...}}. The advertised JSON schema per tool
merges path args (required strings, derived from the REST route template) with a
hand-authored set of named body/query field hints, and always allows additional
properties to pass through undeclared.

Tools are autogenerated from the live FastAPI route table and re-keyed onto stable
names — the tool-name-to-route mapping in source (TOOL_NAME_ALIASES) is the ground
truth if this table ever drifts.

Return shape for every admin tool is dict[str, Any]:

  • success (autonomous): the raw REST JSON response — slot_list, provider_list,
    upstream_list, runner_image_pulls, and backend_list wrap their bare-list REST
    response as {"<key>": [...], "count": N} (_LIST_TOOL_WRAP_KEY in source is the
    authoritative list).
  • pending approval (gated): {"status": "pending_approval", "approval_id": "...", "detail": "..."}.
  • error: {"status": "error", "error": {"code": ..., ...}} — only this full envelope
    (an error dict with a string code) is reported as isError: true. A successful
    read of a slot whose own lifecycle status is "error" returns normally with its
    payload.

Path arguments (the {placeholders} in a tool’s REST route) are checked before a gated
call is queued and before an autonomous call runs, so you are never shown an approval card
for an argument that would be refused. A path argument must be a string, or an integer
below 2^53 (JSON and the URL spell those the same way). Anything else (null, a boolean, a
float, a list, an object), an empty value, ., .., a value containing /, and a value
containing a control character or an invisible formatting character (zero-width space, a
bidi override, a byte-order mark) comes back as
{"status": "error", "error": {"code": "mcp.invalid_arg", ...}}. A missing one is
mcp.missing_arg. Every other value is percent-encoded as one URL segment, so ?, #,
%, spaces, : and non-ASCII reach the server exactly as the card shows them. The check
runs again when an approved call executes. A tool that needs a Hugging Face org/name
(such as model_pull) takes it as a body field (hf_repo), not in the path.

The one exception to the / rule is a path argument whose route declares it {name:path}:
today only comfyui_file_delete’s relpath. It may contain / between valid segments
(styles/a.safetensors); each segment is held to the rules above on its own, so a leading,
trailing or doubled / and a . or .. segment are refused, and each segment is
percent-encoded separately.

Tools fall into three gating tiers. Treat the frozensets in source
(AUTONOMOUS_READ_TOOLS, AUTONOMOUS_WRITE_TOOLS, GATED_TOOLS) as ground truth if
this table drifts.

Full argument schemas are not reproduced per tool here — the REST route each
tool passes through documents its own body/query shape; hand-authored
overrides for the handful that don’t map 1:1 onto their route live in
TOOL_PARAM_HINTS in source.

Slot, model and profile tools

The profile supplies the base tune. slot_create and slot_edit take a profile,
model and placement, plus sparse tune overrides. Legacy root-level tuning
keys remain refused with slot.tuning_key_denied. slot_edit can set context
through tune.ctx_size; free flags stay on the profile. model_edit,
model_register and model_validate take file facts as top-level fields:
chat_template, tokenizer_repo, mtp (MTP heads) and jinja. A defaults
object is 422 model.defaults_removed; model tuning is 422 model.tune_key_removed.
profile_create, profile_update and profile_generate carry flags, context,
thinking, reasoning effort/budget, vision, speculation and runner. A model’s
drafter is the file fact draft on model_edit (path or null to unpair);
model_pull takes draft_filename and draft_repo, and model_inspect returns drafts.

model_list and capability_list return the whole payload when called bare, which is
large enough on a populated box that clients truncate it. Narrow them instead.
model_list takes installed (true keeps models on this host, false the
upstream-advertised ones), type, capability (a model must have every one listed),
device (gpu-rocm, gpu-vulkan, cpu, npu; any match), q (substring of the id or
name), fields (the row keys to return; id always stays), and limit (1-500) with
offset. The list parameters take a comma-separated string, and the reply gains total,
the number of matches before paging. capability_list takes section (any of backends,
catalogs, selections; an unknown name is 400 capability.unknown_section) and
summary=true, which cuts each catalog entry to its id, display_name and downloaded.
With no parameters both replies are unchanged.

slot_status, slot_by_name and slot_by_id return the same enriched payload
for the same local slot, including config and container fields and effective
context and tuning where applicable. slot_by_id resolves the stable id to
the slot’s current name; an unknown id returns 404 slot.not_found.

Autonomous — read (no approval)

Tool REST route
slot_list GET /api/slots
slot_status GET /api/slots/{name}
slot_metrics GET /api/slots/metrics
slot_capacity GET /api/slots/capacity
slot_by_name GET /api/slots/by-name/{name}
slot_by_id GET /api/slots/by-id/{slot_id}
slot_resolved GET /api/slots/{name}/resolved
slot_state GET /api/slots/{name}/state
model_list GET /api/models
model_show GET /api/models/{model_id}
model_scan_preview POST /api/models/scan/preview
model_catalogue GET /api/models/catalogue
model_update_check GET /api/models/updates/check
model_pulls_list GET /api/models/pulls
model_pull_status GET /api/models/{model_id}/pull/status
model_inspect POST /api/models/inspect
model_inspect_draft POST /api/models/inspect-draft
model_store GET /api/settings/models/store
hardware_probe GET /api/stats/hardware
system_info GET /api/system-info
port_list GET /api/ports
capability_list GET /api/capabilities
provider_list GET /api/providers
version_info GET /api/status
upstream_list GET /api/upstreams
stack_list GET /api/stacks
stack_status GET /api/stacks/{slug}
profile_list GET /api/profiles
profile_status GET /api/profiles/{name}
profile_export POST /api/profiles/{name}/export
settings_get GET /api/settings
settings_schema GET /api/settings/schema
settings_apply_plan GET /api/settings/apply-plan
bench_runs GET /api/benchmarks/runs
bench_run_status GET /api/benchmarks/runs/{run_id}
bench_queue GET /api/benchmarks/queue
gpu_target_version in-process host probe, no REST hop
npu_status in-process host probe
env_report in-process host probe
model_store_probe in-process host probe — arg: path (required)
upstream_get GET /api/upstreams/{name}
upstream_test POST /api/upstreams/{name}/test
service_list GET /api/services
service_health GET /api/services/health
comfyui_status GET /api/comfyui/status
comfyui_workflows GET /api/comfyui/workflows
comfyui_models GET /api/comfyui/models — the image-model catalog with install state and each model’s latest install job
comfyui_jobs GET /api/comfyui/jobs
comfyui_files GET /api/comfyui/files
updater_state GET /api/updates/state
updater_check GET /api/updates/check
updater_channel_get GET /api/updates/channel
updater_slot_drift GET /api/updates/slot-drift
updater_job_status GET /api/updates/status/{job_id}
component_status GET /api/updates/components
update_check GET /api/updates/check
doctor_report GET /api/doctor
health_system GET /api/health/system
feature_list GET /api/features
metrics_snapshot GET /api/metrics
hardware_snapshot GET /api/hardware
slot_stats GET /api/stats/slots
stats_overview GET /api/stats
request_metrics GET /api/stats/requests
system_stats GET /api/system-stats
power_stats GET /api/stats/power
throughput_history GET /api/stats/throughput/history
npu_occupancy GET /api/npu/occupancy
npu_swap_status GET /api/npu/swap-status
model_health_check GET /api/models/health
slot_config GET /api/slots/{name}/config
slot_voices GET /api/slots/{name}/voices
flm_model_list GET /api/slots/flm/models
slot_pull_status GET /api/slots/{name}/pull/status
model_validate POST /api/models/validate
hf_search GET /api/hf/search
model_config_read GET /api/config/models
bench_roster GET /api/benchmarks/roster
bench_plan GET /api/benchmarks/plan
bench_cells GET /api/benchmarks/cells
bench_history GET /api/benchmarks/history
bench_evals GET /api/benchmarks/evals
journal_snapshot GET /api/journal
activity_list GET /api/activity
activity_export GET /api/activity/export
approval_list GET /api/agent/approvals
runner_image_list GET /api/runner-images
runner_image_downloaded GET /api/runner-images/downloaded
runner_image_pulls GET /api/runner-images/pulls/list
backend_list GET /api/backends
backend_detail GET /api/backends/{backend_id}
mcp_server_list GET /api/mcp/servers
mcp_client_list GET /api/mcp/clients
mcp_catalog GET /api/mcp/catalog
profile_generate POST /api/profiles/generate
memory_search, memory_list, memory_recall, memory_reflect, memory_history, memory_mental_model_list, memory_mental_model_get, memory_directive_list, memory_directive_get, memory_operation_list, memory_operation_get, memory_tags_list, memory_bank_stats delegate to hal0-memory — see below

Autonomous — write (no approval; reversible, low blast radius)

Tool REST route
model_swap POST /api/slots/{name}/swap
model_assign PUT /api/slots/{name}/config
model_edit PUT /api/models/{model_id}
model_scan POST /api/models/scan
model_pull_cancel POST /api/models/{model_id}/pull/cancel
model_pull_delete DELETE /api/models/pulls/{model_id}
model_set_default POST /api/models/{model_id}/default
slot_load POST /api/slots/{name}/load
slot_unload POST /api/slots/{name}/unload
slot_edit PUT /api/slots/{name}/config
settings_reload POST /api/settings/reload
hardware_reprobe POST /api/hardware/probe
comfyui_job_cancel POST /api/comfyui/jobs/{model_id}/cancel
memory_add, memory_curate, memory_mental_model_create, memory_mental_model_update, memory_mental_model_refresh, memory_directive_create, memory_directive_update, memory_operation_cancel, memory_operation_retry, memory_bank_consolidate delegate to hal0-memory — see below
memory_delete delegates to hal0-memory — gates itself when deleting more than one id, or a foreign/list dataset

Gated (destructive — require operator approval)

Tool REST route
model_pull POST /api/models/{model_id}/pull
model_delete DELETE /api/models/{model_id}
model_register POST /api/models
model_add POST /api/models/add-from-path
model_store_set POST /api/settings/models/store
model_store_migrate POST /api/settings/models/store/migrate
model_update POST /api/models/{model_id}/update
slot_create POST /api/slots
slot_delete DELETE /api/slots/{name}
slot_restart POST /api/slots/{name}/restart
slot_rename POST /api/slots/{name}/rename
component_converge POST /api/updates/components/{component_id}/converge
capability_set POST /api/capabilities/{slot}/{child}
config_write PUT /api/settings
provider_credential_write POST /api/providers/{name}/credentials
stack_create POST /api/stacks
stack_update PUT /api/stacks/{slug}
stack_apply POST /api/stacks/{slug}/apply
stack_import POST /api/stacks/import
stack_export POST /api/stacks/{slug}/export
stack_snapshot POST /api/stacks/snapshot
stack_delete DELETE /api/stacks/{slug}
profile_create POST /api/profiles
profile_update PUT /api/profiles/{name}
profile_import POST /api/profiles/import
profile_delete DELETE /api/profiles/{name}
bench_enqueue POST /api/benchmarks/queue
bench_control POST /api/benchmarks/control
bench_queue_delete DELETE /api/benchmarks/queue/{item_id}
logs_tail GET /api/logs — output is secret-redacted
slot_logs GET /api/slots/{name}/logs — output is secret-redacted
upstream_create POST /api/upstreams
upstream_update PATCH /api/upstreams/{name}
upstream_delete DELETE /api/upstreams/{name}
service_action POST /api/services/{service_id}/action
comfyui_switchover POST /api/comfyui/switchover
comfyui_pin POST /api/comfyui/pin
comfyui_model_install POST /api/comfyui/models/{model_id}/install
comfyui_model_remove DELETE /api/comfyui/models/{model_id} — deletes the model’s weight files and workflow; files other installed models load stay unless include_shared=true, which deletes them too and reports those models as affected
comfyui_file_delete DELETE /api/comfyui/files/{subdir}/{relpath} — deletes one weight file no catalog model loads; relpath is the row’s path as listed, with a plain / between folders (styles/a.safetensors); an empty, . or .. part is refused before approval, and the route refuses catalog-owned files, symlinks, hidden or .. path parts and anything that is not a weight file
comfyui_render_cancel POST /api/comfyui/render/cancel
comfyui_restart POST /api/comfyui/restart
comfyui_workflow_launch POST /api/comfyui/workflows/{name}/launch
comfyui_logs GET /api/comfyui/logs — output is secret-redacted
slot_pull_image POST /api/slots/{name}/pull
model_config_write PUT /api/config/models
bench_run POST /api/benchmarks/run
runner_image_sync POST /api/runner-images/sync
npu_backend_load POST /api/backends/npu/load
npu_backend_unload POST /api/backends/npu/unload
mcp_server_install POST /api/mcp/install
mcp_server_uninstall DELETE /api/mcp/{server_id}
mcp_server_config_write PATCH /api/mcp/{server_id}/config
mcp_server_action POST /api/mcp/{server_id}/{action}
mcp_server_logs GET /api/mcp/{server_id}/logs
memory_mental_model_delete, memory_directive_delete delegate to hal0-memory — destructive, always gated

Deliberately excluded

board_crud, brain_chat, updater_apply, auth_rotate, auth_me,
provider_credential_read, agent_sessions, hermes_command_approvals are explicitly
excluded from the MCP surface (each with a documented reason in source) — not gaps,
policy.

hal0-memory (/mcp/memory)

Unlike hal0-admin, each tool here registers with a real typed signature, so
tools/list returns real per-field schemas, not an opaque args blob.

hal0-memory registers 26 tools total. Gating tier is classified the same way as
the admin tools above — via AUTONOMOUS_READ_TOOLS / AUTONOMOUS_WRITE_TOOLS
/ GATED_TOOLS in hal0.mcp.admin (the standalone mount additionally
hard-gates its two delete tools through its own local list, checked first —
same verdict, belt and braces).

Autonomous — read

Tool Required Returns
memory_search query {results: [{id, text, score, timestamp, dataset, tags, source, metadata}]}
memory_list none {items: [...], next_cursor}
memory_recall query token-budgeted, consolidated memory — {results: [...]}
memory_reflect query LLM-backed synthesis grounded in recalled facts; {status: "unsupported"} on engines without this capability
memory_history id curation history for one memory unit
memory_mental_model_list none a bank’s mental models (stored memory_reflect answers kept refreshed). Metadata only (id, name, tags, staleness) unless detail is content or full; read one model’s content with memory_mental_model_get
memory_mental_model_get id one mental model
memory_directive_list none a bank’s directives (standing instructions injected into prompts)
memory_directive_get id one directive
memory_operation_list none async operations (retain, consolidation, refresh, …)
memory_operation_get id one async operation’s status — the poll target for memory_add’s operation_id, memory_mental_model_refresh, memory_bank_consolidate. A completed retain reports its extraction yield: facts_extracted, plus nothing_learned and a notice when zero facts made it into memory. A zero-yield retain of an already-extracted document instead carries document_fact_count and a “content was already known” notice
memory_tags_list none tags in use in a bank, with usage counts
memory_bank_stats none node/link/observation/operation counts for a bank

Autonomous — write (reversible, low blast radius)

If memory_add cannot connect to the memory engine (including a connect
timeout), it returns status: "error" with code memory.engine_unavailable,
a message asking the caller to retry shortly, and error.details.retry_after_s: 5.
An error response from the engine is not reclassified as engine unavailability.

Tool Required Returns
memory_add text (non-empty) {id, timestamp, operation_id?}
memory_delete ids (non-empty list) {deleted: int}; bulk deletes (>1 id) or a foreign/list dataset instead return {status: "pending_approval", approval_id, detail}
memory_curate id, plus at least one field to change (text, context, occurred_start, occurred_end, fact_type, entities, or state) edits a memory unit’s text/context/occurred-range/fact_type/entities, or soft-invalidates it via state — the non-destructive correction path; id alone returns mcp.memory_schema
memory_mental_model_create name, source_query {mental_model_id, operation_id} — initial content generation runs as an async refresh
memory_mental_model_update id updates name/source_query/max_tokens/tags/trigger
memory_mental_model_refresh id triggers a mental model refresh (async) — {operation_id, ...}
memory_directive_create name, content creates a directive
memory_directive_update id updates name/content/priority/is_active/tags
memory_operation_cancel id aborts pending/processing work
memory_operation_retry id re-queues a failed operation
memory_bank_consolidate none triggers the engine’s world/experience → observation consolidation pass early — {operation_id, deduplicated}

Gated (destructive — require operator approval)

Tool Required Returns
memory_mental_model_delete id deletes a mental model
memory_directive_delete id deletes a directive

Validation errors return {"status": "error", "error": {"code": "mcp.memory_schema", "detail": ...}}.

source on memory_add is server-injected from the caller’s identity — a client
cannot set it directly; attempting to pass it raises.

Namespace rule: dataset defaults to "shared"; the X-hal0-Private header promotes
it to private:<client_id>.

hal0-agents (/mcp/agents)

The agent bridge’s sixteen tools (nine to run and follow a Hermes run, seven to teach a Hermes profile) are on their own page: MCP tools: hal0-agents.

MCP server management REST API

Router: src/hal0/api/routes/mcp.py, mounted at /api/mcp. This manages installed
3rd-party MCP server registrations
for the dashboard — distinct from the tool-calling
protocol surface above. Tool invocation only happens over the actual MCP protocol at
/mcp/admin, /mcp/memory and /mcp/agents; this REST surface cannot invoke a tool.

Method Path Request Response
GET /api/mcp/servers — {servers: [...], count} — bundled servers introspected live via list_tools()/list_resources()/list_prompts(); installed servers come from the registry; state is a 1.5 s TCP connect to the record’s url for streamable-http/sse (reachable, unreachable, or disabled when reachable but enabled = false) and always stopped for stdio.
GET /api/mcp/clients — {clients: [{id, name, role, host, since, connected_to}], count} — derived from the audit log.
GET /api/mcp/catalog — {items: [...], categories} — static installable-MCP catalog (puppeteer, sqlite, gdrive, slack, …).
GET /api/mcp/stream — SSE stream of mcp.tool.* events (audit-log tail, polled every 2s).
GET /api/mcp/{server_id}/logs query limit (1-500, default 100) {server, events: [...], count}.
GET /api/mcp/resolve query url (1-2048 chars) A ResolvedManifest: {id, name, description, spec, transport, tools, resources, prompts, env_required, source_kind, source_url?, author, verified}. Supports oci://, npm:/npx:, uvx:/uv:, git+https://, and generic https://.../manifest.json specs, with an SSRF guard blocking loopback/private/link-local/CGNAT targets.
POST /api/mcp/install {"url": "..."} or {"manifest": {...}} 201 {installed: <InstalledServer>, hermes_sync}; 409 on a bundled-id collision or already-installed.
DELETE /api/mcp/{server_id} — {uninstalled: server_id, hermes_sync}; 409 mcp.bundled if server_id is a bundled server.
PATCH /api/mcp/{server_id}/config {"env"?: {str: str}, "enabled"?: bool} {server: <InstalledServer>}, plus hermes_sync when the body set enabled; 409 if bundled.
POST /api/mcp/{server_id}/test — {server_id, probe, verdicts, agent_exposure} — MCP tools/list handshake against the record’s own url, each tool classified against the record’s own [tools] policy (blocked / gated / allow, else unknown_tool) while hermes/brain exposure is refused (#2415), and against the Hermes seed once it is available (unknown_server when the server isn’t exposed or no seed exists). agent_exposure is {available: false, code, reason} while hermes/brain exposure is refused (#2358, until #2303). 404 mcp.not_found; 501 mcp.supervisor_unavailable for stdio.
PATCH /api/mcp/{server_id}/tools {"allow": [...], "gated": [...], "blocked": [...]} (replaces the whole policy) {server, hermes_sync}; 400 mcp.tools_invalid (e.g. a tool on two lists); 409 mcp.bundled.
PATCH /api/mcp/{server_id}/exposure any of {"hermes", "brain", "openwebui", "opencode"}: bool (merged into the current flags) {server, hermes_sync}; 501 mcp.exposure_unsupported for openwebui/opencode set true; 409 mcp.exposure_needs_supervisor for hermes/brain on a stdio record; 409 mcp.exposure_policy_unenforced for turning hermes/brain on for any installed server until #2303 (turning them off still works); 400 mcp.exposure_invalid; 409 mcp.bundled.
POST /api/mcp/{server_id}/{action} — 501 stub (McpNotImplemented, code mcp.supervisor_unavailable) — start/stop/restart are not implemented yet.

hermes_sync is the join report {hermes, brain, errors, probe}; the join never fails
the request — failures land in errors.

InstalledServer fields: id, name, description, spec, transport, command, args, url, tools, resources, prompts, env, secrets, tool_policy, exposure, enabled, source_url, author, verified, installed_at. On disk (/etc/hal0/mcp-servers/<id>.toml),
tool_policy is serialised as the [tools] table and the integer tools count as
tools_count.

hal0 as an MCP client

hal0 acts as an MCP client in two limited ways — it does not run a general-purpose MCP
client session itself:

  1. Policy layer for bundled agents (hal0.agents.mcp_client.AgentMCPClient) —
    transport-agnostic. It loads /etc/hal0/agents/<name>.toml, resolves bearer tokens
    from env or a systemd credential, and classifies a tool call as allow, gated,
    blocked, unknown_server, or unknown_tool, with workspace-escape path
    sandboxing for filesystem-style MCP args. The actual wire-level MCP client that
    connects to other MCP servers is owned by the separate Hermes agent runtime, not
    this repo.
  2. Manifest resolver (hal0.mcp.manifest) — fetches and parses manifest JSON from
    arbitrary URLs so the dashboard can preview/install third-party MCP servers
    (backs GET /api/mcp/resolve and POST /api/mcp/install). This is manifest
    fetching only, with an SSRF guard blocking loopback/private/link-local/CGNAT
    targets — not live MCP protocol tool-calling.