Zum Inhalt

Info

The below instructions are a work in progress.

Setting up the Exchange4all MCP endpoint

The MCP endpoint lets AI clients such as Claude and Claude Code work with mailbox, calendar and contact data. Clients connect to https://mail.example.com/mcp, and users sign in with their normal Exchange4all login. They don't need to enter anything else.

Replace mail.example.com with the public host name of your Exchange4all server throughout.

The steps below:

  1. Run the MCP server next to the Exchange4all container.
  2. Enable the experimental EWS operations.
  3. Register the MCP server with the sign-in service.
  4. Allow the clients that may sign in.
  5. Let Exchange4all accept OAuth tokens.
  6. Recreate the container and check.

Run all commands from the directory that holds your docker-compose.yml. Changes to the Exchange4all container are made with single docker compose exec commands. Paths below /storage are inside that container.

1. Run the MCP server

The MCP server runs as its own container on the same host as the Exchange4all container and uses host networking. The Exchange4all container already forwards /mcp to it. Add this service to the docker-compose.override.yml next to your docker-compose.yml:

services:
  e4a-mcp:
    image: registry.gitlab.com/exchange4all/system/incubating/e4a-mcp:latest
    network_mode: host
    restart: unless-stopped
    command:
      - serve
      - --oauth-resource=https://mail.example.com/mcp
      - --oauth-authorization-server=https://mail.example.com
      - --oauth-scope=E4A.MCP
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true

--oauth-resource has to be written exactly as the id in step 3, with no trailing slash.

Pull the image and start the container:

docker login registry.gitlab.com
docker compose pull e4a-mcp
docker compose up -d e4a-mcp

2. Enable the experimental EWS operations

The MCP server needs EWS operations that Exchange4all still marks as experimental. Enable them with an environment variable for the Exchange4all container in the same docker-compose.override.yml. If the file already has an exchange4all: service, add the line to its environment: there:

services:
  exchange4all:
    environment:
      - E4A_ENABLE_EWS_EXPERIMENTAL_OPERATIONS=true

It takes effect when the container is recreated in step 6.

3. Register the MCP server with the sign-in service

Add the MCP server to the resources the sign-in service knows. The command can safely be run again:

