AI assistant connector (MCP) Module Included

Vaks PM · Integration guide · MCP · July 2026

What you will end up with. Users connecting an AI assistant — Claude, ChatGPT, Copilot Studio — to Vaks PM, so it can read their projects and, if you allow it, act on their behalf. The assistant signs in as the user and can never do more than the user could. It is opt-in per organization and off until you enable it.

What it does — and what it does not

MCP (Model Context Protocol) is a standard way for an AI assistant to call an external tool. Vaks PM exposes an MCP server so an assistant can query and update the workspace on a signed-in user's behalf.

The permissions model

This is the reassuring part, and worth stating plainly to anyone nervous about connecting an AI to their project data:

An assistant's rights are your rights, intersected with what the admin allowed — never more. The effective permission set is the user's own role, narrowed by the connector's scopes, narrowed again by the read/write and finance toggles. A basic member connecting an assistant cannot reach anything a basic member cannot already reach. There is no way for the connection to escalate.

Concretely, three switches an administrator controls decide the ceiling:

ToggleOff (default)On
Enable the AI connectorNo one can connect; consent is refused.Users can connect and sign in.
Allow AI write actionsRead-only: the assistant observes and suggests.Write tools appear, bounded by each user's permissions.
Expose finance to AIBudgets, margins and rates are invisible to assistants.Finance is readable, still only for users who already have finance access.

Prerequisites

SideWhat you need
LicenceNone. The connector is part of the free core; just enable it (opt-in, mcp.enabled).
Vaks PMorg:manage to enable and configure it. Individual users need only their normal account to connect.
The assistantAn MCP-capable client. Claude and ChatGPT self-register; Copilot Studio and Power Platform need a pre-registered client — see the Copilot Studio guide. Desktop and CLI clients that redirect to a local port (e.g. a VS Code extension) self-register too and are recognised as such, so the varying port they pick each launch is accepted automatically.
ReachabilityThe assistant is a cloud service, so it must be able to reach your MCP endpoint. For a private instance this needs deliberate exposure — see reaching it from outside.

Step 1 — Enable and scope it

Open Admin → Integrations → AI connector (MCP).

  1. Turn on Enable the AI connector (MCP).
  2. Decide on Allow AI write actions. Start read-only. You can turn writes on later once you have seen how assistants are used.
  3. Decide on Expose finance to AI. Leave off unless you specifically want assistants reading financial figures.
  4. Copy the MCP server URL shown in the first card — this is what users paste into their assistant.
There is nothing extra to configure for sign-in. MCP reuses your existing authentication — local accounts and any SSO you have set up. Users log in exactly as they do for the web app. If you use SSO, MCP sign-in goes through it too.
Toggle timing has one subtlety. Write and finance access are decided when a user connects. Turning writes on does not retroactively grant them to already-connected assistants until they refresh; turning writes off takes up to an hour to drop from an existing connection. Finance, by contrast, is checked live on every request. If you need writes cut immediately, disconnect the connections (see managing connections) rather than only flipping the toggle.

Step 2 — Connect an assistant

A user adds Vaks PM as a custom connector in their assistant and pastes the MCP server URL. The assistant discovers how to authenticate on its own and opens a sign-in flow:

  1. The assistant sends the user to Vaks PM to sign in (local or SSO).
  2. The user sees a consent screen stating exactly what the assistant will be able to do.
  3. On approval, the assistant receives a short-lived access token and can start working.

The connection is standard OAuth with PKCE; access tokens last one hour and refresh automatically. How much setup this takes depends on the assistant — each has a step-by-step guide:

AssistantWhat it takesGuide
ClaudeSelf-registers. Paste the URL, sign in. Shortest.Connect Claude →
ChatGPTSelf-registers, via connectors / developer mode (beta UI).Connect ChatGPT →
Copilot StudioPre-registered client + per-user auth + Teams distribution. Longest.Connect Copilot Studio →

Any local or open-source MCP client

