Outlook Semantic MCP - Configuration
12 min read
Outlook Semantic MCP - Configuration
Deployment Modes
The server supports two deployment modes controlled by MCP_BACKEND. Choose your mode before configuring anything else — it determines which infrastructure components you need and which configuration sections apply.
|
| |
|---|---|---|
Search | Semantic (Unique KB) + KQL (Graph), merged | KQL (Graph) only |
Ingestion | Full sync + live catch-up | None |
Tools | 10 standard + 4 debug | 6 standard |
Requires Unique KB | Yes | Yes |
Requires RabbitMQ | Yes | Yes |
Folder filtering | Supported | Not supported |
Configuration impact:
Section | Mode A ( | Mode B ( |
|---|---|---|
Required Secrets | All secrets | Same — |
Ingestion Configuration ( | Required | Omit entirely |
Two values are accepted for MCP_BACKEND:
microsoft_graph_and_unique_api(default) — dual backend mode. Emails are ingested into the Unique Knowledge Base via the full sync pipeline;search_emailsruns both Microsoft Graph KQL search and Unique KB semantic search in parallel and merges the results; all sync tools (sync_progress,run_full_sync, etc.) are registered. Requires themcpConfig.ingestionsection to be configured.microsoft_graph— lean mode. No ingestion pipeline is started;search_emailsandopen_emailcall the Microsoft Graph Search API directly. Sync tools (sync_progress,run_full_sync,pause_full_sync,resume_full_sync,restart_full_sync) are not registered. Folder filtering is not supported because the Graph Search API does not expose a folder-scoped KQL predicate. ThemcpConfig.ingestionsection is not required and is ignored.
Existing deployments that do not set this variable are unaffected — microsoft_graph_and_unique_api is the default.
Environment Variables
All configuration is done via environment variables, either directly or through Helm values.
Required Secrets
These must be provided via Kubernetes secrets:
Variable | Format | Description | Required for |
|---|---|---|---|
|
| PostgreSQL connection string | Both modes |
|
| RabbitMQ connection string (or use individual | Both modes |
| String from Azure portal | Entra app client secret | Both modes |
| 128-character hex string | Webhook validation secret | Both modes |
| 64-character hex string | HMAC-SHA256 session state signing key | Both modes |
| 64-character hex string | AES-256-GCM token encryption key | Both modes |
| String | Zitadel OAuth client secret |
|
Connection string formats:
DATABASE_URL:postgresql://user:password@host:5432/dbname?sslmode=require— see PostgreSQL connection string docsAMQP_URL:amqp://user:password@host:5672/vhost— see RabbitMQ URI specification. Alternatively, use individual fields instead of a connection string:
Variable | Description | Default |
|---|---|---|
| RabbitMQ username | — |
| RabbitMQ password | — |
| RabbitMQ hostname | — |
| RabbitMQ port |
|
| Virtual host | — |
Generating Secrets
The following secrets must be generated with a cryptographically secure random source:
Variable | Command |
|---|---|
|
|
|
|
|
|
Microsoft Configuration
Set via mcpConfig.microsoft in Helm values:
Variable | Helm key | Default | Description |
|---|---|---|---|
|
| (required) | Entra app client ID |
|
| defaults to | Base URL for Microsoft Graph webhook callbacks — see MICROSOFT_PUBLIC_WEBHOOK_URL |
|
|
| UTC hour (0–23) when daily subscription renewals run |
Unique API Configuration
Set via mcpConfig.unique in Helm values:
Variable | Helm key | Default | Description |
|---|---|---|---|
|
|
| Auth mode: |
|
| (required) | Unique ingestion service endpoint |
|
| (required) | Unique scope management service endpoint |
|
|
| Store emails as files in the Knowledge Base — see UNIQUE_STORE_INTERNALLY |
|
| (required for |
|
|
| (required for | Zitadel OAuth client ID |
|
| (required for | Zitadel OAuth token URL |
|
| (required for | Zitadel project ID for audience validation |
Authentication Token Configuration
These tokens are issued by the MCP server to MCP clients (e.g., AI assistants) after a user completes OAuth. They are distinct from Microsoft tokens and control how long a client session remains valid without re-authentication.
Set via mcpConfig.auth in Helm values (optional — defaults are suitable for most deployments):
Variable | Helm key | Default | Description |
|---|---|---|---|
|
|
| TTL of the short-lived access token issued to MCP clients |
|
|
| TTL of the long-lived refresh token issued to MCP clients (30 days) |
Application Configuration
Set via mcpConfig.app in Helm values:
Variable | Helm key | Default | Description |
|---|---|---|---|
|
| (required) | Public URL of the MCP server, used for OAuth callbacks |
| — |
| HTTP port the server binds to — see PORT |
|
|
| Expose debug tools to all connected users. Do not leave enabled in production — see MCP_DEBUG_MODE |
|
|
| Selects the search backend — see Deployment Modes |
|
|
| Buffer logs before writing. Set to |
|
|
| Controls what diagnostic data is logged: |
Delegated Access Configuration
Set via mcpConfig.delegatedAccess in Helm values:
Variable | Helm key | Default | Description |
|---|---|---|---|
|
|
| Delegated access scanning mode — see DELEGATED_ACCESS_SCAN |
|
|
| Cron schedule for delegated access discovery runs. Required when |
|
|
| Cron schedule for delegated access verification runs. Required when |
|
|
| Cron schedule for recovering stuck delegated access discovery and verification jobs. Active when |
|
|
| Hours after which a delegated access account is considered stale for the |
|
|
| Fraction (0–1) of eligible delegated users that may be stale before the |
Ingestion Configuration
Mode A (
This entire section applies only when MCP_BACKEND is microsoft_graph_and_unique_api. If you are deploying in microsoft_graph mode, omit all mcpConfig.ingestion values from your Helm configuration.
Set via mcpConfig.ingestion in Helm values:
Variable | Helm key | Default | Description |
|---|---|---|---|
|
| (required) | JSON email sync filters — see Mail Filters |
|
|
| Minutes to overlap each live catch-up sync run to account for Office 365 eventual consistency. Minimum: |
|
|
| Minutes to overlap live catch-up ready-recheck runs. Minimum: |
|
|
| Cron schedule for stuck full-sync recovery scans |
|
|
| Cron schedule for recovering live catch-ups stuck in |
|
|
| Cron schedule for subscription-user live catch-up ready-recheck runs (retriggers users with an active subscription that haven't run in 30 minutes) |
|
|
| Cron schedule for shared-mailbox live catch-up ready-recheck runs (retriggers shared-mailbox profiles that haven't run in 10 minutes) |
|
|
| Cron schedule for stuck inbox deletion recovery scans |
|
|
| Timeout in milliseconds for the Microsoft Graph connectivity check in |
|
|
| Fraction (0–1) of eligible users that may be failing fullSync or liveCatchup before the |
Runtime Configuration
Set via server.env in Helm values for plain config, or via server.envVars (with valueFrom.secretKeyRef) for secrets:
Variable | Default | Description |
|---|---|---|
|
| Log level: |
|
| Node.js max heap size in MB — see MAX_HEAP_MB |
|
| Node environment |
| — | Path to a PEM file with additional CA certificates for TLS verification |
|
| OpenTelemetry metrics exporter |
|
| Host for the Prometheus metrics scrape endpoint |
|
| Port for the Prometheus metrics scrape endpoint |
|
| Cron schedule for directory (folder tree) delta sync. No dedicated |
Helm Values Reference
Mode A Minimal Values Example
server:
envVars:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: DATABASE_URL
- name: AMQP_URL
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: AMQP_URL
- name: MICROSOFT_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: MICROSOFT_CLIENT_SECRET
- name: MICROSOFT_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: MICROSOFT_WEBHOOK_SECRET
- name: AUTH_HMAC_SECRET
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: AUTH_HMAC_SECRET
- name: ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: ENCRYPTION_KEY
env:
LOG_LEVEL: info
MAX_HEAP_MB: 850
NODE_ENV: production
OTEL_METRICS_EXPORTER: prometheus
OTEL_EXPORTER_PROMETHEUS_HOST: "0.0.0.0"
OTEL_EXPORTER_PROMETHEUS_PORT: "51346"
mcpConfig:
enabled: true
app:
selfUrl: https://outlook.semantic.mcp.example.com
mcpDebugMode: disabled
mcpBackend: microsoft_graph_and_unique_api
delegatedAccess:
scan: disabled
# discoveryCronSchedule: '0 */12 * * *' # required when scan != disabled
# verificationCronSchedule: '0 */4 * * *' # required when scan == granular_access
microsoft:
clientId: "12345678-1234-1234-1234-123456789012"
# publicWebhookUrl: https://outlook.semantic.mcp.example.com # optional, defaults to selfUrl
unique:
serviceAuthMode: cluster_local
ingestionServiceBaseUrl: http://node-ingestion.unique:8091
scopeManagementServiceBaseUrl: http://node-scope-management.unique:8092
serviceExtraHeaders:
x-company-id: "<your-company-id>"
x-user-id: "<your-zitadel-service-user-id>"
# For external auth mode, replace serviceExtraHeaders with zitadel: — see Zitadel Service Account section below
ingestion:
defaultMailFilters:
retentionWindowInDays: 95
ignoredContents: []
ignoredSenders: []
# liveCatchupOverlappingWindowMinutes: 3 # optional, min 2
# liveCatchupRecheckOverlappingWindowMinutes: 10 # optional, min 10
# liveCatchupOauthUsersRecheckCron: "*/10 * * * *" # optional
# liveCatchupSharedMailboxRecheckCron: "*/10 * * * *" # optional
ingress:
enabled: true
ingressClassName: kong
hosts:
- host: outlook.semantic.mcp.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: outlook-semantic-mcp-tls
hosts:
- outlook.semantic.mcp.example.com
grafana:
dashboard:
enabled: true
folder: mcp-servers
alerts:
enabled: true
defaultAlerts:
graphql:
enabled: true
uniqueApi:
enabled: trueMode B Minimal Values Example
server:
envVars:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: DATABASE_URL
- name: AMQP_URL
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: AMQP_URL
- name: MICROSOFT_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: MICROSOFT_CLIENT_SECRET
- name: MICROSOFT_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: MICROSOFT_WEBHOOK_SECRET
- name: AUTH_HMAC_SECRET
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: AUTH_HMAC_SECRET
- name: ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: outlook-semantic-mcp-secrets
key: ENCRYPTION_KEY
mcpConfig:
enabled: true
app:
selfUrl: https://outlook.semantic.mcp.example.com
mcpBackend: microsoft_graph
microsoft:
clientId: "12345678-1234-1234-1234-123456789012"
unique:
serviceAuthMode: cluster_local
ingestionServiceBaseUrl: http://node-ingestion.unique:8091
scopeManagementServiceBaseUrl: http://node-scope-management.unique:8092
serviceExtraHeaders:
x-company-id: "<your-company-id>"
x-user-id: "<your-service-account-user-id>"
# For external auth mode, replace serviceExtraHeaders with zitadel: — see Zitadel Service Account section below
# No mcpConfig.ingestion section — omit entirely for microsoft_graph modeService Auth Modes
cluster_local (Default)
For deployments within the same Kubernetes cluster as Unique. Uses in-cluster service URLs with x-company-id and x-user-id headers passed to all Unique API requests.
The x-user-id value must be the ID of an actual service user created in Zitadel — it cannot be an arbitrary value. See Zitadel Service Account for setup instructions.
mcpConfig:
unique:
serviceAuthMode: cluster_local
ingestionServiceBaseUrl: http://node-ingestion.unique:8091
scopeManagementServiceBaseUrl: http://node-scope-management.unique:8092
serviceExtraHeaders:
x-company-id: "<your-company-id>"
x-user-id: "<your-zitadel-service-user-id>"external
For deployments outside the Unique cluster. Uses Zitadel OAuth for service-to-service authentication. UNIQUE_ZITADEL_CLIENT_SECRET must be provided as a Kubernetes secret.
mcpConfig:
unique:
serviceAuthMode: external
ingestionServiceBaseUrl: https://ingestion.unique.app
scopeManagementServiceBaseUrl: https://scope-management.unique.app
zitadel:
clientId: "<zitadel-client-id>"
oauthTokenUrl: "https://your-zitadel-instance.zitadel.cloud/oauth/v2/token"
projectId: "<zitadel-project-id>"Security Best Practices
Rotate secrets regularly, especially
MICROSOFT_CLIENT_SECRETandENCRYPTION_KEYUse an external secret manager (e.g., AWS Secrets Manager, Azure Key Vault, HashiCorp Vault) rather than static Kubernetes secrets
Keep
LOGS_DIAGNOSTICS_DATA_POLICYset toconceal(the default) in production to avoid logging sensitive dataEnable network policies to restrict inbound and outbound traffic to only required services
Monitor deployments using the provided Grafana dashboards and alert rules (
grafana.dashboard.enabled: true,alerts.enabled: true)
Zitadel Service Account
A Zitadel service account is required for both cluster_local and external auth modes. For cluster_local, its user ID is passed in the x-user-id header. For external, its credentials are used for service-to-service OAuth.
For instructions on creating a service user, see the How To Configure A Service User guide.
Service-Specific Setup
After creating the service user, note the following values for configuration:
Value | Used In | Helm Path |
|---|---|---|
User ID |
|
|
Client ID |
|
|
Client Secret |
| Secret: |
Project ID |
|
|
OAuth Token URL |
|
|
Service Account Permissions
The service account must be assigned the following Zitadel permissions:
Permission | Used For |
|---|---|
| Administrative access required for scope and folder management |
| Reading Knowledge Base scopes and content |
| Ingesting email content and managing scopes via the ingestion and scope management services |
Configuration Reference
Detailed documentation for variables that require more context than the tables above provide.
Runtime
PORT
The server listens on PORT (default 9542). In Helm deployments, server.ports.application (default 51345) overrides this value — the Helm chart injects the port via the deployment spec, so PORT typically does not need to be set explicitly.
MAX_HEAP_MB
Sets the Node.js --max-old-space-size flag. With the default of 850 MB, set the pod memory request/limit to at least ~1 GB to account for non-heap memory overhead (native modules, OS buffers, etc.).
Microsoft
MICROSOFT_PUBLIC_WEBHOOK_URL
Microsoft Graph sends webhook callbacks (change notifications and lifecycle events) to this base URL. Microsoft appends /mail-subscription/notification and /mail-subscription/lifecycle to construct the full endpoints. The URL must be publicly reachable by Microsoft Graph.
This defaults to SELF_URL. Set it explicitly only when the externally reachable webhook URL differs from SELF_URL — for example, when using a dev tunnel in local development. In most production deployments the two values are identical.
Note: the Entra ID app registration redirect URI must match SELF_URL/auth/callback, not this variable.
Unique API
UNIQUE_STORE_INTERNALLY
When enabled (default), emails are ingested into the Unique Knowledge Base and stored as physical files, making them available for semantic search via search_emails.
When disabled, emails are ingested (metadata recorded) but not stored as files. They remain searchable via search_emails, but the email content is not persisted in the Knowledge Base.
UNIQUE_SERVICE_EXTRA_HEADERS
Required for cluster_local auth mode. Provide as a JSON object:
{"x-company-id": "<your-company-id>", "x-user-id": "<your-zitadel-service-user-id>"}x-company-id— your organization's ID in the Unique platform. Find it in the Unique admin dashboard under Settings > Organization, or via the Unique API (GET /api/company).x-user-id— the Zitadel service user ID. See Zitadel Service Account.
Application
MCP_DEBUG_MODE
When set to enabled, exposes four additional debug tools to all connected MCP users: run_full_sync, pause_full_sync, resume_full_sync, and restart_full_sync. These tools are intended for troubleshooting sync issues, but because MCP tools are scoped to the authenticated user there is no way to restrict them to operators only — all users can call them while debug mode is active. Enable only during active troubleshooting and disable immediately after.
DELEGATED_ACCESS_SCAN
For step-by-step Microsoft 365 setup, see Features — Delegated Access — Setup.
Set via mcpConfig.delegatedAccess.scan. Controls whether the service scans for delegated mailbox access granted between users at the Microsoft Exchange level. Three values are accepted:
disabled(default) — delegated access scanning is off. No discovery or verification runs are scheduled. Users only see their own mailbox.full_access_only— discovers users who have been granted Full Access (Read & Manage) on a mailbox via Exchange admin. Uses the/users/{email}/messagesendpoint to detect access. RequiresDELEGATED_ACCESS_DISCOVERY_CRON_SCHEDULE.granular_access— discovers users who have been granted folder-level access (e.g., only "Inbox" or "RFQ" shared, not the entire mailbox). Uses the/users/{email}/mailFoldersendpoint for discovery, followed by a verification pass to determine which folders are actually readable (since the folder listing can include parent folders of shared subfolders that are not themselves accessible). Requires bothDELEGATED_ACCESS_DISCOVERY_CRON_SCHEDULEandDELEGATED_ACCESS_VERIFICATION_CRON_SCHEDULE.
Choosing a mode:
Scenario | Recommended mode |
|---|---|
No delegated mailbox access in your org |
|
Users have Full Access (Read & Manage) granted via Exchange admin |
|
Users share individual folders (e.g., Inbox, RFQ) with others |
|
granular_access subsumes full_access_only — if your org uses both types of delegation, use granular_access.
granular_access requires MCP_BACKEND=microsoft_graph_and_unique_api and will fail to start if configured with microsoft_graph. full_access_only is supported in both modes, but in Mode B only delegates with full mailbox access can search delegated mailboxes — honouring folder-level delegations would require querying every accessible folder individually, which is not implemented due to API rate limits.
Both users must be connected (both modes). Discovery only considers connected users — if the owner has not connected their account, there is nothing to discover or search regardless of mode. In Mode A the owner must also have completed the initial full sync for their emails to be available to the delegate.
full_access_only— consider a more frequent discovery schedule. When usingfull_access_only, discovery is the only revocation detection mechanism. Consider settingDELEGATED_ACCESS_DISCOVERY_CRON_SCHEDULEto run 4 times per day (e.g.0 */6 * * *) to reduce the window during which a revoked delegate can still search the owner's emails. Ingranular_accessmode this is less critical because the verification job already runs every 4 hours.
Mail Filters
The INGESTION_DEFAULT_MAIL_FILTERS value controls which emails are synced during the initial import and ongoing sync. It is configured as a dictionary under mcpConfig.ingestion.defaultMailFilters and serialized to JSON automatically by the Helm chart.
Warning: Changing
INGESTION_DEFAULT_MAIL_FILTERSonly affects newly synced emails. Emails that were already ingested under a previous filter configuration are not removed. To remove previously ingested emails, you must delete them manually.
Field | Type | Description |
|---|---|---|
| Positive integer | Number of days to retain emails. The effective cutoff rolls forward daily as |
| Array of regex patterns | Regex patterns in |
| Array of regex patterns | Regex patterns in |
Patterns must be in /pattern/flags format (e.g. /^noreply@example\.com$/i, /unsubscribe/i). Patterns are validated against ReDoS attacks on ingestion — invalid or unsafe patterns are rejected.
We recommend setting retentionWindowInDays between 95 and 180 days for most deployments:
High mail volume (hundreds of emails per user per day): use a shorter window (closer to 95 days). A large window combined with high volume floods the Knowledge Base with content, degrades search quality, and increases ingestion costs.
Low mail volume or deep-search requirements: a longer window (up to 180 days or beyond) is viable.
Values above 180 days are not recommended unless mail volume is low and the operational cost of a large Knowledge Base is acceptable.
mcpConfig:
ingestion:
defaultMailFilters:
retentionWindowInDays: 95
ignoredContents:
- "/unsubscribe/i"
ignoredSenders:
- "/^noreply@example\\.com$/i"INGESTION_LIVE_CATCHUP_OVERLAPPING_WINDOW_MINUTES
Office 365 uses eventual consistency — messages can appear with a delayed updatedAt timestamp after the actual event. To avoid missing late-arriving messages, each live catch-up sync run re-queries an overlapping window of this many minutes. Default: 3 minutes. Minimum: 2.
INGESTION_LIVE_CATCHUP_RECHECK_OVERLAPPING_WINDOW_MINUTES
Overlapping window (in minutes) for live catch-up ready-recheck runs. Uses a larger window than the standard run to account for higher latency during recheck scenarios. Minimum and default: 10.