AI assistant connector (MCP) Module
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 assistant acts as a person, not as itself. The user signs in through their normal login — local password or your SSO. The assistant then operates strictly within that user's permissions.
- Read by default, write only if you allow it. Out of the box the assistant can observe and suggest. Write tools appear only when an administrator turns them on.
- Finance is separately gated. Financial data stays invisible to assistants unless you deliberately expose it, even to a user who can see it in the browser.
- Every action is attributed. An MCP action is recorded as an agent action in the audit log, distinct from the same person clicking in the browser, and carries the reason the assistant gave.
- It is a premium module. It needs a licence that includes the AI connector, and the per-organization toggle on top.
The permissions model
This is the reassuring part, and worth stating plainly to anyone nervous about connecting an AI to their project data:
Concretely, three switches an administrator controls decide the ceiling:
| Toggle | Off (default) | On |
|---|---|---|
| Enable the AI connector | No one can connect; consent is refused. | Users can connect and sign in. |
| Allow AI write actions | Read-only: the assistant observes and suggests. | Write tools appear, bounded by each user's permissions. |
| Expose finance to AI | Budgets, margins and rates are invisible to assistants. | Finance is readable, still only for users who already have finance access. |
Prerequisites
| Side | What you need |
|---|---|
| Licence | A licence that includes the AI connector (MCP). Without it the section shows a premium-feature notice and the toggles are disabled. |
| Vaks PM | org:manage to enable and configure it. Individual users need only their normal account to connect. |
| The assistant | An MCP-capable client. Claude and ChatGPT self-register; Copilot Studio and Power Platform need a pre-registered client — see step 3. |
| Reachability | The 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).
- Turn on Enable the AI connector (MCP).
- Decide on Allow AI write actions. Start read-only. You can turn writes on later once you have seen how assistants are used.
- Decide on Expose finance to AI. Leave off unless you specifically want assistants reading financial figures.
- Copy the MCP server URL shown in the first card — this is what users paste into their assistant.
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:
- The assistant sends the user to Vaks PM to sign in (local or SSO).
- The user sees a consent screen stating exactly what the assistant will be able to do.
- 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:
- Give it a Name.
- Paste the Callback URL from the connector you are building — for Power Platform this looks like
https://global.consent.azure-apim.net/redirect/... - 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.
The consent screen
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:
- Administrators: Admin → Security → Active connections → AI connections lists every user's connected assistant — who, which client, when last used — with a Disconnect button. Disconnecting is immediate: the short-lived token is revoked, not left to expire.
- Each user: their own Security settings list their AI connections with the same self-service disconnect.
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.
<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:
- Assistants on the internet, instance private — an operator publishes MCP through an isolated gateway that holds no secrets and has no database access. This is the recommended hardened layout for exposing an internal instance. See Option A.
- Assistants and instance on the same network — no separate gateway needed; the endpoint is served on the tenant address and the admin-panel URL is correct as shown. See Option B.
- No OAuth flow at all — a user creates a personal API token scoped to MCP access in their account settings and pastes that into the connector. Simplest for a single power user; no browser sign-in. See Option C.
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.
| Point | Detail |
|---|---|
| Separate container | The 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 box | It stores nothing sensitive. All security (PKCE, consent, licence, RBAC, rate-limit) is enforced by the internal API, never by MCP. |
| One outbound flow | From 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 naming | A 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 prerequisite | Independent 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 mode | VAKS_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
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
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.
Prerequisites, supplied by the operator:
| Item | Detail |
|---|---|
| DMZ host | Docker + 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 cert | Wildcard *.mcp.vaks-pm.com → certs/mcp-wildcard.{crt,key}. |
| Internal CA | If the internal edge uses a self-signed cert: certs/internal-ca.crt (otherwise drop NODE_EXTRA_CA_CERTS + insecureSkipVerify). |
| Firewall | Internet → 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
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.
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 theworkerservice already uses (INTERNAL_API_URL: http://api:3000indocker-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 theINTERNAL_EDGE_URLof 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).
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.
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.
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| The assistant cannot reach the server at all | Almost 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 button | The connector is disabled. Enable it in the admin section. |
| The assistant only offers read tools | Writes 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 disappeared | A 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 assistant | Expected 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 assistant | Assistant-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 connect | It 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.