Outlook Semantic MCP - Configuration
12 min read
Outlook Semantic MCP - Configuration
Choose Your Deployment Mode
Set MCP_BACKEND before configuring anything else — it determines which infrastructure components you need and which configuration sections apply.
Section | Mode A ( | Mode B ( |
|---|---|---|
Required Secrets | All secrets | Same — |
Ingestion Configuration ( | Required | Omit entirely |
Unique API Configuration ( | Required | Required |
RabbitMQ Configuration | Required | Required |
See MCP_BACKEND for the full description of what each mode enables.
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 — see Generating Secrets | Both modes |
| 64-character hex string | HMAC-SHA256 session state signing key — see Generating Secrets | Both modes |
| 64-character hex string | AES-256-GCM token encryption key — see Generating Secrets | Both modes |
| String | Zitadel OAuth client secret (required for | Both modes ( |
Application Configuration
Set via mcpConfig.app in Helm values:
Variable | Helm Path | 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 MCP_BACKEND |
|
|
| Buffer logs before writing. Set to |
### Delegated Access Configuration |
Set via mcpConfig.delegatedAccess in Helm values:
Variable | Helm Path | 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 |
Microsoft Configuration
Set via mcpConfig.microsoft in Helm values:
Variable | Helm Path | 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 |
Ingestion Configuration
Mode A (
This entire section applies only when MCP_BACKEND is MicrosoftGraphAndUniqueApi. If you are deploying in MicrosoftGraph mode, omit all mcpConfig.ingestion values from your Helm configuration.
Set via mcpConfig.ingestion in Helm values:
Variable | Helm Path | 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 stuck live catch-up recovery scans |
|
|
| 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 |
Unique API Configuration
Set via mcpConfig.unique in Helm values:
Variable | Helm Path | 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 |
Logs Configuration
Set via mcpConfig.logs in Helm values:
Variable | Helm Path | Default | Description |
|---|---|---|---|
|
|
| Controls what diagnostic data is logged: |
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 Path | 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) |
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: MicrosoftGraphAndUniqueApi
delegatedAccess:
scan: disabled
# discoveryCronSchedule: '0 */12 * * *' # required when scan != disabled
# verificationCronSchedule: '0 */4 * * *' # required when scan == granularAccess
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>"
ingestion:
defaultMailFilters:
retentionWindowInDays: 95
ignoredContents: []
ignoredSenders: []
# liveCatchupOverlappingWindowMinutes: 3 # optional, min 2
# liveCatchupRecheckOverlappingWindowMinutes: 10 # optional, min 10
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: MicrosoftGraph
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>"
# No mcpConfig.ingestion section — omit entirely for MicrosoftGraph 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>"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 KB_Admin (Knowledge Base Admin) Gatekeeper role. This is the minimum role that grants all permissions the MCP server needs:
Resource | Required Permissions | Used For |
|---|---|---|
|
| Ingesting, updating, and removing email content via the ingestion service |
|
| Creating and managing Knowledge Base scopes via the scope management service |
|
| Managing user access to scopes |
|
| Managing Knowledge Base folders |
The KB_Admin role covers all of the above. Using a broader role such as Company_Admin also works but grants unnecessary privileges.
Gatekeeper enforcement
Authorization behavior differs by mode. In cluster_local mode, Gatekeeper does not check user roles — instead, the MCP server's service ID (outlook-semantic-mcp) must be listed in the guards of the relevant resolvers. In external mode, Gatekeeper authorization is enforced against the Zitadel client credentials, which must have the KB_Admin role assigned.
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.
Fields
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 for ignoredSenders and ignoredContents 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.
Choosing a Retention Window
We recommend setting retentionWindowInDays between 95 and 180 days for most deployments. The right value depends on your mail volume and use-case needs:
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.
Example
mcpConfig:
ingestion:
defaultMailFilters:
retentionWindowInDays: 95
ignoredContents:
- "/unsubscribe/i"
ignoredSenders:
- "/^noreply@example\\.com$/i"The example uses 95 days. Adjust based on your organization's mail volume and search needs.
Database Configuration
Connection String Format
postgresql://username:password@hostname:port/database?sslmode=requireNo special PostgreSQL extensions are required. Database migrations run automatically on deployment and create all necessary tables and indexes.
RabbitMQ Configuration
Connection String Format
amqp://username:password@hostname:5672/vhostAlternative: Individual Fields
Instead of AMQP_URL, you can provide individual connection fields:
Variable | Description | Default |
|---|---|---|
| RabbitMQ username | — |
| RabbitMQ password | — |
| RabbitMQ hostname | — |
| RabbitMQ port |
|
| Virtual host | — |
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)
Variable Details
Generating Secrets
The following secrets must be generated with a cryptographically secure random source:
Variable | Command |
|---|---|
|
|
|
|
|
|
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.
MCP_BACKEND
If you want... | Use |
|---|---|
Semantic search against ingested email history + live KQL |
|
Live KQL search only, no email ingestion |
|
Selects the search and email-open backend at deploy time. Two values are accepted:
MicrosoftGraphAndUniqueApi(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.MicrosoftGraph— lean mode. No ingestion pipeline is started;search_emailsandopen_email_by_idcall 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 — MicrosoftGraphAndUniqueApi is the default.
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.fullAccessOnly— 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.granularAccess— 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 |
|
granularAccess subsumes fullAccessOnly — if your org uses both types of delegation, use granularAccess.
granularAccess requires MCP_BACKEND=MicrosoftGraphAndUniqueApi and will fail to start if configured with MicrosoftGraph. fullAccessOnly 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.
fullAccessOnly— consider a more frequent discovery schedule. When usingfullAccessOnly, 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. IngranularAccessmode this is less critical because the verification job already runs every 4 hours.
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.
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_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.
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.
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.).