Beyond the three managed assistants, MCP is an open standard: local and open-source clients — Cline, Continue, LM Studio, Open WebUI, Goose, and IDE clients such as VS Code, Cursor, Windsurf or Zed — can connect to the same endpoint. Support varies, so there are two paths:

These clients are not individually tested or documented here — they are many and change quickly. Both connection paths above work for any conformant MCP client; the assistant's own documentation covers where to paste the URL or token.

Pre-built workflows (MCP prompts)

Vaks ships a set of pre-built workflows — one-click templates that drive the assistant through a whole task (write a status report, staff a project, produce a portfolio risk radar). Technically these are MCP prompts: a pre-written instruction injected into the conversation, nothing more.

Only Claude surfaces them automatically. Claude shows the list as a menu of slash-commands. ChatGPT and Copilot Studio do not consume this MCP primitive — on those, you recreate the same workflows as starter prompts, topics or instructions (each assistant's guide shows how: Copilot Studio, ChatGPT). Since a prompt is just text, recreating it is copy-and-paste.

The library (write workflows appear only when Allow AI write actions is on):

WorkflowWhat it doesType
Portfolio · Risk radarReviews every project and produces a prioritised list of risks.read
Portfolio · Deliverables awaiting reviewPrioritises what is waiting for my review and prepares it.read
Project · Status reportWrites a shareable progress update for a project.read
Team · Find work for someoneProjects and tasks matching someone's skills and real availability.read
Finance · Budget auditBudget, spend, forecast and variance — with corrective levers.read
Project · Start from a briefTurns a brief into a project: tasks, dependencies, staffing.write
Project · Close outClose-out checklist (tasks, deliverables, skills) then mark completed.write
Project · Announce a changeDrafts and sends a project announcement (PM / sponsors / members / watchers).write
Team · Staff a projectProposes an optimal assignment (skills + real availability).write
Team · Onboard someoneTeam assignment, declared skills and a first mission.write
Time · Timesheet catch-upSpots the gaps in my week and proposes the missing time entries.write

Every connection passes through a consent screen so the user sees, and approves, exactly what they are granting. It shows your organization's name, the account they are signed in as, and a bulleted list of what the assistant will be able to do — view projects and tasks, view workload and reports, optionally view finance, and, when writes are enabled, an emphasised line about creating and modifying on their behalf. It ends with the reassurance that the assistant cannot exceed the user's own permissions.

If the connector is disabled at the moment of consent, the screen says so and offers no approve button — the connection cannot be completed.

Managing connections

Two views let you see and cut connections:

Suspending or deleting a user also cuts their connections automatically.

Confirmation & rate limits

Before a destructive action — deleting a task, a client, a time entry, removing someone from a team, or notifying stakeholders — the assistant is stopped once. It gets back a description of exactly what the action would do, plus a one-time token, and has to show that to you and repeat the call with the token to actually carry it out. It is a genuine round trip bound to that specific action, not a box the assistant can tick on its own: the token expires after ten minutes and cannot be reused for anything else.

This is for a person driving an assistant — not for an autonomous agent. A confirmation only means something if someone is there to see it and approve it; an unattended agent has no one to show it to, so agents skip this check entirely. In practice it rarely comes up: an autonomous agent is already blocked from most of the same actions — deleting clients, time entries, team members — by its own, separately governed permissions.

An administrator sets the policy under Admin → Integrations → AI connector (MCP) → Confirmation guard:

PolicyEffect
OffNo confirmation is asked for any action.
Destructive actions (default)Deletions and outward-facing actions (like notifying stakeholders) require confirmation; ordinary edits do not.
Every write actionAny change, including a simple edit, requires confirmation.

The same section also has per-tool rate limits: an optional cap on how many times a specific tool may be called per minute, independent of the confirmation guard — a safety net against a runaway loop that would otherwise stay invisible until it shows up elsewhere. A live counter next to it shows what each tool is being called for right now.

Reaching it from outside

Cloud assistants must reach your MCP endpoint over the internet. How you expose it is a deployment choice, and it changes the URL — which is the single most common point of confusion.

The admin panel shows both URLs, when there are two. The primary field is always the tenant's own address. If the operator declared a separate public MCP domain to the API (see declaring the public domain below), a second Public MCP URL appears automatically underneath it, computed from that declaration — that is the one to hand internet clients, not the primary one (which only reaches clients on the same network as the instance). This only fails to appear if a DMZ gateway was stood up without that declaration step — in that one case, confirm the actual public hostname with whoever deployed the instance. Getting this wrong is the usual reason a first connection fails.
On domains in the examples below. example.com is a stand-in for your own domain — the one your instance is already served on. Everywhere you see example.com, read your own; everywhere you see demo, read one of your own tenant slugs. Vaks PM is self-hosted: there is no shared address, and nothing resolves to us.

Three topologies, each walked through step by step below:

On Docker Swarm, this is already done for you. The MCP service ships as part of the standard application stack, built and rolled by the same install and upgrade path as api/web/worker — running two replicas with a health check, so a rolling update never drops a client's request mid-call. If your instance was deployed or upgraded through the normal process, it is already running: check with docker service ls | grep mcp. What follows is for whoever operates the servers — not the tenant administrator — and is really about three other cases: Kubernetes, where MCP is not yet templated by the Helm chart and still needs a manual step; the hardened, fully isolated DMZ alternative for exposing a private instance; and understanding what is running, for troubleshooting.

How the MCP server relates to an existing instance

The MCP container is deliberately thin: it holds no secrets, talks to no database, and enforces nothing itself. It receives the AI client's token and relays it to your instance's internal API, which validates everything — PKCE, the consent screen, licence gating, RBAC, rate limiting. In other words you add a front door; you neither move nor duplicate any data.

PointDetail
Bundled by default (Swarm)The vaks-pm/mcp:latest image ships in the standard stack next to api/web/worker — two replicas, spread across nodes, health-checked. The fully isolated DMZ topology (Option A) remains available as a separate docker compose deployment for operators who want the gateway entirely outside the Swarm overlay.
No secrets on the boxIt stores nothing sensitive. All security (PKCE, consent, licence, RBAC, rate-limit) is enforced by the internal API, never by MCP.
One outbound flowFrom the box to the internal API on :443, over a bounded set of paths (Traefik allow-list: local /mcp, forwarded auth surface). Everything else in the app stays unreachable through this door.
Host namingA tenant is reached at <slug>.mcp.<domain>. A single wildcard certificate *.mcp.<domain> covers every tenant: the mcp label is stripped to reconstruct the tenant Host (<slug>.<domain>) passed to the API.
Application prerequisiteIndependent of deployment: the tenant must have enabled the connector (mcp.enabled, step 1). Otherwise the API issues no token (403), whatever the topology. No license required — the connector is included.
Authorization-server modeVAKS_AUTH_SERVER_MODE=self in a DMZ (the AS advertised to AI clients = the box, which forwards auth inward); =tenant internally (the AS = the tenant address, reached directly).

Get the MCP image onto this host

Only needed for the isolated DMZ gateway (Option A) or a hand-rolled stack — a standard Swarm install already builds and ships this image on every node, alongside api/web/worker.

Every command in this section — and in options A through C below — is run directly on the server that will host MCP (the DMZ box, or an internal host), over SSH. There is no piloting station: this host has Docker, since it runs the container.

The simplest path is to build the image in place, from a copy of the repository. The MCP Dockerfile expects the repository root as its build context. No registry is required.

# On the MCP host: get a copy of the repository, then build from its root
git clone <repo-url> vakspm && cd vakspm
sudo docker build -t vaks-pm/mcp:latest -f mcp/Dockerfile .

sudo docker image ls vaks-pm/mcp        # check: the image is present
This host cannot build? (no repository, or you want the build toolchain kept out of the DMZ) — build the image elsewhere, export it with docker save -o mcp.tar vaks-pm/mcp:latest, copy the mcp.tar file and the infra/dmz/ folder onto this host, then load the image locally:
# On the MCP host, after copying mcp.tar here
sudo docker load -i mcp.tar

Option A — Isolated DMZ gateway hardened

The recommended layout for exposing a private instance to cloud assistants. A DMZ box, which does not join the internal Swarm overlay, publishes /mcp to the internet and forwards the auth surface (OAuth + login + consent) to the on-premise API. The internal API is never reachable directly from the internet.

Orchestrator-agnostic. The DMZ box is a standalone Docker Compose stack that never joins any cluster — it only needs one outbound HTTPS flow, to whatever INTERNAL_EDGE_URL you give it. That works identically in front of a Kubernetes install: point it at the cluster's Ingress instead of the internal Traefik, and the rest of this section is unchanged. See Kubernetes deployment for the Ingress side.
AI clients Claude · ChatGPT · Copilot Studio 443 DMZ HOST · OUTSIDE THE OVERLAY Traefik + mcp no secret · no DB access serves /mcp + RFC 9728 metadata locally auth surface only 443 INTERNAL CLUSTER Traefik (internal) same edge that serves the app api · web · worker · controlplane DB / Redis / Garage never exposed
The DMZ box holds no secret and never touches the database, Redis or the object store. It serves /mcp itself and forwards only the authentication surface (OAuth, login, consent, discovery) to the internal API — the single outbound flow the firewall allows (DMZ → internal :443 only, nothing else). The internal cluster stays unreachable directly from the internet.

Prerequisites, supplied by the operator:

ItemDetail
DMZ hostDocker + Docker Compose. Outside the internal Swarm.
Public DNS*.mcp.example.com → the DMZ box's public IP.
Internal DNS (split-horizon)internal.example.com → on-prem Traefik, resolved from the DMZ.
Public TLS certWildcard *.mcp.example.comcerts/mcp-wildcard.{crt,key}.
Internal CAIf the internal edge uses a self-signed cert: certs/internal-ca.crt (otherwise drop NODE_EXTRA_CA_CERTS + insecureSkipVerify).
FirewallInternet → DMZ:443 and DMZ → internal.example.com:443 ONLY. No database / Redis / overlay access.
Public-domain declarationA single line on the API (see "Declaring the public domain" below), ONLY if that domain differs from the internal domain. On the same domain, nothing to declare.
The public name of the MCP server is independent of the instance's internal name. The application may be served on a private name resolved only on the internal network (for example vaks.local) while the MCP server is published on a public domain (for example mcp.example.fr, each organization served at <slug>.mcp.example.fr). Nothing requires the two to resemble each other.

Declaring the public domain

The MCP server passes the public hostname to the API unchanged; the API derives the organization from the first segment (<slug>.mcp.example.frslug). One declaration covers every organization on that domain — there is never a per-tenant entry. This only matters in pooled mode, where several organizations share one instance and the API has to work out which one a request is for from its Host. In dedicated mode there is exactly one organization and the Host is never used to pick a tenant, so this whole declaration is moot there — skip it.

This declaration is required only when the MCP public domain differs from the internal domain. If the MCP server is published on the same domain as the application (for example <slug>.mcp.example.com with the application on example.com), resolution is automatic and nothing is needed.

Several unrelated public domains at once, on one cluster. The list is not a single-domain slot: a pooled operator serving customers under more than one public brand can declare several entirely different domains, and each is resolved the same way — "publicDomains": ["mcp.example.fr", "mcp.example.io"] lets both acme.mcp.example.fr and acme.mcp.example.io reach the same tenant. What this does not give you is one MCP gateway fronting several genuinely separate installations: each MCP process forwards to exactly one fixed backend (its VAKS_API_URL), set once at deploy time. Distinct clusters — each with its own database, its own tenants — each need their own MCP deployment; the multi-domain list only widens the public reach of one cluster's own tenants, it does not bridge clusters.
Deployment methodProcedure
Generator The infra/cluster.json manifest describes the cluster. It is created at first deployment by the bash setup.sh wizard (which now asks for the MCP public domain), or copied from infra/cluster.example.json.

Fill in the mcp block — a list, so several unrelated domains can be declared at once:

"mcp": { "publicDomains": ["mcp.example.fr", "mcp.example.io"] }

then regenerate the artifacts:

bash setup.sh --manifest infra/cluster.json
Hand-written stack Set TENANCY_MCP_DOMAINS on both the api and controlplane services in docker-stack.yml, where a commented line serves as a template:
TENANCY_MCP_DOMAINS="mcp.example.fr,mcp.example.io"
Comma-separated; each entry is matched independently, so unrelated domains are fine.
Do not re-run bash setup.sh with no argument on an already-installed cluster. In that form the command restarts the interactive wizard and rewrites infra/cluster.json from the answers given: any manual change to the manifest is lost. To rebuild the artifacts after a change, use the manifest form, which reads the file without rewriting it:
bash setup.sh --manifest infra/cluster.json

Still on the DMZ box over SSH:

1 · Get the image — build or import vaks-pm/mcp:latest on this host (see get the image onto this host above).

2 · Configure the box — in the infra/dmz/ folder of the repository cloned on this host:

cd infra/dmz
cp .env.example .env            # edit INTERNAL_EDGE_URL to your split-horizon internal edge
mkdir -p certs
#   certs/mcp-wildcard.crt   certs/mcp-wildcard.key   public *.mcp.example.com cert
#   certs/internal-ca.crt                             internal edge CA (only if self-signed)

The docker-compose.yml sets the key variables: VAKS_API_URL (= INTERNAL_EDGE_URL), VAKS_API_PREFIX=api/v1, VAKS_MCP_LABEL=mcp and VAKS_AUTH_SERVER_MODE=self. Typically only INTERNAL_EDGE_URL needs editing.

3 · Start:

docker compose up -d
docker compose logs -f mcp

4 · Verify — from the internet (replace demo with a real tenant slug):

# Resource metadata (served locally by mcp) — resource + authorization_servers = self
curl -sk https://demo.mcp.example.com/.well-known/oauth-protected-resource | jq

# AS discovery (forwarded to the internal API, URLs rewritten to the public host)
curl -sk https://demo.mcp.example.com/.well-known/oauth-authorization-server | jq

# POST /mcp with no token → 401 + WWW-Authenticate pointing at resource_metadata
curl -ski -X POST https://demo.mcp.example.com/mcp -d '{}' | grep -i www-authenticate
Once those three checks are green, point the AI client's connector (Claude / ChatGPT / Copilot Studio) at https://<slug>.mcp.<your-domain>/mcp — the public hostname you chose and put in DNS, not an address belonging to us. The client discovers the AS, starts sign-in (forwarded to the internal app), gets its token and calls tools. That hostname (not the admin-panel URL) is what you hand users.

Option B — Internal / co-located default on Swarm

This is what a standard Swarm install already runs, out of the box: the same MCP image, next to the API, on the Swarm overlay network, with the authorization server advertised directly on the tenant address. There is nothing to add. What follows is for confirming it is healthy, or for reproducing the same shape on Kubernetes or a hand-rolled stack.

AI clients LAN or *.example.com 443 SAME SWARM OVERLAY AS THE REST OF THE STACK Traefik routes /mcp by path, priority 150 /mcp /, /api, … mcp (2 replicas) spread across nodes · /healthz api · web · worker controlplane · same overlay api:3000 overlay · no TLS
The MCP service runs as two replicas on the same Swarm overlay as api/web/worker. Traefik routes /mcp to it and everything else to the rest of the stack; mcp then reaches api directly at http://api:3000 — one hop, no TLS, no separate gateway. This is what a standard install already runs, with no extra step.

Confirm it, on an existing Swarm install:

docker service ls | grep mcp
# vaks-pm_mcp   replicated   2/2   vaks-pm/mcp:latest

docker service ps vaks-pm_mcp --format "{{.Node}} {{.CurrentState}}"
# two tasks, on two different nodes, both Running
"Internal edge" is simply the URL where the MCP container reaches the API (the VAKS_API_URL variable), from wherever it runs. It is not a value to look up elsewhere; it depends on where MCP sits:
  • On the same Swarm overlay as the API (co-located) → the internal service name: http://api:3000. This is exactly the value the worker service already uses (INTERNAL_API_URL: http://api:3000 in docker-stack.yml). No TLS, no Traefik on that hop — you are inside the overlay. On Kubernetes, the equivalent is the API Service in the namespace: http://<release>-api:3000.
  • On a separate host / in a DMZ → MCP cannot reach api:3000 (overlay-only), so it goes through the internal Traefik via a split-horizon name, e.g. https://internal.example.com — that is the INTERNAL_EDGE_URL of Option A. You choose that name: it must resolve to the on-prem Traefik that already serves the app (the manager IP, or a VIP).
Whatever this setting is, MCP passes the tenant identity in an explicit Host header (the tenant host, with the mcp label stripped), so the API resolves the right tenant in pooled mode regardless of the connection URL.

On Docker Swarm — for reference, or to reproduce this on a hand-rolled stack, this is what a standard install already declares:

  mcp:
    image: vaks-pm/mcp:latest
    networks: [vakspm]                 # the same overlay the `api` service is on
    environment:
      VAKS_API_URL: http://api:3000    # the API service on the overlay (same value the worker uses)
      VAKS_API_PREFIX: api/v1
      # VAKS_AUTH_SERVER_MODE is deliberately left unset (defaults to "self").
      # On a co-located deployment with no separate mcp.<tenant> address,
      # "self" and "tenant" resolve to the exact same origin — setting it
      # explicitly would change nothing here.
    healthcheck:
      test: ["CMD-SHELL", "wget -q -O- http://127.0.0.1:8080/healthz >/dev/null 2>&1 || exit 1"]
      interval: 10s
      timeout: 3s
      retries: 3
      start_period: 20s
    stop_grace_period: 30s              # time to finish in-flight requests before a replica is retired
    deploy:
      replicas: 2
      placement:
        preferences: [{ spread: node.id }]           # the two replicas land on different nodes
      update_config: { parallelism: 1, order: start-first }
      labels:
        - traefik.enable=true
        - "traefik.http.routers.mcp.rule=PathPrefix(`/mcp`) || PathPrefix(`/.well-known/oauth-protected-resource`)"
        - traefik.http.routers.mcp.priority=150      # explicit priority, not left to rule-length matching
        - traefik.http.services.mcp.loadbalancer.server.port=8080

That router diverts /mcp (and the RFC 9728 metadata) to the MCP container on any tenant host; the incoming Host (e.g. demo.example.com) is preserved, and a single MCP service serves every tenant. You reuse the instance's existing certificate — no *.mcp wildcard or separate cert.

The health check is the part most worth keeping if adapting this by hand: without it, a rolling update can send a client's request to a replica whose process has not finished starting yet, which briefly breaks the connection. With it, Swarm waits for a new replica to answer /healthz before retiring the old one — so upgrading the server is invisible to a connected assistant.

On Kubernetes — the Helm chart does not template MCP (yet), so you apply the manifests alongside it, in the same namespace as the release. A Deployment + Service for the image, plus Ingress paths that send /mcp to that Service on the tenant host. Same logic as Swarm: VAKS_API_URL points at the namespace's API Service, VAKS_AUTH_SERVER_MODE=tenant.

apiVersion: apps/v1
kind: Deployment
metadata: { name: mcp }
spec:
  replicas: 2
  selector: { matchLabels: { app: mcp } }
  template:
    metadata: { labels: { app: mcp } }
    spec:
      containers:
        - name: mcp
          image: vaks-pm/mcp:latest              # your registry / tag
          ports: [{ containerPort: 8080 }]
          env:
            - { name: VAKS_API_URL,          value: "http://<release>-api:3000" }  # the API Service in this namespace
            - { name: VAKS_API_PREFIX,       value: "api/v1" }
            - { name: VAKS_AUTH_SERVER_MODE, value: "tenant" }
---
apiVersion: v1
kind: Service
metadata: { name: mcp }
spec:
  selector: { app: mcp }
  ports: [{ name: http, port: 8080, targetPort: 8080 }]
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: mcp }
spec:
  ingressClassName: nginx                        # same class as the chart Ingress
  tls:
    - hosts: ["demo.example.com"]
      secretName: vaks-pm-tls                    # same TLS Secret as the chart Ingress
  rules:
    - host: demo.example.com                     # the tenant host
      http:
        paths:
          - path: /mcp
            pathType: Prefix
            backend: { service: { name: mcp, port: { number: 8080 } } }
          - path: /.well-known/oauth-protected-resource
            pathType: Prefix
            backend: { service: { name: mcp, port: { number: 8080 } } }

