Configuration¶
mcp-data-platform uses YAML configuration with environment variable expansion. Variables in the format ${VAR_NAME} are replaced with their environment values at load time.
How Configuration Works¶
File mode (default): Configuration is loaded from a YAML file at startup. This is the simplest deployment — no database required.
File + database: Adding database.dsn unlocks persistent platform features (audit logging, knowledge capture, session externalization). When a database is available, individual config entries stored in the config_entries table override file defaults for whitelisted keys. Changes made via the admin API take effect immediately without restart. File defaults are preserved and used as fallback when database entries are deleted.
| What you configure | What it unlocks |
|---|---|
| YAML file only | Read-only config, in-memory sessions, no audit |
database.dsn |
Audit logging, knowledge capture, OAuth persistence, database-backed sessions, per-key config overrides via admin API |
database.dsn + admin.enabled: true |
REST endpoints for system health, config entries CRUD, personas, auth keys, audit |
See Operating Modes for the full comparison and Admin API for the REST endpoints.
Unknown keys and strict parsing¶
By default the loader accepts a config file that contains keys it does not
recognize: each unknown key is logged as a prominent WARN at startup and then
ignored. This applies at every level that maps to a defined config field: a
stray key under server:, auth.oidc:, or inside a persona definition is
flagged just like a stray top-level key. This keeps older configs loading, but
it also means a typo or a renamed key silently does nothing.
Set config.strict: true to reject unknown keys with a hard error at startup
instead. This is recommended, since it turns typos and stale keys into an
immediate, actionable failure rather than a silent no-op:
Free-form maps are exempt because they accept arbitrary keys by design: the
toolkits tree, each toolkit's config: map, and the persona names under
personas: (the fields within a persona definition, such as display_name
and tools, are still validated). A future release will make strict rejection
the default; you will be able to opt back out with config.strict: false.
Configuration File¶
Create a platform.yaml file:
apiVersion: v1
server:
name: mcp-data-platform
transport: stdio
toolkits:
trino:
enabled: true
instances:
primary:
host: trino.example.com
port: 443
user: ${TRINO_USER}
password: ${TRINO_PASSWORD}
ssl: true
catalog: hive
schema: default
default: primary
datahub:
enabled: true
instances:
primary:
url: https://datahub.example.com
token: ${DATAHUB_TOKEN}
default: primary
s3:
enabled: true
instances:
primary:
region: us-east-1
access_key_id: ${AWS_ACCESS_KEY_ID}
secret_access_key: ${AWS_SECRET_ACCESS_KEY}
default: primary
enrichment:
trino_semantic_enrichment: true
datahub_query_enrichment: true
s3_semantic_enrichment: true
column_context_filtering: true # Only include SQL-referenced columns (default: true)
Naming note: the config-block key is
enrichment:. The legacyinjection:key still loads as a deprecated alias (with a warning) so existing configs keep working, but new configs should useenrichment:. The feature has always been about enriching tool responses with context.
Config Versioning¶
Every configuration file should include an apiVersion field as the first key. This enables safe schema evolution with deprecation warnings and migration tooling.
| Field | Type | Default | Description |
|---|---|---|---|
apiVersion |
string | v1 |
Config schema version. Omitting defaults to v1 for backward compatibility. |
Supported versions: v1 (current)
Version Lifecycle¶
- current: Actively supported, no warnings
- deprecated: Still works, emits a warning at startup with migration guidance
- removed: Rejected at startup with an error pointing to the migration tool
Migration Tool¶
Migrate config files to the latest version:
# From file to stdout
mcp-data-platform migrate-config --config platform.yaml
# From stdin to file
cat platform.yaml | mcp-data-platform migrate-config --output migrated.yaml
# Specify target version
mcp-data-platform migrate-config --config platform.yaml --target-version v1
The migration tool preserves ${VAR} environment variable references.
Server Configuration¶
server:
name: mcp-data-platform # Server name reported to clients
transport: stdio # stdio or http
address: ":8080" # Listen address for HTTP transports
tls:
enabled: false
cert_file: ""
key_file: ""
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | mcp-data-platform |
Server name in MCP handshake |
version |
string | build-injected (dev in unlinked builds) |
Server version reported to clients |
description |
string | - | Explains when to use this MCP server - which business, products, or domains it covers. Agents use this to route questions to the right MCP server; also shown in platform_info |
tags |
array | [] |
Discovery keywords (company names, product names, business domains) that agents match against user questions |
transport |
string | stdio |
Transport protocol: stdio or http (sse accepted for backward compatibility) |
address |
string | :8080 |
Listen address for HTTP transports |
tls.enabled |
bool | false |
Enable TLS for HTTP transport |
tls.cert_file |
string | - | Path to TLS certificate |
tls.key_file |
string | - | Path to TLS private key |
HTTP Transport Security
When using HTTP transport without TLS, a warning is logged. For production deployments, always enable TLS to encrypt credentials in transit.
Prompts¶
The platform registers MCP prompts at three levels:
-
Auto-registered
platform-overview— Built dynamically fromserver.descriptionand enabled toolkits. Lists what the platform can do based on which toolkits (DataHub, Trino, S3, Portal, Knowledge) are configured. -
Operator-configured prompts — Defined in
server.prompts. Support typed arguments with{placeholder}substitution in content. -
Workflow prompts — Registered automatically when required toolkits are present. Provide guided multi-step workflows (e.g.,
explore-available-data,create-interactive-dashboard,create-a-report,trace-data-lineage).
Operator-configured prompts override any auto-registered prompt with the same name. Toolkits (Portal, Knowledge) may also register their own prompts via the PromptDescriber interface.
server:
description: "ACME Corp analytics platform"
prompts:
- name: routing_rules
description: "How to route queries between systems"
content: |
Before querying, determine if you need ENTITY STATE or ANALYTICS...
- name: explore-topic
display_name: "Explore a Topic"
description: "Explore data about a specific topic"
content: "Find all datasets related to {topic} and summarize key metrics."
arguments:
- name: topic
description: "The topic to explore"
required: true
| Field | Type | Default | Description |
|---|---|---|---|
server.prompts[].name |
string | required | Prompt name |
server.prompts[].display_name |
string | - | Human-readable title served as the MCP prompt title (falls back to the name) |
server.prompts[].description |
string | - | Prompt description |
server.prompts[].content |
string | required | Prompt content (supports {arg_name} placeholders) |
server.prompts[].arguments |
array | [] |
Typed arguments for the prompt |
server.prompts[].arguments[].name |
string | required | Argument name (maps to {name} in content) |
server.prompts[].arguments[].description |
string | - | Argument description shown to clients |
server.prompts[].arguments[].required |
bool | false |
Whether the argument is required |
Built-in workflow prompts:
| Prompt | Required Toolkits | Description |
|---|---|---|
explore-available-data |
DataHub | Discover datasets about a topic |
create-interactive-dashboard |
DataHub, Trino, Portal | Full workflow: discover, query, visualize, save |
create-a-report |
DataHub, Trino | Discover data, query it, produce a Markdown report |
trace-data-lineage |
DataHub | Trace upstream/downstream lineage for a dataset |
Each built-in workflow prompt registers automatically when its required toolkits
are present. Turn one off by name with server.builtin_prompts:
| Field | Type | Default | Description |
|---|---|---|---|
server.builtin_prompts |
map[string]bool | {} |
Per-prompt switch for the built-in workflow prompts above, keyed by prompt name. A name absent from the map (or set to true) registers as usual; false suppresses it. Operator prompts in server.prompts override a built-in of the same name regardless of this setting |
All registered prompts (platform + toolkit) are included in the platform_info tool response and visible in the platform-info app's Prompts tab.
Prompt titles and resolution. Database prompts are served on the native MCP prompts surface under per-viewer scope-prefixed names (global-<name>, <persona>-<name>, personal-<name>, shared-<name>), which keeps the surface collision-free by construction; every descriptor carries a title from display_name so clients show the human name regardless. Users never need to know any machine name: agents resolve a prompt from whatever handle the user says (stored name, display name, mcp:prompt:<id>, or free text) with the manage_prompt use command.
Versioning and approval provenance. Every database prompt is versioned: each mutation of its content, display name, description, arguments, or tags snapshots an immutable version row with the author, and approval stamps bind to the specific version that was approved. Editing the content or arguments of an approved global or persona prompt does not change what is served: the edit is saved as a pending draft version, and the approved snapshot keeps serving until an admin approves the draft (admin API POST /api/v1/admin/prompts/{id}/versions/{version}/approve, or reject with .../reject; history via GET .../versions). Metadata-only edits (tags, category, description, display name) apply directly. Personal prompts version silently without review. Served prompts carry their provenance: prompts/get responses stamp prompt_version, prompt_approved_by, prompt_approved_at, and prompt_reference into _meta, and manage_prompt use reports the same in its provenance block, so an agent can state "running Daily Sales Report v4, approved by jane@example.com" before executing. Run counts and last-run timestamps are aggregated from prompt-serve audit events (within the audit retention window) and exposed on manage_prompt get and the GET /api/v1/admin/prompts/usage and GET /api/v1/portal/prompts/usage endpoints.
Streamable HTTP Configuration¶
The HTTP transport serves both legacy SSE (/sse, /message) and Streamable HTTP (/) endpoints. Streamable HTTP session behavior is configured under server.streamable:
| Field | Type | Default | Description |
|---|---|---|---|
session_timeout |
duration | 30m |
How long an idle session persists before cleanup |
stateless |
bool | false |
Disable session tracking (no Mcp-Session-Id validation) |
The MCP SDK caps each Streamable HTTP request body at 4 MiB and rejects a larger one with 413 Request Entity Too Large. The limit applies to inbound JSON-RPC bodies, so it bounds tool-call arguments; it does not bound tool results, managed-resource uploads, or asset exports, which travel other paths with their own limits.
Authentication Configuration¶
auth:
allow_anonymous: false # Require authentication (default)
oidc:
enabled: true
issuer: "https://auth.example.com/realms/platform"
client_id: "mcp-data-platform"
audience: "mcp-data-platform"
role_claim_path: "realm_access.roles"
role_prefix: "dp_"
api_keys:
enabled: true
keys:
- key: ${API_KEY_ADMIN}
name: "admin"
roles: ["admin"]
| Field | Type | Default | Description |
|---|---|---|---|
allow_anonymous |
bool | false |
Allow unauthenticated requests |
oidc.enabled |
bool | false |
Enable OIDC authentication |
oidc.issuer |
string | - | OIDC issuer URL |
oidc.client_id |
string | - | OAuth client ID |
oidc.audience |
string | - | Expected token audience |
oidc.role_claim_path |
string | roles |
Path to roles in token claims |
oidc.role_prefix |
string | - | Filter roles to those with this prefix |
api_keys.enabled |
bool | false |
Enable API key authentication |
api_keys.keys |
array | - | List of API key configurations |
Token clock skew is a fixed 30 seconds and is not exposed as a YAML setting.
The JWKS signing keys are fetched from the issuer at startup and cached for one hour. The cache self-heals on demand: a token whose key is missing because the cache has expired, or because the IdP rotated its keys, triggers a single refresh from the issuer, performed during that request's validation and honoring the request's own deadline. Concurrent requests collapse into one fetch, which runs independently so a slow issuer cannot pin request goroutines past their deadlines. Refreshes are throttled by the outcome of the last fetch: after a success the next on-demand refresh is at most once per minute, so a flood of unknown key IDs cannot hammer the issuer; after a failure a short recovery window applies so a brief issuer outage heals within seconds rather than being held down for the full minute. No restart is required after key rotation, and none of this is exposed as a YAML setting.
Fail-Closed Security
Authentication follows a fail-closed model. Missing tokens, invalid signatures, expired tokens, or missing required claims (sub, exp) all result in denied access. If a JWKS refresh fails while the cache is expired, tokens are rejected rather than accepted unverified.
Browser Sessions (OIDC Login for Portal UI)¶
When both auth.oidc and auth.browser_session are enabled, the portal UI offers SSO login via the configured OIDC provider. The flow uses authorization code with PKCE and stores the session in an HMAC-SHA256 signed JWT cookie.
auth:
oidc:
enabled: true
issuer: "https://auth.example.com/realms/platform"
client_id: "mcp-data-platform"
client_secret: "${OIDC_CLIENT_SECRET}"
audience: "mcp-data-platform"
role_claim_path: "realm_access.roles"
role_prefix: "dp_"
scopes: [openid, profile, email]
browser_session:
enabled: true
signing_key: "${SESSION_SIGNING_KEY}" # openssl rand -base64 32
ttl: 8h
secure: true
same_site: lax
| Field | Type | Default | Description |
|---|---|---|---|
browser_session.enabled |
bool | false |
Enable cookie-based browser sessions |
browser_session.signing_key |
string | - | Base64-encoded HMAC key (32+ bytes) |
browser_session.ttl |
duration | 8h |
Session lifetime |
browser_session.secure |
bool | true |
HTTPS-only cookies (set false for local dev) |
browser_session.cookie_name |
string | mcp_session |
Cookie name |
browser_session.domain |
string | - | Cookie domain restriction |
browser_session.same_site |
string | lax |
Cookie SameSite mode: lax, strict, or none. none requires secure: true and disables the browser's built-in CSRF defense (see below) |
The portal UI automatically detects OIDC availability and shows an SSO button. API key authentication remains as a fallback. MCP protocol clients are unaffected — browser sessions only apply to the portal HTTP endpoints.
Session Limitations
Sessions are stateless (no server-side store). Individual sessions cannot be revoked. Rotating signing_key invalidates all active sessions. Users must re-authenticate after TTL expires.
CSRF Protection¶
Because portal, admin, and managed-resources mutations can be authenticated by the session cookie, which the browser attaches automatically, the platform enforces token-based CSRF protection on cookie-authenticated, state-changing requests (POST, PUT, PATCH, DELETE):
- On login,
GET /api/v1/portal/mereturns acsrf_tokenbound to the session (an HMAC over the session subject under the signing key; stateless, no server store). - The SPA echoes it in the
X-CSRF-Tokenheader on every non-GETrequest. Requests missing or presenting an invalid token are rejected with403. - Read-only requests (
GET,HEAD,OPTIONS) are exempt, as are API-key / Bearer-authenticated requests, because those credentials are not attached automatically by the browser and so are not vulnerable to CSRF.
SameSite=Lax (the default) is retained as defense-in-depth. Setting same_site: none removes that browser-level defense and makes the X-CSRF-Token check the sole protection; the platform logs a startup warning in that case.
OAuth 2.1 Server (Inbound)¶
The built-in oauth: block turns the platform itself into an OAuth 2.1 authorization server, for clients like Claude Desktop that expect to sign in directly to the MCP server rather than through an existing OIDC provider. For most deployments, auth.oidc or auth.api_keys above are simpler and sufficient. See OAuth 2.1 Server for the full config reference, Dynamic Client Registration guidance, and setup walkthrough.
Signing key¶
When oauth.enabled is set on server.transport: http, oauth.signing_key (base64, 32+ bytes) is required: startup fails without it, because an auto-generated per-process key makes each replica reject tokens minted by its peers. Set oauth.allow_ephemeral_signing_key: true to override this for a single-replica dev setup (unsafe for replicas). On stdio the key is auto-generated when omitted (single-process by construction). To rotate the key without logging users out, see Rotating the signing key.
Rate limiting¶
The unauthenticated /token and /register endpoints are rate limited by default. /token runs a bcrypt compare per attempt and /register runs a bcrypt hash plus a database insert per request, so both are CPU (and, for /register, storage) amplification levers. Each endpoint has a per-client-IP limit plus an internal global backstop that bounds total throughput regardless of how requests attribute to IPs.
oauth:
rate_limit:
enabled: true # default: true; set false to disable limiting
trusted_proxies: # CIDRs whose X-Forwarded-For is trusted
- "10.0.0.0/8"
token:
requests_per_minute: 60 # default: 60
burst: 10 # default: 10
register:
requests_per_minute: 10 # default: 10
burst: 3 # default: 3
| Field | Type | Default | Description |
|---|---|---|---|
rate_limit.enabled |
bool | true |
Enable rate limiting for /token and /register |
rate_limit.trusted_proxies |
list | [] |
CIDRs whose X-Forwarded-For is trusted for client attribution. Empty trusts none: the direct peer address is used and forwarding headers are ignored. Set this to your ingress/load-balancer CIDRs so per-client limiting works behind a proxy without being spoofable |
rate_limit.token.requests_per_minute |
int | 60 |
Per-IP /token limit |
rate_limit.token.burst |
int | 10 |
Per-IP /token burst allowance |
rate_limit.register.requests_per_minute |
int | 10 |
Per-IP /register limit |
rate_limit.register.burst |
int | 3 |
Per-IP /register burst allowance |
On limit, the endpoint returns HTTP 429 with a Retry-After header and an {"error":"slow_down"} JSON body. The global backstop for each endpoint is sized at ten times its per-IP rate and burst.
Dynamically-registered (DCR) clients that are never issued a token are reaped 24 hours after registration by the OAuth store's cleanup routine, bounding oauth_clients growth from the unauthenticated /register endpoint. Pre-registered (config-file) clients are never eligible.
Database Configuration¶
The database block configures the PostgreSQL connection used by audit logging, knowledge capture, session externalization, OAuth persistence, and (optionally) the config store.
| Field | Type | Default | Description |
|---|---|---|---|
dsn |
string | - | PostgreSQL connection string |
max_open_conns |
int | 25 |
Maximum open database connections |
What the database unlocks
Setting dsn enables audit logging, knowledge capture, session externalization, and OAuth persistence. Without it, these features degrade to in-memory or noop implementations.
Config Store¶
When a database is available (database.dsn is set), the platform uses a granular key/value config store. Individual config entries in the config_entries table override file defaults for whitelisted keys.
The store is the authority for these keys: nothing is copied into memory at startup and nothing is patched in place on a write. Every read resolves the key from the store and falls back to the file value when no row exists. A change made through the admin API is therefore in force on every replica as soon as it commits, with no restart and no cross-replica notification, and deleting a row restores the file default everywhere on the next read.
If the store cannot be read, the file-config value is used. A database outage degrades to the YAML the operator shipped rather than to an empty value, so agent instructions and deny patterns survive it.
Whitelisted keys (phase 1):
| Key | Description |
|---|---|
server.description |
Platform description shown in platform-overview prompt and platform_info tool |
server.agent_instructions |
Business/deployment context layered beneath the platform-owned instruction baseline (see below) |
Only whitelisted keys can be set via the admin API. Attempting to set a non-whitelisted key returns 400 Bad Request.
Agent instruction composition¶
The instructions an agent receives via platform_info are composed in layers:
[platform baseline] platform-owned, versioned with the release, always present:
how to operate (search-first / topology discovery, capture
proactively). Names only tools the caller's persona can reach.
+ server.agent_instructions admin: business/deployment context (which backends hold what,
data origins, domain rules)
+ persona suffix/override persona tuning (override replaces the admin layer only)
+ runtime notes e.g. the uploaded-resources hint
The platform baseline is non-overridable and updates automatically when the platform is upgraded, so the operating model never has to be re-authored per deployment. A persona's agent_instructions_override replaces the admin layer only; the baseline is always present. Because the baseline names a tool (search, memory_capture) only when that tool is registered and the persona is allowed to call it, it never points an agent at a tool it cannot use. The agent receives the baseline as part of the composed agent_instructions in the platform_info response; admins can see the baseline on its own read-only in the portal's Agent Instructions screen and via GET /api/v1/admin/config/agent-instructions-baseline.
The config store is selected automatically: setting database.dsn makes config
database-backed (mutations to personas, auth keys, and config entries persist to
PostgreSQL and survive restarts); without a database the config is read-only from
the YAML file and mutations are blocked. There is no separate mode switch.
See Operating Modes for the full comparison of deployment configurations.
Tool Visibility Configuration¶
The tools block controls which tools appear in tools/list responses. This is a visibility filter for reducing LLM token usage — it hides tools from discovery but does not affect authorization. Persona-level tool filtering (see Tool Filtering) remains the security boundary for tools/call.
| Field | Type | Default | Description |
|---|---|---|---|
tools.allow |
array | [] |
Tool name patterns to include in tools/list |
tools.deny |
array | [] |
Tool name patterns to exclude from tools/list |
tools.description_overrides |
map | {} |
Override tool descriptions in tools/list (key: tool name, value: description text). Config values take precedence over built-in defaults, e.g. the built-in trino_query/trino_execute overrides that guide agents to call search first |
Semantics:
- No patterns configured: all tools visible (default)
- Allow only: only matching tools appear
- Deny only: all tools appear except denied
- Both: allow patterns are evaluated first, then deny removes from that set
Patterns use filepath.Match syntax — * matches any sequence of non-separator characters. For example, trino_* matches trino_query, trino_execute, and trino_describe_table.
When to use this
Deployments that only use a subset of toolkits (e.g., only Trino) can hide unused tools to save tokens. A full tool list is 26-33 tools; filtering to trino_* reduces it to 8.
Not a security boundary
Tool visibility filtering only affects tools/list responses. A user who knows a tool name can still call it via tools/call if their persona allows it. Use persona tool filtering for access control.
Admin API Configuration¶
The admin block enables and configures the REST API for system health, configuration management, persona CRUD, auth key management, and audit queries.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable admin REST API. Set false to disable. The routes always require admin-role auth to use, so enabling exposes the route, not open access. |
persona |
string | admin |
Persona required for admin access |
path_prefix |
string | /api/v1/admin |
URL prefix for admin endpoints |
HTTP transport required
The admin API is served over HTTP. It is not available when running in stdio transport mode.
The admin portal provides a web-based dashboard for audit log exploration, tool execution testing, and system monitoring. Enable with portal.enabled: true. When enabled, it is served at /portal/. See Admin API for the full endpoint reference and Admin Portal for the visual guide.
Portal Configuration¶
The portal block enables the asset portal - the web UI plus REST API that persists AI-generated assets (JSX dashboards, HTML reports, SVG charts, exports) to S3 with PostgreSQL metadata tracking. See Admin Portal for branding and public-viewer walkthroughs and User Portal for the end-user feature tour.
portal:
enabled: true
brand_name: "ACME" # Deployment brand; the title becomes "ACME Portal"
brand_url: "https://acme.example.com" # Brand site the portal's brand mark links to
version_url: "https://acme.example.com/changelog" # Optional link target for the header version number
title: "ACME Data Platform" # Overrides the brand-composed title
tagline: "Sign in to access your data." # Login-screen subtitle
oidc_button_label: "Sign in with ACME Keycloak" # Login-screen SSO button text
logo: https://example.com/logo.svg # Logo URL (fallback for both themes)
logo_light: https://example.com/logo-light.svg # Logo for light theme
logo_dark: https://example.com/logo-dark.svg # Logo for dark theme
s3_connection: primary # S3 toolkit instance for asset storage
s3_bucket: portal-assets # Bucket for asset content
s3_prefix: "artifacts/" # Key prefix within the bucket (storage key, unchanged)
public_base_url: "https://portal.example.com" # Base URL for portal links
max_content_size: 10485760 # Max asset size in bytes (default: 10MB)
implementor: # Optional implementor brand (left zone of public viewer header)
name: "ACME Corp"
logo: "https://acme.com/logo.svg"
url: "https://acme.com"
terms_url: "https://example.com/terms" # Optional terms-of-service link (notification email footers)
privacy_url: "https://example.com/privacy" # Optional privacy-policy link (notification email footers)
about_text: "The ACME data portal delivers curated datasets and reports." # Optional footer block on all outgoing email
support_contact: "[email protected]" # Optional help contact (email or URL) rendered with about_text
reply_to: "[email protected]" # Optional Reply-To header on all outgoing email
rate_limit: # Public portal viewer rate limiting
requests_per_minute: 60
burst_size: 10
trusted_proxies: # CIDRs whose X-Forwarded-For is trusted
- "10.0.0.0/8"
export: # trino_export configuration
enabled: true # auto-enabled when portal + trino are configured
max_rows: 100000 # hard row cap per export
max_bytes: 104857600 # hard byte cap (100 MB)
default_timeout: "5m" # default query timeout
max_timeout: "10m" # maximum allowed timeout
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable the portal SPA frontend and asset API |
brand_name |
string | mcpapps brand_name |
Deployment brand. Names the brand once and the portal title, the public-viewer header, the branded denial pages, and the built-in MCP Apps all follow it. Falls back to brand_name in the mcpapps.apps.platform-info.config block when that subsystem is enabled |
brand_url |
string | mcpapps brand_url |
Brand home page. The portal's brand mark (sidebar logo and name) links to it in a new tab; unset leaves the mark inert. Falls back to brand_url in the mcpapps.apps.platform-info.config block when that subsystem is enabled |
version_url |
string | - | Link target for the version number in the portal header (release notes or a changelog). Unset leaves the version as plain text. Served on the unauthenticated branding endpoint, so point it at a URL you are willing to disclose publicly |
title |
string | <brand_name> Portal, else MCP Data Platform |
Sidebar/branding title text. Composed from brand_name when unset, so a branded deployment needs no second string to keep in sync. A brand already ending in "Portal" is not doubled. Only a brand_name set in the portal block composes the title: a brand inherited from the mcpapps block leaves an existing deployment's title unchanged |
tagline |
string | Sign in to access the platform. |
Login-screen subtitle text |
oidc_button_label |
string | Sign in with OIDC |
Login-screen SSO button text |
logo |
string | - | URL to logo image (used for both themes if no theme-specific logo is set) |
logo_light |
string | - | URL to logo for light theme (overrides logo) |
logo_dark |
string | - | URL to logo for dark theme (overrides logo) |
s3_connection |
string | - | Name of the S3 toolkit instance to use for asset storage |
s3_bucket |
string | portal-assets |
S3 bucket for storing asset content |
s3_prefix |
string | artifacts/ |
Key prefix within the bucket |
public_base_url |
string | - | Base URL for portal links returned in save_asset responses |
max_content_size |
int | 10485760 |
Maximum asset size in bytes (10 MB) |
implementor.name |
string | - | Implementor display name shown in the left zone of the public viewer header |
implementor.logo |
string | - | URL to implementor SVG logo (fetched once at startup, max 1 MB) |
implementor.url |
string | - | Clickable link wrapping the implementor name and logo |
terms_url |
string | - | Terms-of-service URL rendered as a small footer link in notification emails. Omitted when unset |
privacy_url |
string | - | Privacy-policy URL rendered as a small footer link in notification emails. Omitted when unset |
about_text |
string | - | A sentence or two about the platform, rendered as a help/about footer block on all outgoing email (HTML and text parts). Gives first-contact recipients sender context and adds body text content filters look for. Omitted when unset |
support_contact |
string | - | Help contact rendered with about_text: an email address (linked as mailto:) or an http(s) URL. Omitted when unset |
reply_to |
string | - | Reply-To address applied to every outgoing email so recipient replies reach a monitored mailbox. Validated at startup; unset leaves the header off |
rate_limit.requests_per_minute |
int | 60 |
Public portal viewer per-IP rate limit |
rate_limit.burst_size |
int | 10 |
Public portal viewer per-IP burst allowance |
rate_limit.trusted_proxies |
list | [] |
CIDRs whose X-Forwarded-For is trusted for client attribution. Empty trusts none: the direct peer address is used and forwarding headers are ignored. Set this to your ingress/load-balancer CIDRs so per-client limiting works behind a proxy without being spoofable. A global backstop bounds total throughput regardless of attribution |
export.enabled |
bool | auto | Enable trino_export tool. Auto-enabled when portal and Trino are both configured. Set false to disable |
export.max_rows |
int | 100000 |
Hard row cap for exports |
export.max_bytes |
int64 | 104857600 |
Hard byte cap for formatted output (100 MB) |
export.default_timeout |
string | 5m |
Default query timeout for exports |
export.max_timeout |
string | 10m |
Maximum allowed query timeout for exports |
Prerequisites
Portal requires database.dsn to be configured for metadata storage, and at least one S3 toolkit instance for asset content storage.
Audit Configuration¶
The audit block controls audit logging of MCP tool calls. By default audit events are written asynchronously to PostgreSQL.
audit:
enabled: true
log_tool_calls: true
log_parameters: true
redact_keys: ["password", "token"]
delivery: async # async (default) | sync
retention_days: 90
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true (when a database is available) |
Enable audit logging. Set false to disable. |
log_tool_calls |
bool | true |
Log MCP tool call events. Set false to keep audit on but skip per-tool-call rows. |
log_parameters |
bool | true |
Capture tool-call arguments on each event. Set false to store a null parameters field when arguments may carry sensitive data that redaction cannot make safe to retain. |
redact_keys |
list of strings | [] |
Top-level argument keys whose values are replaced with [REDACTED] before the event leaves the request path. Matching is case-insensitive; nested keys are not matched (top-level only). |
delivery |
string | async |
Store-write path: async (best-effort, never blocks the tool call) or sync (writes on the request goroutine for backpressure and zero queue drops). See below. |
retention_days |
int | 90 |
Days to retain audit events |
Requires database
Audit logging requires database.dsn to be configured. With a database available and no audit: block, both audit and per-tool-call logging are on by default. Setting enabled: false disables audit entirely; log_tool_calls: false keeps audit on but stops recording per-tool-call events.
Delivery semantics and data captured¶
What is captured per event. Each audit event records: identifiers (id, request_id, session_id, user_id, user_email, persona), the call target (tool_name, toolkit_kind, toolkit_name, connection, event_kind), the raw tool-call arguments (parameters), the outcome (success, error_message, authorized), timing and size (timestamp, duration_ms, request_chars, response_chars, content_blocks), transport metadata (transport, source), and enrichment accounting (enrichment_applied, enrichment_tokens_full, enrichment_tokens_dedup, enrichment_mode, enrichment_match_kind).
Sensitive data in parameters. The parameters field stores tool-call arguments verbatim, including complete SQL text and anything embedded in it. Unless you set redact_keys (to mask named top-level values) or log_parameters: false (to drop the field entirely), sensitive values pasted into a query or argument are retained in the audit table. A built-in baseline additionally masks the well-known keys password, secret, token, api_key, authorization, and credentials, but this is a safety net, not a substitute for configuring redact_keys for your own sensitive argument names.
Async delivery (default). Events are enqueued on a bounded in-memory writer and persisted by a single background goroutine, so a tool call is never blocked by store latency. This is best-effort: under a sustained store outage or a crash, events in the queue are dropped rather than retained. Every lost event increments the audit_events_dropped_total metric, which also covers writes that fail or exceed the per-write timeout.
Sync delivery. Set delivery: sync when a compliance posture requires durability over latency. Each event is written on the request goroutine with a per-write timeout (5s), so a slow store applies backpressure to the tool call (it waits) rather than shedding events: there are no queue-overflow drops. A store write that still fails or times out is logged and counted (audit_events_dropped_total) but, as in async mode, never fails the tool call: audit must not break tools. Two tradeoffs to weigh: under a stalled store every tool call blocks for up to the timeout before returning, and sync writes draw from the same database connection pool as OAuth, sessions, and portal queries, so under load against a slow store they can contend with those subsystems (the async writer's single drain goroutine caps audit at one connection and avoids both). Graceful shutdown cancels in-flight sync writes.
See Audit Logging for query examples and retention details.
Notifications Configuration¶
The notifications block controls the email-notification substrate: the
delivery queue, send worker, and daily-digest scheduling. See Email
Notifications for the full feature (admin SMTP settings,
per-user preferences, delivery semantics).
notifications:
enabled: true # default: on when a database is available
digest_hour_utc: 13 # UTC hour daily digests are sent
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable email notifications. Set false to disable enqueue and delivery entirely. |
digest_hour_utc |
int | 13 |
UTC hour of day (0-23) at which daily-digest emails are scheduled. Out-of-range values fall back to the default. |
Requires database
Email notifications require database.dsn. The SMTP connection itself
is not configured here: admins set host, credentials, and TLS mode at
runtime in the portal (Admin, then Settings) or via
/api/v1/admin/settings/smtp, with the password encrypted at rest.
The knowledge review-queue alert threshold is admin-configured the same
way, under /api/v1/admin/settings/review-queue-alert; see
Review queue alerts.
Session Configuration¶
The sessions block controls how MCP session state is stored. In-memory sessions are lost on restart; database-backed sessions survive restarts and support multi-replica deployments.
| Field | Type | Default | Description |
|---|---|---|---|
store |
string | memory |
Backend: memory or database |
ttl |
duration | streamable session_timeout |
Session lifetime |
cleanup_interval |
duration | 1m |
Cleanup routine interval |
Requires database
The database store requires database.dsn to be configured.
See Session Externalization for architecture details and multi-replica considerations.
Explicit Session Handles¶
The sessions.handles block controls explicit session handles (issue #792). When enabled, platform_info mints a session_id that the model passes back as an ordinary argument on every subsequent tool call. This is the pattern the MCP 2026-07-28 release candidate recommends after removing the protocol-level session and the Mcp-Session-Id header (SEP-2567).
This makes platform_info structurally unskippable (no handle exists until it is called, and gated tools require one), gives audit and provenance a deliberate session key, and keeps the platform working unchanged when clients move to the sessionless protocol.
sessions:
handles:
enabled: true # mint, advertise, and validate handles (default on)
ttl: 8h # handle lifetime, refreshed on use
require: true # a gated caller must have an established session
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Mint a session_id from platform_info, advertise it on every tool's input schema, validate and strip it on each call. Set false for byte-identical legacy transport-session behavior. |
ttl |
duration | 8h |
Handle lifetime, refreshed on use. |
require |
bool | true |
Require a gated caller to have an established session, not that a handle is threaded on every call. A call carrying a valid handle uses it; a call without one adopts the caller's own most-recently-active session, resolved from their authenticated identity, so an MCP App's sandboxed calls (which cannot thread the handle) are scoped rather than refused. Only a caller with no session at all is refused with SESSION_REQUIRED, which keeps platform_info structurally required for a genuinely fresh agent. The model still threads and validates the handle exactly as before, so nothing about a compliant agent's behavior changes; the fallback only affects calls that arrive without a handle, which in practice are an app's. Set false to drop the requirement entirely, where a handle-less call falls back to the transport session. |
Every tool advertises the injected session_id argument except platform_info (which mints it). Upstream toolkits never see the argument: the platform strips it before the handler runs. A handle presented by a different authenticated identity, or an unknown/expired handle, is refused with SESSION_EXPIRED. With require: true, platform_info mints and threads a handle on every transport (stdio, SSE, Streamable HTTP); there is no stdio carve-out. The mcp_session_resolution_total{source} metric (explicit, transport, stdio, none) shows how much traffic still relies on a transport session; with require: true only explicit and none occur on gated tools.
Toolkit Configuration¶
Trino¶
toolkits:
trino:
enabled: true
instances:
primary: # Instance name (can be any identifier)
host: trino.example.com
port: 443
user: analyst
password: ${TRINO_PASSWORD}
catalog: hive
schema: default
ssl: true
ssl_verify: true
timeout: 120s
default_limit: 1000
max_limit: 10000
read_only: false
connection_name: primary
default: primary
| Field | Type | Default | Description |
|---|---|---|---|
host |
string | required | Trino coordinator hostname |
port |
int | 8080 (443 if SSL) | Trino coordinator port |
user |
string | required | Trino username |
password |
string | - | Trino password (if auth enabled) |
catalog |
string | - | Default catalog |
schema |
string | - | Default schema |
ssl |
bool | false |
Enable SSL/TLS |
ssl_verify |
bool | true |
Verify SSL certificates |
timeout |
duration | 120s |
Query timeout |
default_limit |
int | 1000 |
Default row limit for queries |
max_limit |
int | 10000 |
Maximum allowed row limit |
read_only |
bool | false |
Reject write SQL on this connection. Set per instance: the other instances of the same toolkit are unaffected, and a call that omits connection is judged by the default instance's setting |
connection_name |
string | instance name | Display name for this connection |
descriptions |
map | {} |
Override tool descriptions for this instance (key: tool name, value: description text) |
read_only became per connection in #1269. Before that it was read from the
default instance alone and applied to the whole Trino toolkit, which cut both
ways: read_only: true on the default instance refused write SQL on every
Trino connection, and read_only: true on any other instance did nothing. A
deployment that was relying on the default instance's read_only to cover its
other Trino connections must now set read_only: true on each connection it
wants refused.
Connections stored in the database (Admin > Connections) carry the same key, with a Read Only toggle in the Trino connection form. A connection the toolkit holds no setting for — one just added, or a name that is not configured — refuses write SQL until its setting is recorded.
DataHub¶
toolkits:
datahub:
enabled: true
instances:
primary:
url: https://datahub.example.com
token: ${DATAHUB_TOKEN}
timeout: 30s
default_limit: 10
max_limit: 100
max_lineage_depth: 5
connection_name: primary
read_only: true
default: primary
| Field | Type | Default | Description |
|---|---|---|---|
url |
string | required | DataHub GMS URL |
token |
string | - | DataHub access token |
timeout |
duration | 30s |
API request timeout |
default_limit |
int | 10 |
Default search result limit |
max_limit |
int | 100 |
Maximum search result limit |
max_lineage_depth |
int | 5 |
Maximum lineage traversal depth |
connection_name |
string | instance name | Display name for this connection |
read_only |
bool | false |
Restrict to read operations (disables write tools) |
descriptions |
map | {} |
Override tool descriptions for this instance (key: tool name, value: description text) |
S3¶
toolkits:
s3:
enabled: true
instances:
primary:
region: us-east-1
endpoint: "" # Custom endpoint for MinIO, etc.
public_endpoint: "" # Public endpoint for presigned URLs (see below)
access_key_id: ${AWS_ACCESS_KEY_ID}
secret_access_key: ${AWS_SECRET_ACCESS_KEY}
session_token: ""
profile: "" # AWS profile name
use_path_style: false # Use path-style URLs
timeout: 30s
disable_ssl: false
read_only: true # Restrict to read operations
max_get_size: 10485760 # 10MB
max_put_size: 104857600 # 100MB
connection_name: primary
bucket_prefix: "" # Filter to buckets with this prefix
default: primary
| Field | Type | Default | Description |
|---|---|---|---|
region |
string | us-east-1 |
AWS region |
endpoint |
string | - | Custom S3 endpoint for data operations (for MinIO, SeaweedFS, etc.) |
public_endpoint |
string | - | Public-facing endpoint used only to sign presigned URLs (s3_presign_url). When set to an externally resolvable address, presigned URLs are signed against it instead of endpoint, while data traffic keeps using endpoint. Empty falls back to endpoint. |
access_key_id |
string | - | AWS access key ID |
secret_access_key |
string | - | AWS secret access key |
session_token |
string | - | AWS session token (for temporary creds) |
profile |
string | - | AWS credentials profile name |
use_path_style |
bool | false |
Use path-style S3 URLs |
timeout |
duration | 30s |
Request timeout |
disable_ssl |
bool | false |
Disable SSL (for local testing) |
read_only |
bool | false |
Restrict to read operations |
max_get_size |
int64 | 10485760 |
Max bytes to read from objects |
max_put_size |
int64 | 104857600 |
Max bytes to write to objects |
connection_name |
string | instance name | Display name for this connection |
bucket_prefix |
string | - | Only show buckets with this prefix |
descriptions |
map | {} |
Override tool descriptions for this instance (key: tool name, value: description text) |
MCP Gateway¶
The mcp toolkit kind proxies upstream MCP servers and re-exposes their
tools as <connection_name>__<remote_tool>. Connections are managed
exclusively through the admin portal — no per-instance config goes in
platform.yaml. The only YAML knob is enabled, which turns the kind on.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Register the gateway toolkit kind. When false, mcp connections in connection_instances are ignored. |
Required environment for the OAuth + at-rest encryption path:
| Variable | Required for | Notes |
|---|---|---|
ENCRYPTION_KEY |
Encrypted credentials in connection_instances, gateway_oauth_tokens, oauth_pkce_states, and the SMTP password in platform_settings |
32 bytes of key material, accepted in three forms: 64 hex characters, 44-character base64, or 32 raw bytes (set via printf / file). Without it, sensitive fields are stored in plaintext and the platform logs a warning. Required for any production gateway deployment. |
DATABASE_URL |
OAuth authorization_code grant (refresh-token persistence) and multi-replica deployments |
Without a database, OAuth tokens live in process memory only and don't survive restarts. Multi-replica deployments additionally need this so PKCE state is shared across pods. |
See Gateway Toolkit for the connection-config reference,
auth modes (none/bearer/api_key/oauth), OAuth grant types
(client_credentials and authorization_code + PKCE), and the
cross-enrichment rule schema.
Cross-Enrichment Configuration¶
enrichment:
trino_semantic_enrichment: true # Add DataHub context to Trino results
datahub_query_enrichment: true # Add Trino availability to DataHub results
s3_semantic_enrichment: true # Add DataHub context to S3 results
datahub_storage_enrichment: true # Add S3 availability to DataHub results
unwrap_json: true # Auto-unwrap single-row VARCHAR-of-JSON (default: true)
column_context_filtering: true # Only include SQL-referenced columns (default: true)
estimate_row_counts: false # Run COUNT(*) for availability enrichment (default: false)
semantic_fallback: false # Suggest similar tables on a URN miss (default: false)
semantic_fallback_top_k: 1 # Suggestions per miss, 1-10 (default: 1)
# Memory-enrichment payload budget (issue #761): keeps recalled memories a
# supporting note rather than crowding out the analyzed data.
memory_limit: 5 # Max memory records recalled per tool call (default: 5)
memory_context_budget_bytes: 1500 # Byte budget for rendered summaries; over-budget records become fetchable stubs; 0 disables (default: 1500)
memory_summary_bytes: 280 # Per-record summary excerpt cap; 0 = full content (default: 280)
# Session metadata deduplication (avoids repeating metadata for same table)
session_dedup:
enabled: true # Default: true
mode: reference # reference (default), summary, none
entry_ttl: 5m # Defaults to semantic.cache.ttl
session_timeout: 30m # Defaults to server.streamable.session_timeout
| Field | Type | Default | Description |
|---|---|---|---|
trino_semantic_enrichment |
bool | true |
Enrich Trino results with DataHub metadata. Default on; read-only and no-ops without a semantic provider. Set false to disable. |
datahub_query_enrichment |
bool | true |
Add query availability to DataHub search results. Default on; set false to disable. |
s3_semantic_enrichment |
bool | true |
Enrich S3 results with DataHub metadata. Default on; set false to disable. |
datahub_storage_enrichment |
bool | true |
Add S3 availability to DataHub results. Default on; set false to disable. |
unwrap_json |
bool | true |
Auto-unwrap single-row VARCHAR-of-JSON results |
column_context_filtering |
bool | true |
Limit column enrichment to SQL-referenced columns |
estimate_row_counts |
bool | false |
Run SELECT COUNT(*) when reporting table availability, so enriched DataHub results carry an estimated row count. Also what lets the insight review path state a row count beside a pending claim, and what the advisory claim-conflict marker compares against (Knowledge governance). Off by default: COUNT(*) can trigger a full table scan and make search enrichment very slow |
semantic_fallback |
bool | false |
When a URN-equality lookup misses, fall back to similarity search and surface the top hit as a suggested match, annotated match_kind=semantic so the model knows it was inferred rather than resolved. Audit rows record enrichment_match_kind so operators can measure the false-positive rate. Requires a semantic provider supporting the semantic search mode (DataHub does) |
semantic_fallback_top_k |
int | 1 |
Suggestions surfaced per miss when semantic_fallback is on. Clamped to 1-10 to keep suggested-match output bounded |
memory_limit |
int | 5 |
Max memory records recalled and rendered into memory_context per tool call |
memory_context_budget_bytes |
int | 1500 |
Byte budget for the rendered memory summaries; records beyond it are listed as compact id+reference stubs in memory_context_omitted (still fetchable, at least one always rendered). 0 disables the budget |
memory_summary_bytes |
int | 280 |
Per-record summary-first excerpt cap; the full record is fetchable via its mcp:memory:<id> reference. 0 renders full content |
session_dedup.enabled |
bool | true |
Whether session dedup is active |
session_dedup.mode |
string | reference |
Repeat query content: reference, summary, none |
session_dedup.entry_ttl |
duration | semantic cache TTL | How long a table stays "already sent" |
session_dedup.session_timeout |
duration | streamable session timeout | Idle session cleanup interval |
Tuning Configuration¶
Static operational rules that shape agent behavior via platform_info guidance and tool descriptions.
| Field | Type | Default | Description |
|---|---|---|---|
rules.quality_threshold |
float | 0.7 |
Minimum DataHub quality score below which a warning is surfaced |
prompts_dir |
string | - | Directory of additional prompt resource files |
Search-First Gate Configuration¶
A hard gate that refuses query tools until the agent calls search in the session. When a query tool (trino_query, trino_execute) is called before any discovery tool, the tool handler does not run; a SEARCH_REQUIRED error result is returned instructing the agent to call search first. Once search has been called at least once in a session, every subsequent query tool call in that session proceeds normally with no further check. The gate is enabled by default; set require_search: false to disable gating (and hinting) entirely.
workflow:
require_search: false # Default: true (gate on). false disables it entirely.
# discovery_tools: [] # Tools that satisfy the gate (defaults to search + the datahub_* tools)
# query_tools: [] # Tools that are gated (defaults to trino_query, trino_execute)
| Field | Type | Default | Description |
|---|---|---|---|
require_search |
bool | true |
Enable the search-first hard gate. false disables gating with no block and no hint. |
discovery_tools |
array | search + the datahub_* tools |
Tool names that satisfy the gate. search is the front door agents are steered toward, and the datahub_* discovery tools also count so a persona granted datahub_* (but not search) is not locked out. |
query_tools |
array | trino_query, trino_execute |
Tool names gated by discovery |
Behavior change
require_search replaces the former workflow.require_discovery_before_query and its warn-after-execution behavior. It is a breaking rename (not aliased) and a hard gate: a deployment that never touched workflow gating will begin refusing trino_query/trino_execute until search is called once per session. The older tuning.rules.require_datahub_check static hint has been removed.
Session Gate Configuration¶
A hard gate that refuses every non-exempt tool until the agent calls the session-initialization tool (platform_info by default) once in the session. Before the init tool has run, any other tool call is short-circuited before its handler executes and a SETUP_REQUIRED error result is returned (error category setup_required) telling the agent to call the init tool first, then retry. Once the init tool has been called in a session, subsequent tool calls proceed normally until the session's TTL expires.
Unlike most platform sections, this gate is off by default: enabled is a plain bool, so an absent session_gate block means the gate is disabled (it does not follow the default-on *bool convention used by sections like progress or audit).
session_gate:
enabled: true # Default: false. true activates the gate.
init_tool: platform_info # Tool that initializes the session (default: platform_info)
exempt_tools: # Tools that bypass the gate entirely
- list_connections
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Activate the session-initialization gate. |
init_tool |
string | platform_info |
The tool that initializes a session. Calling it records the session as initialized; it is always exempt from the gate. |
exempt_tools |
array | (empty) | Tool names that bypass the gate and may be called before the init tool. |
The gate's memory of an initialized session expires after the session TTL, which is derived from Session Configuration (sessions.ttl, falling back to the Streamable HTTP session timeout) rather than from a field on this block.
Distinct from the search-first gate
The session gate and the search-first gate are independent. The session gate requires an init tool (platform_info) before any tool; the search-first gate requires a discovery tool (search) before query tools. Both can be enabled at once.
Superseded by explicit session handles
When explicit session handles (sessions.handles) are enabled, the session gate is skipped: handle resolution enforces initialization instead, and the gate's exempt_tools are carried into the handle resolver. Enabling both does not double-gate.
Tool-Call Rate Limiting¶
A per-identity safety net on authenticated tools/call requests. It bounds a runaway agent loop or a compromised account before it can saturate the audit pipeline, the shared database pool, or an upstream (Trino, DataHub, S3, a proxied MCP server). It is not a throughput throttle: the default limit is generous enough that ordinary interactive and agent use never touches it. When a user exceeds the limit, the offending call is short-circuited before its handler runs and a RATE_LIMITED error result is returned (error category rate_limited) with a retry hint, so an agent backs off and retries rather than seeing a transport failure. platform_info is always exempt so a throttled agent can re-read platform guidance.
The limit is keyed on the authenticated user, not the client IP. A multi-user connector delivers every user's traffic from one egress address, so per-IP limiting would be both useless (one bucket for everyone) and harmful (one busy user starves the rest); identity keying matches the abuse shape the limiter exists to catch (a single runaway authenticated principal), regardless of source address. Callers with a shared/anonymous identity (auth disabled) fall back to a per-session key; a call with no attributable identity is not limited (fail-open, since the call has already passed auth).
Enabled by default; set rate_limit.enabled: false to remove the limiter from the chain entirely.
rate_limit:
enabled: true # Default: true. false removes the limiter.
requests_per_minute: 240 # Default: 240. Sustained per-user tools/call rate.
burst: 60 # Default: 60. Largest instantaneous per-user burst.
exempt_tools: # Tools never limited (platform_info is always exempt)
- search
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Enable the per-user tool-call limiter. false removes the middleware from the chain. |
requests_per_minute |
int | 240 |
Sustained per-user tools/call rate (token refill). 240 is 4 calls/second per user. |
burst |
int | 60 |
Token-bucket depth: the largest burst a single user may issue before the sustained rate governs. |
exempt_tools |
array | (empty) | Tool names never rate limited, in addition to platform_info (which is always exempt). |
Each refusal increments the mcp_rate_limited_total metric and logs a warning naming the throttled identity and tool.
Per-replica limit
The token bucket is in-memory per replica: behind a load balancer the effective ceiling is (replica count x the configured limit). This is intentional for a backstop: distributed coordination (Redis/DB round-trips on the hot tool path) is not warranted to bound abuse that per-replica limiting already bounds. Size the limit with the per-replica semantics in mind; see Tuning and Scaling.
Distinct from the OAuth endpoint limiter
This top-level rate_limit: block governs authenticated MCP tools/call requests and is keyed on identity. It is unrelated to the oauth.rate_limit block, which is a per-IP limiter on the unauthenticated /token and /register OAuth endpoints.
Semantic and Query Provider Configuration¶
Specify which toolkit instance provides semantic metadata and query execution:
semantic:
provider: datahub # Provider type: datahub or noop
instance: primary # Which DataHub instance to use
cache:
enabled: true
ttl: 5m
query:
provider: trino # Provider type: trino or noop
instance: primary # Which Trino instance to use
storage:
provider: s3 # Provider type: s3 or noop
instance: primary # Which S3 instance to use
| Field | Type | Default | Description |
|---|---|---|---|
semantic.provider |
string | - | Provider type: datahub or noop |
semantic.instance |
string | - | Toolkit instance name |
semantic.cache.enabled |
bool | false |
Enable semantic metadata caching |
semantic.cache.ttl |
duration | 5m |
Cache TTL |
query.provider |
string | - | Provider type: trino or noop |
query.instance |
string | - | Toolkit instance name |
storage.provider |
string | - | Provider type: s3 or noop |
storage.instance |
string | - | Toolkit instance name |
URN mapping (semantic.urn_mapping, query.urn_mapping) translates catalog and platform names when Trino and DataHub name the same data differently - see Trino to DataHub for the full config reference. Lineage-aware enrichment (semantic.lineage) inherits column metadata from upstream datasets when a table's own columns lack it - see Lineage Inheritance for the full config reference and worked examples.
Persona Configuration¶
Personas define tool access based on user roles. The security model follows a default-deny approach.
Persona names are keyed directly under personas: (the config's Definitions
field is an inline map, so there is no definitions: wrapper key).
personas:
analyst:
display_name: "Data Analyst"
roles: ["analyst", "data_engineer"]
tools:
allow: ["*"]
deny: ["*_delete_*", "*_drop_*"]
connections:
allow: ["*"]
admin:
display_name: "Administrator"
roles: ["admin"]
tools:
allow: ["*"]
connections:
allow: ["*"]
| Field | Type | Default | Description |
|---|---|---|---|
<name> |
map | - | Named persona configuration, keyed directly under personas: |
<name>.display_name |
string | - | Human-readable name |
<name>.roles |
array | - | Roles that map to this persona |
<name>.tools.allow |
array | [] |
Allowed tool patterns |
<name>.tools.deny |
array | [] |
Denied tool patterns |
<name>.context.description_prefix |
string | - | Prepended to platform description |
<name>.context.description_override |
string | - | Replaces platform description entirely |
<name>.context.agent_instructions_suffix |
string | - | Appended to the admin agent_instructions layer |
<name>.context.agent_instructions_override |
string | - | Replaces the admin agent_instructions layer only; the platform baseline is always present |
No persona means no access
A caller whose roles match no persona has no access at all: tool calls resolve to the built-in deny-all persona and are refused, the portal answers 403 with a branded page, and the managed-resources API refuses the request. There is no fallback persona, so every user who should reach anything needs a role one of your personas lists.
personas.default_persona was removed. It assigned its persona to every caller whose roles matched nothing, including accounts carrying no claims. A config that still sets it is refused at startup with an error naming the key.
Some tools must be granted together
search returns references and fetch is the only tool that dereferences one; memory_capture and apply_knowledge both write into a body of knowledge that search is the only way back into. Granting one half of a pair without the other leaves the persona able to start something it can never finish, so the server logs a warning naming the persona, the missing tool, and the fix — at startup and on every persona write. Prefer allow: ["*"] with a targeted deny, as analyst does above: an enumerated allow-list silently loses each tool a later upgrade adds. See Personas: some tools are a unit.
Knowledge Capture Configuration¶
Knowledge capture records domain knowledge shared during AI sessions and provides a workflow for applying approved insights to the DataHub catalog. See Knowledge Capture for the full feature documentation.
knowledge:
enabled: true
apply:
enabled: true
datahub_connection: primary
require_confirmation: true
reflexive_capture:
enabled: true
pages:
dedup_threshold: 0.85
dedup_disabled: false
oversize_bytes: 16384
oversize_sections: 12
catalog_index:
enabled: true
sync_interval: 30m
max_entries: 5000
verifiable_insights: true
search_provider_timeout: 5s
search_embed_timeout: 5s
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true (when a database is available) |
Enable the knowledge review and write-back toolkit (apply_knowledge). Knowledge capture lives in the memory toolkit (memory_capture) and is enabled with the memory layer, not this flag |
apply.enabled |
bool | true (when a database is available) |
Enable the apply_knowledge tool for admin review and catalog write-back. Set false to disable. Still gated behind database availability. |
apply.datahub_connection |
string | - | DataHub instance name for write-back operations |
apply.require_confirmation |
bool | false |
Require explicit confirm: true on apply actions |
pages.dedup_threshold |
float | 0.85 |
Cosine similarity, in [0,1], at or above which creating a knowledge page is blocked as a near-duplicate of an existing one. The gate acts only when a real embedding provider is configured, since cosine similarity is undefined without one. A non-positive value selects the default; disable the gate with dedup_disabled rather than by zeroing this |
pages.dedup_disabled |
bool | false |
Turn the duplicate gate off entirely. Explicit, so "no gate" is never confused with "left at default" |
pages.oversize_bytes |
int | 16384 |
Body size, in bytes, at or above which a page write returns a non-blocking suggestion to split it. A negative value disables this arm. This is an editorial nudge toward focused, cross-linked pages, not a bound on what search can reach: a page's content is embedded as chunks sized to the provider's input budget, so a page of any size is semantically searchable end to end |
pages.oversize_sections |
int | 12 |
Markdown heading count at or above which the same split suggestion fires. A negative value disables this arm |
reflexive_capture.enabled |
bool | true |
Auto-capture a "misconception + fix" correction when a Trino query errors and a later related same-session query on the same connection succeeds (#635). Source automation, reviewed sink-class (enters review, never live), gated by the persona's memory_capture grant. Default-on when the memory subsystem is available; set false to disable |
catalog_index.enabled |
bool | true |
Index the catalog's dataset descriptions into the platform's own semantic search, so a fact applied to a description is reachable from a topical query that names no entity. Requires a DataHub semantic provider, a database, and an embedding provider; without any of those it is inert. Set false to opt out, leaving catalog datasets ranked by DataHub's own keyword search alone |
catalog_index.sync_interval |
duration | 30m |
How often the catalog is re-enumerated into that index. The sweep runs as a background index job, so raising it trades freshness for load on DataHub; lowering it makes a newly applied description searchable sooner |
catalog_index.max_entries |
int | 5000 |
Cap on how many datasets are mirrored. The cap bounds both the table and one sweep's working set. A catalog larger than this indexes the first max_entries datasets in catalog order and logs the truncation |
verifiable_insights |
bool | true |
Mark a delivered insight as checkable: when the catalog entity its claim is about resolves to a queryable table, the delivered record carries a verifiable block naming that table and connection, on every delivery surface (search insight hits, fetch of mcp:insight:<id>, and the memory_context enrichment block). Additive and absent whenever nothing resolves, so a deployment with no query provider is unaffected; it honors the persona connection boundary, and resolves without running the COUNT(*) that enrichment.estimate_row_counts enables. Set false to deliver insights with no marker |
search_provider_timeout |
duration | 5s |
Per-provider deadline for the search fan-out arms. Each knowledge source (catalog, memory, insights, endpoints, …) is bounded by this, so one slow source drops out as a collected error while the rest still return, instead of stalling the whole search. Set a negative duration to disable the bound (a search then waits for its slowest provider). |
search_embed_timeout |
duration | 5s |
Deadline for the serial intent-embedding step in search, independent of search_provider_timeout. A slow or unreachable embedder degrades to lexical ranking rather than stalling the search; because that silently loses semantic relevance, this knob lets you give a slow (cold or CPU-only) embedder more headroom to preserve hybrid ranking without loosening the fan-out bound. Set a negative duration to disable the bound. |
Prerequisites
Knowledge capture requires database.dsn to be configured. The apply_knowledge tool requires the admin persona.
Memory Layer Configuration¶
The memory layer provides persistent memory for agent and analyst sessions with vector search, cross-enrichment, and staleness detection. See Memory Layer for the full feature documentation.
memory:
enabled: true
embedding:
provider: ollama
ollama:
url: "http://localhost:11434"
model: "nomic-embed-text"
timeout: 30s
max_input_bytes: 6000
staleness:
enabled: true
interval: 15m
batch_size: 50
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true (when database available) |
Enable memory layer. Set false to explicitly disable. |
embedding.provider |
string | noop |
Embedding provider: ollama or noop |
embedding.ollama.url |
string | http://localhost:11434 |
Ollama API base URL |
embedding.ollama.model |
string | nomic-embed-text |
Ollama embedding model (768-dim) |
embedding.ollama.timeout |
duration | 30s |
Embedding API timeout |
embedding.ollama.max_input_bytes |
int | 6000 |
Per-text input cap (bytes) applied before embedding. The platform truncates input itself on a UTF-8 boundary because Ollama's truncate flag is unreliable for content over the model's context. The default sits below nomic-embed-text's ~2048-token boundary; raise it only for a larger-context model. Only the embedded text is trimmed; stored content is unaffected. Knowledge pages are not trimmed at all: this value sizes the chunks a page's content is embedded as, so raising it for a larger-context model widens those chunks. |
staleness.enabled |
bool | false |
Enable background staleness watcher |
staleness.interval |
duration | 15m |
Staleness check interval |
staleness.batch_size |
int | 50 |
Records per check cycle |
Prerequisites
Memory requires database.dsn to be configured and the pgvector PostgreSQL extension installed. Memory tools are opt-in per persona (memory_* in tools.allow).
Ollama batch endpoint
Batch embedding calls (API gateway spec indexing) issue a single POST /api/embed request per batch against modern Ollama servers. Servers that lack the batch endpoint (response: HTTP 404) are detected on the first call; the platform logs a WARN and transparently falls back to one POST /api/embeddings request per text. Upgrading the Ollama server is recommended for substantially faster batch indexing on multi-spec catalogs. Memory writes embed one record at a time and always use /api/embeddings.
API Gateway Configuration¶
Cluster-wide tuning for the API gateway toolkit's background work. Connection-level configuration (base_url, auth_mode, credentials) lives in the connection store; this section is for knobs that apply to every API connection.
apigateway:
embed_jobs:
workers: 1
embed_timeout: 5m
lease_duration: 10m
batch_size: 32
retention_days: 14
| Field | Type | Default | Description |
|---|---|---|---|
embed_jobs.workers |
int | 1 |
Number of embedding-worker goroutines per pod. Each goroutine independently claims and processes jobs; the lease + SKIP LOCKED predicate in the queue's claim path prevents two goroutines (in the same pod or across pods) from picking the same job. Increase to 2-4 for deployments with many specs and a fast embedder; CPU-only embedders typically saturate at 1 because the bottleneck is the embedding model. |
embed_jobs.embed_timeout |
duration | 5m |
HTTP timeout the worker applies to its batched /api/embed POSTs against Ollama. Scoped to the worker only so the shared 30s memory.embedding.ollama.timeout continues to govern request-path callers (memory recall, memory_capture, etc.); a wedged Ollama therefore fails MCP tool calls in 30s while the worker tolerates the longer batched-inference floor. Lower this on GPU embedders to tighten the failure floor. |
embed_jobs.lease_duration |
duration | 10m |
Time a claim stamps on a job; the worker heartbeat re-stamps it at lease_duration / 3 cadence so a long embed pass is not reaped mid-flight. Must be greater than embed_timeout. Caps "pod went silent", not "embed batch is slow". |
embed_jobs.batch_size |
int | 32 |
Texts per upstream EmbedBatch call. Sets the starting chunk size only: when a chunk exceeds embed_timeout, the worker automatically halves it and retries the sub-chunks down to a floor of one text, so a batch too large for a slow (e.g. CPU-only) embedder converges to a size that completes and persists partial progress instead of failing the whole unit at a fixed size. Non-timeout provider errors (5xx, malformed response) still fail fast without subdividing. Lower this to skip the initial shrink cycles on a known-slow embedder; raise it on GPU embedders where per-call overhead dominates. |
embed_jobs.retention_days |
int | 14 |
Age past which finished index_jobs history is purged by the background retainer: succeeded rows and failed rows that were resolved (superseded by a later success or operator-dismissed). The reconciler records one row per unit per sweep, so this keeps the table bounded while preserving a recent window for the admin Indexing dashboard's throughput, latency, and job-log views. Open failures (failed with no resolved_at) and in-flight jobs (pending / running) are never purged regardless of age. 0 uses the default (14); a negative value disables retention (history grows unbounded, for externally-managed cleanup). |
MCP Apps Configuration¶
MCP Apps provide interactive UI components that enhance tool results. The platform provides the infrastructure; you provide the HTML/JS/CSS apps.
mcpapps:
enabled: true
apps:
query_results:
enabled: true
assets_path: "/etc/mcp-apps/query-results"
tools:
- trino_query
- trino_execute
csp:
resource_domains:
- "https://cdn.jsdelivr.net"
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable MCP Apps infrastructure |
apps |
map | - | Named app configurations |
apps.<name>.enabled |
bool | true |
Enable this app |
apps.<name>.assets_path |
string | required | Absolute path to app directory |
apps.<name>.tools |
array | required | Tools this app enhances |
apps.<name>.csp.resource_domains |
array | - | Allowed CDN origins |
See MCP Apps Configuration for complete options.
Resource Templates Configuration¶
Resource templates expose platform data as browseable, parameterized MCP resources using RFC 6570 URI templates.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | true |
Serve the resource templates below and the DataHub-to-Trino resource links. Read-only, so it is on by default; set false to disable |
When enabled, the platform registers these resource templates:
schema://{catalog}.{schema}/{table}— Table schema with column types and descriptionsglossary://{term}— Glossary term definitionsavailability://{catalog}.{schema}/{table}— Query availability and row counts
Clients that support resource browsing (e.g., Claude Desktop) will show these as navigable resources alongside tools.
Managed Resources¶
Managed resources are the human-uploaded files people attach through the portal
(reference material, specifications, images), stored as rows in PostgreSQL with
their bytes in S3 and served back over MCP and the REST API. They are a separate
subsystem from the read-only templates above, configured under
resources.managed. See Content Model for how
they relate to knowledge pages and assets.
resources:
managed:
enabled: true # auto-enabled when a database is available
uri_scheme: "mcp" # URI prefix for resource URIs
s3_connection: "primary" # name of the S3 toolkit instance holding the blobs
s3_bucket: "managed-resources"
max_versions: 10 # content revisions kept per resource
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | auto | Enable managed resources. Unset means enabled whenever a database is configured; set false to disable the subsystem outright |
uri_scheme |
string | mcp |
Scheme of the URIs minted for managed resources (<scheme>://global/<category>/<filename>, and the persona/user equivalents). Changing it after resources exist changes the URIs the platform serves for them |
s3_connection |
string | first configured S3 instance | Name of the S3 toolkit instance used for blob storage |
s3_bucket |
string | managed-resources |
Bucket the uploaded bytes are written to |
max_versions |
int | 10 |
Content revisions a resource keeps, counting the current one. A revision past the cap prunes the oldest stored file; live content is never pruned. A non-positive value selects the default, and anything below 2 is raised to 2, since a cap of 1 would keep no history at all |
Managed resources require a database. With none configured the block has no effect, and the platform runs the read-only templates alone.
Argument Autocompletion¶
The platform answers the MCP completion/complete request so clients that support autocompletion (the MCP Inspector, IDE clients) can suggest valid values as a user types a prompt argument or a resource-template variable. There is nothing to configure: the capability is advertised automatically whenever prompts or resource templates are available, and it uses the catalog the platform already knows.
Completions are served for:
- Prompt arguments, routed by argument name across built-in and database prompts:
dataset: dataset names from the semantic search index (e.g. thetrace-data-lineageprompt).topic: domains, data products, and glossary terms (e.g.explore-available-data,create-a-report,create-interactive-dashboard).connection: configured connection names the caller's persona may reach.- Resource-template variables:
schema://{catalog}.{schema_name}/{table}andavailability://...: catalog, schema, and table names from the query engine (schema_namecompletes once acatalogis chosen;tableonce both are).glossary://{term}: business glossary terms.
Completions are persona-filtered exactly like tools/list and search: a caller only receives values it could already discover through the corresponding tool (dataset/topic/glossary require search; catalog/schema/table require trino_browse; connection names require list_connections and are further filtered by the persona's connection rules). Unauthenticated sessions receive no completions, and each lookup runs under a short latency budget so an unavailable upstream degrades to an empty list rather than an error.
A response carries at most 100 values, and the two optional fields beside them are reported only when they are provable. hasMore is set when the catalog counted more matches than the response holds — read from the catalog's own match count, not inferred from how many rows a page happened to return, since a catalog is free to return fewer rows than were asked for. total is set only when the returned set is the complete one; when the catalog cannot report a count, both fields are omitted rather than asserting a completeness the platform cannot verify.
Custom Resources Configuration¶
Custom resources let you expose arbitrary static content as named MCP resources — brand assets, operational limits, environment docs, or any structured blob that agents can read by URI. They are registered whenever resources.custom is non-empty, independent of resources.enabled.
resources:
custom:
- uri: "brand://theme"
name: "Brand Theme"
description: "Primary brand colors and site URL"
mime_type: "application/json"
content: |
{
"colors": {"primary": "#FF6B35", "secondary": "#004E89"},
"url": "https://example.com"
}
- uri: "brand://logo"
name: "Brand Logo SVG"
mime_type: "image/svg+xml"
content_file: "/etc/platform/logo.svg"
| Field | Type | Required | Description |
|---|---|---|---|
uri |
string | Yes | Unique resource URI (e.g., brand://theme, docs://limits) |
name |
string | Yes | Human-readable name shown in resources/list |
description |
string | No | Optional description for MCP clients |
mime_type |
string | Yes | MIME type (e.g., application/json, image/svg+xml, text/plain) |
content |
string | One of | Inline content (text, JSON, SVG, etc.) |
content_file |
string | One of | Absolute path to a file; read on every request (supports hot-reload) |
content and content_file are mutually exclusive. Invalid entries (missing required fields, both or neither content fields set) are skipped with a warning at startup; valid entries in the same list are still registered.
Progress Notifications Configuration¶
Progress notifications send granular updates to MCP clients during long-running Trino queries. The client must include _meta.progressToken in the request to receive updates. Enabled by default; set enabled: false to opt out.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
*bool |
true (nil = enabled) |
Enable progress notifications |
When enabled, Trino query execution sends progress updates including rows scanned, bytes processed, and query stage information. Clients that don't send a progressToken receive no notifications (zero overhead).
Client Logging Configuration¶
Client logging sends server-to-client log messages via the MCP logging/setLevel protocol. Messages include enrichment decisions, timing data, and platform diagnostics. Enabled by default; set enabled: false to opt out.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
*bool |
true (nil = enabled) |
Enable client logging |
Zero overhead if the client hasn't subscribed via logging/setLevel. When active, log messages report semantic cache hits/misses, enrichment timing, and cross-enrichment decisions.
Elicitation Configuration¶
Elicitation requests user confirmation before potentially expensive or sensitive operations. Requires client-side elicitation support (e.g., Claude Desktop). Gracefully degrades to a no-op if the client doesn't support elicitation. Enabled by default (including cost_estimation and pii_consent); set enabled: false at any level to opt out.
elicitation:
enabled: false # only needed to opt out; defaults to true
cost_estimation:
enabled: false # only needed to opt out; defaults to true
row_threshold: 1000000
pii_consent:
enabled: false # only needed to opt out; defaults to true
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
*bool |
true (nil = enabled) |
Enable elicitation |
cost_estimation.enabled |
*bool |
true (nil = enabled) |
Prompt before expensive queries |
cost_estimation.row_threshold |
int | 1000000 |
Row count threshold from EXPLAIN IO estimates |
pii_consent.enabled |
*bool |
true (nil = enabled) |
Prompt when query accesses PII-tagged columns |
Client support required
Elicitation uses the MCP elicitation/create capability. Clients that don't support elicitation will not receive prompts — queries proceed without confirmation.
Behavior change for existing deployments
Elicitation is user-facing: with no elicitation block at all, cost-estimation and PII-consent prompts now fire out of the box. cost_estimation still respects row_threshold (default 1,000,000 rows), so it only prompts on large queries. Deployments that relied on the previous silent-off default should add elicitation.enabled: false (or disable the sub-features individually) to keep the prior behavior.
Icons Configuration¶
Icons add visual metadata to tools, resources, and prompts in MCP list responses. Upstream toolkits (Trino, DataHub, S3) provide default icons; this configuration overrides or extends them. Enabled by default; set enabled: false to opt out.
icons:
enabled: false # only needed to opt out; defaults to true
tools:
trino_query:
src: "https://example.com/custom-trino.svg"
mime_type: "image/svg+xml"
resources:
"schema://{catalog}.{schema}/{table}":
src: "https://example.com/schema.svg"
prompts:
knowledge_capture:
src: "https://example.com/knowledge.svg"
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
*bool |
true (nil = enabled) |
Enable icon injection middleware |
tools |
map | - | Icon overrides keyed by tool name |
resources |
map | - | Icon overrides keyed by resource URI |
prompts |
map | - | Icon overrides keyed by prompt name |
*.src |
string | - | Icon source URL |
*.mime_type |
string | - | Icon MIME type (e.g., image/svg+xml) |
Default icons
Each upstream toolkit provides a default icon for all its tools. You only need this configuration if you want to customize or override those defaults.
Environment Variables¶
Common environment variables:
| Variable | Description |
|---|---|
TRINO_USER |
Trino username |
TRINO_PASSWORD |
Trino password |
DATAHUB_TOKEN |
DataHub access token |
AWS_ACCESS_KEY_ID |
AWS access key |
AWS_SECRET_ACCESS_KEY |
AWS secret key |
AWS_SESSION_TOKEN |
AWS session token |
DATABASE_URL |
PostgreSQL connection string (for audit/OAuth) |
Complete Example¶
apiVersion: v1
server:
name: mcp-data-platform
transport: http
address: ":8080"
database:
dsn: ${DATABASE_URL}
portal:
enabled: true
admin:
enabled: true
persona: admin
# Hide unused tools from tools/list to save LLM tokens
tools:
allow:
- "trino_*"
- "datahub_*"
- "memory_capture"
deny:
- "*_delete_*"
audit:
enabled: true
log_tool_calls: true
retention_days: 90
sessions:
store: database
ttl: 30m
cleanup_interval: 1m
auth:
api_keys:
enabled: true
keys:
- key: ${API_KEY_ADMIN}
name: "admin"
roles: ["admin"]
toolkits:
trino:
enabled: true
instances:
primary:
host: trino.example.com
port: 443
user: ${TRINO_USER}
password: ${TRINO_PASSWORD}
ssl: true
catalog: hive
schema: default
default_limit: 1000
max_limit: 10000
default: primary
datahub:
enabled: true
instances:
primary:
url: https://datahub.example.com
token: ${DATAHUB_TOKEN}
default_limit: 10
max_limit: 100
default: primary
s3:
enabled: true
instances:
primary:
region: us-east-1
read_only: true
default: primary
semantic:
provider: datahub
instance: primary
cache:
enabled: true
ttl: 5m
query:
provider: trino
instance: primary
storage:
provider: s3
instance: primary
enrichment:
trino_semantic_enrichment: true
datahub_query_enrichment: true
s3_semantic_enrichment: true
unwrap_json: true
column_context_filtering: true
resources:
enabled: true
# progress, client_logging, and elicitation (with cost_estimation and
# pii_consent) are all enabled by default, so no block is needed here unless
# you want to opt out or customize (e.g. a lower cost_estimation.row_threshold).
personas:
analyst:
display_name: "Data Analyst"
roles: ["analyst"]
tools:
allow: ["*"]
deny: ["*_delete_*"]
connections:
allow: ["*"]
admin:
display_name: "Administrator"
roles: ["admin"]
tools:
allow: ["*"]
connections:
allow: ["*"]
Next Steps¶
- Operating Modes - Standalone, file + DB, and bootstrap + DB config modes
- Admin API - REST endpoints for system, config, personas, auth keys, audit
- Tools - Available tools and parameters
- Multi-Provider - Configure multiple instances
- Authentication - Add authentication
- Personas - Role-based access control
- MCP Apps - Interactive UI for tool results
- Middleware Reference - Request processing chain details