Skip to content

feat: add MCP tool execution governance with virtual key allow-lists#1940

Open
Pratham-Mishra04 wants to merge 1 commit into03-05-feat_add_option_to_disable_automatic_mcp_tool_injection_per_requestfrom
03-05-feat_virtual_key_mcp_configs_now_act_as_an_execution-time_allow-list
Open

feat: add MCP tool execution governance with virtual key allow-lists#1940
Pratham-Mishra04 wants to merge 1 commit into03-05-feat_add_option_to_disable_automatic_mcp_tool_injection_per_requestfrom
03-05-feat_virtual_key_mcp_configs_now_act_as_an_execution-time_allow-list

Conversation

@Pratham-Mishra04
Copy link
Collaborator

Summary

This PR implements MCP tool governance by enforcing virtual key MCP configurations as an execution-time allow-list. When virtual keys have empty MCPConfigs, all MCP tools are denied. When non-empty, each tool is validated against the configured allow-list at both inference time and MCP tool execution.

Changes

  • Context parameter updates: Changed MCP-related functions to use *schemas.BifrostContext instead of context.Context to enable tool tracking
  • Tool tracking: Added BifrostContextKeyMCPAddedTools context key to track which MCP tools are added to requests
  • Governance enforcement: Virtual key MCP configurations now act as execution-time allow-lists with validation in both PreMCPHook and evaluateGovernanceRequest
  • Auto-injection control: Added DisableAutoToolInject configuration option that respects the toggle and skips auto-injection when headers are already set by callers
  • Decision type: Added DecisionMCPToolBlocked for MCP tool governance violations
  • UI improvements: Updated MCP view description and sidebar item naming for better clarity

Type of change

  • Feature
  • Bug fix
  • Refactor
  • Documentation
  • Chore/CI

Affected areas

  • Core (Go)
  • Transports (HTTP)
  • Providers/Integrations
  • Plugins
  • UI (Next.js)
  • Docs

How to test

Test MCP tool governance with virtual keys:

# Core/Transports
go version
go test ./...

# Test with virtual key having empty MCPConfigs (should deny all MCP tools)
curl -X POST /v1/chat/completions \
  -H "x-bf-virtual-key: test-vk-empty-mcp" \
  -d '{"model": "gpt-4", "messages": [{"role": "user", "content": "test"}]}'

# Test with virtual key having specific MCP tool allowlist
curl -X POST /v1/chat/completions \
  -H "x-bf-virtual-key: test-vk-with-mcp" \
  -d '{"model": "gpt-4", "messages": [{"role": "user", "content": "test"}]}'

# Test disable auto tool inject configuration
curl -X PUT /v1/config/mcp/disable-auto-tool-inject \
  -d '{"disable": true}'

# UI
cd ui
pnpm i || npm i
pnpm test || npm test
pnpm build || npm run build

New configuration options:

  • disable_auto_tool_inject: Boolean flag to disable automatic MCP tool injection
  • Virtual key MCPConfigs: Array of MCP client configurations that act as allow-lists

Screenshots/Recordings

UI changes include updated MCP configuration view with clearer descriptions for the disable auto tool injection toggle and improved sidebar navigation labels.

Breaking changes

  • Yes
  • No

Impact: MCP-related function signatures now require *schemas.BifrostContext instead of context.Context. Virtual keys with empty MCPConfigs will now deny all MCP tools by default.

Migration: Update any custom MCP integrations to use the new context parameter type. Configure MCPConfigs on virtual keys that need MCP tool access.

Related issues

Implements MCP tool governance and execution-time validation for virtual key configurations.

Security considerations

  • Access control: Virtual key MCP configurations now enforce strict allow-lists for tool execution
  • Context isolation: Tool tracking is isolated per request context to prevent cross-request leakage
  • Validation: Both pre-execution and execution-time validation prevent unauthorized tool access

Checklist

  • I read docs/contributing/README.md and followed the guidelines
  • I added/updated tests where appropriate
  • I updated documentation where needed
  • I verified builds succeed (Go and UI)
  • I verified the CI pipeline passes locally if applicable

Copy link
Collaborator Author