Find the exact API Service name with kubectl get svc -l app.kubernetes.io/component=api (typically <release>-api). Reuse the same ingressClassName and TLS secretName as the chart's Ingress; the /mcp path wins over / (more specific prefix), so it does not clash with the existing routing. A single MCP deployment serves every tenant: add one host: per tenant you expose.

In this mode the admin-panel URL is correct as shown — the endpoint is served on the tenant's own address. This is the only case where you can hand users the panel URL without correcting it.

Option C — Static token, no OAuth

If the authorization server must stay strictly internal, or for a single power user, you can skip the OAuth flow entirely: the user creates a personal API token scoped to mcp:access in their account settings and pastes it into the connector. No browser sign-in, no auth surface to forward. The token is still subject to the same gating (mcp.enabled) and the same permissions as the user.

What is logged

Every MCP action is attributed as an agent action in the audit log — deliberately distinguished from the same person acting in the browser, and from a plain script using the same token. Each write also carries the justification the assistant supplied, a one-line reason for the action, and which assistant made the call (Claude Desktop, ChatGPT, and so on). The audit trail therefore tells you not just what changed, but that an assistant did it, on whose behalf, from which client, and why. Connections themselves are visible and revocable as above.

Justifications and the client name are declared, not verified. The connector asks assistants to explain each write and to identify themselves, and well-behaved ones do — but nothing on the server checks either claim, and the justification field is optional, so a write can succeed without one. Treat both as helpful context, not as proof.

