Goal: give the agent a capability it does not have, in one file plus one line.
Before writing anything, check the capability passes the test in Designing the tool surface: a tool models an intention, not an endpoint.
Create mcp-nextcloud/src/modules/<domain>.js exporting register(server, ctx):
ctx gives you baseUrl, authorization (verbatim from the request), uid()
(async, memoised — at most one lookup per HTTP request), ocs(), dav, and
log().
One import and one array entry in mcp-nextcloud/src/server.js. Nothing else: the
approval policy comes from the annotations, and the context pruner derives what it
needs from the tool name.
Annotations decide whether the user is asked. A tool with an effect and no
destructiveHint runs without a confirmation, and nothing reports an error.
Omitting annotations entirely is safe — the policy fails closed — but a wrong one
is not.
Never write permission logic. A 403 or a 404 from Nextcloud is the answer. Do not check first, do not retry with a different path, do not fall back to another account.
Every error tells the model what to do next. Use toolError for anything
upstream and refuse for anything you reject yourself, and make the refusal name
the next action:
If the tool modifies something, fetch the current state and merge, rather than sending the fields you were given. Two capabilities in this codebase were shipped without doing that, and both silently destroyed user data:
done untick a finished card.Both now read, merge and write, and both have a regression test that fails if anyone simplifies it back.
Pure logic — anything under src/format/ or nextcloud/paths.js — imports nothing
and is testable offline:
Add the file to the test script in package.json; it lists test files
explicitly, so a new one is silently skipped otherwise.
For anything that talks to Nextcloud, the integration suite is gated on a real credential:
To drive the tool by hand against the running stack, the server accepts Basic auth in development, so no OIDC flow is needed:
That harness is how the last four bugs in this codebase were found, including two that every unit test passed through happily.
docker compose up -d alone neither rebuilds on a source change nor recreates on
an .env change. Three separate debugging dead-ends came from exactly that.
Then check the tool is actually exposed, rather than assuming:
Another MCP server, a local tool, or a deterministic workflow goes in
orchestrator/src/tools/registry.ts, and only there. agent/ receives a tool set
as an argument and imports nothing from the tool layer — which is what keeps
"adding a capability" from touching the loop.