AI assistant connector (MCP) Module

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
LicenceA licence that includes the AI connector (MCP). Without it the section shows a premium-feature notice and the toggles are disabled.
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 step 3.
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.

Claude and ChatGPT handle this end to end with no configuration on your side beyond enabling the connector. The connection is standard OAuth with PKCE; access tokens last one hour and refresh automatically.

Step 3 — Pre-registered clients (Copilot Studio, Power Platform)

Some platforms do not register themselves automatically. For those, create a client in advance, under Pre-registered OAuth clients in the same section:

  1. Give it a Name.
  2. Paste the Callback URL from the connector you are building — for Power Platform this looks like https://global.consent.azure-apim.net/redirect/...
  3. Press Create client. The client secret is shown once — copy it immediately, it is never shown again.

Claude and ChatGPT do not need this; they self-register.

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.

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 URL shown in the admin screen assumes the endpoint is served on the tenant's own address. If your operator exposed MCP on a separate hostname — a hardened setup often puts it on <tenant>.mcp.<domain> behind an isolated gateway — then the address to hand users is that one, not the one the admin panel displays. Confirm with whoever deployed the instance which hostname actually answers, and share that. Getting this wrong is the usual reason a first connection fails.

Three topologies, each walked through step by step below:

The rest of this section is for whoever operates the servers — not the tenant administrator. The MCP server deploys as a separate container, with its own docker compose file; it is not part of the Swarm or Kubernetes application stack. The steps below show how to build it, where to place it, and how it attaches to an already-running Vaks PM instance.

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
Separate containerThe vaks-pm/mcp:latest image, deployed by its own docker compose (infra/dmz/) — never mixed into the api/web/worker stack.
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) and the licence must include the mcp feature. Otherwise the API issues no token (403), whatever the topology.
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

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.

Internet ──443──► [DMZ · Traefik]──┬─ /mcp, /.well-known/oauth-protected-resource → mcp (local) └─ /oauth, /login, /oauth-consent, /assets, ─443─► internal API /.well-known/oauth-authorization-server (the only outbound flow)

Prerequisites, supplied by the operator:

ItemDetail
DMZ hostDocker + Docker Compose. Outside the internal Swarm.
Public DNS*.mcp.vaks-pm.com → the DMZ box's public IP.
Internal DNS (split-horizon)internal.vaks-pm.com → on-prem Traefik, resolved from the DMZ.
Public TLS certWildcard *.mcp.vaks-pm.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.vaks-pm.com:443 ONLY. No database / Redis / overlay access.

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.vaks-pm.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.vaks-pm.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.vaks-pm.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.vaks-pm.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.vaks-pm.com/mcp: 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 same network

If the AI clients already reach the instance without going over the internet, no DMZ box is needed: you run the same MCP image next to the API, on the Swarm overlay network, and the AS is advertised directly on the tenant address.

"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.vaks-pm.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 — add an mcp service to the stack, on the API's overlay, with VAKS_AUTH_SERVER_MODE=tenant:

  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: tenant     # the authorization server IS the tenant's own address
    deploy:
      labels:
        - traefik.enable=true
        - "traefik.http.routers.mcp.rule=PathPrefix(`/mcp`) || PathPrefix(`/.well-known/oauth-protected-resource`)"
        - traefik.http.routers.mcp.priority=100      # beat the per-tenant catch-all on these two paths
        - 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.vaks-pm.com) is preserved, and a single MCP service serves every tenant. You reuse the instance's existing certificate — no *.mcp wildcard or separate cert.

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.vaks-pm.com"]
      secretName: vaks-pm-tls                    # same TLS Secret as the chart Ingress
  rules:
    - host: demo.vaks-pm.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 (licence + 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. The audit trail therefore tells you not just what changed, but that an assistant did it, on whose behalf, and why. Connections themselves are visible and revocable as above.

Justifications are encouraged, not enforced. The connector asks assistants to explain each write, and well-behaved ones do. But the field is optional, so a write can succeed without one. Treat its presence as helpful context, not as a guarantee.

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. Create a pre-registered client and use its callback URL, per step 3.

Related: OIDC single sign-on, reused for MCP login · product & features for the agent governance model · all integrations.