> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getpostern.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP tools

> How an agent connects: the address, the transport, the key it sends, what every tool call returns, and where each tool is documented.

<Info>
  **Before you start**

  * **A running Postern of your own.** Every address here belongs to one install.
    There is no shared address. [Install Postern](/start/install).
  * **An agent key.** The sectors granted to it decide what every tool below
    answers. [Connect an agent](/start/connect-an-agent).
  * **An agent that speaks MCP over Streamable HTTP.** That is the one transport
    Postern offers. Most agents speak it.
  * **The address, copied from the Console.** A guessed address looks right and
    silently does not work.
</Info>

MCP is the standard way AI assistants connect to outside tools. Postern speaks it,
so Claude, ChatGPT and Cursor connect the same way.

Nothing on this page was captured from a run. The shapes are the declared types, and
the quoted strings are Postern's own.

## The seven tools

Seven tools, three pages. Each page answers one question, so the traps that belong
together stay together.

| Tool               | What it answers                                                | Where it is written up                                                |
| ------------------ | -------------------------------------------------------------- | --------------------------------------------------------------------- |
| `get_schema`       | The objects and fields you can ask for                         | [Read data](/reference/mcp-reading#get_schema)                        |
| `query`            | A structured read over one sector                              | [Read data](/reference/mcp-reading#query)                             |
| `get_record`       | One stored record by id                                        | [Read data](/reference/mcp-reading#get_record)                        |
| `describe_context` | What this key can reach, and how old each connection's data is | [Freshness and live reads](/reference/mcp-freshness#describe_context) |
| `fetch_live`       | The one read that leaves the cache                             | [Freshness and live reads](/reference/mcp-freshness#fetch_live)       |
| `list_actions`     | What the connected sources let you do                          | [Act on a source](/reference/mcp-actions#list_actions)                |
| `invoke_action`    | Perform one of those actions                                   | [Act on a source](/reference/mcp-actions#invoke_action)               |

A hosted agent — claude.ai or ChatGPT, running on a company's servers — cannot send
a fixed key at all, and signs in through a bridge instead.
[The OAuth bridge](/reference/oauth-bridge).

## The endpoint

Postern listens on two ports. Agents use **the agent port**. The Console uses **the
Console port**. Everything here is the agent port, except
[the REST read mirror](#the-rest-read-mirror).

The agent port answers `POST`, `GET` and `DELETE` on the path `/mcp`. Until you give
Postern a public address, that is the only path it serves.

| Request                                                | Answer                                                  |
| ------------------------------------------------------ | ------------------------------------------------------- |
| `POST /mcp`, `initialize`, no key or a key it refuses  | 401, code `-32001`, `Unauthorized`                      |
| `POST /mcp`, `initialize`, past 512 open sessions      | 503, code `-32000`, `Too many sessions`                 |
| `POST /mcp` with no session header and no `initialize` | 400, code `-32000`, `Missing or invalid Mcp-Session-Id` |
| `GET /mcp` with no session header                      | the same 400                                            |
| `DELETE /mcp` naming a session Postern does not hold   | 404, code `-32001`, `Unknown session`                   |
| Any path but `/mcp`                                    | 404, code `-32601`, `Not found`                         |

A session starts with a `POST` that carries an `initialize` request and no session
header. Postern reads the agent key, resolves it once, and returns an
`Mcp-Session-Id`. Every later request carries that header. `DELETE` ends the session.

Past 512 sessions Postern refuses a new one and never drops a live one. Only a caller
that already holds a valid key reaches that check.

### Copy the address from the Console

1. In the left nav, click **Agents & keys**.
2. Scroll to the **Connect an agent** band.
3. Read the value under **MCP endpoint**.
4. Click **Copy** beside it.

Postern reports the address it is actually serving. Do not assemble one by hand.

If that band prints `This gateway isn’t serving MCP, so it has no address to point an
agent at. Set MCP_HTTP_PORT on the box and restart it.`, no agent port is running.

The port is a setting, not a constant, and `8788` is the default. To move it:

1. On the machine Postern runs on, open `.env` — the settings file beside
   `docker-compose.yml`.
2. Set `MCP_HTTP_PORT` to the port you want. Save the file.
3. In that same folder, run `docker compose up -d`.

Postern serves the new port after that restart.

### Postern offers no stdio

Some agents can only start a program and talk to it directly. That is stdio, and
Postern does not offer it.

stdio works only when your agent starts Postern itself, as its own child process. The
Postern that serves your Console already runs and already owns its stdin. Under the
Docker setup there is no stdin to reach at all. A second Postern beside it would use
its own encryption key, and it could open none of the accounts you have connected.

Postern does hold one stdio session of its own, on its own stdin. That session resolves
as the local operator, with every sector granted and no key involved. That is exactly
why Postern never offers it to an agent.

An agent that only speaks stdio needs `mcp-remote` in front of the same URL. The
Console's own answer is the **Remote · bearer** config block below.

### Claude Desktop

**Claude Desktop is supported, through `mcp-remote`.** It takes the same `/mcp`
address and the same `Authorization: Bearer <your-agent-key>` header as every
other client. It reaches the same agent port and meets the same key check. Its
config file is `claude_desktop_config.json`; on macOS that is
`~/Library/Application Support/Claude/claude_desktop_config.json`.

<Note>
  **Nobody at Postern has run that recipe end to end.** The exact `command` and
  `args` for the `mcp-remote` bridge come from that project's own documentation,
  not from a run against Postern. So this page does not print them as a verified
  block. Copy the **Remote · bearer** config from the Console, put `mcp-remote` in
  front of the URL, and expect to adjust it.
</Note>

## How a client authenticates

Nobody types an HTTP header. You paste a config block:

```json theme={"system"}
{
  "mcpServers": {
    "postern": {
      "type": "http",
      "url": "<your MCP endpoint>",
      "headers": { "Authorization": "Bearer <your-agent-key>" }
    }
  }
}
```

`type: "http"` marks this as Streamable HTTP in the `mcpServers` shape. It is the key
those config files read.

To get it filled in: click **Agents & keys**, scroll to **Connect an agent**, then
click **Copy recipe · keyless** under **Remote · bearer**. Paste the whole object into
your agent's MCP config file — `.mcp.json` for Claude Code, `mcp.json` for Cursor.
Replace `<your-agent-key>` with your own key. Restart the agent.

Postern matches the header name and the word `Bearer` without regard to case. A
`Bearer` with no key behind it counts as no key at all.

Postern compares the SHA-256 of the key you present against the hash stored with the
grant. It never stores the key, never logs it, and never puts it in the record. A lost
key is created again, not recovered.
[What an agent key is](/reference/agent-keys#what-an-agent-key-is).

Postern checks the key once, when the agent connects, and remembers the answer for that
session. Later tool calls do not re-read the database.

Three states end a key — unknown, revoked and expired — and the refusal at connect
names which one it was. [Why an agent was refused](/reference/refusals).

A key is dead **at** its expiry instant, and Postern checks the deadline on every
call. An open session stops answering the moment its key expires.
[Expiry](/reference/agent-keys#expiry).

Four acts end every live session that agent holds, at once: an edit to its
grants, an edit to its expiry, a rotation of its key, and a revoke or delete.
A session that was still starting up is refused too. Postern only has
to sweep the agent port. The REST read mirror re-checks the key on every single
request, so it never held a stale answer to sweep. The agent must connect again,
and the new grants apply from that new session.
[Revoking, rotating and removing](/reference/agent-keys#revoking-rotating-and-removing).

**A grant decides what every tool answers.** Grants are per sector and default-deny: a
new key can reach nothing until you switch on a sector, and a granted sector covers
reading **and** acting there. A key granted no sectors connects successfully and is
then denied on every read, so nothing in your agent looks wrong; `describe_context`
gives it away, coming back with `sectors: []`.
[What the grant decides, tool by tool](/reference/grants-and-sectors#what-the-grant-decides-tool-by-tool)
is the per-tool effect, and [why an agent was refused](/reference/refusals) is the
shape of each refusal.

## What a tool call returns

Every tool answers with one text block holding the result as JSON, indented two spaces:

```ts theme={"system"}
{ content: [{ type: "text", text: string }] }
```

Tools are the entire surface. Postern registers no resources and no prompts.

A refusal or a failure comes back as a **tool error** carrying Postern's own message. It
is not a transport error and not a broken session. This page quotes only Postern's half
of the string; how your agent wraps it is not recorded here.

An action the source itself rejects is not an error. `invoke_action` returns `ok: false`
as an ordinary result. See [invoke\_action](/reference/mcp-actions#invoke_action).

There are no rate limits in the tool path. The bridge's own addresses are metered.

Postern writes a row for every tool call inside a live session — served, refused or
failed. The write happens off the response path, so it costs the call nothing, and the
key is never one of the fields. A key refused at *connect* is a different path: the
agent port records that, and the REST read mirror does not. Read it at
Console → **The ledger**. [What gets a row](/reference/audit-log#what-gets-a-row).

## What the agent is told at connect

Postern introduces itself as `postern` and ships this text in the `initialize` result,
where it stays in the agent's context for the whole session:

```text theme={"system"}
Postern is this person's personal context gateway: one authenticated surface over
the sectors of their life (finance, health, mail, contacts, calendar, home). Reads
are served from a local cache in milliseconds; `fetch_live` is the only path that
touches a source at read time. Start with `describe_context` (what you are granted,
and each connection's freshness), then `get_schema` for the object you want — read
its `notes` and `fieldDocs` before composing any filter or aggregate; they carry
sign conventions and unit traps that the types do not. `query` reads ONE sector:
there are no cross-sector joins, so fan out and correlate yourself. Responses are
capped — `has_more: true` means you are looking at a page, not an answer.
`list_actions` / `invoke_action` act on a source; a granted sector covers both
reading and acting. Never assume a field's meaning from its name.
```

Each tool carries its own description on top of that, and those descriptions are where
the per-tool traps live.

## The REST read mirror

Every read an agent can make over MCP is also available over plain HTTP, for tools that
do not speak MCP. The Console port serves five of them, gated by the same agent key and
recorded the same way:

```text theme={"system"}
GET  /api/context
GET  /api/schema?sector=
POST /api/query
GET  /api/records/:sector/:id
POST /api/fetch_live
```

They call the same core functions and answer the same shapes as the tools above.

**The mirror carries reads only.** There is no `invoke_action` on it. To act on a
source — which today means Home Assistant and nothing else — an agent must use MCP.

Postern resolves the identity on **every** request here rather than once per session,
so the mirror has no session to sweep and no session-cached grant.

The mirror is on the Console port, not the agent port, and `docker-compose.yml` opens
that port to this computer only. A public address opens the agent port to the
internet. It never opens this one.

## If something went wrong

Every string below is Postern's own. Your agent may print more around it.

| What you see                                                                         | What to do                                                                                                                                                                                                                                |
| ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The agent connects and every read is denied                                          | No sector is switched on. Click **Agents & keys**, click that agent's row, switch on a sector under **Grants**, then reconnect the agent.                                                                                                 |
| 404, `Not found`, on a path you expected to exist                                    | Postern serves `/mcp` and nothing else until it has a public address. Check that the `url` in your config ends in `/mcp`.                                                                                                                 |
| A hosted agent says it could not reach you, and nothing appears in your logs         | Nothing arrived. Hosted agents connect out on port 443 only. Move the address to 443, wait five minutes, then try once. [Hosted agents connect out on port 443 only](/reference/oauth-bridge#hosted-agents-connect-out-on-port-443-only). |
| Your agent can only launch a command and will not take a web address                 | Postern offers no stdio. Put `mcp-remote` in front of the same URL, or use the **Remote · bearer** config block above.                                                                                                                    |
| `access denied: unknown agent`, `access denied: revoked` or `access denied: expired` | The key is not one Postern will resolve. [Why an agent was refused](/reference/refusals).                                                                                                                                                 |

## Next

<Columns cols={2}>
  <Card title="Read data" href="/reference/mcp-reading">
    `get_schema`, `query` and `get_record` — every parameter, every refusal.
  </Card>

  <Card title="Freshness and live reads" href="/reference/mcp-freshness">
    `describe_context` and `fetch_live` — how old the data is, and how to refresh it.
  </Card>

  <Card title="Act on a source" href="/reference/mcp-actions">
    `list_actions` and `invoke_action` — the write side.
  </Card>

  <Card title="Connect an agent" href="/start/connect-an-agent">
    Create a key and paste it into your agent.
  </Card>
</Columns>
