Skip to content

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.

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, and sse. Use when you only need tools and want resilient connection handling.

Both support the same transport types. See Protocol support for a summary.

Three transport modes are supported: stdio, http, and sse.

[mcp.filesystem]
type = "stdio"
command = "node"
args = ["/path/to/mcp-server.js"]
timeout = 120
disabled = false
disabled_tools = ["some-tool-name"]
[mcp.filesystem.env]
NODE_ENV = "production"
[mcp.github]
type = "http"
url = "https://api.githubcopilot.com/mcp/"
timeout = 120
disabled = false
disabled_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:

Terminal window
export BMO_MCP_BEARER_TOKEN_GITHUB="$(gh auth token)"
[mcp.streaming-service]
type = "sse"
url = "https://example.com/mcp/sse"
timeout = 120
disabled = false
[mcp.streaming-service.headers]
API-Key = "$(echo $API_KEY)"

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 MCP
  • BMO_MCP_HEADERS_<ID> — JSON headers for one MCP server
  • BMO_MCP_BEARER_TOKEN_<ID> — a bearer token merged as Authorization: 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.

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.

[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.

[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"

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:

  1. Store in Infisical — Create a secret at a path such as /bmo/mcp/slack-mcp-auth (project bmo, environment prod, 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
  2. Sync to Kubernetes — Use External Secrets (or Infisical → AWS Secrets Manager → ESO) so that path becomes a Kubernetes Secret (e.g. slack-mcp-auth in bmo-system) with the same keys.
  3. Inject into the BMO pod — In the BMO Helm chart, set extraEnv so each token is provided from that Secret (e.g. valueFrom.secretKeyRef with name: slack-mcp-auth and key: SLACK_MCP_XOXC_TOKEN, and similarly for SLACK_MCP_XOXD_TOKEN).
  4. Config — Keep [mcp.slack].env as above with $SLACK_MCP_XOXC_TOKEN and $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 the slack-mcp-server subprocess.

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.

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:

  1. Configure the server — Add the MCP under [mcp.<id>] (or [extensions.<id>]). For the Slack sidecar that is [mcp.slack] with type = "http" and url = "http://slack-mcp.bmo-system.svc.cluster.local:8080/mcp" (see Kubernetes sidecar above).

  2. 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)
  3. Grant agents access — By default, if an agent has no allowed_mcp set, it sees all configured MCPs. To restrict or explicitly allow:

    • Allow all tools from Slack for one agent: [agents.coder] then allowed_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] → id slack).

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.

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 = true

BMO exposes prompt/resource parity tools for full MCP servers:

  • list_mcp_prompts — list available prompts from connected servers
  • get_mcp_prompt — render a specific prompt with arguments
  • list_mcp_resources — list all resources from connected servers
  • read_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).

When you want Open WebUI lifecycle prompts to summarize Kargo or Octopus state, configure full MCP servers under [mcp.*] with these canonical IDs:

  • kargo_readonly
  • octopus_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.

The default infra_chat agent expects full MCP servers under [mcp.*] for platform state and observability questions. Use these canonical IDs:

  • argocd_readonly
  • grafana
  • infisical_readonly
  • elasticsearch
  • mongodb_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, and AppProject
  • 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.

  • MCP maintainer review invariants — bounded stdio shutdown, stable permission identity, and {extension}__{tool} parsing for changes under internal/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.