docker compose exec exchange4all yq -i '
  .resources = [(.resources // [])[] | select(.id != "https://mail.example.com/mcp")] +
  [{"id": "https://mail.example.com/mcp", "name": "Exchange4all MCP server", "scopes": ["E4A.MCP"]}]
' /storage/lico/identifier-registration.yaml

Define the E4A.MCP scope, with its description on the consent screen. This command can also safely be run again:

docker compose exec exchange4all yq -i '
  .scopes."E4A.MCP" = {
    "description": "Access groupware data (MCP)",
    "descriptions": {
      "de": "Auf Groupware-Daten zugreifen (MCP)",
      "es": "Acceder a los datos de groupware (MCP)",
      "fr": "Accéder aux données groupware (MCP)",
      "it": "Accedere ai dati groupware (MCP)",
      "nl": "Toegang tot groupware-gegevens (MCP)",
      "zh-CN": "访问群件数据 (MCP)"
    }
  }
' /storage/lico/scopes.yaml

Exchange4all keeps this entry when it updates its own scopes on start.

The sign-in service runs as its own user, so give both files back to it:

docker compose exec exchange4all chown lico:e4a /storage/lico/identifier-registration.yaml /storage/lico/scopes.yaml

4. Allow the clients that may sign in

MCP clients identify themselves with a Client ID Metadata Document. This is a document the client publishes on its own web site, with an https URL as its client ID. The sign-in service fetches that document instead of keeping a registration for each client. It only does so for the hosts you list.

Set the hosts in /storage/lico/env:

echo 'client_id_metadata_document_hosts="claude.ai"' | docker compose exec -T exchange4all tee -a /storage/lico/env

The file is created if it doesn't exist. If the setting is in the file more than once, the last line counts.

  • Separate several hosts with spaces, for example "claude.ai app.example.com".
  • *.example.com allows every subdomain of example.com.
  • * allows any public host, so any client that publishes such a document can sign in.
  • Claude, Claude Desktop and Claude Code all use client IDs on claude.ai. For other clients, see their documentation for the host of their client ID.

Users still sign in and give their consent for every client. The consent screen names the client by the host of its client ID.

Optional: let clients register themselves

Some MCP clients don't publish a Client ID Metadata Document. This includes clients written for MCP specification versions before 2025-11-25. Instead, such a client registers itself with the sign-in service when it first connects (OAuth 2.0 Dynamic Client Registration) and gets a client ID back. If a client can't sign in and asks for a client ID or reports that the server doesn't support client registration, it needs this.

Enable it in /storage/lico/env:

echo 'allow_dynamic_client_registration=yes' | docker compose exec -T exchange4all tee -a /storage/lico/env

Any value turns it on, even no or false. To turn it off again, remove the line from the file.

Unlike the hosts in step 4, this doesn't limit which clients can sign in: any program that can reach the server can register. Users still sign in and give their consent, but the consent screen can only show the name the client gave itself, and marks the client as unverified. Only enable it if users need a client that doesn't support Client ID Metadata Documents.

5. Let Exchange4all accept OAuth tokens

By default Exchange4all only accepts passwords. Switch the authentication mode in /storage/config.yaml:

docker compose exec exchange4all yq -i '.e4a.common.authentication.mode = "modern"' /storage/config.yaml

modern accepts both passwords and OAuth tokens on the whole server, not just on the MCP endpoint.

6. Recreate the container and check

Recreate the Exchange4all container. This applies the environment variable from step 2, and the settings from steps 3 to 5 (and the optional self-registration) are read when it starts.

docker compose up -d --force-recreate exchange4all

When it is up again, check the chain that clients follow. Run the checks from any machine:

curl -si -X POST https://mail.example.com/mcp | grep -i www-authenticate
This should show Bearer resource_metadata="https://mail.example.com/.well-known/oauth-protected-resource/mcp", scope="E4A.MCP". If it shows Basic realm=… instead, check the command: of the MCP server in step 1.

curl -s https://mail.example.com/.well-known/oauth-protected-resource/mcp
This should show "resource":"https://mail.example.com/mcp" and "scopes_supported":["E4A.MCP"].

curl -s https://mail.example.com/.well-known/oauth-authorization-server | grep -o '"client_id_metadata_document_supported": *[a-z]*'
This should show "client_id_metadata_document_supported": true. If it shows false, check step 4.

If you enabled self-registration, also check:

curl -s https://mail.example.com/.well-known/oauth-authorization-server | grep -o '"registration_endpoint": *"[^"]*"'
This should show "registration_endpoint": "https://mail.example.com/konnect/v1/register". If it shows nothing, check the line in /storage/lico/env.

Connecting a client

  • Claude Code: run claude mcp add --transport http --scope user exchange4all https://mail.example.com/mcp. --scope user makes the server available in all of the user's projects. With --scope project it is written to .mcp.json in the current project instead, so everyone working on that project gets it. Then type /mcp in Claude Code, pick exchange4all and choose Authenticate. A browser window opens for sign-in and consent.
  • Claude and Claude Desktop: under Settings → Connectors, add a custom connector with the URL https://mail.example.com/mcp, then connect and sign in. These clients connect from Anthropic's servers, so the endpoint has to be reachable from the internet.

Troubleshooting

The MCP server logs every refused request with the reason. Add --log-level=debug to its command: and read docker compose logs e4a-mcp. The sign-in service logs into the Exchange4all container log (docker compose logs exchange4all), on lines starting with libregraph-lico:. It logs more detail when the Exchange4all container runs with the environment variable E4A_LICO_LOG_LEVEL=debug.

What you see Cause and fix
The client can't find a sign-in page, or /.well-known/oauth-protected-resource/mcp returns 404 The MCP server isn't running, or runs without the options from step 1.
Sign-in fails with invalid_target or unknown resource The id in step 3 is missing or doesn't match --oauth-resource exactly.
The sign-in service doesn't start, and its log says resource … declares unsupported scope: E4A.MCP The scope definition from step 3 is missing from /storage/lico/scopes.yaml.
Sign-in fails because the client is unknown, and the debug log says client metadata documents of host … are not allowed The client's host isn't in client_id_metadata_document_hosts (step 4).
The client asks for a client ID, or says the server doesn't support client registration The client has no Client ID Metadata Document. Enable self-registration (step 4).
Sign-in works, but every MCP request is refused again (MCP server log: refused_by="the authentication endpoint") authentication.mode isn't modern, or the container wasn't recreated (step 5).
403 with insufficient_scope The token lacks E4A.MCP. Check the scopes of the resource in step 3 and sign in again.