A self-hosted Nextcloud + OnlyOffice workspace with an AI agent that can actually act on it — read files, check calendars, move cards, draft mail — as the signed-in user, and never beyond what that user is allowed to see.
That last clause is the whole design. Everything on these pages exists to keep it true.
→ What we built — start here if you read one page.
| You want to… | Go to |
|---|---|
| grasp what this system is and why | What we built |
| run it and watch it work | Get it running |
| see a message cross all five services | Follow one message |
| give the agent a new capability | Add a tool |
| change what it says or remembers | Change how the agent behaves |
| deploy, or find out what broke | Deploy and diagnose |
| look up a tool, a port, a schema | Reference |
| avoid re-learning something the hard way | Verified facts |
A tool's result renders itself. A file search comes back as a clickable table, a calendar range as a week grid, a composed mail as the mail. The chat surface does not know what a calendar looks like: the tool ships its own UI, hosted in a sandbox with no cookies and no network. → MCP Apps
Documents are read, not skipped. A 15 MB PDF becomes 140 kB of text through the
OnlyOffice server already in the stack — 0.9% of the source, 5 s cold and 10 ms
warm. files_read never names a format: text is sliced, an image is looked at, a
document is converted. → Reading a document
Search inside documents, with an index that holds no permissions. Ask where it says the budget is forty thousand and you get the passage, its section, and an offset to read on from. The index ranks the whole corpus without knowing who is asking; every candidate is then put to Nextcloud under the caller's own token, so revoking a share stops the hits immediately, with no re-index. → Searching inside documents
The agent cannot send mail, and cannot create a public link. It composes; a person clicks Send. The dispatch tools are declared visible to the interface only, so they are structurally absent from the model's tool set — the capability exists and the agent cannot reach it. → What it will not do
Five deployables, two languages. The agent holds no credential of its own: it borrows the user's, per request, and Nextcloud answers 403 or 404 on its own terms. There is no permission logic anywhere in this codebase. → How the pieces fit
Four files, not interchangeable, kept apart on purpose. A fact stated in two places ends up stated two different ways.
| File | Answers | Written for |
|---|---|---|
README.md | how do I run this? | someone installing it |
CLAUDE.md | what are the rules and traps here? | an agent editing the code |
DESIGN.md | what are the design tokens? | anyone touching the UI |
docs/ (here) | why is it built this way? | a human trying to understand or extend it |
CLAUDE.md is imperative and terse — do this, never that. These pages explain.
When they overlap, the rule lives in CLAUDE.md and the reason lives here, with a
link between them.
The agent works end to end: it calls real tools against real Nextcloud data, reads documents, searches mail, renders results as their own UI, stops for confirmation before anything with an effect, and records every call.
| Suite | Tests |
|---|---|
orchestrator | 110 (3 skipped offline) |
mcp-nextcloud | 143 unit, plus 24 integration against a live Nextcloud |
mcp-mail | 12 |
| React island | 37 |
Every number in these pages that says "measured" was measured on this stack, in a browser or against the running containers. Several of them contradict the obvious guess — Verified facts exists for exactly that reason.
What is deliberately absent, and what each choice was made over, is in Decisions.