opendesk-mcp
by Nraitschew
README.md
# opendesk-mcp
A [Model Context Protocol](https://modelcontextprotocol.io) server for
[openDesk](https://www.opendesk.eu), the sovereign workplace suite that ZenDiS
builds for German public administration on behalf of the Federal Ministry of
the Interior.
It gives an MCP-capable assistant read-only access to the files, projects,
knowledge pages, calendar, contacts, mail and chat of one openDesk instance.
## Why this exists
openDesk publishes no MCP server, and there is no unified openDesk API to write
one against. openDesk is a curated bundle of separate open-source products
behind a shared Keycloak, and the
[architecture documentation](https://gitlab.opencode.de/bmi/opendesk/deployment/opendesk/-/blob/main/docs/architecture/apis.md)
lists one API per component rather than one for the suite.
That leaves every openDesk deployment, roughly 100,000 administrative
workstations at the time of writing, without a way to point an assistant at its
own content. This server closes that gap by talking to each bundled product
directly.
| Module | Product | Protocol | Default host |
| --- | --- | --- | --- |
| Files | Nextcloud | WebDAV | `files.<domain>` |
| Projects | OpenProject | API v3, HAL+JSON | `projects.<domain>` |
| Knowledge | XWiki | REST, `media=json` | `wiki.<domain>` |
| Calendar | Open-Xchange | CalDAV | `dav.<domain>` |
| Contacts | Open-Xchange | CardDAV | `dav.<domain>` |
| Mail | Dovecot | IMAP | none, see below |
| Chat | Synapse / Matrix | client-server API | `matrix.<domain>` |
The default hosts are openDesk's own deployment defaults, from
`helmfile/environments/default/global.yaml.gotmpl`, where every service gets its
own subdomain. Each one is overridable.
**Calendar, contacts and mail are Open-Xchange, not Nextcloud.** Nextcloud in
openDesk serves files. Pointing a calendar tool at the Nextcloud host would read
a different calendar, usually an empty one, and look like a data problem rather
than a configuration one.
## Scope and safety
Every tool in this server is read-only. There is no code path that creates,
updates or deletes anything in openDesk, that sends a message or an email, and
none that writes to disk or contacts a host other than the configured modules.
Each module authenticates as one account and therefore sees exactly what that
account sees. openDesk's own permissions remain the access boundary; this server
does not widen them.
Modules are independent. If your XWiki allows single sign-on only and rejects
the password, the Knowledge tools are simply not registered and everything else
keeps working. The server prints which modules it started with on stderr.
Responses are size-capped before they reach the assistant. A response body is
read up to a limit and then aborted, long text is truncated with an explicit
`[truncated]` marker, and a file above the read limit returns metadata with a
note instead of its contents.
## Known limitations of openDesk itself
Two of these modules depend on things that are not this server's to fix. Read
this before filing a bug.
**Calendar and contacts may fail on your deployment.** openDesk issue
[#155, "openxchange CalDav & CardDav do not work"](https://gitlab.opencode.de/bmi/opendesk/deployment/opendesk/-/work_items/155),
has been open since February 2025, is assigned upstream to Open-Xchange and sits
in the backlog. It reports a broken ingress rewrite plus an unresolved "No
session created" authentication failure on `/servlet/dav`. On an affected
deployment these two modules cannot authenticate no matter which credentials you
supply. The error message from this server says so rather than leaving you to
suspect your password.
Check before configuring them:
```
curl -u 'username:password' 'https://dav.example.org/servlet/dav/'
```
**Mail needs IMAP to be exposed at all.** IMAP is not HTTP, so it cannot travel
through the cluster's HTTP ingress that serves every other module. Whether
Dovecot is reachable from outside is a decision your operator makes, which is
why `OPENDESK_MAIL_HOST` has no default and must be set by hand.
## Requirements
- Node.js 20 or newer
- An openDesk instance reachable from the machine running the server
- Credentials for at least one module
### Obtaining the credentials
**Files, a Nextcloud app password**
1. Open the Files app in openDesk.
2. Go to Settings, then Security.
3. Under "Devices & sessions", enter a name and choose "Create new app password".
4. Copy the generated password. It is shown once.
**Projects, an OpenProject API key**
1. Open the Projects app in openDesk.
2. Open your avatar menu and choose "My account".
3. Go to "Access tokens".
4. Under "API", choose "Generate" and copy the key.
**Knowledge, calendar, contacts and mail: the account password**
XWiki, the Open-Xchange DAV endpoints and IMAP have no app-password concept of
their own, so these modules use the account's credentials. Whether that works
depends on how your deployment configured single sign-on. Test each one with
`curl` (see above) before wiring it up.
**Chat, a Matrix access token**
1. Open the Chat app (Element) in openDesk.
2. Go to Settings, then Help & About.
3. Expand "Advanced" and copy the access token.
A Matrix access token is as powerful as the account. Treat it like a password.
## Installation
```
git clone https://github.com/Nraitschew/opendesk-mcp.git
cd opendesk-mcp
npm install
npm run build
```
The build writes an executable entry point to `dist/index.js`.
## Configuration
The server is configured through environment variables. Only `OPENDESK_DOMAIN`
and the credentials of the modules you want are required.
### Domain
| Variable | Required | Meaning |
| --- | --- | --- |
| `OPENDESK_DOMAIN` | yes | The domain of your openDesk instance, for example `example.org`. A pasted URL and a leading `portal.` are stripped. |
Every module URL is derived from it, and every one can be overridden.
### Files, Nextcloud
| Variable | Required | Default |
| --- | --- | --- |
| `OPENDESK_FILES_USERNAME` | for this module | |
| `OPENDESK_FILES_APP_PASSWORD` | for this module | |
| `OPENDESK_FILES_URL` | no | `https://files.<domain>` |
### Projects, OpenProject
| Variable | Required | Default |
| --- | --- | --- |
| `OPENDESK_PROJECTS_API_KEY` | for this module | |
| `OPENDESK_PROJECTS_URL` | no | `https://projects.<domain>` |
### Knowledge, XWiki
| Variable | Required | Default |
| --- | --- | --- |
| `OPENDESK_WIKI_USERNAME` | for this module | |
| `OPENDESK_WIKI_PASSWORD` | for this module | |
| `OPENDESK_WIKI_URL` | no | `https://wiki.<domain>` |
| `OPENDESK_WIKI_NAME` | no | `xwiki`, the main wiki. Only relevant on a multi-wiki farm. |
### Calendar and contacts, Open-Xchange
One credential enables both, because in openDesk they are the same product on
the same host.
| Variable | Required | Default |
| --- | --- | --- |
| `OPENDESK_DAV_USERNAME` | for these modules | |
| `OPENDESK_DAV_PASSWORD` | for these modules | |
| `OPENDESK_DAV_URL` | no | `https://dav.<domain>` |
### Mail, IMAP
| Variable | Required | Default |
| --- | --- | --- |
| `OPENDESK_MAIL_HOST` | for this module | none, deliberately |
| `OPENDESK_MAIL_USERNAME` | for this module | |
| `OPENDESK_MAIL_PASSWORD` | for this module | |
| `OPENDESK_MAIL_PORT` | no | `993` |
| `OPENDESK_MAIL_TLS` | no | `true`, implicit TLS. Set to `false` for STARTTLS on port 143. |
### Chat, Matrix
| Variable | Required | Default |
| --- | --- | --- |
| `OPENDESK_CHAT_TOKEN` | for this module | |
| `OPENDESK_CHAT_URL` | no | `https://matrix.<domain>`, the Synapse homeserver. Not `chat.<domain>`, which serves the Element web client. |
### Limits
| Variable | Default | Meaning |
| --- | --- | --- |
| `OPENDESK_TIMEOUT_MS` | `15000` | Per-request timeout in milliseconds. |
| `OPENDESK_MAX_RESPONSE_BYTES` | `4194304` | Hard cap on a single response body. |
| `OPENDESK_MAX_TEXT_CHARS` | `50000` | Cap on text handed to the assistant. |
| `OPENDESK_MAX_DOWNLOAD_BYTES` | `2097152` | Cap on bytes downloaded for one file read. |
Half a credential is an error rather than a silently disabled module: setting
`OPENDESK_FILES_USERNAME` without `OPENDESK_FILES_APP_PASSWORD` makes the server
exit with a message naming the missing variable.
## Connecting a client
### Claude Desktop
Edit the MCP configuration file:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"opendesk": {
"command": "node",
"args": ["/absolute/path/to/opendesk-mcp/dist/index.js"],
"env": {
"OPENDESK_DOMAIN": "example.org",
"OPENDESK_FILES_USERNAME": "first.last",
"OPENDESK_FILES_APP_PASSWORD": "your-app-password",
"OPENDESK_PROJECTS_API_KEY": "your-api-key",
"OPENDESK_WIKI_USERNAME": "first.last",
"OPENDESK_WIKI_PASSWORD": "your-password",
"OPENDESK_DAV_USERNAME": "first.last",
"OPENDESK_DAV_PASSWORD": "your-password",
"OPENDESK_MAIL_HOST": "mail.example.org",
"OPENDESK_MAIL_USERNAME": "first.last",
"OPENDESK_MAIL_PASSWORD": "your-password",
"OPENDESK_CHAT_TOKEN": "your-matrix-token"
}
}
}
}
```
Restart Claude Desktop afterwards. Leave out the variables of any module you do
not want.
### Claude Code
```
claude mcp add opendesk \
--env OPENDESK_DOMAIN=example.org \
--env OPENDESK_PROJECTS_API_KEY=your-api-key \
-- node /absolute/path/to/opendesk-mcp/dist/index.js
```
### Any other MCP client
The server speaks MCP over stdio. Start it with `node dist/index.js` and the
environment set; any client that can spawn a stdio server will work.
## Tools
Only the tools of the configured modules are registered.
### Files
| Tool | Purpose |
| --- | --- |
| `files_search` | Search the drive by file and folder name. Matches names, not contents. |
| `files_list` | List the immediate contents of one folder. |
| `files_read` | Read one file. Text is returned inline; other types return metadata with a note. |
### Projects
| Tool | Purpose |
| --- | --- |
| `projects_whoami` | Identify the OpenProject account, for questions that say "my" or "me". |
| `projects_list` | List visible projects, optionally filtered by name. |
| `projects_search_work_packages` | Full-text search over work packages, optionally scoped to a project or the current user. |
| `projects_get_work_package` | One work package in full, including its description. |
| `projects_get_work_package_comments` | The comments on a work package. |
### Knowledge
| Tool | Purpose |
| --- | --- |
| `wiki_search` | Full-text search across page names, titles and content. |
| `wiki_read_page` | One page's content, with its version and syntax. |
| `wiki_list_spaces` | The spaces of the wiki. |
| `wiki_list_pages` | The pages directly inside one space. |
### Calendar
| Tool | Purpose |
| --- | --- |
| `calendar_list_calendars` | The calendars this account can see. |
| `calendar_list_events` | Appointments across all calendars in a date range, default the next 30 days. |
### Contacts
| Tool | Purpose |
| --- | --- |
| `contacts_search` | Search address books by name, email, phone, job title or organisation. |
### Mail
| Tool | Purpose |
| --- | --- |
| `mail_list_mailboxes` | The folders of the mailbox, with the paths to search in. |
| `mail_search` | Search one mailbox by sender, subject, body text, date or unread state. |
| `mail_read` | One message in full, with its body text and attachment list. |
### Chat
| Tool | Purpose |
| --- | --- |
| `chat_whoami` | Identify the Matrix account, to tell own messages from others'. |
| `chat_list_rooms` | The rooms this account has joined. |
| `chat_read_room` | The most recent messages of one room. |
| `chat_search` | Full-text search across the rooms this account can see. |
Tool results are JSON. OpenProject's HAL envelopes are flattened, so a work
package carries `status`, `assignee` and `project` as plain names.
## Notes on the data
**Recurring appointments are not expanded.** A CalDAV time-range query returns
the recurring master object, and this server reports the appointment as it is
stored, with a `recurring` flag, rather than computing occurrences. Expanding
recurrence correctly is a calendar engine's worth of edge cases.
**Times without a zone stay without one.** An iCalendar value qualified with a
`TZID` is returned as a local wall-clock time rather than being converted, since
resolving the zone needs the VTIMEZONE component. Treating such a value as UTC
would shift half the appointments in the country by an hour.
**Mail UIDs are per mailbox.** A uid from `mail_search` is only valid in the
mailbox it came from, and it changes when a message moves. Pass the mailbox
along with the uid to `mail_read`.
## Wiki page references
XWiki addresses a page as `Space.PageName`, and a nested space as
`A.B.PageName`. Two details are easy to get wrong, and this server handles both:
- A literal dot inside a name is escaped as `\.`. Splitting on every dot would
break exactly the pages whose names contain one, such as release notes or
dates.
- A reference may be fully qualified as `wiki:Space.Page`. The wiki part is a
separate REST path segment, not a dot-separated one.
A bare page name with no space is rejected rather than guessed at. Always pass a
reference that `wiki_search` or `wiki_list_pages` returned.
## Errors
A failing tool call returns a result marked as an error whose text starts with a
stable machine code and names the module, so a rejected OpenProject key does not
read as a broken Nextcloud:
| Code | Meaning |
| --- | --- |
| `unauthorized` | The module's credentials were rejected, or the instance requires single sign-on. |
| `forbidden` | Authenticated, but not permitted to read that resource. |
| `not_found` | No such file, work package, page or message. |
| `rate_limited` | The module is rate limiting this client. |
| `unreachable` | The module did not answer, or the request timed out. |
| `invalid_reference` | A wiki page reference names no space. |
| `invalid_response` | The response was not the expected document. Usually a wrong module URL. |
| `too_large` | The response exceeded the configured byte limit. |
| `http_error` | Any other non-success status, with the status code in the message. |
## Testing against your instance
```
npx @modelcontextprotocol/inspector node dist/index.js
```
The MCP Inspector lists the tools and lets you call them by hand, which is the
fastest way to confirm the domain and credentials are right. The server also
prints the configured modules to stderr on startup.
## Development
```
npm install
npm run typecheck
npm test
npm run build
```
The unit tests cover configuration and URL derivation, the WebDAV multistatus
parser and search-body escaping, the OpenProject filter grammar, the XWiki
reference grammar, the iCalendar and vCard parsers, and the IMAP search
criteria. They make no network requests, so they run anywhere.
## Limitations
- Read-only by design.
- Video conferencing (Jitsi, NeoDateFix), collaborative documents (Collabora),
CryptPad, Notes and the Nubus portal are not covered.
- File contents are read for text files only. Office documents and PDFs return
metadata.
- Attachments are listed but never downloaded, in files, wiki pages and mail
alike.
- Searches page once with the requested limit rather than paging through a
large result set.
- Every tool call opens its own IMAP connection. That costs a connection setup
per call, and buys not having to keep a session alive between questions.
## Related
If you run only one of the bundled products rather than a whole openDesk
instance, the standalone servers are a better fit:
- [openproject-mcp](https://github.com/Nraitschew/openproject-mcp)
- [xwiki-mcp](https://github.com/Nraitschew/xwiki-mcp)
For Nextcloud on its own, the community already has several servers, including
[cbcoutinho/nextcloud-mcp-server](https://github.com/cbcoutinho/nextcloud-mcp-server).
## Contributing
Issues and pull requests are welcome, particularly:
- Reports from real openDesk deployments about which modules accept password
authentication, and whether CalDAV and CardDAV work on yours.
- Corrections to the API assumptions documented in the source comments.
## License
Apache License 2.0. See [LICENSE](LICENSE).
This project is independent. It is not affiliated with or endorsed by ZenDiS
GmbH, the German Federal Ministry of the Interior, Nextcloud GmbH, OpenProject
GmbH, Open-Xchange, XWiki SAS or Element. "openDesk", "Nextcloud",
"OpenProject", "Open-Xchange", "XWiki", "Element" and "Matrix" are trademarks of
their respective owners and are used here only to describe what this software
connects to.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues