docs: MCP servers in the architecture and the guide; no longer out of scope

Section 7 declares them and their permission, 8.3 says where they run, when they start and what the model gets, 4.3
how their secrets travel, 5.3 and 9.6 the routes, 8.8 the engine's manager. The guide has a section on them, the two
commands, and the README one row.
This commit is contained in:
feder-cr committed 2026-10-11 04:02:57 +02:00
1 parent 71bdf9db85
commit 2afd73b0fe
3 files changed
+147 -11

No files matched your search

+1
View File
@@ -60,6 +60,7 @@ Anything that needs a computer and a person's judgement, for as long as it takes
| **[A computer of its own](docs/guide.md#what-a-dot-can-do)** | A hardware-accelerated VM with a Linux desktop and a persistent disk. It sleeps when idle and wakes for the next message, task or automation. |
| **[A browser that is not blocked](docs/guide.md#the-browser)** | [invisible_playwright_mcp](https://github.com/feder-cr/invisible_playwright_mcp): Firefox patched in C++, the fingerprint set inside the engine. Each identity keeps its own cookies and logins. |
| **[Memory and skills](docs/guide.md#what-a-dot-can-do)** | It writes its own notes and how-tos in its home folder, as Claude Code does, and reads them on the next task. |
| **[Your MCP servers](docs/guide.md#mcp-servers)** | Any MCP server you declare, a program on its computer or a URL, as in Claude Code. Its keys stay out of the config. |
| **[Permissions you set](docs/guide.md#approvals)** | Every tool belongs to a permission: allow, ask or deny. An ask waits in your Inbox, survives a restart and runs the call once. |
| **[Tasks and automations](docs/guide.md#the-web-ui)** | A queue with priorities and start times, and its own recurring jobs, for which its computer is started on time. |
| **[Many ways to reach it](docs/guide.md#talk-to-it-from-your-phone)** | Web UI, command line, HTTP API with live events, Telegram, and WhatsApp as an opt-in. |
+109 -7
View File
@@ -631,6 +631,22 @@ provider of the next turn; a running turn keeps the one it began with. The
engine reads no credential from its environment: no provider spec names an
environment variable.
The secrets of the MCP servers a Dot declares (section 7) travel the same
way: stored encrypted under the Dot's scope as `mcp/<server>/<NAME>`, set and
cleared write-only (section 9.6), pushed in the same `POST /secrets` as the key
(`mcp_secrets`, by server and name, only those the config names), held in
memory by `McpSecrets` (`nanobot/dots/secrets.py`) and masked in the
conversation files. Unlike the key, a secret leaves the engine for the server
that names it, because that is what it is for, as in every MCP host: a
command's secret is an environment variable of its process, which the relay
receives in its own environment (only its name is on the relay's command line,
`--env-from`), and a URL's is a header of its requests. Its value is printable
ASCII, a space allowed (`Bearer <token>`): one rule, `MCP_SECRET_PATTERN` and
`MCP_SECRET_RULE` in `packages/shared`, applied by the host where it enters and
by the engine on `POST /secrets`. A config that no longer names a secret (its
server removed, its name dropped) deletes its value in the same transaction, so
none is kept that nothing uses.
A secret also never travels in an error: a failed push of the key is reported
by route, status and code only (`the guest did not take the OpenRouter key
(status 502, ...)`), never with the text the guest or a proxy answered, which
@@ -725,7 +741,7 @@ command detached into another session outlives it.
| method and path | body | answer |
|---|---|---|
| `GET /health` | | `{ status: "ok"\|"starting", state: AgentState, openrouter_configured: bool, browser: { identities: n, open: n }, checks }`; `identities` is the number of rows and `open` the number of identities with a live browser |
| `POST /secrets` | `{ openrouter_api_key }` | `204` |
| `POST /secrets` | `{ openrouter_api_key, mcp_secrets }`; `mcp_secrets` is `{ <server>: { <NAME>: value } }`, the secrets of the config's MCP servers that are set | `204`; `400 invalid_secret` holds neither, and never says a value |
| `PUT /config` | `DotRuntimeConfig` (section 7) | `204`, validated, persisted in the engine's database (`dots_kv`) and projected onto the engine's settings in process (section 8.8); a config that does not validate is `400 invalid_config` |
| `POST /events` | `InboundEvent` | `202 { accepted: true }` |
| `GET /events/stream` | `?after=<seq>` | `text/event-stream`, one SSE message per outbound event, `id: <seq>` |
@@ -736,7 +752,7 @@ command detached into another session outlives it.
| `DELETE /browser-identities/:id` | | `204` after the identity's browser is closed and its directory removed; `404 not_found` |
| `GET /browser-identities/:id/frame` | | `200 image/jpeg` (`Cache-Control: no-store`), one frame of the identity's window, taken with the server's `browser_watch`; `404 not_found`; `409 not_open` when the identity is closed; `503 busy` when a call of the Dot held the identity for longer than 5 seconds; `502 frame_failed` (the server has no page to show, or sent a frame that is not a JPEG: the engine is the one owner of that rule, the host passes the bytes on as `image/jpeg`) or `crashed` |
| `POST /browser-identities/:id/close` | | `204` after the identity's browser is closed through `browser_close` and its server has ended; the profile stays. Closing a closed identity is a `204` too; `404 not_found` |
| `GET /tools` | | `{ tools: [{ name, permission, offered, description }] }`: the engine's tool table (section 8.8) in its order; `offered` is whether the model is offered the tool now (its permission is not `deny`, and a tool that creates or deletes a browser identity needs the Dot to manage its identities; before the first config, none); `description` is the one in the tool's schema |
| `GET /tools` | | `{ tools: [{ name, permission, offered, description }], mcp_servers: [{ name, state, error, tools }] }`: the engine's tool table (section 8.8) in its order, then the tools of the declared MCP servers that are connected (permission `mcp.<server>`); `offered` is whether the model is offered the tool now (its permission is not `deny`, and a tool that creates or deletes a browser identity needs the Dot to manage its identities; before the first config, none); `description` is the one in the tool's schema. `mcp_servers` is every declared server by name, `state` `connecting`, `connected` or `failed`, `error` why it failed (section 8.3) |
| `GET /skills` | | `{ skills: [{ name, description, source, path, content }] }`: the Dot's skills (section 8.6) by name, the built-in ones and its own (`source` is `builtin` or `dot`), each with the whole of its SKILL.md |
| `POST /prepare-sleep` | | `204` after the state is flushed and browser sessions are closed; the agent then starts no new work. A model request in flight is abandoned; a tool in flight gets up to 20 seconds to finish and record its result, then is aborted (section 8.7). A `POST /secrets` (the READY procedure of a VM whose stop failed, so no shutdown followed) lifts that, and so does a new inbound event |
@@ -1093,6 +1109,17 @@ permissions: # allow | ask | deny, keyed by permission
limits:
max_steps_per_task: 60 # model turns before a task is failed
max_cost_per_task_usd: 1.00 # USD of model spend of a task or a chat turn, 0.01..100; the last request may exceed it (section 8.2)
mcp_servers: # optional: MCP servers whose tools the Dot may use, by name
time: # [a-z0-9-], 1..32, starting with a letter or a digit
command: uvx # run on the Dot's computer, as dot, in its home
args: [mcp-server-time]
env: {LOG_LEVEL: warning} # optional: environment written here
secrets: [TIME_API_KEY] # optional: environment variables whose values are secrets (section 9.6)
timeout_s: 120 # optional: the longest one call may take, 1..600
search:
url: https://search.example/mcp # streamable HTTP, or SSE for a URL ending in /sse
headers: {X-Client: dots} # optional
secrets: [Authorization] # optional: headers whose values are secrets
```
A Dot has no goal: what it is for is what its person asks of it, in the chat, in a task or in its
@@ -1135,6 +1162,30 @@ every permission the registry knows, defaults applied. The guest applies the
map as it is and denies a permission missing from it, so the defaults live in
one place (`resolvePermission` in `packages/shared`).
`mcp_servers` are the MCP servers the person lets the Dot use, declared the way
Claude Code (`.mcp.json`), Codex (`[mcp_servers]`) and nanobot declare them: a
`command` with its `args` and `env`, or a `url` with its `headers`. Only the
person declares one; the Dot can install the program a server needs (section
4.2), as it installs anything, but it cannot add a server to its own config,
which would let it give itself tools. A server's name is the prefix of its tools
as the model sees them, `mcp_<server>_<tool>` (nanobot's naming), so it holds no
`_` and the engine reads the server back from a tool's name. `secrets` names the
environment variables (of a command) or headers (of a URL) whose values are
secrets: they are set apart from the config, write-only (section 9.6), never in
it, so the config can be shown, exported and edited as YAML. A secret named twice,
or named as a value the entry also writes, is refused.
Each declared server has a permission, `mcp.<server>`, that covers all of its
tools: what a server's tool does is that server's to say, which the host cannot
know, so a server is one decision, as it is in Claude Code's permission rules
(`mcp__<server>`), and it asks by default. Its risk is `high`, as running
commands is. A permission of a server the config does not declare is refused,
and `resolvePermission` denies it. `MCP_SERVER_NAME_PATTERN` and
`MCP_TIMEOUT_BOUNDS` are in `packages/shared`; the guest's `protocol.py` keeps a
copy of the name rule. The servers are listed by name everywhere (the status,
the prompt, the settings): the database keeps a config as `jsonb`, which does
not keep the order of an object's keys.
## 8. Agent runtime
Sections 8.1 to 8.7 are the contract of a Dot's runtime. Section 8.8 is the
@@ -1254,6 +1305,44 @@ server's instructions. A browser action on an identity that is not open does
not launch it, it says so (section 6). An image the server answers with is
shown to the model and not stored (section 6).
The tools of the MCP servers the person declares (`mcp_servers`, section 7)
are not rows of the table: which they are is each server's to say, at run time.
`nanobot/dots/mcp_servers.py` keeps one nanobot `MCPProvider` per declared
server, on a registry of its own, as the browser has one per identity, so
spawning, `initialize`, `tools/list`, the per-call timeout (`timeout_s`) and
the reconnect are the client's. What is the Dot's:
- where it runs. A `command` is started through `dot-agentd relay`, as the
user `dot`, in its home, like every program of the model: it reads and writes
what the Dot's commands can, and nothing of the engine's. Its `env` is passed
with `--env`, its secrets with `--env-from` (their names only, the values in
the relay's own environment), so no secret is on a command line. A `url` is
reached from the engine, its secrets as headers.
- when. A server is started when it is declared, and again when a config or a
secret changes its entry (that server only). The next turn waits for the
servers being started, each for at most 60 s (`STARTUP_TIMEOUT_S`: a first
start through `uvx` or `npx` downloads the server). One that failed (its
program is not installed yet, it exited) is started again when the next turn
starts, so a server whose program the Dot installs works from the next
message. One whose secret is not set is not started until the person sets it.
- why it is not connected. The client says why a connection failed, and a
command's standard error is drained into a pipe of the engine, of which the
last 2000 characters are kept: the error a person and the model read is what
the server wrote (`npx: not found`, `the API key is missing`), with the
server's secrets masked.
- what the model gets. Each tool of a connected server, under the client's name
`mcp_<server>_<tool>`, is offered while the permission `mcp.<server>` is
`allow` or `ask`, and the gate decides each call by it (section 8.4). The
system prompt has a section for those servers: each one's instructions (what
it said at `initialize`, cut at 2048 characters as Claude Code cuts them), or
why it is not connected and that it is started again at the next turn. An
image a tool answers with is shown to the model as the browser's are. A call
shows no target in `tool.called`, and an approval shows all its arguments.
`GET /tools` lists those tools after the table's, each under its server's
permission, and every declared server with its state: `connecting`,
`connected` (with how many tools) or `failed` (with why).
The tool calls of one response run one at a time, in the order the model
gave them. Only the response's `tool_calls` count: a call written in the
assistant's text runs nothing. The registry validates a call's arguments
@@ -1620,9 +1709,19 @@ other engine owns the state.
decides every call by the same table. The table also says which arguments
of a call an `approval.requested` may carry (all of them, but for the proxy
of `browser_identity_create`).
The engine has no MCP server of its own to configure: the one server it runs
is `invisible-playwright-mcp`, and only through the `BrowserManager`, on a
registry no turn sees.
The browser's server, `invisible-playwright-mcp`, runs only through the
`BrowserManager`, on registries no turn sees. The servers the person
declares (`mcp_servers`) are `McpServers`' (`nanobot/dots/mcp_servers.py`,
section 8.3): it registers each connected server's tools on the Dot's
registry as `McpServerTool`s, which call the client's wrapper of the
server's own registry at call time, and the permission table maps a tool
named `mcp_<server>_<tool>` to `mcp.<server>` (`tool_permission`). The
projection keeps the servers whose permission is not `deny`
(`EngineSettings.mcp_servers`); a turn first awaits `McpServers.ready()`,
then offers the table's tools and those servers' tools, and the prompt
carries what each said at `initialize` or why it is not connected. The fork's
MCP client keeps a server's instructions, says why a connection failed, and
takes a stdio server's standard error (`UPSTREAM.md`).
- Browser identities. `BrowserManager` (`nanobot/dots/browser.py`) owns the
identities (their rows in `dots_browser_identities`, their directories under
`/home/dot/browsers` and their servers' homes under `/var/lib/invisible-dots/mcp`,
@@ -2000,7 +2099,10 @@ POST /api/approvals/:id/reject body: { note? }
GET /api/dots/:id/events ?after=<id>&before=<id>&limit=&types=<a,b>&tools=<a,b>&task_id=&order=asc|desc `types` are event type names (an unknown one is a 400), `tools` narrows `tool.called` to those tools (`data.tool`) and leaves other types alone, `task_id` keeps the events whose `data.task_id` it is, `order=desc` is the newest first so that a `limit` keeps the newest, and `before` (the id of the oldest event of the previous page, desc only) goes on, older, from there
GET /api/dots/:id/files/list ?path= { path, entries: [{ name, type, size, mtime }] }: a directory under /home/dot (home when omitted)
GET /api/dots/:id/files ?path= the bytes of a file under /home/dot, at most 16 MiB (413 `file_too_large`)
GET /api/dots/:id/tools { tools: [{ name, permission, offered, description }] }: the engine's tool table and what the model is offered now; needs the computer running
GET /api/dots/:id/tools { tools: [{ name, permission, offered, description }], mcp_servers: [{ name, state, error, tools }] }: the engine's tool table and what the model is offered now, and where each declared MCP server is; needs the computer running
GET /api/dots/:id/mcp-secrets { dot_id, secrets: [{ server, name, set }] }: every secret the config's MCP servers name, and whether it is set; never a value
PUT /api/dots/:id/mcp-secrets/:server/:name body: { value } sets it (404 for a secret the config does not name, 400 for a value that breaks MCP_SECRET_RULE) and pushes the secrets to a running guest, which starts the server again with it
DELETE /api/dots/:id/mcp-secrets/:server/:name clears it
GET /api/dots/:id/skills { skills: [{ name, description, source, path, content }] }: the Dot's skills (section 8.6); needs the computer running
GET /api/dots/:id/usage ?since=<ISO 8601 timestamp> { dot_id, since, spent_usd }
GET /api/stream SSE: every event, ?dot_id= to filter
@@ -2645,7 +2747,7 @@ then each code as a QR for the terminal until the number is linked, then
## 10. Out of scope for this version
Snapshots and rollback, MCP integrations beyond the browser, remote desktop
Snapshots and rollback, remote desktop
and interactive terminal, artifacts, backups, quotas, network policies,
multiple hosts, organisations and RBAC, macOS hosts. The tables and states
above leave room for them; nothing here pretends to implement them.
+37 -4
View File
@@ -218,6 +218,9 @@ Its tools, each behind a permission
and https only), read the page, take screenshots, click, type, select,
scroll, go back and forward.
- **Look at its desktop** with a screenshot.
- **Use the MCP servers you give it**: a program on its computer or a URL, each
one declared in its settings, with its own permission (asked by default). See
[MCP servers](#mcp-servers).
- **Schedule itself**: add, list and remove its own automations (at a time,
every interval, or a cron expression). This one asks you first by default;
to pause or remove one, ask the Dot.
@@ -331,6 +334,8 @@ while you have stopped it.
| `invisible-dots reject <id> [--note text]` | refuse it; the model is told no |
| `invisible-dots computer <dot> start\|stop\|reboot` | power its computer on or off by hand |
| `invisible-dots browser <dot> identities` | its browser identities |
| `invisible-dots mcp <dot>` | its MCP servers: where each is, and which secrets are set |
| `invisible-dots secret mcp --dot <dot> <server> <NAME> [--clear]` | set a secret of one of its MCP servers, asked for like the key |
| `invisible-dots channel list [--dot <dot>]` | linked chats and who is paired |
| `invisible-dots channel remove telegram\|whatsapp --dot <dot>` | unlink a chat; its token or keys and paired people are deleted |
| `invisible-dots secret openrouter --dot <dot>` | a key for one Dot instead of the global one |
@@ -392,6 +397,34 @@ on the suites it names. invisible_dots adds no claim of its own: a site can
still refuse a Dot for what it does, and the terms of the sites you point it at
still apply.
## MCP servers
A Dot uses the tools of any MCP server you declare, the way Claude Code and
Codex do: in its settings (MCP servers), or in its YAML.
```yaml
mcp_servers:
time:
command: uvx # runs on the Dot's computer, as the Dot's user
args: [mcp-server-time]
search:
url: https://search.example/mcp
secrets: [Authorization] # its value is set apart, never in the YAML
```
A server's tools are named `mcp_<server>_<tool>`, and one permission covers
them all, `mcp.<server>`, which asks you before each call until you allow it
(or "Always allow" an approval). A secret, an API key or a token, is set in the
settings next to its server or with
`invisible-dots secret mcp --dot <dot> <server> <NAME>` (asked for like the
key; `--clear` removes it): it reaches the server as an environment variable or
a header, never through the YAML or a command line.
Only you add a server. The Dot can install the program one needs: `uvx` is
there; for `npx`, it runs `sudo dot-install nodejs npm`. A server that cannot
start says why, in its settings, in `invisible-dots mcp <dot>` and to the Dot
itself, and it is started again when the Dot's next message or task begins.
## Talk to it from your phone
**Telegram.** Make a bot with @BotFather (one bot per Dot), then:
@@ -621,13 +654,13 @@ inside their VMs.
## Configuration
A Dot is one YAML file, checked by one schema: name, instructions,
model, computer (cpu, memory, disk, idle timeout), permissions and
limits. Every field and its range is in
model, computer (cpu, memory, disk, idle timeout), permissions, limits and MCP
servers. Every field and its range is in
[architecture: Dot configuration](architecture.md#7-dot-configuration);
`invisible-dots init` writes a sample.
One server runs per data directory. Secrets (the OpenRouter key, channel
tokens, WhatsApp keys) are stored with AES-256-GCM under
tokens, WhatsApp keys, MCP servers' secrets) are stored with AES-256-GCM under
`config/master.key`; anyone who can read the data directory can decrypt them,
so it is kept private to your user.
@@ -706,7 +739,7 @@ Alpha. Nothing is released yet; the golden image is the one thing published.
- **Channels carry text only**, one Telegram bot per Dot; the official
WhatsApp Cloud API is a later adapter on the same hub.
- **Not in this version**: snapshots and rollback, backups, quotas, network
policies, MCP integrations beyond the browser, controlling the desktop or an
policies, controlling the desktop or an
interactive terminal for you (the web UI shows the desktop as pictures, and
nothing you do there reaches the computer), artifacts, several hosts,
organisations and roles, macOS hosts