Troubleshooting

SymptomCause & fix
The assistant cannot reach the server at allAlmost always the URL: in a gateway deployment, the admin-panel URL is not the public one. Confirm the actual hostname with your operator. Check too that the assistant, a cloud service, can reach it over the internet.
Consent screen refuses, no approve buttonThe connector is disabled. Enable it in the admin section.
The assistant only offers read toolsWrites are off, or the connection predates turning them on. Turn on Allow AI write actions; existing connections pick it up at their next refresh, so a reconnect is the quick fix.
Writes worked, then briefly disappearedA transient loss of contact with the API drops the session to read-only rather than erroring. It restores itself. Persistent loss is worth checking with your operator.
Finance is invisible to the assistantExpected unless Expose finance to AI is on — and even then only for users who already have finance access.
The user sees French error text in their assistantAssistant-facing tool descriptions and some runtime messages are in French in this release, while the admin and consent screens are English. It is cosmetic; the behaviour is unaffected.
Copilot Studio cannot connectIt does not self-register, and has its own licensing and per-user pitfalls. See the Copilot Studio guide.
The assistant asks the user to confirm before deleting somethingExpected — see confirmation & rate limits. Set the policy to Off if you would rather it not ask.
A large export (e.g. a full personal-data download) seems to hangLarge requests run as a background job: the assistant starts it, then polls for completion rather than holding one connection open. For a very large tenant this can take a little while — it is working, not stuck.

Per-assistant guides: Copilot Studio · Claude · ChatGPT. Related: OIDC single sign-on, reused for MCP login · product & features for the agent governance model · all integrations.