MCP Client
BMO can connect to any Model Context Protocol server, exposing the server’s tools to the agent alongside BMO’s built-in tools.
MCP vs extensions
Section titled “MCP vs extensions”You can configure external MCP servers in two ways:
- [mcp.*] — Full MCP: tools, prompts, and resources. Transports:
stdio,sse,http. Use when you need prompts or resources from the server. - [extensions.*] — Tools only, with circuit-breaker protection. Transports:
stdio,http, andsse. Use when you only need tools and want resilient connection handling.
Both support the same transport types. See Protocol support for a summary.
Transport types
Section titled “Transport types”Three transport modes are supported: stdio, http, and sse.
[mcp.filesystem]type = "stdio"command = "node"args = ["/path/to/mcp-server.js"]timeout = 120disabled = falsedisabled_tools = ["some-tool-name"]
[mcp.filesystem.env]NODE_ENV = "production"[mcp.github]type = "http"url = "https://api.githubcopilot.com/mcp/"timeout = 120disabled = falsedisabled_tools = ["create_issue", "create_pull_request"]auth = "github_cli"Use auth = "github_cli" for GitHub’s hosted MCP endpoint when the local
GitHub CLI is already logged in. BMO runs gh auth token at connect time and
attaches the result as an Authorization bearer header. If Authorization is
already supplied through headers or BMO_MCP_BEARER_TOKEN_GITHUB, BMO uses that
explicit value and does not call gh.
With auth = "oauth" and a binary built with mcp_go_client_oauth, BMO offers
its hosted Client ID Metadata Document at
https://instagrim-dev.github.io/bmo/oauth/bmo-mcp-client.json before falling
back to preregistered client credentials or dynamic client registration. The
same OAuth keys work under [mcp.*] and legacy tools-only [extensions.*]
blocks; prefer [mcp.<id>] with tools_only = true for new tools-only config.
For servers that reject CIMD and dynamic client registration, configure a preregistered host-app OAuth client:
[mcp.github]type = "http"url = "https://api.githubcopilot.com/mcp/"auth = "oauth"auth_client_id = "$BMO_GITHUB_MCP_CLIENT_ID"auth_client_secret = "$BMO_GITHUB_MCP_CLIENT_SECRET"Legacy tools-only extensions use the same fields:
[extensions.github]type = "http"url = "https://api.githubcopilot.com/mcp/"auth = "oauth"auth_client_id = "$BMO_GITHUB_MCP_CLIENT_ID"auth_client_secret = "$BMO_GITHUB_MCP_CLIENT_SECRET"Servers that require PAT-style auth can still use headers:
[mcp.github.headers]Authorization = "Bearer $GH_PAT"The equivalent environment-only path is:
export BMO_MCP_BEARER_TOKEN_GITHUB="$(gh auth token)"SSE (Server-Sent Events)
Section titled “SSE (Server-Sent Events)”[mcp.streaming-service]type = "sse"url = "https://example.com/mcp/sse"timeout = 120disabled = false
[mcp.streaming-service.headers]API-Key = "$(echo $API_KEY)"Environment variable expansion
Section titled “Environment variable expansion”Use $(echo $VAR) syntax in header values for runtime expansion. Standard $VAR references in config values are also expanded.
For Kubernetes deployments, BMO also supports env-driven MCP headers:
BMO_MCP_EXTRA_HEADERS— JSON headers merged into every HTTP/SSE MCPBMO_MCP_HEADERS_<ID>— JSON headers for one MCP serverBMO_MCP_BEARER_TOKEN_<ID>— a bearer token merged asAuthorization: Bearer ...for one MCP server
The bearer-token form is useful when a chart needs to source a token directly from a Secret without building JSON in the manifest.
Custom CA bundles for HTTP/SSE
Section titled “Custom CA bundles for HTTP/SSE”Use options.mcp_client.tls.ca_bundle_path when HTTP or SSE MCP servers are
behind a corporate TLS proxy or signed by a private CA:
[options.mcp_client.tls]ca_bundle_path = "/etc/ssl/certs/internal-mcp-ca.pem"The bundle must be PEM encoded. This is a global MCP client transport setting;
it applies to HTTP/SSE MCP connections instead of changing each [mcp.*]
entry. Prefer this over disabling certificate verification.
Notion example
Section titled “Notion example”[mcp.notion]type = "http"url = "http://notion-mcp.bmo-system.svc.cluster.local:8080/mcp"Then inject BMO_MCP_BEARER_TOKEN_NOTION from a Secret in the BMO pod so
requests to mcp.notion carry the MCP server’s required bearer token.
GitHub example
Section titled “GitHub example”[mcp.github]type = "http"url = "http://github-mcp.bmo-system.svc.cluster.local:8080/mcp"For a local GitHub MCP service, the server itself usually authenticates to
GitHub via GITHUB_PERSONAL_ACCESS_TOKEN, so BMO typically does not need an
extra bearer header for mcp.github.
The Slack MCP Server supports stdio, SSE, and HTTP. It exposes tools for channel history, threads, search, unreads, user groups, and optional posting/reactions, plus directory resources for channels and users.
stdio — Run the server as a subprocess. Install the binary (e.g. go install github.com/korotovsky/slack-mcp-server/cmd/slack-mcp-server@latest) and pass Slack credentials via env. Use either browser tokens (xoxc + xoxd) or OAuth/bot tokens (xoxp or xoxb).
[mcp.slack]type = "stdio"command = "slack-mcp-server"timeout = 30
[mcp.slack.env]# Option A: browser session tokens (stealth mode)SLACK_MCP_XOXC_TOKEN = "$SLACK_MCP_XOXC_TOKEN"SLACK_MCP_XOXD_TOKEN = "$SLACK_MCP_XOXD_TOKEN"# Option B: user OAuth token# SLACK_MCP_XOXP_TOKEN = "$SLACK_MCP_XOXP_TOKEN"# Option C: bot token (limited: invited channels only, no search)# SLACK_MCP_XOXB_TOKEN = "$SLACK_MCP_XOXB_TOKEN"HTTP/SSE — Run the Slack MCP server separately (e.g. SLACK_MCP_PORT=13080 slack-mcp-server). Default listen is 127.0.0.1:13080. If you set SLACK_MCP_API_KEY on the server, pass it as a bearer header from BMO.
[mcp.slack]type = "http"url = "http://127.0.0.1:13080/mcp"timeout = 30
# Optional: when the server requires SLACK_MCP_API_KEY[mcp.slack.headers]Authorization = "Bearer $SLACK_MCP_API_KEY"Kubernetes sidecar
Section titled “Kubernetes sidecar”Deploy the Slack MCP server as a standalone service in bmo-system with
SLACK_MCP_ENABLED=true. By default the sidecar uses the upstream image
ghcr.io/korotovsky/slack-mcp-server:latest
(docker pull ghcr.io/korotovsky/slack-mcp-server:latest). Provide tokens via
a Secret (for example slack-mcp-auth) with the catalog keys; set
BMO_SLACK_MCP_ENABLED=true so BMO gets [mcp.slack] with
url = "http://slack-mcp.bmo-system.svc.cluster.local:8080/mcp". Configure the
matching Kubernetes env vars and verification steps in your deployment system
alongside that sidecar.
Posting and reactions are disabled by default on the server; enable via the server’s SLACK_MCP_ADD_MESSAGE_TOOL and SLACK_MCP_MARK_TOOL env vars. Use disabled_tools in BMO to hide specific tools if needed.
Slack secrets from Infisical (e.g. Kubernetes) — BMO does not fetch secrets from Infisical directly. It resolves $VAR in MCP env and headers from the process environment at config load time. To use Infisical-stored Slack tokens with BMO:
- Store in Infisical — Create a secret at a path such as
/bmo/mcp/slack-mcp-auth(projectbmo, environmentprod, or your convention) with keys that match the Slack MCP server’s env var names:- Browser/stealth:
SLACK_MCP_XOXC_TOKEN,SLACK_MCP_XOXD_TOKEN - Or user OAuth:
SLACK_MCP_XOXP_TOKEN - Or bot:
SLACK_MCP_XOXB_TOKEN
- Browser/stealth:
- Sync to Kubernetes — Use External Secrets (or Infisical → AWS Secrets Manager → ESO) so that path becomes a Kubernetes Secret (e.g.
slack-mcp-authinbmo-system) with the same keys. - Inject into the BMO pod — In the BMO Helm chart, set
extraEnvso each token is provided from that Secret (e.g.valueFrom.secretKeyRefwithname: slack-mcp-authandkey: SLACK_MCP_XOXC_TOKEN, and similarly forSLACK_MCP_XOXD_TOKEN). - Config — Keep
[mcp.slack].envas above with$SLACK_MCP_XOXC_TOKENand$SLACK_MCP_XOXD_TOKEN. At startup, BMO reads those from the process environment (set by Kubernetes from the Secret) and passes the resolved values to theslack-mcp-serversubprocess.
Rotation: update the secret in Infisical, re-sync to the K8s Secret, then roll/restart BMO pods so they pick up the new env. The canonical catalog for promoting MCP auth into Infisical is deploy/phase2/infisical-core-mcp-catalog.yaml; a Slack entry there defines the recommended path and key names for your pipeline.
Enabling an MCP with dynamic MCP support
Section titled “Enabling an MCP with dynamic MCP support”To have BMO discover and refresh tools from your MCP servers (including the Slack sidecar) and control which agents see which MCPs, use dynamic MCP and per-agent allowed_mcp:
-
Configure the server — Add the MCP under
[mcp.<id>](or[extensions.<id>]). For the Slack sidecar that is[mcp.slack]withtype = "http"andurl = "http://slack-mcp.bmo-system.svc.cluster.local:8080/mcp"(see Kubernetes sidecar above). -
Enable dynamic MCP — Turn on client-driven tool refresh so the tool list is updated from servers/extensions (on a schedule and/or on-demand):
[options.dynamic_mcp]enabled = true# interval_seconds = 60 # 0 = on-demand only (default) -
Grant agents access — By default, if an agent has no
allowed_mcpset, it sees all configured MCPs. To restrict or explicitly allow:- Allow all tools from Slack for one agent:
[agents.coder]thenallowed_mcp = { slack = [] }(empty list = all tools from that MCP). - Allow only specific tools:
allowed_mcp = { slack = ["slack_list_channels", "slack_search"] }. - No MCPs for an agent:
allowed_mcp = {}(empty map). - Multiple MCPs:
allowed_mcp = { slack = [], github = [] }.
Use the MCP id that matches your config section (e.g.
[mcp.slack]→ idslack). - Allow all tools from Slack for one agent:
In Kubernetes: Inject [options.dynamic_mcp] and any [agents.<id>] overrides via the BMO chart’s config.extraTOML (or your baseline values overlay). The chart does not currently expose dynamic_mcp or allowed_mcp as dedicated values.
No new build needed: If dynamic MCP is enabled and [mcp.slack] is configured, you can add Slack to an agent via config only: set allowed_mcp = { slack = [] } (and any other MCPs you want) for that agent in config.extraTOML. The built-in defaults for Task/Infra agents also include slack so a redeploy with current code works without config change.
See mcp.md
and the Configuration reference for
options.dynamic_mcp and allowed_mcp.
Per-server tool filtering
Section titled “Per-server tool filtering”Disable specific tools from an MCP server without disabling the whole server:
[mcp.github]disabled_tools = ["create_issue", "delete_repo"]Or disable the entire server temporarily:
[mcp.github]disabled = trueMCP resources
Section titled “MCP resources”BMO exposes prompt/resource parity tools for full MCP servers:
list_mcp_prompts— list available prompts from connected serversget_mcp_prompt— render a specific prompt with argumentslist_mcp_resources— list all resources from connected serversread_mcp_resource— read a specific resource by URI
These are intentionally read-only. BMO does not provide CRUD tools for MCP
prompts/resources because they are server-owned by the MCP protocol: servers
publish prompt/resource inventories, and any “write” semantics are
implementation-defined. If you need create/update/delete behavior, expose it as
server-specific tools (and then allowlist/disable them in BMO via disabled_tools
or per-agent allowed_mcp).
Lifecycle MCP servers for Open WebUI
Section titled “Lifecycle MCP servers for Open WebUI”When you want Open WebUI lifecycle prompts to summarize Kargo or Octopus state,
configure full MCP servers under [mcp.*] with these canonical IDs:
kargo_readonlyoctopus_readonly
Those names are what the default lifecycle_chat agent expects. BMO uses them
for read-only lifecycle routing in the OpenAI-compatible API.
Example:
[mcp.kargo_readonly]type = "http"url = "http://kargo-mcp.bmo-system.svc.cluster.local:8080/mcp"
[mcp.octopus_readonly]type = "http"url = "http://octopus-mcp.bmo-system.svc.cluster.local:8080/mcp"Use full MCP instead of [extensions.*] for this flow. The lifecycle route
relies on MCP resources, so tools-only extensions are not sufficient.
Infra MCP servers for Open WebUI
Section titled “Infra MCP servers for Open WebUI”The default infra_chat agent expects full MCP servers under [mcp.*] for
platform state and observability questions. Use these canonical IDs:
argocd_readonlygrafanainfisical_readonlyelasticsearchmongodb_readonly
Crossplane claims, composites, and provider state are read via kubectl MCP (no dedicated Crossplane MCP). See crossplane-aws-landing-zone.md for kubectl-based workflows.
Example:
[mcp.argocd_readonly]type = "http"url = "http://argocd-mcp.bmo-system.svc.cluster.local:8080/mcp"
[mcp.grafana]type = "http"url = "http://grafana-mcp.bmo-system.svc.cluster.local:8080/mcp"disabled_tools = ["apply", "delete"]Use these MCPs for questions about:
- Argo CD applications, sync state, drift,
ApplicationSet, andAppProject - Grafana dashboards, datasources, and alert metadata
- Infisical project, environment, folder, and secret-location metadata
- Elasticsearch index health, mappings, and search-backed operational context
- MongoDB database, collection, and schema metadata
If a prompt requires one of those internal systems and the matching MCP is not configured, BMO’s OpenAI-compatible path fails closed rather than silently falling back to generic web research.
Related
Section titled “Related”- MCP maintainer review invariants — bounded stdio shutdown, stable permission identity, and
{extension}__{tool}parsing for changes underinternal/mcp/and permission wiring. - Protocol support - overview of MCP, ACP, and A2A transport roles.
- MCP Server - expose BMO tools to editors and agents.
- Open WebUI - drive BMO through an OpenAI- compatible API.
- Zed - editor setup examples for MCP and ACP.