Karsten Lehmann 27 October 2026 09:00:00
MindooDB is an open-source (Apache 2.0), end-to-end encrypted, local-first document database: data is encrypted on the device, every change is signed and appended to a history, and servers only store and relay ciphertext. Haven is the client we built on top of it, a workspace in which many small apps run side by side in sandboxes and share the user’s encrypted data under permissions the user controls. Who we are and how MindooDB and Haven came about, from many years of building on IBM Notes/Domino to a fresh start, is told in The story behind MindooDB Haven.
Over the last weeks we gave twelve of these apps their own tools for AI agents: editors for Word, Excel and PowerPoint files, a mind map and task app, a field map, a CRM, contacts, mail, a website CMS and a teacher’s planner, among others. This post is about what we learned: which tool designs models can actually use, how we keep the tool list small, what went wrong, and what has to be in place before you let an agent work on real data.
What MCP is #
The Model Context Protocol (MCP) lets an agent call tools: a server describes each tool with a name, a description written for a model and a JSON schema for its input, and the agent’s model decides which one to call. Anthropic published it in November 2024; since December 2025 it belongs to the Agentic AI Foundation under the Linux Foundation, together with Block’s agent goose and OpenAI’s AGENTS.md. Most MCP servers are local programs started by the agent, or web services it reaches over HTTP.
Instead of guessing where to click on a screenshot, a model calls teamgrid_add_chart with a range and a chart type, the way a program calls a function.
WebMCP: the tools live in the page #
Classic MCP servers run next to the application, often with their own login and API keys. For a web application that is a detour: the data, the user’s session and the logic are already in the browser tab.
WebMCP moves the idea into the page. A web page registers its tools with the browser through document.modelContext.registerTool(…), and an agent that has access to the browser can list and call them. The tools run in the page, with the user’s session, under the page’s own rules. There is no separate server, no extra API key, and nothing the page does not already allow the user.
A complete, if tiny, example:
document.modelContext.registerTool({
name: "add_todo",
description: "Adds an item to the user's todo list. Returns the new item's id.",
inputSchema: {
type: "object",
required: ["title"],
properties: { title: { type: "string", description: "What needs to be done" } },
},
async execute({ title }) {
const todo = await todos.add(title); // the page's own code, with the user's session
return { id: todo.id, title: todo.title };
},
});
An agent connected to the browser now sees a tool add_todo and can call it; the call runs this function in the tab.
| MCP | WebMCP | |
|---|---|---|
| Who offers the tools | a separate server process or web service | the web page itself |
| Where calls run | in that server | in the open tab, with the user’s session |
| Authentication | API keys, OAuth per server | whatever the page already has |
| Tools change when | the server is updated | the page changes state, e.g. another document opens |
| Status | open protocol at the Agentic AI Foundation | Draft Community Group Report of the W3C Web Machine Learning Community Group, editors from Microsoft and Google; in Chrome an origin trial since version 149, not yet in stable |
The last row matters. WebMCP is young, and browsers do not yet hand the tools to agents by themselves. Chrome’s WebMCP documentation describes the current state of the API and how to try it. Until agents can pick up page tools by themselves, we bridge them to MCP, more on that below.
The model as integrator #
Integration between applications has always been expensive. Every pair of apps needs an interface, a mapping and someone who maintains both. With ten apps you quickly have dozens of connections, and most of them are never built.
With tools, the model becomes the translator. It reads the minutes of last week’s meeting in TeamEdit, understands which decisions were made, and creates tasks with owners and due dates in a Vega map. It takes the inspection findings of a Cairn workbook and turns them into a table and a chart in TeamGrid. It finds a customer in the CRM, looks up the contact person in the tenant directory and drafts an email in MindooMail. None of these apps knows the others. Each one only describes what it can do.
In Haven this happens on one surface. Haven is the broker between the agent and the apps:
- Haven’s own tools start with
haven_. With them an agent learns where the user is and what is open (haven_get_context), lists and describes the installed apps, opens an app, arranges workspace pages and tiles, installs apps from the App Store, passes files through an exchange folder and starts a sync. - The apps’ tools are named after the app:
vega_…,teamgrid_…,cairn_…. An agent sees the tools of the app in front of the user, plus the tools of editors that app embeds, for example a TeamEdit document inside a CRM record. - The user decides. Nothing is offered until agent tools are switched on in Preferences, and every app needs its own consent. A tenant admin can decide in the signed tenant policy who may use agents with the tenant’s data and which model endpoints are allowed.
Because all apps share one shell, one permission model and one naming scheme, the same patterns work in every app, and so does what a model has learned in the conversation so far.
This is what that surface looks like. The Start tab of a Haven workspace holds Haven’s own tiles and a launcher for every installed app:
Opened apps sit side by side as tiles on a workspace page, each in its own sandboxed frame:
Three ways to connect an agent #
In the browser: the Haven Agent Bridge. For the Haven PWA in Chrome we built the open-source Haven Agent Bridge. A Chrome extension picks up the WebMCP tools of one chosen Haven tab, and a small local service passes them on to any MCP client. goose, Claude Code and other clients start it as a local program; clients that speak Streamable HTTP connect to http://127.0.0.1:13300/mcp. The bridge only listens on 127.0.0.1, guards every connection with a token, only talks to sites on its allowlist and records no content unless you turn that on. When Haven’s tools change because the user opens another app, connected clients are told to reload the list. A debug GUI shows every call with its arguments, result and timing; while we designed the app tools, it was the tool we used most.
In the desktop apps: a built-in MCP connection. The native Haven apps for macOS and Windows, which are coming soon, have the connection built in, without Chrome, Node.js or an open network port. Under Agent access, Haven lists the agent apps it knows. For goose, Claude Desktop and Codex one click writes Haven into their configuration and keeps a backup of the old file; for Cursor, VS Code and LM Studio Haven opens their install link; for Claude Code, Gemini CLI and others it shows the command to copy. When an agent connects for the first time, Haven asks whether you trust that program and checks it by path and code signature. While it is connected, a banner names it, with Disconnect one click away.
Haven’s native apps for iOS and Android, also coming soon, do not offer agent tools.
Inside Haven: the built-in assistant. Haven also has an assistant of its own (part of the commercial Enterprise edition) that uses exactly the same tools. The tools for outside agents are free in every edition.
What an agent harness does #
The model itself only produces text: an answer, or a request to call a tool. Everything else is done by the agent harness, the program around the model. goose, Claude Code, Codex and Cursor are harnesses, and so is Haven’s built-in assistant.
A harness runs a loop: send the conversation and the tool list to the model, run the tool calls it asks for, add the results to the conversation, and repeat until the model answers without a tool call. Around that loop it does the work that decides whether an agent is useful or dangerous:
- It chooses which tools the model sees in each round, and keeps the list small.
- It adds context, such as Haven’s
haven_get_context, so the model does not start blind. - It asks the user before consequential steps and shows what happened.
- It shortens large results, keeps a budget of steps, tokens and cost, and lets the user stop at any time.
- It can run code to convert or compute values, so the model does not have to do the arithmetic itself. Haven’s assistant will get this next: a sandbox based on quickjs-emscripten , the QuickJS JavaScript engine compiled to WebAssembly, isolated from the page, the network and the user’s data.
- It talks to the model provider, which is where privacy is decided (more below).
Haven’s built-in assistant runs this loop in the Haven shell, never inside an app. It shows every step as a card with input and result, and plans larger tasks as a short checklist. Confirmations are drawn by Haven itself, so a model cannot click them away.
How we chose the tools #
After many rounds with different models, a handful of rules held up in every app. They are now part of our App SDK documentation, and the open-source SDK example app and TodoManager show them in code.
Domain operations, not clicks. A tool does what a user would say: todo_tasks_complete(taskId), not “click the checkbox in row 3”. It works on a small public model in plain words, never on internal storage fields, and never on the underlying CRDT API.
Context first. Every app has a get_context tool: today’s date and weekday, the signed-in user, whether the data is read-only, what is open and selected. Models are surprisingly bad at knowing what “next Friday” means without it.
Find precisely, and find roughly. Agents need both. Exact criteria such as status, dates or assignee go through a database query over the document summaries; topics go through full-text search. Every tool that takes an id says which tool returns that id, so the model never invents one or passes a title instead.
People are directory entries, not names. When an app stores people, the agent looks them up in the tenant directory and stores the directory id. “Assign it to Anna” then means one specific Anna.
Few tools at a time, revealed step by step. Every tool costs context in every request, and smaller and local models get confused by long lists. So no agent sees all tools of all apps at once. It gets more of them as the work moves closer to a particular app, a particular area and a particular document. More on that in the next section.
Let the app do the work. The app computes, the model decides. Write tools take lists, so one call can create twenty tasks, and they return what changed instead of a bare “ok”. Long results are bounded and say when they were cut off.
Let the model see its result. Some apps can render what they show, such as a map from Cairn or a slide from TeamSlides, and return it as an image to models that understand images. A model that sees its own slide fixes overlapping text without being told.
Say what a tool is. Annotations tell Haven and the harness that a tool only reads, that it does something the user should confirm (send, share, delete, anything that leaves the app), or that its result contains text other people wrote. That last one matters: a document can contain instructions, and the model has to treat them as data. MindooMail goes a step further and filters hidden text out of HTML emails before an agent sees them.
Files do not travel through the model. An export is written to the user’s exchange folder and the agent gets a reference, not the bytes.
Avoid duplicates when apps work together. When an agent copies data from one app to another, hidden origin fields remember where an entry came from, so running the same request twice updates instead of duplicating.
Ask a model that has never seen the code. We build the apps with a coding agent, Claude Code running Opus 5.5, and it turned out to be good at judging tools from a model’s point of view. For each app it set up a small test environment and gave a Sonnet model a realistic task, such as “turn these minutes into tasks with owners” or “find all trees with overdue inspections and put them in a table”. The test model saw only what any agent sees: the tool list, the descriptions and the results. It knew nothing about the code base. Wherever it hesitated, guessed an id, called tools in a strange order or simply could not finish the task, Claude Code read the transcript, found the unclear description or the missing operation, and improved the WebMCP tools. Then the test ran again. Many of the rules in this section were written down after such a round.
Test it like code. Every example in an ops_guide is executed in a unit test, and a test helper in the SDK fails when an app’s agent profile names a tool that does not exist.
Revealing tools step by step #
All apps together offer well over a hundred tools. No model works well with that, and every request would carry a long list of tools nobody needs right now. Haven therefore reveals tools in layers, and the agent climbs down them as the task gets more specific. The snippets below are shortened, but the fields are real.
1. Haven first. At the start an agent only sees Haven’s own haven_* tools. haven_get_context says where the user is and what is open, and it explains how the tool set behaves, so the agent knows from the first call that more tools will come.
Example: haven_get_context #
{
"locked": false,
"user": "Karsten",
"page": "workspace",
"workspaceTab": { "id": "tab-office", "label": "Office" },
"focusedApp": null,
"runningApps": [],
"data": {
"online": true,
"tenants": [
{ "tenantId": "t-mindoo", "label": "Mindoo", "syncState": "idle", "lastSuccessfulSync": "2026-10-24T18:02:11Z" }
]
},
"contextVersion": 41,
"toolModel": {
"dynamic": true,
"rules": [
"Tools named haven_* are always available while the user has agent tools switched on.",
"The tools you see now are not all of Haven's capabilities: opening or focusing an app can add tools for that app, and closing it removes them.",
"Before opening an app to do something in it, call haven_apps_describe to learn what it is for, which of its tools do what and how to go about it; haven_apps_list only gives a summary.",
"…"
]
}
}
2. Read about an app before opening it. haven_apps_list gives each installed app a short summary, and haven_apps_describe returns its full agent profile: what it is for, which tools do what, example requests, limits and a short guide. This works for closed apps too, and even for apps whose tools the user has not allowed yet, so the agent can tell the user why switching them on would help. The same profile appears in the Haven App Store under Use with AI, so people see what an agent can do with an app before they install it or switch its tools on.
Example: haven_apps_describe for Teacher's Desk #
{
"appId": "teachersdesk",
"label": "Mindoo Teacher's Desk",
"profile": "agent",
"summary": "A teacher's workspace: classes and learners, a timetable whose lessons appear in the calendar by themselves, lesson series, appointments, tasks, conversations with minutes, grades, notes and teaching materials. How much learner data and grades agents may read is set by the teacher on each device.",
"capabilities": [
{ "id": "orientation", "title": "Find classes, learners and content, open pages",
"tools": ["teacher_get_context", "teacher_resolve", "teacher_search", "teacher_show", "teacher_area_open"] },
{ "id": "calendar", "title": "Read the agenda and manage appointments",
"tools": ["teacher_agenda", "teacher_event_create", "teacher_event_update"] },
{ "id": "classes", "title": "Classes, learners, observations, absences and grades",
"tools": ["teacher_class_get", "teacher_learner_get", "teacher_observation_add", "teacher_tallies_summary", "teacher_grades_get"] },
"…"
],
"examples": { "en": ["Which appointments do I have this week, apart from lessons?", "Is my lesson planning for next week complete?", "…"] },
"guide": "# Working with Teacher's Desk\n…",
"running": false,
"agentTools": "allowed"
}
3. Open or focus the app, and its tools appear. When the agent opens an app with haven_apps_open, or the user brings it to the front, Haven adds that app’s tools. The answer already lists them, and connected MCP clients receive the standard notification that the tool list has changed. The built-in assistant only offers the tools of the focused app. When the app closes, its tools disappear again.
Example: haven_apps_open and the notification an MCP client receives #
// call
{ "appId": "teachersdesk" }
// result
{
"appId": "teachersdesk",
"launchId": "l-7f3c",
"reused": false,
"agentTools": "offered",
"toolPrefix": "teacher",
"tools": ["teacher_get_context", "teacher_resolve", "teacher_search", "teacher_agenda", "teacher_tasks_list", "teacher_task_create", "teacher_area_open", "teacher_undo", "…"],
"note": "These tools are available from your next step on."
}
// notification to connected MCP clients
{ "jsonrpc": "2.0", "method": "notifications/tools/list_changed" }
4. Inside the app: the area that is open. Larger apps split their tools into areas. Teacher’s Desk always offers a small core: context, search, the agenda, appointments, tasks and undo. The tools for lessons, classes, grades, conversations with parents, notes or analytics come in with teacher_area_open, or when the teacher opens that page, and stay for ten minutes after leaving it, so an agent does not lose them in the middle of a task. Editors offer tools for one document only while that document is open, and an app that embeds other editors, like the CRM with a TeamEdit document in a tab, offers the tools of the editor that is active.
Example: teacher_agenda for today, from the always-available core #
// call
{ "range": "today" }
// result
{
"from": "2026-10-19",
"to": "2026-10-19",
"total": 7,
"items": [
{ "kind": "lesson", "lessonId": "…",
"date": "2026-10-19", "start": "08:00", "end": "08:45",
"period": 1, "class": "7b", "subject": "Math", "room": "107",
"planned": true, "topic": "Adding fractions" },
{ "kind": "supervision", "title": "Break duty, schoolyard",
"date": "2026-10-19", "start": "09:35", "end": "09:55" },
{ "kind": "event", "eventId": "…", "title": "Parents' evening 7b",
"date": "2026-10-19", "start": "19:00", "end": "20:30",
"category": "meeting", "class": "7b", "location": "Room 107" },
"…"
]
}
Example: teacher_area_open #
// call
{ "area": "grades" }
// result
{
"area": "grades",
"tools": ["teacher_grades_get"]
}
5. The details on request. Editors with a rich edit language keep their tool descriptions short and offer an ops_guide tool. Without arguments it lists the groups of operations and one signature line per operation, a few kilobytes. Asked for one or more groups, it returns field tables, units, runnable examples and common mistakes for just those.
Example: the two answers of an ops_guide (schematic) #
// call without arguments
{
"groups": "text — paragraphs, runs and styles\nshapes — frames, positions and fills\ndesign — grid, type sizes, colors and slide patterns",
"signatures": "## text\nset_text { target, text }\n…\n## shapes\nmove { target, x, y }\n…"
}
// call with { "groups": ["shapes"] }
{
"guide": "### move\n`move { target, x, y }`\n| field | meaning | unit | … |\n…\nExample: …\nCommon mistakes: …"
}
This keeps the tool list short enough for local models, and what an agent can do follows what the user is looking at.
What did not work #
Some of our first attempts were wrong, and the fixes shaped the rules above.
- Marketing text as app descriptions.
haven_apps_listfirst returned each app’s App Store description. For one app that alone was about 6 KB of sales copy in the user’s language, without a single tool name. The agent profile with a short, neutral summary replaced it. - Guides that drift. A guide once pointed agents at tools that did not exist in that app. Nothing noticed until a model tried to call them. Since then, a test fails when a profile or guide names a tool that is not registered.
- Two descriptions of the same app. The manifest of one app still described a plain mind map tool while the catalog described Kanban boards, Gantt charts and spreadsheets. Now the app’s own manifest is the single source, and the catalog copies it.
- Not every view can be rendered. For TeamEdit we left out a page image on purpose: the pages are DOM with embedded fonts, and drawing them through an SVG
foreignObjectlost the fonts, so the model would have judged a picture that looks nothing like the document. - Free code execution for app tools. It is tempting to give an agent one “run this script” tool instead of many domain operations. We decided against it for app tools: a script changes data past the app’s own rules, and nothing in its result tells the user what happened.
Undo, and a history that forgets nothing #
You only hand work to an agent when you can take it back. So every write call of an agent becomes exactly one undo step, marked as made by an agent. undo reverts the agent’s last step, and it refuses when the user has changed the same document in the meantime, so an agent never overwrites the user’s work while “cleaning up”. Haven’s workspace tools follow the same rule: every change to pages and tiles is a step that can be taken back.
Undo covers the last steps of a conversation. The stronger guarantee is underneath it. MindooDB stores every change append-only: nothing is overwritten, every change is digitally signed and chained to the one before. That means:
- You can open any document as it was before the agent started, and compare it with now, as in Cairn’s time travel.
- You can see which changes were made, when, and by whom.
- A rollback is possible even days later, when the undo history of the conversation is long gone.
A mistake by an agent that you can see and reverse is an annoyance. One you cannot see or reverse is a risk, and the append-only history is what keeps it in the first category.
A short excursion: where your data goes #
Haven encrypts end to end, and agent tools run locally on the user’s device against the local replica. But the language model has to read what the tools return. With a cloud model, that part leaves the device in plaintext and goes to the provider. Which provider that is, is the real privacy decision.
Decide how much each app reveals
The first lever is not the provider but the amount of data. Not every task needs everything an app knows, so the apps let the user choose how much their tools may return.
Teacher’s Desk has three privacy levels under Settings → AI agents. At level A an agent only sees the organisation of teaching: lesson content, lesson plans, the timetable and the calendar. Level B adds the names of learners, parent contacts and notes on conversations about learners. Only level C adds grades. The app filters the data before a tool returns anything, so below level B learners appear without names, and below level C no result contains a grade, however the model phrases its request. And while the teacher works in lesson focus mode, for example with the screen on the projector, Teacher’s Desk drops to level A by itself.
We are bringing the same idea to contacts, mail and calendars: an agent that schedules a meeting needs names and companies, but not necessarily phone numbers, addresses or birthdays, and one that sorts the calendar does not need to read every email. For mail accounts Haven already decides per app and per account whether an app may only read or also send.
Choose the model provider
OpenAI and Anthropic are the obvious choices, but not the only ones. Several European providers now run capable open models in the EU behind an OpenAI-compatible API, which is exactly what Haven’s assistant and most harnesses speak. As of October 2026, for example:
- IONOS AI Model Hub : inference in German data centres, according to IONOS without third-party sub-processors. Models include Qwen3.5 and Qwen3.8 with image input, Mistral Small, Llama 3.3 and gpt-oss.
- STACKIT AI Model Serving : the cloud of the Schwarz Group, with Qwen3-VL 235B and Qwen3.8 27B (both understand images), Gemma 4, gpt-oss and Llama 3.3.
- mittwald AI hosting : Qwen3.6 35B and Qwen3.8 27B with image input and tool calling, plus Ministral, gpt-oss and Whisper for speech.
The multimodal Qwen models matter for us because they can look at the images our render tools return. Model line-ups change quickly, so check the current list and each provider’s data processing agreement before you decide.
A tenant admin can make this decision for everyone: the signed tenant policy lists the model endpoints the assistant may send the tenant’s data to, and access rules decide who may use agents with that tenant at all. For members who are not allowed, the tenant simply disappears from what agents see.
And then there is the option where nothing leaves the building. With suitable hardware, a Mac with plenty of unified memory or a workstation with a strong GPU, a model runs locally in Ollama or LM Studio, and goose or Haven’s assistant talks to it on localhost. Tool calling still varies a lot between local models; the larger Qwen and gpt-oss variants work best in our tests, and Haven’s Test connection button checks whether a model calls tools at all before you rely on it.
Try it #
- Switch on agent tools in Haven under Preferences → General, and allow the apps you want to use.
- Connect goose or Claude Code through the Haven Agent Bridge ; the README has the configuration for each client.
- Open an app’s page in the Haven App Store and look at Use with AI for example requests.
- If you build your own Haven app, start from the SDK example app or theApp Builder , which writes apps with agent tools from the start.
We are still learning with every app and every model. If you try it, I would like to hear which requests work for you and which do not, and how you design tools in your own apps.
About me: I’m Karsten Lehmann, co-founder of Mindoo. I have worked with IBM Notes/Domino since 2001, was part of the IBM Design Partner Program from 2008 to 2017 and wrote Domino JNA, whose successor Domino JNX is the foundation of the HCL Domino REST API. How that led to MindooDB and Haven is told in the story behind MindooDB Haven.