Skip to main content
Version: v2

MCP — AI Tooling Integration

DevPortal exposes an MCP (Model Context Protocol) server that lets external AI tools — Claude Code, Codex CLI, Cursor — query the catalog, read TechDocs, and execute scaffolder templates using your portal as a context source.

Two independent capabilities:

  1. MCP server for external clients — OAuth per-user authentication, no LLM API key required
  2. AI Chat in-portal — optional; requires an LLM API key (OpenAI, Anthropic, Gemini, or Ollama)

For platform teams​

SaaS (customer portal)​

Go to Configure → Integrations and open the MCP + AI Chat card.

  1. Click Connect
  2. The dialog opens with the default action "Enable MCP server"
  3. (Optional) Toggle "Also add AI Chat in portal"
  4. If AI Chat is on, select provider (OpenAI, Anthropic, Gemini, or Ollama) and provide the API key
  5. Confirm — the button label reflects what will be provisioned:
    • Enable MCP server — MCP server only
    • Enable MCP + AI Chat — MCP server + in-portal chat

What happens on confirm:

Always:

  • Provisions the MCP server for external clients
  • Activates mcp-actions-backend + catalog, TechDocs, and scaffolder extras

If AI Chat is enabled:

  • Validates the API key with the provider
  • Automatically resolves a compatible model
  • Activates mcp-chat-backend and mcp-chat
  • Injects mcpChat block in app-config with provider and model