Pratham-Mishra04 commented Mar 5, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Virtual key MCP configs enforced as execution-time allow-lists; disallowed tools are blocked.
    • Option to disable automatic MCP tool injection; auto-injection is skipped when the caller supplies the header.
    • Empty MCP configurations now deny all MCP tools.
  • Documentation

    • Updated MCP tool filtering guidance and auto-injection/header behavior.
  • UI Updates

    • Renamed "Pricing config" to "Pricing Settings" and clarified Disable Auto Tool Injection wording.

Walkthrough

Switch MCP-related APIs from context.Context to *schemas.BifrostContext, add tracking of MCP-added tools in context, and add governance features: virtual-key MCP configs as execution-time allow-lists and a configurable DisableAutoToolInject that affects header auto-injection and per-tool validation.

Changes

Cohort / File(s) Summary
Core: Bifrost & MCP signature migration
core/bifrost.go, core/mcp/interface.go, core/mcp/mcp.go, core/mcp/toolmanager.go
Replaced context.Context parameters with *schemas.BifrostContext across MCP APIs and callers. Tool parsing/get-available flows updated to accept the new context type; GetAvailableTools now appends processed tool names into the Bifrost context.
Core: Context keys
core/schemas/bifrost.go
Added BifrostContextKeyMCPAddedTools constant for tracking MCP-added tools in the Bifrost context.
Governance plugin: allow-list & auto-inject control
plugins/governance/main.go, plugins/governance/resolver.go
Added DisableAutoToolInject config/state; implemented isMCPToolAllowedByVK helper; enforced VK MCPConfigs as execution-time allow-lists; added DecisionMCPToolBlocked decision and 403 behavior for blocked tools; pre-hook and evaluation updated to validate MCP tools per VK.
Transport: wiring & server changes
transports/bifrost-http/server/plugins.go, transports/bifrost-http/server/server.go
Wired DisableAutoToolInject into governance plugin initialization; GetAvailableMCPTools now wraps incoming context with a NoDeadline Bifrost context before delegating; UpdateMCPDisableAutoToolInject returns underlying client errors.
Docs, changelogs & UI text
core/changelog.md, plugins/governance/changelog.md, transports/changelog.md, docs/features/governance/mcp-tools.mdx, ui/app/workspace/config/views/mcpView.tsx, ui/components/sidebar.tsx
Updated changelogs and docs to describe VK MCP allow-list behavior and auto-inject rules; adjusted UI text for Disable Auto Tool Injection and renamed a sidebar label.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Transport
  participant Governance
  participant MCPManager
  participant ToolRuntime

  Client->>Transport: HTTP request
  Transport->>Governance: HTTPTransportPreHook (may inject header)
  Governance->>Transport: header decision (respect DisableAutoToolInject / VK)
  Transport->>MCPManager: GetAvailableMCPTools(BifrostContext)
  MCPManager->>MCPManager: Append MCPAddedTools to BifrostContext
  MCPManager->>ToolRuntime: ExecuteToolCall (per-tool, validated)
  ToolRuntime-->>MCPManager: tool result
  MCPManager-->>Transport: tools/results
  Transport-->>Client: response (or 403 if DecisionMCPToolBlocked)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A hop, a tweak, a Bifrost leap,

Tools tracked in pockets, secrets keep,
Governance watches each tool's right,
Auto-inject sleeps when toggled light,
Hooray — the meadow's safe tonight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature: adding MCP tool governance with virtual key allow-lists, which directly matches the primary objective of the PR.
Description check ✅ Passed The description comprehensively covers all required template sections including Summary, Changes, Type of change, Affected areas, How to test, Breaking changes, Security considerations, and a complete checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 03-05-feat_virtual_key_mcp_configs_now_act_as_an_execution-time_allow-list

