Service accounts, client credentials, and your first tool call.
Machine integrations authenticate as a service account using
the OAuth client_credentials grant. No browser, no user session,
no interactive consent.
An administrator does this from Admin → Service Accounts:
The service account's id is the client_id, and it
is safe to store in config. The secret is not — treat it like a password.
A new service account can authenticate but can see nothing. Access is granted per brain by an administrator, from Admin → Brains → the brain → its service-account access section. Note this is the admin brain page: the user-facing brain page manages user and group access, not service accounts.
Pick the service account and choose a level:
| Level | Can do |
|---|---|
read | Search and read. The right default for a retrieval agent. |
read_write | Also create, update and delete neurons and synapses. |
owner | Also manage access and brain settings. |
Grant the least you need. An agent that only answers questions should be
read — that way a prompt-injection attempt in retrieved content
cannot lead to writes.
POST /brain/{brain_id}/token. Form-encoded or JSON both work.
The access token is a bearer JWT valid for 1 hour; the
refresh token lasts 30 days.
curl -X POST https://hive.example.com/brain/$BRAIN_ID/token \
-d grant_type=client_credentials \
-d client_id=$HIVE_CLIENT_ID \
-d client_secret=$HIVE_CLIENT_SECRET
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "eyJhbGciOi..."
}
To rotate without re-sending the secret, post
grant_type=refresh_token with the refresh_token.
Refresh tokens rotate on use — store the new one.
The MCP endpoint is POST /brain/{brain_id}/mcp, JSON-RPC 2.0 over
the streamable-HTTP transport. Send the access token as a bearer header and
accept both content types:
curl -X POST https://hive.example.com/brain/$BRAIN_ID/mcp \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "aiqbee_search",
"arguments": {"query": "what did we decide about rate limiting?"}
}
}'
tools/list returns the full tool set with schemas — useful for
building your tool definitions programmatically rather than hard-coding them.
Most MCP SDKs will handle this for you. If you are wiring it by hand, the whole surface is this small:
import os, httpx
BASE = os.environ["HIVE_BASE_URL"]
BRAIN_ID = os.environ["HIVE_BRAIN_ID"]
class Hive:
"""Thin Hive MCP client. Fetches a token lazily and retries once on 401."""
def __init__(self) -> None:
self._client = httpx.Client(timeout=30)
self._token: str | None = None
def _get_token(self) -> str:
r = self._client.post(
f"{BASE}/brain/{BRAIN_ID}/token",
data={
"grant_type": "client_credentials",
"client_id": os.environ["HIVE_CLIENT_ID"],
"client_secret": os.environ["HIVE_CLIENT_SECRET"],
},
)
r.raise_for_status()
return r.json()["access_token"]
def call(self, tool: str, **arguments: object) -> dict:
if self._token is None:
self._token = self._get_token()
def _post() -> httpx.Response:
return self._client.post(
f"{BASE}/brain/{BRAIN_ID}/mcp",
headers={
"Authorization": f"Bearer {self._token}",
"Accept": "application/json, text/event-stream",
},
json={
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {"name": tool, "arguments": arguments},
},
)
response = _post()
if response.status_code == 401: # token expired mid-session
self._token = self._get_token()
response = _post()
response.raise_for_status()
return response.json()
hive = Hive()
print(hive.call("aiqbee_search", query="rate limiting decision"))
| Symptom | Cause |
|---|---|
invalid_client (401) at the token endpoint |
Wrong client_id/secret, the account is deactivated,
or its secret has expired. Reissue from the admin page. |
| Token works, every tool reports no access | Step 2 was skipped — the service account has no grant on that brain, or you are calling a different brain's id. |
| Reads fine, writes refused | Access level is read, or the brain has
allow_mcp_editing switched off. |
| Audit history tools refuse | Audit is off for that brain. Enabling it needs a paid licence (Licensing & tiers), but reading existing history does not — a brain that already has audit on keeps serving its history. |
| Audit history refuses for a service account | Audit history is owner-only. A service account with
read or read_write cannot read it even
though its own writes are recorded there. |
| 406 Not Acceptable | Missing Accept: application/json, text/event-stream.
The streamable-HTTP transport requires both. |