After connecting, the card shows:

  • The MCP endpoint URL (e.g., https://<instance>/api/mcp-actions/v1)
  • A switch for enabling/disabling AI Chat independently

Disabling AI Chat only (without disconnecting MCP): Use the switch on the connected card. This removes mcp-chat-backend, mcp-chat, the mcpChat: block, and the LLM API key — without affecting the MCP server or external clients.

Disconnecting entirely: The trash icon on the connected card removes both the MCP server and AI Chat. Both this full disconnect and the AI-Chat-only switch above require an instance restart to take effect.

Self-hosted (preset activation)​

Activate the mcp preset (and optionally mcp-chat) via VEECODE_PRESETS:

# MCP server only — no LLM API key required
VEECODE_PRESETS=recommended,mcp

# MCP server + in-portal AI Chat
VEECODE_PRESETS=recommended,mcp,mcp-chat
MCP_CHAT_PROVIDER=openai
MCP_CHAT_API_KEY=sk-xxxx
MCP_CHAT_MODEL=gpt-4o

The mcp preset (presets/mcp.yaml) has no required variables — the OAuth/DCR configuration is already baked into the base image's app-config.production.yaml. It enables the following plugins at boot:

  • backstage-plugin-mcp-actions-backend (exposes /api/mcp-actions/v1)
  • red-hat-developer-hub-backstage-plugin-software-catalog-mcp-extras
  • red-hat-developer-hub-backstage-plugin-techdocs-mcp-extras
  • red-hat-developer-hub-backstage-plugin-scaffolder-mcp-extras

The mcp-chat preset (presets/mcp-chat.yaml) requires:

VariableDescription
MCP_CHAT_PROVIDERLLM provider ID: openai or claude
MCP_CHAT_API_KEYAPI key for the selected provider
MCP_CHAT_MODELModel name (e.g., gpt-4o for openai, claude-sonnet-5 for claude — check the provider's current model catalogue)
note

For self-hosted (preset) deployments, MCP_CHAT_PROVIDER accepts only openai or claude. Gemini and Ollama are SaaS-only.

warning

mcp-chat talks loopback to mcp-actions-backend. Without the mcp preset, the MCP backend does not mount and every tool invocation in chat will fail. Always compose as mcp,mcp-chat.

warning

Do not also activate mcp-actions-backend via a raw dynamic-plugins.yaml mount when using the mcp preset (or via a static import in a custom backend build). Duplicate registration causes a boot failure: Plugin 'mcp-actions' is already registered.

Available toolsets:

pluginSourceOriginTools exposed
mcp-actionsVeeCode overlayexplain
catalogBackstage built-inget-catalog-entity, validate-entity
scaffolderBackstage built-invalidate-scaffolder
software-catalog-mcp-extrasRHDHquery-catalog-entities, register-entity, unregister-entity
techdocs-mcp-extrasRHDHanalyze-techdocs-coverage, fetch-techdocs, retrieve-techdocs-content
scaffolder-mcp-extrasRHDHexecute-template, fetch-template-metadata, list-scaffolder-tasks, get-scaffolder-task-logs, list-scaffolder-actions, dry-run-template
Restricting the exposed toolset (self-hosted only)

pluginSources is itself an overridable list, not a fixed set. To expose fewer tools — for example, dropping explain — mount a dynamic-plugins.yaml with the same plugin and a trimmed list:

plugins:
- disabled: false
package: oci://quay.io/veecode/backstage-plugin-mcp-actions-backend:bs_1.52.0__0.1.14!backstage-plugin-mcp-actions-backend
pluginConfig:
backend:
actions:
pluginSources:
- catalog
- scaffolder

See Adding Plugins for how operator-mounted overrides interact with preset fragments. Not available on SaaS, where the mcp preset's default toolset is fixed.

OAuth / DCR configuration (self-hosted)​

External clients authenticate via OAuth 2.1 with Dynamic Client Registration (DCR) — each user authenticates as their own Backstage identity (user:default/<name>), established on first authorization in the client, so permissions and RBAC apply per that identity rather than a shared service account. This configuration is already baked into the base image when using the V2 unified image and running its default config chain. If your deployment assembles its own --config chain (an operator-rendered fork chart, an RHDH-style shell, or any setup that does not load the image's app-config.production.yaml), the blocks below are NOT picked up automatically — declare them explicitly in your app-config. Without them the backend never registers the root /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server routes, the frontend catch-all answers those URLs with index.html, and every MCP client fails at discovery with an error like SDK auth failed: Failed to parse JSON. The relevant app-config blocks are:

auth:
experimentalDynamicClientRegistration:
enabled: true
allowedRedirectUriPatterns:
- 'http://127.0.0.1:*/callback'
- 'http://localhost:*/callback'
experimentalRefreshToken:
enabled: true
tokenLifetime: { days: 30 }
maxRotationLifetime: { years: 1 }
maxTokensPerUser: 20

backend:
auth:
dangerouslyDisableDefaultAuthPolicy: false

Prerequisites:

  • permission.enabled: true
  • Persistent database (Postgres) to preserve signing keys and sessions across restarts

Enabling mcp-actions-backend and the DCR blocks above is not enough on its own: during authorization the backend redirects the user's browser to /oauth2/authorize/:sessionId, a frontend page where the user approves or rejects the client. The flow needs that page to exist:

  • Default DevPortal frontend (base image / SaaS): the consent page ships built-in — nothing to do.
  • Dynamic-plugin shells (RHDH-style frontends, fork charts): the route does not exist and the browser lands on a 404 after the redirect. Enable the backstage-plugin-auth dynamic plugin (upstream Backstage's @backstage/plugin-auth frontend, which mounts the consent page under /oauth2/*):
plugins:
- disabled: false
package: oci://quay.io/veecode/backstage-plugin-auth:bs_1.52.0__0.1.9!backstage-plugin-auth
pluginConfig:
dynamicPlugins:
frontend:
backstage.plugin-auth:
dynamicRoutes:
- path: /oauth2/*
importName: Router

For developers​

Claude Code​

Add to .mcp.json at the project root or ~/.mcp.json globally:

{
"mcpServers": {
"devportal": {
"type": "http",
"url": "https://<your-instance>/api/mcp-actions/v1"
}
}
}

Or via the CLI:

claude mcp add --transport http --scope user devportal https://<your-instance>/api/mcp-actions/v1

Codex CLI​

Add to ~/.codex/config.toml:

[mcp_servers.devportal]
url = "https://<your-instance>/api/mcp-actions/v1"

On first use, the client opens a browser window for the OAuth authorization flow. No manual token management is needed.

Cursor​

Add to your Cursor MCP configuration (.cursor/mcp.json in the project root, or the global Cursor settings under Settings → MCP):

{
"mcpServers": {
"devportal": {
"url": "https://<your-instance>/api/mcp-actions/v1"
}
}
}

On first use, Cursor triggers the OAuth authorization flow and stores the resulting token. The server will appear in the Cursor MCP panel under Tools.

Advanced configuration​

To customize prompts, models, or add additional MCP servers to the in-portal chat, use Configure → App Config:

mcpChat:
mcpServers:
- id: devportal
name: DevPortal MCP
url: http://localhost:7007/api/mcp-actions/v1
type: streamable-http
- id: github
name: GitHub MCP
url: https://api.githubcopilot.com/mcp/
type: streamable-http
headers:
Authorization: Bearer ${GITHUB_MCP_TOKEN}
note

Use http://localhost:7007/... (loopback) for the DevPortal MCP entry — mcp-chat-backend and mcp-actions-backend run in the same process. Do not substitute the public hostname here.

Troubleshooting​

SymptomLikely causeFix
Plugin 'mcp-actions' is already registered on bootActivated via both static import and dynamic pluginRemove one of the activations
401 when connecting an MCP clientExpired session or tokenRe-run the OAuth authorization flow in the client
SDK auth failed: Failed to parse JSON when connecting/.well-known/oauth-* discovery routes answered by the frontend catch-all (HTML) — DCR blocks not in the active config chainDeclare the OAuth/DCR app-config blocks explicitly (see OAuth / DCR configuration)
Browser opens /oauth2/authorize/... and shows a portal 404Consent page missing from the frontend (dynamic-plugin shells don't ship it)Enable the backstage-plugin-auth dynamic plugin (see Consent page requirement)
405 on GET /api/mcp-actions/v1Expected — MCP uses POSTValidate via an MCP client, not a browser
Tools missing in clientIncomplete pluginSources listReview pluginSources in pluginConfig
AI Chat not activating in portalInvalid API key or wrong providerFix the key and reapply the configuration
Disabling AI Chat disconnected external MCP clientsFull disconnect used instead of partialReconnect the card and use the AI Chat switch to disable chat only