Agent: MCP Servers
Section titled “Agent: MCP Servers”These endpoints expose the agent daemon’s MCP (Model Context Protocol) server pool: list the effective merged config, preview a bulk import, write individual entries, begin a write transaction, probe a candidate, and force a reconnect.
A configured stdio MCP server runs as an ordinary subprocess under the operator’s privileges. It is not sandboxed, confined, or otherwise vetted — the daemon merely starts the binary you named. What the daemon does scrub is the child’s environment: ambient provider keys and the platform token are stripped from the inherited environment before the child starts, so credentials must be supplied through env / headers in the server entry (or, where supported, through ${VAR} expansion in those fields) rather than by relying on whatever happened to be set in the shell that launched the daemon.
All routes in this group require an active session — supply its id as session_id on every call. Every write route additionally requires a single-use nonce minted by mcp.createWriteIntent for the matching op, AND an expect_hash echoing the mcp_servers content hash you last read. expect_hash is REQUIRED — even for a first write into a settings file that does not yet exist, in which case pass the empty-array hash that mcp.listServers or mcp.createWriteIntent returned for the missing file. A hash mismatch returns 409 mcp_conflict instead of silently overwriting a concurrent edit.
Container hostnames below use the form {projectId}-{containerId}-agent-1.{server}.containers.hoody.com. Both project and container ids are 24 lowercase hex characters; {server} is the deployment node label.
Reading state
Section titled “Reading state”GET /api/v1/agent/mcp/servers
Section titled “GET /api/v1/agent/mcp/servers”Returns the EFFECTIVE merged mcp_servers config for a live session, the per-layer settings files behind that merge, and the LIVE runtime state of each server — connected, negotiated protocol_version, tool_count, pid, revoked / revoked_reason, and the recent stderr_tail. env and headers values are never returned; only the key names (env_keys, header_keys) are reported, because a redacted placeholder invites a client to write the placeholder back as the real secret. Other fields — including url, command, and args — are echoed verbatim, so a credential embedded in one of those (a token in a URL, for example) is not redacted; keep secrets in env / headers.
The files[] entries each carry a content hash; pass it back as expect_hash on the next write.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
session_id | query | string | Yes | Live session id. MCP config is resolved against the session’s settings layers. |
realm | query | string | No | Per-request realm selector — the query-string alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
Response
Section titled “Response”{ "status": "ok", "servers": [ { "name": "filesystem", "scope": "user", "transport": "stdio", "enabled": true, "require_confirmation": false, "connected": true, "protocol_version": "2025-03-26", "server_name": "filesystem", "server_version": "1.0.4", "tool_count": 12, "pid": 4711, "env_keys": ["FILESYSTEM_ROOT"], "header_keys": [] }, { "name": "remote-tools", "scope": "project", "transport": "http", "enabled": false, "require_confirmation": false, "connected": false, "env_keys": [], "header_keys": ["Authorization"] } ], "files": [ { "path": "/home/alice/.config/hoody/settings.json", "layer": "user", "hash": "sha256:9f1c…e0a4" }, { "path": "/work/proj/.hoody/settings.json", "layer": "project", "hash": "sha256:1a4b…7d2f" } ], "warnings": []}| Field | Type | Description |
|---|---|---|
status | string | "ok" on success. |
servers | array | The EFFECTIVE merged entries, each with its originating layer and its live runtime state flattened onto it. name, scope, transport, enabled, require_confirmation, and connected are always present; protocol_version, server_name, server_version, tool_count, and pid appear for any build the session’s current generation holds — one that connected at some point, whether connected is still true or it has since died or been revoked — and revoked / revoked_reason / stderr_tail only when they apply; a disabled or never-connected entry carries connected: false and little else. Credential values are reported as key NAMES only (env_keys, header_keys). |
files | array | The per-layer settings files behind that merge, each with the content hash to pass as expect_hash on a write. |
warnings | array | Config problems the daemon found while merging (an unreadable layer, an entry it had to drop). Always present, usually empty. |
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "session not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | Unknown, closed, cross-realm or foreign-owner session, or a realm the login does not serve: one answer, so nothing leaks which. | List sessions and retry against a live one in a realm the login serves. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | Either the gateway’s per-client request limiter (Retry-After header present), or — on reconnect — the daemon’s per-session reconnect interval, which answers with the remaining wait in message and no Retry-After header. | Honor Retry-After when present; otherwise wait the interval named in the message and retry. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
cURL / TypeScript SDK
Section titled “cURL / TypeScript SDK”curl -sS "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/mcp/servers?session_id=sess_01HV8XYZABCDEF" \ -H "Authorization: Bearer $HOODY_TOKEN"import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.mcp.listServers({ session_id: 'sess_01HV8XYZABCDEF' });Previewing an import
Section titled “Previewing an import”POST /api/v1/agent/mcp/parse
Section titled “POST /api/v1/agent/mcp/parse”Parses a pasted config document into the entries an import WOULD write, without touching any file. Credential values are stripped from the preview. Use it to show a user what they are about to import; it needs no nonce because it writes nothing.
Supported dialects are hoody (mcp_servers list), claude / cursor (mcpServers map), and vscode (servers map). The parser refuses a document carrying more than one of these rather than guessing.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
realm | query | string | No | Per-request realm selector — the query-string alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id. |
document | string | Yes | A config document in any supported dialect. |
Response
Section titled “Response”{ "status": "ok", "dialect": "claude", "servers": [ { "name": "filesystem", "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/srv/data"], "env_keys": ["FILESYSTEM_ROOT"], "header_keys": [] }, { "name": "remote-tools", "type": "http", "url": "https://mcp.example.com/v1", "env_keys": [], "header_keys": ["Authorization"] } ], "count": 2, "warnings": [ "vscode variable ${workspaceFolder} cannot be resolved; replace before enabling" ]}| Field | Type | Description |
|---|---|---|
status | string | "ok" on success. |
dialect | string | Which dialect the document was read as: hoody | claude | vscode | single (one bare server object) | bare (a bare name→server map). |
servers | array | The entries an import WOULD write, redacted for display — credential values appear as key names (env_keys, header_keys). Nothing is written by this call. |
count | integer | How many entries the preview contains. |
warnings | array | Import advisories that are not errors: a VS Code ${input:…} or ${workspaceFolder} reference hoody cannot resolve (it would expand to empty — replace it before enabling), or declared VS Code inputs that were not carried. ${env:VAR} is translated to ${VAR} silently. Always present, usually empty. |
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "session not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | Unknown, closed, cross-realm or foreign-owner session, or a realm the login does not serve: one answer, so nothing leaks which. | List sessions and retry against a live one in a realm the login serves. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge (http.MaxBytesReader) before the handler reads it — a well-formed-but-large body is a size violation, not a JSON syntax error. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | Either the gateway’s per-client request limiter (Retry-After header present), or — on reconnect — the daemon’s per-session reconnect interval, which answers with the remaining wait in message and no Retry-After header. | Honor Retry-After when present; otherwise wait the interval named in the message and retry. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
cURL / TypeScript SDK
Section titled “cURL / TypeScript SDK”curl -sS "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/mcp/parse" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_01HV8XYZABCDEF", "document": "{\"mcpServers\":{\"filesystem\":{\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-filesystem\",\"/srv/data\"]}}}" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.mcp.previewImport({ session_id: 'sess_01HV8XYZABCDEF', document: '{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/srv/data"]}}}',});Beginning a write
Section titled “Beginning a write”POST /api/v1/agent/mcp/write-intents
Section titled “POST /api/v1/agent/mcp/write-intents”Mints the single-use nonce every MCP write requires and returns the target settings path plus its current mcp_servers hash. The nonce binds {session, op, resolved path}: a write presenting a nonce minted for a different op or scope fails closed. Pass the returned hash back as expect_hash so a concurrent edit is reported as a conflict rather than silently overwritten.
Mint a nonce close to the write — it expires. The scope defaults to user, or project when there is no user layer (which is the case under --config-dir).
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
realm | query | string | No | Per-request realm selector — the query-string alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id. MCP config is resolved against the session’s settings layers. |
op | string | Yes | Which write the nonce authorizes. One of upsert, delete, set_enabled, import. The minted nonce is valid for this op alone. |
scope | string | No | Settings layer to write. One of user, project, local. Defaults to user, or project when there is no user layer (the case under --config-dir). |
Response
Section titled “Response”{ "status": "ok", "nonce": "nce_01HV8XYZ4F2K3P8WJ9D5T6Y7ZA", "path": "/home/alice/.config/hoody/settings.json", "hash": "sha256:9f1c…e0a4"}| Field | Type | Description |
|---|---|---|
status | string | "ok" on success. |
nonce | string | Single-use nonce bound to {session, op, resolved path}. Expires, so mint it when you submit rather than when a form opens. |
path | string | The resolved settings file the write will target. |
hash | string | Current mcp_servers hash for that file; pass it back as expect_hash. |
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "session not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | Unknown, closed, cross-realm or foreign-owner session, or a realm the login does not serve: one answer, so nothing leaks which. | List sessions and retry against a live one in a realm the login serves. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge (http.MaxBytesReader) before the handler reads it — a well-formed-but-large body is a size violation, not a JSON syntax error. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | Either the gateway’s per-client request limiter (Retry-After header present), or — on reconnect — the daemon’s per-session reconnect interval, which answers with the remaining wait in message and no Retry-After header. | Honor Retry-After when present; otherwise wait the interval named in the message and retry. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
cURL / TypeScript SDK
Section titled “cURL / TypeScript SDK”curl -sS "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/mcp/write-intents" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_01HV8XYZABCDEF", "op": "upsert", "scope": "user" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.mcp.createWriteIntent({ session_id: 'sess_01HV8XYZABCDEF', op: 'upsert', scope: 'user',});Modifying the server list
Section titled “Modifying the server list”PUT /api/v1/agent/mcp/servers
Section titled “PUT /api/v1/agent/mcp/servers”Writes one mcp_servers entry, merging field-by-field over any existing entry of the same name — fields you omit keep their stored value, including fields this build does not model. env and headers merge per key: a key you send sets that one credential, a key you omit is kept, a key set to JSON null is DELETED, and an empty map ({}) clears the whole map. A JSON null for a top-level field is refused (omit the key to keep the stored value); so is a field name that differs from a server field only in case (Args), a null inside args, and two header names that differ only in case.
Restating the identity you read back is not a re-point — type may be spelled by any accepted alias and an absent args equals []; command and url are compared byte for byte. A genuine re-point clears env / headers unless the same request re-supplies them. A stored entry the loader refuses to load — two header names differing only in case, a field name in another case (Args), a null inside args — is repaired by a write that supplies the exact field (args, or headers: {} then the one spelling) and refused otherwise, naming the repair.
Requires the begin-write nonce for op:upsert AND the expect_hash from your most recent read (use the empty-array hash for a first write into a file that does not yet exist). A body carrying the redaction placeholder for a credential is REFUSED rather than stored.
On success a re-pointed server is REVOKED in every live session before the response, and the new config is APPLIED to the session you named before the response — its reconnect is AWAITED rather than merely started, so nothing is still pending when this returns. Awaited is NOT succeeded: a server that fails to start or handshake still returns 200, so read servers[].connected in this reply before calling its tools. A session that is MID-TURN keeps the tool set it was shown and picks the new one up at the next turn boundary. Other live sessions reconnect in the BACKGROUND, so this never waits out an unrelated session’s slow server (it may pause briefly for a free background slot under sustained write pressure); that background pass is best-effort and is skipped under sustained pressure, in which case those sessions pick the change up on the next write or an explicit reconnect. Revocation is never best-effort.
The server entry shape:
name— letters, digits,_and-, max 64 chars, no__(it separates the tool name), may not behoodyormcp(both are reserved namespaces). That check alone is case-insensitive, soHoodyis refused too.type—stdio(default),http(aliasesurl,streamable,streamable-http) for Streamable HTTP, orssefor the deprecated 2024-11-05 HTTP+SSE transport, matched case-insensitively.command+argsfor stdio, orurlfor the remote transports.env/headers— values support full${VAR}expansion, so a token lives in your environment rather than insettings.json.allowed_tools— restrict which of the server’s tools are advertised AND dispatchable.require_confirmation— park every call from this server for human approval.enabled— defaults totrue; a disabled server keeps its config but is neither connected nor advertised.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
realm | query | string | No | Per-request realm selector — the query-string alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id. |
nonce | string | Yes | Single-use nonce from mcp.createWriteIntent minted for op:upsert and this scope; the RPC fails closed without it. |
scope | string | No | Settings layer to write. One of user, project, local. Must match the scope the nonce was minted for. |
expect_hash | string | Yes | The mcp_servers hash you last read, as returned by mcp.createWriteIntent or mcp.listServers. A mismatch returns a conflict instead of overwriting a concurrent edit; a first write into a file that does not exist yet states its expectation with the empty-array hash rather than omitting this. |
server | object | Yes | The server entry. See the field description above for the accepted shape. |
Response
Section titled “Response”{ "status": "ok", "sessions": 3, "partial_sessions": 0, "revoked": 2, "deferred_sessions": 2, "deferred_started": true, "servers": [ { "name": "filesystem", "scope": "user", "transport": "stdio", "enabled": true, "require_confirmation": false, "connected": true, "protocol_version": "2025-03-26", "server_name": "filesystem", "server_version": "1.0.4", "tool_count": 12, "pid": 4711, "env_keys": ["FILESYSTEM_ROOT"], "header_keys": [] } ], "warnings": [], "path": "/home/alice/.config/hoody/settings.json", "hash": "sha256:c4d2…8f10"}| Field | Type | Description |
|---|---|---|
status | string | "ok" on success. |
sessions | integer | How many live sessions this write reconciled the CONNECT half for (the caller’s own synchronously, the rest in the background). Revocation was applied to every live session — these and the partial ones — before this response returned. Sessions already tearing down are not counted. |
partial_sessions | integer | How many live sessions had a settings layer that failed to read or parse. They were revoked against the layers that DID load (fail-closed: a server defined only in the broken layer is cut off until the layer is fixed and reinstated in place on the next clean reconcile) and their connect half was skipped. Non-zero means: fix the layer, then reconnect. When the caller’s own session is among them, servers below describes the file, not what that session has connected. |
revoked | integer | How many builds were cut off across every live session. A COUNT, not a list of names. |
deferred_sessions | integer | How many sessions OTHER than the one you named had their reconnect left to the background pass. Their revocation already happened; only reconnecting is deferred. |
deferred_started | boolean | False when that background pass was skipped entirely because every fanout slot was busy — those sessions MAY hold a stale tool list until the next write or an explicit reconnect (a no-op change leaves them already current). True when it was started, or when there was nothing to defer. It cannot report how many SUCCEEDED: the pass outlives this response by design. |
servers | array | The effective server list for the session you named, after the write — the same entries mcp.listServers returns. |
warnings | array | The loader’s warnings for that session’s layers after the write — the same lines mcp.listServers returns. Always present, usually empty; read it when a server you just wrote is missing from servers. |
path | string | The settings file that was written. |
hash | string | The mcp_servers hash AFTER this write. Pass it as expect_hash on your next write instead of round-tripping mcp.createWriteIntent — an empty expect_hash is refused. |
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "session not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | Unknown, closed, cross-realm or foreign-owner session, or a realm the login does not serve: one answer, so nothing leaks which. | List sessions and retry against a live one in a realm the login serves. |
{ "code": "mcp_conflict", "message": "the MCP server list changed since you read it; refresh and try again"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
mcp_conflict | MCP server list conflict | The mcp_servers content changed between the hash you read and this write. Another writer — a second client, the TUI, or a hand edit — got there first. | Re-read the current hash (mcp.listServers, or mcp.createWriteIntent which returns it with the nonce), re-apply your change on top of what is there now, and re-issue the write. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge (http.MaxBytesReader) before the handler reads it — a well-formed-but-large body is a size violation, not a JSON syntax error. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | Either the gateway’s per-client request limiter (Retry-After header present), or — on reconnect — the daemon’s per-session reconnect interval, which answers with the remaining wait in message and no Retry-After header. | Honor Retry-After when present; otherwise wait the interval named in the message and retry. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
cURL / TypeScript SDK
Section titled “cURL / TypeScript SDK”curl -sS -X PUT "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/mcp/servers" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_01HV8XYZABCDEF", "nonce": "nce_01HV8XYZ4F2K3P8WJ9D5T6Y7ZA", "scope": "user", "expect_hash": "sha256:9f1c…e0a4", "server": { "name": "filesystem", "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/srv/data"], "env": {"FILESYSTEM_ROOT": "/srv/data"}, "enabled": true, "require_confirmation": false } }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.mcp.upsertServer({ session_id: 'sess_01HV8XYZABCDEF', nonce: 'nce_01HV8XYZ4F2K3P8WJ9D5T6Y7ZA', scope: 'user', expect_hash: 'sha256:9f1c…e0a4', server: { name: 'filesystem', type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '/srv/data'], env: { FILESYSTEM_ROOT: '/srv/data' }, enabled: true, require_confirmation: false, },});POST /api/v1/agent/mcp/servers/enable
Section titled “POST /api/v1/agent/mcp/servers/enable”Flips one entry’s enabled flag without touching the rest of its config, so credentials and options survive a disable. Requires the begin-write nonce for op:set_enabled AND the expect_hash from your most recent read (use the empty-array hash for a first write into a file that does not yet exist). Disabling revokes the server in live sessions immediately.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
realm | query | string | No | Per-request realm selector — the query-string alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id. |
nonce | string | Yes | Single-use nonce from mcp.createWriteIntent minted for op:set_enabled and this scope. |
scope | string | No | Settings layer to write. One of user, project, local. Must match the scope the nonce was minted for. |
name | string | Yes | The server name. |
enabled | boolean | Yes | true to enable, false to disable. |
expect_hash | string | Yes | The mcp_servers hash you last read. |
Response
Section titled “Response”{ "status": "ok", "sessions": 3, "partial_sessions": 0, "revoked": 1, "deferred_sessions": 2, "deferred_started": true, "servers": [ { "name": "filesystem", "scope": "user", "transport": "stdio", "enabled": false, "require_confirmation": false, "connected": false, "env_keys": ["FILESYSTEM_ROOT"], "header_keys": [] } ], "warnings": [], "path": "/home/alice/.config/hoody/settings.json", "hash": "sha256:b1f4…91aa"}| Field | Type | Description |
|---|---|---|
status | string | "ok" on success. |
sessions | integer | How many live sessions this write reconciled the CONNECT half for (the caller’s own synchronously, the rest in the background). Revocation was applied to every live session — these and the partial ones — before this response returned. Sessions already tearing down are not counted. |
partial_sessions | integer | How many live sessions had a settings layer that failed to read or parse. They were revoked against the layers that DID load and their connect half was skipped. Non-zero means: fix the layer, then reconnect. |
revoked | integer | How many builds were cut off across every live session. A COUNT, not a list of names. |
deferred_sessions | integer | How many sessions OTHER than the one you named had their reconnect left to the background pass. |
deferred_started | boolean | False when the background pass was skipped because every fanout slot was busy; true otherwise. |
servers | array | The effective server list for the session you named, after the write. |
warnings | array | The loader’s warnings for that session’s layers after the write. Always present, usually empty. |
path | string | The settings file that was written. |
hash | string | The mcp_servers hash AFTER this write. Pass it as expect_hash on your next write. |
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "session not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | Unknown, closed, cross-realm or foreign-owner session, or a realm the login does not serve: one answer, so nothing leaks which. | List sessions and retry against a live one in a realm the login serves. |
{ "code": "mcp_conflict", "message": "the MCP server list changed since you read it; refresh and try again"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
mcp_conflict | MCP server list conflict | The mcp_servers content changed between the hash you read and this write. Another writer — a second client, the TUI, or a hand edit — got there first. | Re-read the current hash (mcp.listServers, or mcp.createWriteIntent which returns it with the nonce), re-apply your change on top of what is there now, and re-issue the write. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge (http.MaxBytesReader) before the handler reads it — a well-formed-but-large body is a size violation, not a JSON syntax error. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | Either the gateway’s per-client request limiter (Retry-After header present), or — on reconnect — the daemon’s per-session reconnect interval, which answers with the remaining wait in message and no Retry-After header. | Honor Retry-After when present; otherwise wait the interval named in the message and retry. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
cURL / TypeScript SDK
Section titled “cURL / TypeScript SDK”The SDK reaches this operation through two methods:
client.agent.mcp.enableServer(...): setsenabled: true.client.agent.mcp.disableServer(...): setsenabled: false.
curl -sS -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/mcp/servers/enable" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_01HV8XYZABCDEF", "nonce": "nce_01HV8XYZ4F2K3P8WJ9D5T6Y7ZA", "scope": "user", "name": "filesystem", "enabled": false, "expect_hash": "sha256:9f1c…e0a4" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.mcp.disableServer({ session_id: 'sess_01HV8XYZABCDEF', nonce: 'nce_01HV8XYZ4F2K3P8WJ9D5T6Y7ZA', scope: 'user', name: 'filesystem', expect_hash: 'sha256:9f1c…e0a4',});DELETE /api/v1/agent/mcp/servers
Section titled “DELETE /api/v1/agent/mcp/servers”Removes one mcp_servers entry from the named settings layer. Requires the begin-write nonce for op:delete AND the expect_hash from your most recent read (use the empty-array hash for a first write into a file that does not yet exist). The server is REVOKED in every live session before the response returns, so a caller mid-turn cannot still reach it; a stdio child is reaped when its last holder releases.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
realm | query | string | No | Per-request realm selector — the query-string alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id. |
nonce | string | Yes | Single-use nonce from mcp.createWriteIntent minted for op:delete and this scope. |
scope | string | No | Settings layer to write. One of user, project, local. Must match the scope the nonce was minted for. |
name | string | Yes | The server name to remove. |
expect_hash | string | Yes | The mcp_servers hash you last read. |
Response
Section titled “Response”{ "status": "ok", "sessions": 3, "partial_sessions": 0, "revoked": 1, "deferred_sessions": 2, "deferred_started": true, "servers": [], "warnings": [], "path": "/home/alice/.config/hoody/settings.json", "hash": "sha256:e02c…7751"}| Field | Type | Description |
|---|---|---|
status | string | "ok" on success. |
sessions | integer | How many live sessions this write reconciled the CONNECT half for (the caller’s own synchronously, the rest in the background). Revocation was applied to every live session — these and the partial ones — before this response returned. |
partial_sessions | integer | How many live sessions had a settings layer that failed to read or parse. They were revoked against the layers that DID load and their connect half was skipped. |
revoked | integer | How many builds were cut off across every live session. A COUNT, not a list of names. |
deferred_sessions | integer | How many sessions OTHER than the one you named had their reconnect left to the background pass. |
deferred_started | boolean | False when the background pass was skipped because every fanout slot was busy; true otherwise. |
servers | array | The effective server list for the session you named, after the write. |
warnings | array | The loader’s warnings for that session’s layers after the write. Always present, usually empty. |
path | string | The settings file that was written. |
hash | string | The mcp_servers hash AFTER this write. Pass it as expect_hash on your next write. |
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "session not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | Unknown, closed, cross-realm or foreign-owner session, or a realm the login does not serve: one answer, so nothing leaks which. | List sessions and retry against a live one in a realm the login serves. |
{ "code": "mcp_conflict", "message": "the MCP server list changed since you read it; refresh and try again"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
mcp_conflict | MCP server list conflict | The mcp_servers content changed between the hash you read and this write. Another writer — a second client, the TUI, or a hand edit — got there first. | Re-read the current hash (mcp.listServers, or mcp.createWriteIntent which returns it with the nonce), re-apply your change on top of what is there now, and re-issue the write. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge (http.MaxBytesReader) before the handler reads it — a well-formed-but-large body is a size violation, not a JSON syntax error. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | Either the gateway’s per-client request limiter (Retry-After header present), or — on reconnect — the daemon’s per-session reconnect interval, which answers with the remaining wait in message and no Retry-After header. | Honor Retry-After when present; otherwise wait the interval named in the message and retry. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
cURL / TypeScript SDK
Section titled “cURL / TypeScript SDK”curl -sS -X DELETE "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/mcp/servers" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_01HV8XYZABCDEF", "nonce": "nce_01HV8XYZ4F2K3P8WJ9D5T6Y7ZA", "scope": "user", "name": "filesystem", "expect_hash": "sha256:9f1c…e0a4" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.mcp.deleteServer({ session_id: 'sess_01HV8XYZABCDEF', nonce: 'nce_01HV8XYZ4F2K3P8WJ9D5T6Y7ZA', scope: 'user', name: 'filesystem', expect_hash: 'sha256:9f1c…e0a4',});POST /api/v1/agent/mcp/import
Section titled “POST /api/v1/agent/mcp/import”Imports a batch of servers from a pasted config document or an explicit array. Understands the hoody (mcp_servers list), Claude / Cursor (mcpServers map), and VS Code (servers map) dialects; a document carrying more than one of them is REFUSED rather than guessed at. Validation is WHOLE-BATCH — one bad entry aborts everything, because a partial import leaves a config the operator did not author.
Imported servers land DISABLED for review; enable them with mcp.enableServer / mcp.disableServer. Requires the begin-write nonce for op:import AND the expect_hash from your most recent read (use the empty-array hash for a first write into a file that does not yet exist).
The document and servers fields are mutually exclusive. The CLI exposes document only (hoody agent mcp import --document), because servers is an object array, which the command line does not take; the hoody dialect is accepted in document, so anything expressible via servers can be passed as a document. replace: true overwrites entries whose name already exists; without it, a collision aborts the whole import.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
realm | query | string | No | Per-request realm selector — the query-string alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id. |
nonce | string | Yes | Single-use nonce from mcp.createWriteIntent minted for op:import and this scope. |
scope | string | No | Settings layer to write. One of user, project, local. Must match the scope the nonce was minted for. |
document | string | No | A pasted config document in any supported dialect. Mutually exclusive with servers. |
servers | array | No | Explicit server entries, in hoody’s own shape. Mutually exclusive with document. API / SDK only. |
replace | boolean | No | Overwrite entries whose name already exists. Without it, a collision aborts the whole import. |
expect_hash | string | Yes | The mcp_servers hash you last read. |
Response
Section titled “Response”{ "status": "ok", "sessions": 3, "partial_sessions": 0, "revoked": 2, "deferred_sessions": 2, "deferred_started": true, "servers": [ { "name": "filesystem", "scope": "user", "transport": "stdio", "enabled": false, "require_confirmation": false, "connected": false, "env_keys": ["FILESYSTEM_ROOT"], "header_keys": [] }, { "name": "remote-tools", "scope": "user", "transport": "http", "enabled": false, "require_confirmation": false, "connected": false, "env_keys": [], "header_keys": ["Authorization"] } ], "warnings": [ "vscode variable ${workspaceFolder} cannot be resolved; replace before enabling" ], "path": "/home/alice/.config/hoody/settings.json", "hash": "sha256:7d3a…c8e2", "imported": 2}| Field | Type | Description |
|---|---|---|
status | string | "ok" on success. |
sessions | integer | How many live sessions this write reconciled the CONNECT half for (the caller’s own synchronously, the rest in the background). Revocation was applied to every live session — these and the partial ones — before this response returned. Sessions already tearing down are not counted. |
partial_sessions | integer | How many live sessions had a settings layer that failed to read or parse. They were revoked against the layers that DID load and their connect half was skipped. |
revoked | integer | How many builds were cut off across every live session. A COUNT, not a list of names. |
deferred_sessions | integer | How many sessions OTHER than the one you named had their reconnect left to the background pass. |
deferred_started | boolean | False when that background pass was skipped because every fanout slot was busy; true otherwise. |
servers | array | The effective server list for the session you named, after the write — the same entries mcp.listServers returns. |
warnings | array | The loader’s warnings for that session’s layers after the write, followed by the parser’s advisories for the document: a ${…} placeholder in a field other than env/headers left literal, an unresolvable or VS Code-predefined variable (${workspaceFolder}, ${file}) flagged, a dropped envFile. Always present, usually empty. |
path | string | The settings file that was written. |
hash | string | The mcp_servers hash AFTER this write. Pass it as expect_hash on your next write. |
imported | integer | How many entries were written. A COUNT, not a list of names. |
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "session not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | Unknown, closed, cross-realm or foreign-owner session, or a realm the login does not serve: one answer, so nothing leaks which. | List sessions and retry against a live one in a realm the login serves. |
{ "code": "mcp_conflict", "message": "the MCP server list changed since you read it; refresh and try again"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
mcp_conflict | MCP server list conflict | The mcp_servers content changed between the hash you read and this write. Another writer — a second client, the TUI, or a hand edit — got there first. | Re-read the current hash (mcp.listServers, or mcp.createWriteIntent which returns it with the nonce), re-apply your change on top of what is there now, and re-issue the write. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge (http.MaxBytesReader) before the handler reads it — a well-formed-but-large body is a size violation, not a JSON syntax error. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | Either the gateway’s per-client request limiter (Retry-After header present), or — on reconnect — the daemon’s per-session reconnect interval, which answers with the remaining wait in message and no Retry-After header. | Honor Retry-After when present; otherwise wait the interval named in the message and retry. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
cURL / TypeScript SDK
Section titled “cURL / TypeScript SDK”curl -sS -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/mcp/import" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_01HV8XYZABCDEF", "nonce": "nce_01HV8XYZ4F2K3P8WJ9D5T6Y7ZA", "scope": "user", "expect_hash": "sha256:9f1c…e0a4", "document": "{\"mcpServers\":{\"filesystem\":{\"command\":\"npx\",\"args\":[\"-y\",\"@modelcontextprotocol/server-filesystem\",\"/srv/data\"],\"env\":{\"FILESYSTEM_ROOT\":\"/srv/data\"}}}}", "replace": false }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.mcp.importServers({ session_id: 'sess_01HV8XYZABCDEF', nonce: 'nce_01HV8XYZ4F2K3P8WJ9D5T6Y7ZA', scope: 'user', expect_hash: 'sha256:9f1c…e0a4', document: '{"mcpServers":{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","/srv/data"],"env":{"FILESYSTEM_ROOT":"/srv/data"}}}}', replace: false,});Diagnostics
Section titled “Diagnostics”POST /api/v1/agent/mcp/probe
Section titled “POST /api/v1/agent/mcp/probe”Connects to a candidate server config and reports the tools it advertises and the protocol revision it negotiated, then tears the connection down. Nothing is written. The deny list is enforced on the candidate config before anything is started.
This route documents no 200 status because no machine caller can ever reach it. The other non-2xx statuses remain reachable for malformed input.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
realm | query | string | No | Per-request realm selector — the query-string alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id (supplies the deny list and transport policy). |
server | object | Yes | The candidate entry, same shape as mcp.upsertServer’s server. |
Response
Section titled “Response”{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "human_only", "message": "probing an MCP server requires direct human confirmation and is not available to machine callers"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
human_only | Probing requires a human | Probing STARTS A PROCESS (stdio) or makes an outbound request to a caller-chosen URL, so an autonomous caller may not self-approve it. The gateway stamps the machine marker on every call it forwards, so there is NO successful HTTP probe path — this refusal is not a transient condition to retry. | Do not probe over HTTP; get the same information from the write itself. Create the entry with mcp.upsertServer, then read mcp.listServers: it reports connected, the negotiated protocol_version, tool_count, and stderr_tail when a stdio child dies. A DISABLED entry is never started, so it will never report connected; enable it to learn anything. |
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "session not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | Unknown, closed, cross-realm or foreign-owner session, or a realm the login does not serve: one answer, so nothing leaks which. | List sessions and retry against a live one in a realm the login serves. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge (http.MaxBytesReader) before the handler reads it — a well-formed-but-large body is a size violation, not a JSON syntax error. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | Either the gateway’s per-client request limiter (Retry-After header present), or — on reconnect — the daemon’s per-session reconnect interval, which answers with the remaining wait in message and no Retry-After header. | Honor Retry-After when present; otherwise wait the interval named in the message and retry. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
cURL / TypeScript SDK
Section titled “cURL / TypeScript SDK”curl -sS -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/mcp/probe" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_01HV8XYZABCDEF", "server": { "name": "filesystem", "type": "stdio", "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/srv/data"] } }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.mcp.testServer({ session_id: 'sess_01HV8XYZABCDEF', server: { name: 'filesystem', type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '/srv/data'], },});POST /api/v1/agent/mcp/reconnect
Section titled “POST /api/v1/agent/mcp/reconnect”Re-reads the settings layers and reconciles every live session’s MCP pool: servers that vanished or were re-pointed are revoked, the rest are reconnected, and a healthy unchanged server is NOT restarted. Revocation lands in EVERY live session before the response. Reconnection is AWAITED only for the session you named — awaited, not guaranteed: a server that will not start leaves connected false and still returns 200. Other sessions reconnect in the background, best-effort.
Use it after editing a settings file by hand, or to recover a server that died. Note that on reconnect the daemon’s per-session reconnect interval may answer with 429 rate_limited carrying the remaining wait in message and no Retry-After header — honor the interval rather than retrying immediately.
Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Hoody-Cwd | header | string | No | Per-request working-directory scope: the .hoody project layer / record cwd / tool+workflow cwd. Required by routes that resolve a cwd (e.g. POST /todos; todos.create also accepts a body cwd). |
X-Hoody-Config-Dir | header | string | No | Per-request --config-dir override selecting which on-disk .hoody install a stateless read/write resolves. |
X-Hoody-Container | header | string | No | Per-request bound remote container (omitted = local). Rejected (400) on routes with no container dimension. |
X-Hoody-Realm | header | string | No | Per-request realm selector: "global" or a 24-hex id (also accepted as ?realm=). Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
realm | query | string | No | Per-request realm selector — the query-string alias of the X-Hoody-Realm header (read only when the header is absent): "global" or a 24-hex id. Rejected (400 realm_scope_unsupported) on active-only / no-realm routes. |
Request Body
Section titled “Request Body”| Field | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Live session id. |
Response
Section titled “Response”{ "status": "ok", "sessions": 3, "partial_sessions": 0, "revoked": 1, "deferred_sessions": 2, "deferred_started": true, "servers": [ { "name": "filesystem", "scope": "user", "transport": "stdio", "enabled": true, "require_confirmation": false, "connected": true, "protocol_version": "2025-03-26", "server_name": "filesystem", "server_version": "1.0.4", "tool_count": 12, "pid": 4802, "env_keys": ["FILESYSTEM_ROOT"], "header_keys": [] } ], "warnings": []}| Field | Type | Description |
|---|---|---|
status | string | "ok" on success. |
sessions | integer | How many live sessions this write reconciled the CONNECT half for (the caller’s own synchronously, the rest in the background). Revocation was applied to every live session — these and the partial ones — before this response returned. |
partial_sessions | integer | How many live sessions had a settings layer that failed to read or parse. They were revoked against the layers that DID load and their connect half was skipped. Non-zero means: fix the layer, then reconnect. |
revoked | integer | How many builds were cut off across every live session. A COUNT, not a list of names. |
deferred_sessions | integer | How many sessions OTHER than the one you named had their reconnect left to the background pass. |
deferred_started | boolean | False when that background pass was skipped because every fanout slot was busy — those sessions MAY hold a stale tool list until the next write or an explicit reconnect (a no-op change leaves them already current). True when it was started, or when there was nothing to defer. |
servers | array | The effective server list for the session you named, after the write — the same entries mcp.listServers returns. Check connected for the outcome of the reconnect. |
warnings | array | The loader’s warnings for that session’s layers after the write. Always present, usually empty. |
{ "code": "bad_request", "message": "invalid request"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
bad_request | Bad request | The request was malformed or carried invalid parameters. | Correct the request body or query parameters. |
realm_scope_unsupported | Realm scope unsupported | A per-request realm header was supplied to an active-only / global-no-realm RPC, which has no realm dimension to scope. | Omit the realm header on this route, or open a session to scope by realm. |
invalid_realm | Invalid realm selector | The realm selector is malformed (not ""/“global”/a 24-hex id). | Pass a valid realm selector. |
{ "code": "forbidden", "message": "request must arrive through the Hoody proxy"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
forbidden | Forbidden (Source IP Guard) | The request did not come through the program’s URL. Hoody Kit programs are reached through their URLs only. | Call the agent through its URL, for example with hoody agent …. |
{ "code": "not_found", "message": "session not found"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
not_found | Not found | Unknown, closed, cross-realm or foreign-owner session, or a realm the login does not serve: one answer, so nothing leaks which. | List sessions and retry against a live one in a realm the login serves. |
{ "code": "payload_too_large", "message": "request body exceeds the configured size limit"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
payload_too_large | Payload too large | The request body exceeds the configured size cap (MaxBodyBytes). The gateway rejects an oversized body at the edge (http.MaxBytesReader) before the handler reads it — a well-formed-but-large body is a size violation, not a JSON syntax error. | Reduce the request body below the configured limit (default 8 MiB); split a large payload into smaller requests. |
{ "code": "rate_limited", "message": "request rate limit exceeded"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
rate_limited | Too many requests | Either the gateway’s per-client request limiter (Retry-After header present), or — on reconnect — the daemon’s per-session reconnect interval, which answers with the remaining wait in message and no Retry-After header. | Honor Retry-After when present; otherwise wait the interval named in the message and retry. |
{ "code": "internal_error", "message": "internal server error"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
internal_error | Internal error | An unexpected error occurred while handling the request. | Retry; if persistent, inspect the daemon logs. |
{ "code": "service_unavailable", "message": "service unavailable"}| Error Code | Title | Description | Resolution |
|---|---|---|---|
service_unavailable | Service unavailable | The daemon could not service the request (too busy, or a per-client stream concurrency cap was hit). | Honor Retry-After and retry. |
restriction_unknown | Login restriction unknown | The agent cannot read yet which realms its login may serve (GET /hoody/auth/status reports restriction “unknown”), so it refuses realm-scoped requests until it can. Nothing was read or changed. | Retry after the restriction is readable. |
cURL / TypeScript SDK
Section titled “cURL / TypeScript SDK”curl -sS -X POST "https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com/api/v1/agent/mcp/reconnect" \ -H "Authorization: Bearer $HOODY_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "session_id": "sess_01HV8XYZABCDEF" }'import { HoodyClient } from 'hoody-sdk';
const client = new HoodyClient({ baseURL: 'https://{projectId}-{containerId}-agent-1.{server}.containers.hoody.com', token: process.env.HOODY_TOKEN });
await client.agent.mcp.reconnect({ session_id: 'sess_01HV8XYZABCDEF' });