Comment @coderabbitai help to get the list of available commands and usage tips.

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 03-05-feat_add_option_to_disable_automatic_mcp_tool_injection_per_request branch from b515efc to 1ee6d71 Compare March 5, 2026 12:48
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 03-05-feat_virtual_key_mcp_configs_now_act_as_an_execution-time_allow-list branch 2 times, most recently from 8af3ad3 to dafd4b6 Compare March 5, 2026 13:07
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 03-05-feat_add_option_to_disable_automatic_mcp_tool_injection_per_request branch from 1ee6d71 to 57671fd Compare March 5, 2026 13:07
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 03-05-feat_virtual_key_mcp_configs_now_act_as_an_execution-time_allow-list branch from dafd4b6 to df4061c Compare March 5, 2026 13:11
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 03-05-feat_add_option_to_disable_automatic_mcp_tool_injection_per_request branch from 57671fd to dae2e01 Compare March 5, 2026 13:11
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/mcp/toolmanager.go`:
- Around line 197-203: In GetAvailableTools, avoid dereferencing
tool.Function.Name before nil checks and make tool suppression deterministic:
first check tool.Function != nil && tool.Function.Name != "" and only then call
schemas.AppendToContextList(ctx, schemas.BifrostContextKeyMCPAddedTools,
tool.Function.Name); next, replace the current global gating on
includeCodeModeTools with a per-tool check (e.g., skip adding the tool only if
!includeCodeModeTools && tool.IsCodeMode or equivalent flag on the tool) and
then use seenToolNames[tool.Function.Name] to deduplicate and append to
availableTools; update references to clientTools, includeCodeModeTools,
seenToolNames, availableTools, and schemas.AppendToContextList accordingly.

In `@plugins/governance/main.go`:
- Around line 1202-1228: The current blind single-tool check using
p.store.GetVirtualKey(virtualKeyValue) lets execution proceed (return req, nil,
nil) if the virtual key is missing/inactive even though
evaluateGovernanceRequest already ran, which creates a race that can allow
unauthorized tool execution; change this to fail-closed: when GetVirtualKey
returns !ok || vk == nil || !vk.IsActive, return an MCPPluginShortCircuit
BifrostError (DecisionMCPToolBlocked, 403) denying execution, and keep the
existing handling for len(vk.MCPConfigs) == 0 and isMCPToolAllowedByVK(vk,
toolName) as-is so empty configs or disallowed tools also return the same
short-circuit error.

In `@transports/changelog.md`:
- Around line 7-8: Remove the duplicated changelog entries by keeping only one
instance of the "feat: add option to disable automatic MCP tool injection per
request" entry and one instance of the "fix: preserve original audio filename in
transcription requests" entry; locate the duplicate lines in
transports/changelog.md (the exact strings above) and delete the repeated
occurrences so each change appears only once in the file.

In `@ui/app/workspace/config/views/mcpView.tsx`:
- Line 185: Update the explanatory sentence in the MCP view text (the JSX string
in mcpView.tsx where the paragraph about header-based tool inclusion is
rendered) to explicitly state that using the x-bf-mcp-include-tools header does
not bypass Virtual Key (VK) MCP execution-time allow-lists; tool injection via
the header is still subject to VK MCP allow-list checks and may be blocked by
them. Locate the paragraph around the existing wording "When enabled, MCP tools
are not automatically included..." and replace it with a clarified sentence that
mentions both the header and the VK MCP allow-lists (keep the existing header
code element <code className="text-xs">x-bf-mcp-include-tools</code> intact).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e14b7c87-859a-40e2-95da-57c4be9301a8

📥 Commits

Reviewing files that changed from the base of the PR and between dae2e01 and df4061c.

📒 Files selected for processing (15)
  • core/bifrost.go
  • core/changelog.md
  • core/mcp/interface.go
  • core/mcp/mcp.go
  • core/mcp/toolmanager.go
  • core/schemas/bifrost.go
  • docs/features/governance/mcp-tools.mdx
  • plugins/governance/changelog.md
  • plugins/governance/main.go
  • plugins/governance/resolver.go
  • transports/bifrost-http/server/plugins.go
  • transports/bifrost-http/server/server.go
  • transports/changelog.md
  • ui/app/workspace/config/views/mcpView.tsx
  • ui/components/sidebar.tsx

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 03-05-feat_add_option_to_disable_automatic_mcp_tool_injection_per_request branch from dae2e01 to fc9336c Compare March 5, 2026 14:42
@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 03-05-feat_virtual_key_mcp_configs_now_act_as_an_execution-time_allow-list branch from df4061c to 9255034 Compare March 5, 2026 14:42
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
core/mcp/toolmanager.go (1)

193-203: ⚠️ Potential issue | 🟠 Major

Fix nondeterministic tool availability and incorrect MCPAddedTools tracking in GetAvailableTools.

At Line 200, regular tool append is globally gated by includeCodeModeTools, so once any code-mode client is encountered, regular tools may be dropped depending on map iteration order.
At Line 199, BifrostContextKeyMCPAddedTools is updated before confirming the tool was actually added. Also, code-mode tools appended at Lines 212-217 are not tracked.

💡 Proposed fix
 	for clientName, clientTools := range availableToolsPerClient {
 		client := m.clientManager.GetClientByName(clientName)
 		if client == nil {
 			m.logger.Warn("%s Client %s not found, skipping", MCPLogPrefix, clientName)
 			continue
 		}
-		if client.ExecutionConfig.IsCodeModeClient {
+		isCodeModeClient := client.ExecutionConfig.IsCodeModeClient
+		if isCodeModeClient {
 			includeCodeModeTools = true
+			// Code mode tools are injected from m.codeMode.GetTools() below.
+			continue
 		}
 		// Add tools from this client, checking for duplicates
 		for _, tool := range clientTools {
-			if tool.Function != nil && tool.Function.Name != "" && !seenToolNames[tool.Function.Name] {
-				schemas.AppendToContextList(ctx, schemas.BifrostContextKeyMCPAddedTools, tool.Function.Name)
-				if !includeCodeModeTools {
-					availableTools = append(availableTools, tool)
-					seenToolNames[tool.Function.Name] = true
-				}
-			}
+			if tool.Function == nil || tool.Function.Name == "" {
+				continue
+			}
+			if seenToolNames[tool.Function.Name] {
+				continue
+			}
+			availableTools = append(availableTools, tool)
+			seenToolNames[tool.Function.Name] = true
+			schemas.AppendToContextList(ctx, schemas.BifrostContextKeyMCPAddedTools, tool.Function.Name)
 		}
 	}
@@
 	if includeCodeModeTools && m.codeMode != nil {
 		codeModeTools := m.codeMode.GetTools()
 		// Add code mode tools, checking for duplicates
 		for _, tool := range codeModeTools {
 			if tool.Function != nil && tool.Function.Name != "" {
 				if !seenToolNames[tool.Function.Name] {
 					availableTools = append(availableTools, tool)
 					seenToolNames[tool.Function.Name] = true
+					schemas.AppendToContextList(ctx, schemas.BifrostContextKeyMCPAddedTools, tool.Function.Name)
 				}
 			}
 		}
 	}

As per coding guidelines: core/mcp/*.go: “MCP tool access filtering follows a 4-level hierarchy: Global filter → Client-level filter → Tool-level filter → Per-request filter (HTTP headers). All four levels must agree for a tool to be available.”

Also applies to: 212-217

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/mcp/toolmanager.go` around lines 193 - 203, The GetAvailableTools loop
currently gates adding regular tools via a global includeCodeModeTools flag and
updates schemas.BifrostContextKeyMCPAddedTools before confirming addition, which
causes nondeterministic drops and missed tracking; update the logic inside the
clientTools iteration (referencing client.ExecutionConfig.IsCodeModeClient,
includeCodeModeTools, seenToolNames, availableTools, and
schemas.AppendToContextList) so that: evaluate per-tool whether code-mode allows
adding it (do not let a single client set includeCodeModeTools globally to veto
other tools), only call schemas.AppendToContextList when the tool is actually
appended to availableTools or otherwise accepted, mark seenToolNames only upon
successful addition, and ensure the same tracking is applied for code-mode tool
additions (the code-path that appends code-mode tools must also update
seenToolNames and MCPAddedTools).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/governance/main.go`:
- Around line 405-407: The header presence check currently uses a case-sensitive
map lookup on req.Headers (e.g., _, headerPresent :=
req.Headers["x-bf-mcp-include-tools"]) which can miss differently-cased incoming
headers and cause unintended injection; change the logic in the handler that
decides whether to call p.addMCPIncludeTools (and the similar check around lines
412-415) to perform a case-insensitive lookup — either normalize keys with
http.CanonicalHeaderKey or iterate req.Headers and use strings.EqualFold to
detect "x-bf-mcp-include-tools" — and only call addMCPIncludeTools(nil,
virtualKey) when no matching header is found ignoring case.

---

Duplicate comments:
In `@core/mcp/toolmanager.go`:
- Around line 193-203: The GetAvailableTools loop currently gates adding regular
tools via a global includeCodeModeTools flag and updates
schemas.BifrostContextKeyMCPAddedTools before confirming addition, which causes
nondeterministic drops and missed tracking; update the logic inside the
clientTools iteration (referencing client.ExecutionConfig.IsCodeModeClient,
includeCodeModeTools, seenToolNames, availableTools, and
schemas.AppendToContextList) so that: evaluate per-tool whether code-mode allows
adding it (do not let a single client set includeCodeModeTools globally to veto
other tools), only call schemas.AppendToContextList when the tool is actually
appended to availableTools or otherwise accepted, mark seenToolNames only upon
successful addition, and ensure the same tracking is applied for code-mode tool
additions (the code-path that appends code-mode tools must also update
seenToolNames and MCPAddedTools).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5847046b-32f7-41bf-a6ab-c734e9f6a6a4

📥 Commits

Reviewing files that changed from the base of the PR and between df4061c and 9255034.

📒 Files selected for processing (15)
  • core/bifrost.go
  • core/changelog.md
  • core/mcp/interface.go
  • core/mcp/mcp.go
  • core/mcp/toolmanager.go
  • core/schemas/bifrost.go
  • docs/features/governance/mcp-tools.mdx
  • plugins/governance/changelog.md
  • plugins/governance/main.go
  • plugins/governance/resolver.go
  • transports/bifrost-http/server/plugins.go
  • transports/bifrost-http/server/server.go
  • transports/changelog.md
  • ui/app/workspace/config/views/mcpView.tsx
  • ui/components/sidebar.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • ui/app/workspace/config/views/mcpView.tsx
  • core/mcp/mcp.go
  • plugins/governance/resolver.go
  • core/bifrost.go
  • core/changelog.md
  • plugins/governance/changelog.md

Comment on lines +405 to +407
_, headerPresent := req.Headers["x-bf-mcp-include-tools"]
if !headerPresent {
headers, err := p.addMCPIncludeTools(nil, virtualKey)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Case-sensitive header check can break the “caller already provided header” contract.

req.Headers["x-bf-mcp-include-tools"] is case-sensitive, so X-BF-MCP-Include-Tools won’t be detected and auto-injection may run unexpectedly. Use case-insensitive key detection before injecting.

💡 Suggested fix
-			_, headerPresent := req.Headers["x-bf-mcp-include-tools"]
+			headerPresent := false
+			for h := range req.Headers {
+				if strings.EqualFold(h, "x-bf-mcp-include-tools") {
+					headerPresent = true
+					break
+				}
+			}
 			if !headerPresent {
+				if req.Headers == nil {
+					req.Headers = make(map[string]string)
+				}
 				headers, err := p.addMCPIncludeTools(nil, virtualKey)
 				if err != nil {
 					p.logger.Error("failed to add MCP include tools: %v", err)
 					return nil, nil
 				}

Also applies to: 412-415

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/governance/main.go` around lines 405 - 407, The header presence check
currently uses a case-sensitive map lookup on req.Headers (e.g., _,
headerPresent := req.Headers["x-bf-mcp-include-tools"]) which can miss
differently-cased incoming headers and cause unintended injection; change the
logic in the handler that decides whether to call p.addMCPIncludeTools (and the
similar check around lines 412-415) to perform a case-insensitive lookup —
either normalize keys with http.CanonicalHeaderKey or iterate req.Headers and
use strings.EqualFold to detect "x-bf-mcp-include-tools" — and only call
addMCPIncludeTools(nil, virtualKey) when no matching header is found ignoring
case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant