Skip to content

Conversation

@akshaydeo
Copy link
Contributor

  • MCP gateway with code mode
  • New plugins architecture

Pratham-Mishra04 and others added 5 commits November 28, 2025 16:14
## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (Next.js)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

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

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

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## 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
## Summary

Implement MCP (Model Context Protocol) server functionality over HTTP, enabling Bifrost to act as an MCP server that can receive requests from MCP clients and execute tools.

## Changes

- Added a new `MCPServerHandler` that implements the MCP protocol over HTTP streaming
- Created endpoints for MCP server communication (`/mcp`) supporting both JSON-RPC (POST) and Server-Sent Events (GET)
- Implemented tool synchronization from connected MCP clients to the MCP server
- Added virtual key validation for MCP server requests when governance enforcement is enabled
- Extended the HTTP server to register MCP server routes and implement tool execution

## Type of change

- [x] Feature

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)

## How to test

Test the MCP server functionality by sending MCP protocol requests to the `/mcp` endpoint:

```sh
# Test JSON-RPC endpoint
curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk-bf-your-virtual-key" \
  -d '{"jsonrpc":"2.0","method":"list_tools","id":1}'

# Test SSE endpoint
curl -N http://localhost:8080/mcp \
  -H "Accept: text/event-stream" \
  -H "Authorization: Bearer sk-bf-your-virtual-key"
```

## Breaking changes

- [x] No

## Security considerations

The implementation includes security checks for virtual keys when governance enforcement is enabled, ensuring that MCP server requests are properly authenticated.

## Checklist

- [x] I added/updated tests where appropriate
- [x] I verified builds succeed (Go and UI)
@github-actions
Copy link
Contributor

🧪 Test Suite Available

This PR can be tested by a repository admin.

Run tests for PR #1153

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added MCP code mode enabling TypeScript execution within the platform with tool integration
    • Added agent mode for iterative, multi-turn tool orchestration with configurable depth limits
    • Added auto-execute tool configuration for automatic tool invocation in code mode
    • Added MCP client health monitoring for connection stability
    • Added tool execution support in Responses API format
  • Improvements

    • Renamed MCP Tools to MCP Gateway with enhanced server configuration UI
    • Added tool auto-execution settings per MCP server
    • Enhanced request copying functionality in logs with JSON formatting
    • Improved error messaging for virtual key and budget validation
  • Configuration

    • Added MCP agent depth and tool execution timeout settings to core configuration

✏️ Tip: You can customize this high-level summary in your review settings.

Walkthrough

This pull request introduces comprehensive MCP (Model Context Protocol) agent mode with auto-executable and non-auto-executable tool classification, TypeScript code execution within sandboxed VMs, health monitoring for MCP clients, and support for both Chat and Responses API formats. Includes extensive test coverage, UI updates for MCP server management, and configuration persistence across the framework.

Changes

Cohort / File(s) Summary
MCP Core Agent & Orchestration
core/mcp/agent.go, core/mcp/agent_adaptors.go, core/mcp/agent_test.go
Introduces agent execution framework with iterative tool-calling up to configured depth, auto-executable tool classification via code-mode binding levels, parallel execution for auto-tools, and result aggregation with conversation history updates. Adapters abstract Chat vs Responses API differences. Comprehensive test scaffolding with mock LLM/logger/client manager.
MCP Tool Management & Execution
core/mcp/toolmanager.go, core/mcp/utils.go, core/mcp/mcp.go
Centralized tool discovery, request augmentation with integration-aware deduplication, context-based tool filtering, timeout-controlled execution, and agent delegation. MCPManager bridges Bifrost with MCP servers, manages clients, and orchestrates agent-mode flows.
MCP Code Mode Execution
core/mcp/codemode_executecode.go, core/mcp/codemode_listfiles.go, core/mcp/codemode_readfile.go
TypeScript code execution in goja VM with import stripping, transpilation to ES5, console logging, environment context exposure, and tool invocation with argument validation and timeouts. List/read tools provide VFS-like access to server declarations and TypeScript definitions. Error handling with hints for TypeScript/runtime failures.
MCP Client Management & Connection
core/mcp/clientmanager.go, core/mcp/health_monitor.go
Connection establishment for HTTP/STDIO/SSE/InProcess transports with tool retrieval and locking. Health monitoring with periodic checks, failure thresholds, and state transitions. Client state tracking with connection metadata.
Bifrost Core Integration
core/bifrost.go, core/chatbot_test.go
MCP manager wired as singleton via sync.Once, agent-mode checks on Chat/Responses paths when configured. Public API refactored with internal helpers and MCP-aware routing. Renamed ExecuteMCPTool → ExecuteChatMCPTool, added ExecuteResponsesMCPTool. Tool registration/execution delegated to MCP manager.
Schema & Type Definitions
core/schemas/bifrost.go, core/schemas/mcp.go, core/schemas/mux.go, core/schemas/utils.go, core/schemas/context.go
New MCPToolManagerConfig (timeout, max depth, code-mode binding level), CodeModeBindingLevel ("server"/"tool"), MCPClientState tracking connection metadata, IsCodeModeClient flag, ToolsToAutoExecute configuration. ResponsesToolMessage ↔ ChatAssistantMessageToolCall conversion helpers. Deep copy utility for ChatTool. BifrostContext lazy initialization for user values.
Framework Configuration & Persistence
framework/configstore/rdb.go, framework/configstore/clientconfig.go, framework/configstore/tables/clientconfig.go, framework/configstore/tables/mcp.go, framework/configstore/utils.go, framework/configstore/migrations.go, framework/configstore/migrations_test.go, framework/configstore/store.go
MCPAgentDepth, MCPToolExecutionTimeout, MCPCodeModeBindingLevel persisted in client config with defaults. IsCodeModeClient and ToolsToAutoExecute JSON columns added to MCP client table. Name normalization migration to enforce underscore naming. RetryOnNotFound utility for eventual consistency. GetRateLimits added to store interface.
HTTP Server & Routes
transports/bifrost-http/handlers/config.go, transports/bifrost-http/handlers/mcp.go, transports/bifrost-http/handlers/mcp_server.go, transports/bifrost-http/handlers/inference.go, transports/bifrost-http/lib/config.go, transports/bifrost-http/server/server.go
MCP tool manager config updates via runtime handler. Validation for tools_to_auto_execute consistency and MCP client names. New MCPServerHandler managing global + per-VK MCP servers with tool synchronization and SSE support. CORS header removed from streaming responses. MCPConfig FetchNewRequestIDFunc wired to UUID generation.
Governance Plugin Refactor
plugins/governance/main.go, plugins/governance/resolver.go, plugins/governance/store.go, plugins/governance/tracker.go
GovernanceStore extracted as interface; LocalGovernanceStore as in-memory implementation with sync.Map for rate limits. BudgetResolver store field changed to value type. RateLimitHierarchy checks via unified store method. UsageTracker background worker interval reduced to 10s with in-memory resets and periodic DB dumps. InitFromStore added for custom store injection.
Governance Comprehensive Test Suite
plugins/governance/e2e_test.go, plugins/governance/advanced_scenarios_test.go, plugins/governance/config_update_sync_test.go, plugins/governance/customer_budget_test.go, plugins/governance/in_memory_sync_test.go, plugins/governance/provider_budget_test.go, plugins/governance/rate_limit_*.go, plugins/governance/team_budget_test.go, plugins/governance/edge_cases_test.go, plugins/governance/fixtures_test.go, plugins/governance/resolver_test.go, plugins/governance/store_test.go, plugins/governance/tracker_test.go, plugins/governance/test_utils.go, plugins/governance/changelog.md, plugins/governance/go.mod
11 new test files covering multi-VK fairness, hierarchical budgets, rate-limit enforcement, provider budgets, team/customer cascades, concurrent requests, in-memory sync, boundary conditions, and deletion cascades. Mock logger, test fixtures, cost calculators, and API request helpers. Governance store tests validate concurrent reads and budget/rate-limit checks.
MCP Test Suite
tests/core-mcp/ (10 files + go.mod)
Comprehensive test infrastructure: setup helpers for test Bifrost instances with code-mode support, tool registration (echo, add, multiply, get_data, error_tool, slow_tool, complex_args_tool), fixtures with 50+ TypeScript code snippets, utilities for tool calling and result assertion. Tests cover client config, auto-execute behavior, code-mode execution, agent mode, edge cases, integration workflows, Responses API, tool execution, and MCP connections.
UI Components & Schemas
ui/lib/types/config.ts, ui/lib/types/mcp.ts, ui/lib/types/logs.ts, ui/lib/types/schemas.ts, ui/app/workspace/config/views/mcpView.tsx, ui/app/workspace/config/views/clientSettingsView.tsx, ui/app/workspace/config/views/...View.tsx (5 files), ui/app/workspace/mcp-gateway/ (2 files), ui/components/ui/sheet.tsx, ui/components/ui/tristateCheckbox.tsx, ui/components/sidebar.tsx, ui/app/workspace/logs/... (3 files)
MCPView component for managing MCP tool manager config (agent depth, execution timeout). TriStateCheckbox for multi-select with wildcard support. Expanded MCP client form with IsCodeModeClient toggle and tools_to_auto_execute management. Updated client table with code-mode badge, enabled/auto-execute tool counts, reconnect/delete actions. Log display enhancements for Responses API messages with output field. Sidebar rename "MCP Tools" → "MCP Gateway". Sheet component showCloseButton prop.
Minor Updates & Fixes
core/providers/openai/openai.go, core/providers/nebius/nebius.go, core/mcp.go, framework/modelcatalog/main.go, framework/modelcatalog/sync.go, plugins/logging/main.go, docs/features/governance/virtual-keys.mdx, docs/changelogs/v1.3.47.mdx, .editorconfig, transports/config.schema.json, transports/go.mod, core/go.mod, ui/app/_fallbacks/enterprise/components/api-keys/APIKeysView.tsx, ui/lib/utils/validation.ts, framework/configstore/tables/budget.go, framework/configstore/tables/ratelimit.go
Dependency updates (aws-sdk, mcp-go v0.43.2, sonic, oauth2, goja, go-typescript, etc.). ShouldSyncPricingFunc hook in ModelCatalog. LastDBUsage/LastDBTokenUsage virtual fields for budget/rate-limit tracking. Go tab indentation rule. Removed useState from APIKeysView. ValidateOrigins early exit. Error message text updates. Schema additions for global proxy config and MCP tool manager config.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant Bifrost as Bifrost<br/>(Chat Request)
    participant Agent as Agent<br/>Executor
    participant MCP as MCP<br/>Manager
    participant LLM as LLM<br/>Provider
    participant Tool as Tool<br/>Executor
    
    Client->>Bifrost: ChatCompletionRequest
    Bifrost->>Bifrost: makeChatCompletionRequest()
    Bifrost->>Bifrost: Check if MCP configured
    alt MCP Configured
        Bifrost->>Agent: CheckAndExecuteAgentForChatRequest()
        Agent->>LLM: Initial LLM call via makeReq
        LLM-->>Agent: Response with tool_calls
        
        loop Until max depth or no tools
            Agent->>Agent: Classify tools (auto vs non-auto)
            
            par Auto-executable tools
                Agent->>Tool: Execute in parallel
                Tool-->>Agent: Results
            and Monitor
                Agent->>Agent: Aggregate results
            end
            
            Agent->>Agent: Update conversation history
            Agent->>LLM: Next LLM call with results
            LLM-->>Agent: Next response
            
            alt Has tool_calls
                Agent->>Agent: Extract and classify
            else No tool_calls
                Agent->>Agent: Exit loop
            end
        end
        
        Agent-->>Bifrost: Final response
    else No MCP
        Bifrost-->>Client: Response as-is
    end
    
    Bifrost-->>Client: ChatCompletionResponse
    
    rect rgb(200, 220, 255)
        note over Agent,Tool: Agent mode handles iterative<br/>tool execution with depth limits
    end
Loading
sequenceDiagram
    participant Client
    participant CodeExec as Code<br/>Executor Tool
    participant VM as goja<br/>VM
    participant Tools as MCP<br/>Tools
    
    Client->>CodeExec: executeToolCode(TypeScript code)
    
    CodeExec->>CodeExec: Preprocess (strip imports/exports)
    CodeExec->>CodeExec: Transpile TS → ES5
    
    alt Transpilation Success
        CodeExec->>VM: Create VM with console shim
        CodeExec->>VM: Bind servers & tools as properties
        CodeExec->>VM: Execute code with timeout
        
        loop While code executes
            alt Tool call detected
                VM->>Tools: Call MCP tool
                Tools-->>VM: Result
                VM->>VM: Capture console output
            end
        end
        
        VM-->>CodeExec: Result + logs + environment
        CodeExec-->>Client: ExecutionResult{success}
    else Transpilation Failure
        CodeExec-->>Client: ExecutionResult{error=TypeScript}
    end
    
    rect rgb(200, 255, 200)
        note over CodeExec,VM: Code-mode sandbox execution<br/>with tool integration
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

This is a massive, multi-system refactor introducing agent orchestration, code execution, governance store abstraction, and extensive test coverage across core, framework, plugins, and UI. The changes are heterogeneous—spanning deeply interconnected logic (agent loop, MCP client management, health monitoring), persistence layers (migrations, config tables), HTTP handlers, and new TypeScript execution semantics. High logic density in agent.go, codemode_executecode.go, and governance/store.go requires careful reasoning. The governance plugin refactor introduces interface extraction and behavioral changes to rate-limit/budget checking. Comprehensive test suites (11 governance files, 10 MCP test files) demand validation of integration patterns. UI and schema changes are numerous but mostly straightforward additions.

Possibly related PRs

Suggested reviewers

  • danpiths
  • roroghost17

Poem

🐰 Hop, hop! The burrows of MCP are deep,
Where agents leap through tool-calls they keep,
TypeScript dances in sandboxed VM dreams,
Code-mode shimmers with governance schemes,
Tests bloom like carrots in springtime glow,
Oh what a garden of logic does grow! 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is incomplete; it only lists two high-level features without explaining the purpose, detailed changes, affected areas, testing instructions, or breaking changes as required by the template. Complete the description following the provided template: add a summary section, detailed changes, type of change, affected areas, testing steps, and breaking changes documentation.
Docstring Coverage ⚠️ Warning Docstring coverage is 62.99% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'v1.4.0' is vague and does not describe the actual changes made in the PR; it only indicates a version bump without conveying the primary changes. Update the title to be more descriptive of the main changes, such as 'Add MCP gateway with code mode and agent execution support' or 'Implement MCP gateway, code mode, and new plugins architecture'.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch v1.4.0

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

@akshaydeo akshaydeo changed the title V1.4.0 v1.4.0 Dec 22, 2025
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: 14

🧹 Nitpick comments (56)
ui/lib/utils/validation.ts (1)

374-376: LGTM! Sensible optimization with early exit.

The early return for empty or undefined origins is correct and improves performance by avoiding unnecessary filtering operations. Returning isValid: true for an empty input follows the principle of vacuous truth.

Optional refinement: Consider making the type signature explicit if origins can be undefined:

🔎 Type signature refinement
-export function validateOrigins(origins: string[]): { isValid: boolean; invalidOrigins: string[] } {
+export function validateOrigins(origins: string[] | undefined): { isValid: boolean; invalidOrigins: string[] } {

This would make the function signature align with the defensive !origins check and provide better type safety at call sites.

core/providers/openai/openai.go (1)

1933-2044: Consider refactoring transcription handling to use HandleProviderResponse

The TODO correctly points out that this path is the odd one out: it decodes with sonic.Unmarshal and then (optionally) decodes again into rawResponse. When you pick this up, it would be cleaner to mirror the other handlers and centralize JSON/RawResponse handling via providerUtils.HandleProviderResponse, keeping the existing HTML-response guard intact to avoid regressions.

ui/lib/types/schemas.ts (1)

601-647: MCP client name and tool constraints—check for backward compatibility and consider deduping validators

  • The stricter name rules (no hyphens/spaces and not starting with a digit) make sense for identifier-like usage, but they may prevent editing existing MCP clients whose names don’t meet these constraints. If such clients already exist, consider either:

    • migrating their names, or
    • handling legacy names more leniently on update in the UI/schema.
  • tools_to_auto_execute reuses the same wildcard/duplicate semantics as tools_to_execute, which is good. The two refine blocks are identical; if this evolves further, extracting a shared helper validator would reduce duplication.

transports/bifrost-http/handlers/mcp.go (1)

348-357: Simplify unreachable conditional branch.

The nested check if !hasWildcardInExecute at line 351 is always true at this point in the code. When hasWildcardInExecute is true, the function returns early at line 341. This makes the inner condition redundant.

🔎 Proposed simplification
 		// Validate each tool in ToolsToAutoExecute
 		for _, tool := range toolsToAutoExecute {
 			if tool == "*" {
-				// Wildcard is allowed if "*" is in ToolsToExecute
-				if !hasWildcardInExecute {
-					return fmt.Errorf("tool '%s' in tools_to_auto_execute is not in tools_to_execute", tool)
-				}
+				// At this point, hasWildcardInExecute is always false
+				// (if it were true, we would have returned at line 341)
+				return fmt.Errorf("tool '%s' in tools_to_auto_execute is not in tools_to_execute", tool)
 			} else if !allowedTools[tool] {
 				return fmt.Errorf("tool '%s' in tools_to_auto_execute is not in tools_to_execute", tool)
 			}
 		}
tests/core-mcp/client_config_test.go (2)

24-31: Consider extracting the repeated client lookup into a helper.

The pattern of finding bifrostInternal client by iterating through clients and matching on ID is repeated in nearly every test function. Extracting this to a helper would reduce duplication.

🔎 Example helper function
// In utils.go or setup.go
func findBifrostInternalClient(clients []schemas.MCPClient) *schemas.MCPClient {
	for i := range clients {
		if clients[i].Config.ID == "bifrostInternal" {
			return &clients[i]
		}
	}
	return nil
}

Usage in tests:

bifrostClient := findBifrostInternalClient(clients)
require.NotNil(t, bifrostClient, "bifrostInternal client should exist")

129-158: Test name suggests mixed mode validation but only checks code mode count.

TestMixedCodeModeAndNonCodeModeClients implies it should verify both code mode and non-code mode clients exist, but only asserts on codeModeCount. Consider also asserting that non-code mode clients exist, or clarify the test's intent.

🔎 Proposed enhancement
 	codeModeCount := 0
+	nonCodeModeCount := 0

 	for _, client := range clients {
 		if client.Config.IsCodeModeClient {
 			codeModeCount++
+		} else {
+			nonCodeModeCount++
 		}
 	}

 	// At minimum, we should have bifrostInternal as code mode
 	assert.GreaterOrEqual(t, codeModeCount, 1, "Should have at least one code mode client")
+	// For a true "mixed" test, verify both types exist
+	// Note: This may require adding another client with non-code mode
ui/app/workspace/logs/views/columns.tsx (1)

40-50: Consider defensive handling for array block mapping.

On line 46, if lastMessage.output is an array but some blocks don't have a text property, the map will produce undefined values that get joined. Consider filtering or providing a fallback.

🔎 Proposed fix
 			} else if (Array.isArray(lastMessage.output)) {
-				return lastMessage.output.map((block) => block.text).join("\n");
+				return lastMessage.output.map((block) => block.text ?? "").filter(Boolean).join("\n");
 			} else if (lastMessage.output.type && lastMessage.output.type === "computer_screenshot") {
tests/core-mcp/edge_cases_test.go (1)

81-104: Consider testing actual timeout behavior.

The test name mentions "Timeout" but the test uses a 100ms delay which should complete successfully. Consider adding a separate test that actually triggers a timeout (e.g., delay longer than the configured tool execution timeout) to verify timeout error handling.

core/schemas/mux.go (1)

325-338: Function-call output routing looks consistent; consider future-proofing for block outputs.

The changes cleanly:

  • Omit Role for function_call_output messages and gate rm.Role on role != "".
  • Ensure function_call_output content is kept solely in ResponsesToolMessage.Output instead of rm.Content.
  • Skip Content / ContentBlocks population for function_call_output while preserving the existing block-type mapping for all other messages.

This matches the Responses API model and avoids duplicated content paths. One limitation to be aware of: if ChatMessageRoleTool ever starts using ContentBlocks for structured outputs, those blocks are currently ignored and not copied into ResponsesToolMessage.Output. If you plan to support structured tool outputs later, it may be worth extending the outputContent handling to cover block outputs as well (or documenting that tool outputs are string-only for now).

Also applies to: 351-366, 368-425, 435-448

tests/core-mcp/utils.go (1)

15-30: Tighten test helper robustness around JSON errors and generic error-string matching.

  • createToolCall currently ignores json.Marshal errors. Even in tests, this can silently convert bad argument shapes into empty {} payloads. Consider returning an error from createToolCall (and using require.NoError at the call site) or panicking on marshal failure so tests fail loudly rather than masking invalid inputs.
  • In assertExecutionResult, the generic assert.NotContains(t, responseText, "Error:") is stricter than the comment (“but allow console.error output”) and may produce false negatives if the successful tool output legitimately includes Error: as part of normal text. The two explicit checks for "Execution runtime error" and "Execution typescript error" already cover the structured failure modes; you might drop or relax the generic "Error:" assertion to avoid accidental test brittleness.

Also applies to: 32-60

core/mcp/clientmanager.go (1)

21-36: MCP client lifecycle, locking, and connection setup look solid; fix small doc inconsistencies.

  • GetClients, AddClient, ReconnectClient, RemoveClient/removeClientUnsafe, and connectToMCPClient follow a good pattern: short lock-held sections around m.clientMap, heavy network/IO setup done without holding the lock, SSE CancelFunc is stored and later invoked for cleanup, and transports are closed on error or removal.
  • RegisterTool correctly ensures the local in-process server/client exist, validates tool names, avoids duplicate registrations, wraps the typed handler via CallToolRequest.GetArguments(), and keeps the internal ToolMap in sync.

Two minor polish items:

  • The EditClient comment block still says it “refreshes its tool list” and retrieves tools from the server, but the implementation intentionally only updates execution filters and flags (Name/IsCodeModeClient/Headers/ToolsToExecute/ToolsToAutoExecute) and relies on ReconnectClient for tool refresh. Updating the comment to match behavior will avoid confusion.
  • In connectToMCPClient, the initial existingClient.ConnectionInfo.Type = config.ConnectionType is immediately superseded by the subsequent m.clientMap[config.ID] = &schemas.MCPClientState{...} assignment. You can safely drop that assignment (or reuse the existing struct instead of always allocating a new one) to reduce redundancy.

Also applies to: 47-110, 134-162, 176-200, 291-431, 433-534, 540-583, 591-679

transports/bifrost-http/lib/config.go (2)

217-231: New MCP client defaults are reasonable; confirm migrations initialize them for existing installs.

Adding MCPAgentDepth: 10 and MCPToolExecutionTimeout: 30 to DefaultClientConfig provides sensible defaults for agent depth and per-tool timeout. For fresh setups they flow through loadDefaultClientConfig as expected.

For existing deployments where client config already lives in the DB, these fields will remain 0 unless:

  • The DB migration sets sane non-zero defaults, or
  • A config file provides explicit values (handled in loadClientConfigFromFile).

It’s worth double‑checking the migration layer to ensure upgraded instances don’t silently run with unlimited/zero depth or timeout when no file-based overrides exist.


445-507: Inline client-config merge logic is fine; consider deleting or reusing mergeClientConfig to avoid drift.

loadClientConfigFromFile now inlines a clear, field-by-field merge:

  • DB config wins overall, with file values only filling zero/empty ints/slices.
  • Boolean flags are only flipped false → true based on the file.
  • New fields MCPAgentDepth and MCPToolExecutionTimeout are included in the same pattern.

This is correct, but the old mergeClientConfig helper immediately below implements almost the same logic (without the new MCP fields) and is no longer invoked here. To keep maintenance overhead down and avoid future divergence, consider either:

  • Deleting mergeClientConfig if it’s truly unused, or
  • Refactoring loadClientConfigFromFile to delegate to it and extending the helper to cover the new fields.

Also applies to: 534-573

ui/lib/types/config.ts (1)

326-327: Add documentation for new MCP config fields.

The new mcp_agent_depth and mcp_tool_execution_timeout fields lack documentation. Add JSDoc comments to clarify their purpose and valid ranges.

🔎 Suggested documentation
 	enable_litellm_fallbacks: boolean;
+	/** Maximum depth for MCP agent mode tool execution (minimum: 1) */
 	mcp_agent_depth: number;
+	/** Timeout for individual MCP tool execution in seconds (minimum: 1) */
 	mcp_tool_execution_timeout: number;
 }
transports/config.schema.json (1)

1766-1782: Consider adding maximum constraints for MCP config fields.

The tool_execution_timeout and max_agent_depth fields only have minimum constraints but no maximum values. Consider adding reasonable upper bounds to prevent misconfiguration that could cause resource exhaustion.

For example:

  • tool_execution_timeout: Maximum of 300 seconds (5 minutes) seems reasonable for a tool execution
  • max_agent_depth: Maximum of 50 to prevent infinite recursion scenarios
🔎 Proposed schema enhancement
       "tool_execution_timeout": {
         "type": "integer",
         "description": "Tool execution timeout in seconds",
         "minimum": 1,
+        "maximum": 300,
         "default": 30
       },
       "max_agent_depth": {
         "type": "integer",
         "description": "Max agent depth",
         "minimum": 1,
+        "maximum": 50,
         "default": 10
       }
tests/core-mcp/agent_mode_test.go (1)

73-77: Assertion doesn't verify the configured MaxAgentDepth value.

The test configures MaxAgentDepth = 2 but only asserts that clients is not nil. This doesn't verify that the max depth configuration was actually applied. Consider adding an assertion that validates the configured value is accessible and correct.

🔎 Suggested improvement
 	// Verify max depth is configured
 	clients, err := b.GetMCPClients()
 	require.NoError(t, err)
 	assert.NotNil(t, clients, "Should have clients")
+
+	// Verify the actual max depth configuration
+	toolManagerConfig := b.GetMCPToolManagerConfig()
+	assert.Equal(t, 2, toolManagerConfig.MaxAgentDepth, "MaxAgentDepth should be 2")
+	assert.Equal(t, 30*time.Second, toolManagerConfig.ToolExecutionTimeout, "ToolExecutionTimeout should be 30s")

If GetMCPToolManagerConfig() or equivalent is not exposed, consider whether the test should verify behavior (e.g., agent stops at depth 2) rather than just configuration presence.

framework/configstore/migrations_test.go (2)

34-41: Log capture may interfere with parallel tests.

captureLogOutput modifies the global log output, which can cause race conditions or log interleaving if tests run in parallel with t.Parallel(). While not currently using parallel execution, this could become an issue.

🔎 Consider using a custom logger
-// captureLogOutput captures log output during a function execution
-func captureLogOutput(fn func()) string {
-	var buf bytes.Buffer
-	log.SetOutput(&buf)
-	defer log.SetOutput(os.Stderr)
-
-	fn()
-	return buf.String()
-}
+// captureLogOutput captures log output during a function execution
+// Note: This modifies global log state and is not safe for parallel tests
+func captureLogOutput(fn func()) string {
+	var buf bytes.Buffer
+	log.SetOutput(&buf)
+	defer log.SetOutput(os.Stderr)
+
+	fn()
+	return buf.String()
+}

Alternatively, consider passing a *log.Logger instance to the functions under test to enable isolated testing.


248-259: Consider calling the actual normalizeMCPClientName function.

This local helper duplicates the normalization logic from migrations.go. If the production implementation changes, this test helper could become stale and tests might pass while actual behavior differs.

🔎 Suggested improvement
-	// Helper function to normalize (same logic as in migrations.go)
-	normalizeName := func(name string) string {
-		normalized := strings.ReplaceAll(name, "-", "_")
-		normalized = strings.ReplaceAll(normalized, " ", "_")
-		normalized = strings.TrimLeftFunc(normalized, func(r rune) bool {
-			return r >= '0' && r <= '9'
-		})
-		if normalized == "" {
-			normalized = "mcp_client"
-		}
-		return normalized
-	}
+	// Use the actual normalizeMCPClientName function from migrations.go
+	normalizeName := normalizeMCPClientName

This ensures tests exercise the actual production code path.

ui/app/workspace/config/views/mcpView.tsx (2)

11-26: Duplicate default config definition across views.

This defaultConfig object is duplicated across multiple view files (clientSettingsView.tsx, governanceView.tsx, observabilityView.tsx, performanceTuningView.tsx, securityView.tsx). Consider extracting this to a shared constant to ensure consistency and reduce maintenance burden when adding new fields.


76-100: Validation duplicates logic already enforced by input handlers.

The handleSave validation re-parses and validates localValues, but localConfig already contains validated positive integers from the change handlers. Consider simplifying by directly using localConfig.mcp_agent_depth and localConfig.mcp_tool_execution_timeout in the guard, or removing the duplicate parse:

🔎 Suggested simplification
 const handleSave = useCallback(async () => {
   try {
-    const agentDepth = Number.parseInt(localValues.mcp_agent_depth);
-    const toolTimeout = Number.parseInt(localValues.mcp_tool_execution_timeout);
-
-    if (isNaN(agentDepth) || agentDepth <= 0) {
+    if (!localConfig.mcp_agent_depth || localConfig.mcp_agent_depth <= 0) {
       toast.error("Max agent depth must be a positive number.");
       return;
     }
 
-    if (isNaN(toolTimeout) || toolTimeout <= 0) {
+    if (!localConfig.mcp_tool_execution_timeout || localConfig.mcp_tool_execution_timeout <= 0) {
       toast.error("Tool execution timeout must be a positive number.");
       return;
     }
framework/configstore/clientconfig.go (1)

103-113: Inconsistent hashing approach for integer fields.

The new MCP integer fields use strconv.Itoa with string concatenation, while existing integer fields (InitialPoolSize, LogRetentionDays, MaxRequestBodySizeMB at lines 116-132) use sonic.Marshal. Both are deterministic, but the inconsistency could cause confusion for future maintainers. Consider aligning the approach.

Additionally, the > 0 conditional means negative values hash identically to zero. If negative values should be rejected, consider validation elsewhere; if they're valid, they should produce distinct hashes.

🔎 Option to align with existing pattern
-	if c.MCPAgentDepth > 0 {
-		hash.Write([]byte("mcpAgentDepth:" + strconv.Itoa(c.MCPAgentDepth)))
-	} else {
-		hash.Write([]byte("mcpAgentDepth:0"))
-	}
-
-	if c.MCPToolExecutionTimeout > 0 {
-		hash.Write([]byte("mcpToolExecutionTimeout:" + strconv.Itoa(c.MCPToolExecutionTimeout)))
-	} else {
-		hash.Write([]byte("mcpToolExecutionTimeout:0"))
-	}
+	data, err = sonic.Marshal(c.MCPAgentDepth)
+	if err != nil {
+		return "", err
+	}
+	hash.Write(data)
+
+	data, err = sonic.Marshal(c.MCPToolExecutionTimeout)
+	if err != nil {
+		return "", err
+	}
+	hash.Write(data)
framework/configstore/utils.go (1)

186-202: Redacted value restoration logic is correct but pattern matching could have edge cases.

The restoration of redacted header values from existingHeaders correctly handles the UI flow where sensitive values are masked. However, the check strings.Contains(value, "****") could match legitimate header values containing four asterisks. Consider a more specific redaction pattern if one is consistently used (e.g., exact match or a distinctive prefix).

tests/core-mcp/mcp_connection_test.go (1)

182-217: Consider improving placeholder test documentation.

The skipped test is well-documented with a placeholder comment block showing intended usage. Consider adding a TODO or issue reference to track when this should be enabled.

ui/components/ui/tristateCheckbox.tsx (1)

90-104: Unreachable style condition detected.

Line 97 condition !isChecked && !isIndeterminate && state !== "none" can never be true. Given the three states:

  • state === "all"isChecked = true
  • state === "some"isIndeterminate = true
  • state === "none" → excluded by state !== "none"

This condition is dead code and can be removed.

🔎 Proposed fix
 				state === "none" && "bg-destructive/5 border-destructive/30 text-destructive dark:bg-destructive/20 dark:border-destructive/40",
-				!isChecked && !isIndeterminate && state !== "none" && "bg-background",
 				"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
ui/app/workspace/mcp-gateway/views/mcpClientsTable.tsx (3)

54-56: Missing dependency in useEffect.

The useEffect calls loadClients on mount but doesn't include it in the dependency array. This could cause stale closures if loadClients changes.

🔎 Proposed fix
 	useEffect(() => {
 		loadClients();
+		// eslint-disable-next-line react-hooks/exhaustive-deps
 	}, []);

Or better, call refetch directly:

 	useEffect(() => {
-		loadClients();
+		refetch();
 	}, []);

163-175: Use strict equality for state comparison.

Lines 165, 171, 195, 204, 190 use loose equality (==) for comparing c.state. Use strict equality (===) for consistency and to avoid type coercion issues.

🔎 Proposed fix
-							c.state == "connected"
+							c.state === "connected"
-								? c.config.tools_to_execute?.includes("*")
-									? c.tools?.length
-									: (c.config.tools_to_execute?.length ?? 0)
-								: 0;
+								? c.config.tools_to_execute?.includes("*")
+									? c.tools?.length
+									: (c.config.tools_to_execute?.length ?? 0)
+								: 0;
 							const autoExecuteToolsCount =
-								c.state == "connected"
+								c.state === "connected"

184-192: Use strict equality for code mode badge state check.

Line 187 and 190 use loose equality. Apply consistent strict equality.

🔎 Proposed fix
-										c.state == "connected" ? MCP_STATUS_COLORS[c.config.is_code_mode_client ? "connected" : "disconnected"] : ""
+										c.state === "connected" ? MCP_STATUS_COLORS[c.config.is_code_mode_client ? "connected" : "disconnected"] : ""
 									}
 								>
-									{c.state == "connected" ? <>{c.config.is_code_mode_client ? "Enabled" : "Disabled"}</> : "-"}
+									{c.state === "connected" ? <>{c.config.is_code_mode_client ? "Enabled" : "Disabled"}</> : "-"}
core/mcp/codemode_listfiles.go (1)

62-73: Consider sorting client names for deterministic output.

Map iteration order in Go is not deterministic. If consistent ordering is desired for the file listing (e.g., for testing or user experience), consider sorting the client names.

🔎 Proposed fix
+	// Collect and sort client names for deterministic output
+	clientNames := make([]string, 0, len(availableToolsPerClient))
+	for clientName := range availableToolsPerClient {
+		clientNames = append(clientNames, clientName)
+	}
+	sort.Strings(clientNames)
+
 	// Build tree structure
 	treeLines := []string{"servers/"}
 	codeModeServerCount := 0
-	for clientName := range availableToolsPerClient {
+	for _, clientName := range clientNames {

Add "sort" to imports.

framework/configstore/migrations.go (3)

1280-1315: Consider backfilling default values for existing rows.

Unlike other migrations in this file, this one doesn't backfill default values for existing rows. Based on TableClientConfig struct (lines 23-24 in tables/clientconfig.go), defaults are 10 for mcp_agent_depth and 30 for mcp_tool_execution_timeout. Existing rows may have NULL values.

🔎 Proposed fix
 		if !migrator.HasColumn(&tables.TableClientConfig{}, "mcp_tool_execution_timeout") {
 			if err := migrator.AddColumn(&tables.TableClientConfig{}, "mcp_tool_execution_timeout"); err != nil {
 				return err
 			}
 		}
+		// Backfill default values for existing rows
+		if err := tx.Exec("UPDATE config_client SET mcp_agent_depth = 10 WHERE mcp_agent_depth IS NULL OR mcp_agent_depth = 0").Error; err != nil {
+			return fmt.Errorf("failed to backfill mcp_agent_depth: %w", err)
+		}
+		if err := tx.Exec("UPDATE config_client SET mcp_tool_execution_timeout = 30 WHERE mcp_tool_execution_timeout IS NULL OR mcp_tool_execution_timeout = 0").Error; err != nil {
+			return fmt.Errorf("failed to backfill mcp_tool_execution_timeout: %w", err)
+		}
 		return nil

1360-1404: Consider using structured logging instead of log.Printf.

The migration uses log.Printf for logging name transformations while other parts of the codebase may use a structured logger. Consider using consistent logging patterns.


1437-1441: Document the one-way nature of the migration in the ID or description.

The rollback explicitly states it cannot restore original names. Consider documenting this limitation more prominently.

🔎 Suggested documentation
 	m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
-		ID: "normalize_mcp_client_names",
+		ID: "normalize_mcp_client_names_one_way",
 		Migrate: func(tx *gorm.DB) error {

Or add a more prominent comment at the migration function level.

ui/app/workspace/mcp-gateway/views/mcpClientSheet.tsx (1)

225-236: UI label says "Code Mode Client" but component manages "MCP server configuration".

There's a terminology inconsistency. The SheetDescription at line 193 says "MCP server configuration" and the section header at line 209 says "Basic Information", but the label here says "Code Mode Client". Consider using "Code Mode Server" for consistency with the rest of the UI changes in this PR (as done in mcpClientForm.tsx at line 209).

🔎 Suggested fix for consistency
-							<FormLabel>Code Mode Client</FormLabel>
+							<FormLabel>Code Mode Server</FormLabel>
ui/app/workspace/mcp-gateway/views/mcpClientForm.tsx (1)

101-109: Validation rules have redundancy - pattern already covers some checks.

The regex pattern /^[a-zA-Z0-9_]+$/ at line 104 already ensures no hyphens or spaces are allowed. The explicit checks at lines 105-106 are redundant but harmless - they provide more specific error messages. However, the pattern check will fail first, so users will see the generic pattern error rather than the specific hyphen/space messages.

Consider reordering validations so specific checks come before the pattern, or remove the redundant checks:

🔎 Option 1: Reorder for better error messages
 const validator = new Validator([
   // Name validation
   Validator.required(form.name?.trim(), "Server name is required"),
+  Validator.custom(!(form.name || "").includes("-"), "Server name cannot contain hyphens"),
+  Validator.custom(!(form.name || "").includes(" "), "Server name cannot contain spaces"),
+  Validator.custom((form.name || "").length === 0 || !/^[0-9]/.test(form.name || ""), "Server name cannot start with a number"),
   Validator.pattern(form.name || "", /^[a-zA-Z0-9_]+$/, "Server name can only contain letters, numbers, and underscores"),
-  Validator.custom(!(form.name || "").includes("-"), "Server name cannot contain hyphens"),
-  Validator.custom(!(form.name || "").includes(" "), "Server name cannot contain spaces"),
-  Validator.custom((form.name || "").length === 0 || !/^[0-9]/.test(form.name || ""), "Server name cannot start with a number"),
   Validator.minLength(form.name || "", 3, "Server name must be at least 3 characters"),
   Validator.maxLength(form.name || "", 50, "Server name cannot exceed 50 characters"),
core/mcp/utils.go (1)

438-466: Regex-based tool call extraction may have false positives.

The regex (?:await\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\.\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\( will match any method call pattern, including standard JavaScript calls like console.log(, JSON.stringify(, Math.random(. While line 471-474 in isToolCallAllowedForCodeMode handles this by checking against known client names, consider documenting this behavior.

🔎 Suggested documentation improvement
 // extractToolCallsFromCode extracts tool calls from TypeScript code
 // Tool calls are in the format: serverName.toolName(...) or await serverName.toolName(...)
+// Note: This will match all method call patterns including built-in objects (console.log, JSON.stringify, etc.)
+// The caller (isToolCallAllowedForCodeMode) is responsible for filtering out non-MCP tool calls
+// by checking if the server name exists in the list of known MCP clients.
 func extractToolCallsFromCode(code string) ([]toolCallInfo, error) {
core/mcp/codemode_readfile.go (1)

258-264: Consider using slices.Sort instead of manual bubble sort.

Go 1.21+ provides slices.Sort which is more efficient and idiomatic. The current bubble sort is O(n²) while slices.Sort is O(n log n).

🔎 Suggested improvement
+import "slices"

-			// Simple alphabetical sort
-			for i := 0; i < len(propNames)-1; i++ {
-				for j := i + 1; j < len(propNames); j++ {
-					if propNames[i] > propNames[j] {
-						propNames[i], propNames[j] = propNames[j], propNames[i]
-					}
-				}
-			}
+			// Sort properties for consistent output
+			slices.Sort(propNames)
tests/core-mcp/fixtures.go (1)

121-277: Consider using a raw string literal or external file for readability.

The LongCodeExecution fixture is functional but hard to read with concatenated strings. For test maintainability, consider using backtick raw strings or loading from an external .ts file.

That said, this is a minor readability concern and doesn't affect functionality.

tests/core-mcp/tool_execution_test.go (2)

107-126: Clarify the expected behavior in the assertion.

The test comment says "should have no code mode clients" but the assertion checks for "No servers" message. The conditional if bifrostErr == nil && result != nil on line 122 is redundant since requireNoBifrostError already ensures bifrostErr is nil.

🔎 Suggested simplification
 	// listToolFiles should still work but return empty/no servers message
-	if bifrostErr == nil && result != nil {
-		responseText := *result.Content.ContentStr
-		assert.Contains(t, responseText, "No servers", "Should indicate no servers")
-	}
+	responseText := *result.Content.ContentStr
+	assert.Contains(t, responseText, "No servers", "Should indicate no code-mode servers available")

152-159: Same redundant conditional pattern.

Similar to the previous test, the conditional check is unnecessary after requireNoBifrostError. Also, the assertion logic with len(responseText) == 0 || ... may be overly permissive.

tests/core-mcp/codemode_auto_execute_test.go (1)

19-39: Consider extracting config preservation to a helper function.

The pattern of fetching current config and preserving fields (lines 19-38) is repeated across multiple tests. A helper like getAndPreserveConfig(b, clientID string) could reduce duplication.

This is a minor maintainability suggestion and not blocking.

transports/bifrost-http/handlers/mcp_server.go (1)

206-211: Potential issue: Blank identifier in range.

Line 209 uses for toolName, _ := range toolMap which can be simplified to for toolName := range toolMap.

🔎 Suggested fix
 	// Clear existing tools
 	toolMap := server.ListTools()
-	for toolName, _ := range toolMap {
+	for toolName := range toolMap {
 		server.DeleteTools(toolName)
 	}
tests/core-mcp/responses_test.go (2)

14-14: Consider reducing the test timeout for faster CI feedback.

A 10-minute timeout (TestTimeout = 10 * time.Minute in setup.go) is quite long for unit tests. If tests are expected to complete quickly, a shorter timeout (e.g., 30 seconds to 1 minute) would provide faster feedback on failures.


237-265: Timeout test may not reliably trigger the timeout scenario.

The test configures a 100ms tool execution timeout and sends a request asking the LLM to "Call slow_tool with delay 500ms". However, this relies on the LLM interpreting the message correctly and actually calling slow_tool. The test's assertion only checks if a timeout error occurred but doesn't guarantee the slow_tool was actually invoked.

Consider directly calling ExecuteMCPTool with the slow_tool to ensure deterministic timeout behavior:

🔎 Suggested approach
// Test direct tool execution timeout
slowCall := createToolCall("slow_tool", map[string]interface{}{
    "delay_ms": float64(500), // 500ms delay, but timeout is 100ms
})
result, bifrostErr := b.ExecuteMCPTool(ctx, slowCall)
require.NotNil(t, bifrostErr, "Should timeout when tool execution exceeds timeout")
assert.Contains(t, bifrost.GetErrorMessage(bifrostErr), "timeout")
core/mcp/codemode_executecode.go (3)

69-124: Consider extracting the lengthy tool description to a constant.

The tool description is quite lengthy (40+ lines). While comprehensive documentation is valuable, consider extracting this to a package-level constant for better readability and maintainability of the createExecuteToolCodeTool function.


871-900: Regex compiled on every call - consider hoisting to package level.

The importExportRegex is compiled on every call to stripImportsAndExports. Consider hoisting it to a package-level variable for better performance:

🔎 Suggested fix
+// Package-level compiled regex for import/export detection
+var importExportRegex = regexp.MustCompile(`^\s*(import|export)\b`)
+
 func stripImportsAndExports(code string) (string, []int) {
     lines := strings.Split(code, "\n")
     keptLines := []string{}
     strippedLineNumbers := []int{}
 
-    importExportRegex := regexp.MustCompile(`^\s*(import|export)\b`)
-
     for i, line := range lines {

959-961: Regex compiled on every call in generateErrorHints.

Same issue as above - the regex (\w+)\s+is not defined is compiled on every error. Consider hoisting to package level.

🔎 Suggested fix
+var undefinedVarRegex = regexp.MustCompile(`(\w+)\s+is not defined`)
+var notFunctionRegex = regexp.MustCompile(`(\w+(?:\.\w+)?)\s+is not a function`)
+
 func generateErrorHints(errorMessage string, serverKeys []string) []string {
     hints := []string{}
 
     if strings.Contains(errorMessage, "is not defined") {
-        re := regexp.MustCompile(`(\w+)\s+is not defined`)
-        if match := re.FindStringSubmatch(errorMessage); len(match) > 1 {
+        if match := undefinedVarRegex.FindStringSubmatch(errorMessage); len(match) > 1 {
tests/core-mcp/setup.go (2)

23-31: Missing validation for OPENAI_API_KEY environment variable.

If OPENAI_API_KEY is not set, tests will silently use an empty string, leading to confusing failures. Consider adding validation or skipping tests when the key is unavailable:

🔎 Suggested improvement
func (a *TestAccount) GetKeysForProvider(ctx *context.Context, providerKey schemas.ModelProvider) ([]schemas.Key, error) {
    apiKey := os.Getenv("OPENAI_API_KEY")
    if apiKey == "" {
        return nil, fmt.Errorf("OPENAI_API_KEY environment variable is not set")
    }
    return []schemas.Key{
        {
            Value:  apiKey,
            Models: []string{},
            Weight: 1.0,
        },
    }, nil
}

142-360: Consider using table-driven tool registration to reduce repetition.

The tool registration code is highly repetitive. A table-driven approach would reduce duplication and make adding new test tools easier:

🔎 Example refactor
type testToolDef struct {
    name        string
    description string
    handler     func(args any) (string, error)
    params      *schemas.ToolFunctionParameters
}

func registerTestTools(b *bifrost.Bifrost) error {
    tools := []testToolDef{
        {
            name:        "echo",
            description: "Echoes back the input message",
            handler: func(args any) (string, error) {
                argsMap := args.(map[string]interface{})
                return argsMap["message"].(string), nil
            },
            params: &schemas.ToolFunctionParameters{
                Type: "object",
                Properties: &map[string]interface{}{
                    "message": map[string]interface{}{"type": "string", "description": "The message to echo"},
                },
                Required: []string{"message"},
            },
        },
        // ... other tools
    }
    
    for _, tool := range tools {
        schema := schemas.ChatTool{
            Type: schemas.ChatToolTypeFunction,
            Function: &schemas.ChatToolFunction{
                Name:        tool.name,
                Description: schemas.Ptr(tool.description),
                Parameters:  tool.params,
            },
        }
        if err := b.RegisterMCPTool(tool.name, tool.description, tool.handler, schema); err != nil {
            return fmt.Errorf("failed to register %s tool: %w", tool.name, err)
        }
    }
    return nil
}
core/mcp/agent.go (1)

196-201: Consider logging the actual unmarshal error for better debugging.

When sonic.Unmarshal fails, the error is logged but the original toolCall.Function.Arguments value is not included, which would help diagnose malformed JSON payloads in production.

🔎 Suggested improvement
 				if err := sonic.Unmarshal([]byte(toolCall.Function.Arguments), &arguments); err != nil {
-					logger.Debug(fmt.Sprintf("%s Failed to parse tool arguments: %v", CodeModeLogPrefix, err))
+					logger.Debug(fmt.Sprintf("%s Failed to parse tool arguments: %v (raw: %s)", CodeModeLogPrefix, err, toolCall.Function.Arguments))
 					nonAutoExecutableTools = append(nonAutoExecutableTools, toolCall)
 					continue
 				}
core/bifrost.go (2)

1887-1899: New public API UpdateToolManagerConfig exposes runtime configuration.

This allows hot-reloading of maxAgentDepth and toolExecutionTimeout. The implementation correctly delegates to the MCP manager. Consider adding input validation for edge cases (e.g., negative values).

🔎 Suggested validation
 func (bifrost *Bifrost) UpdateToolManagerConfig(maxAgentDepth int, toolExecutionTimeoutInSeconds int) error {
 	if bifrost.mcpManager == nil {
 		return fmt.Errorf("MCP is not configured in this Bifrost instance")
 	}
+	if maxAgentDepth < 0 {
+		return fmt.Errorf("maxAgentDepth must be non-negative")
+	}
+	if toolExecutionTimeoutInSeconds < 0 {
+		return fmt.Errorf("toolExecutionTimeoutInSeconds must be non-negative")
+	}
 
 	bifrost.mcpManager.UpdateToolManagerConfig(&schemas.MCPToolManagerConfig{
 		MaxAgentDepth:        maxAgentDepth,
 		ToolExecutionTimeout: time.Duration(toolExecutionTimeoutInSeconds) * time.Second,
 	})
 	return nil
 }

593-598: Refactor context passing to avoid anti-pattern: pass by value instead of by pointer.

The code passes &ctx to CheckAndExecuteAgentForChatRequest, allowing the agent function to mutate the caller's context variable via *ctx = context.WithValue(...) (confirmed at agent.go lines 159, 339). Passing context.Context by pointer is an anti-pattern in Go that offers no benefits and introduces the risk of unintended side effects by allowing modification of the caller's variable. The Go standard library packages all accept context.Context by value.

Refactor CheckAndExecuteAgentForChatRequest to accept and return context.Context by value instead of by pointer.

core/schemas/mcp.go (1)

4-10: Consider aliasing or avoiding direct dependency on mcp-go in schemas package.

The schemas package now directly imports github.com/mark3labs/mcp-go/client and github.com/mark3labs/mcp-go/server. This couples the schema definitions to a specific external library version. If the mcp-go library changes its API, the schemas package must also change. This may be acceptable given the tight MCP integration, but consider whether these types could be defined as interfaces in schemas to reduce coupling.

core/mcp/agent_adaptors.go (2)

82-90: Unchecked type assertions could cause panics.

Multiple methods perform unchecked type assertions (e.g., response.(*schemas.BifrostChatResponse) at lines 83, 88, 93, 126, 136). While the adapter pattern guarantees these types at compile-time design level, a programming error could cause a panic at runtime. Consider using the two-value form for defensive programming, especially in the makeLLMCall methods which involve external calls.

🔎 Example defensive assertion
func (c *chatAPIAdapter) makeLLMCall(ctx context.Context, request interface{}) (interface{}, *schemas.BifrostError) {
	chatRequest, ok := request.(*schemas.BifrostChatRequest)
	if !ok {
		return nil, &schemas.BifrostError{
			IsBifrostError: false,
			Error: &schemas.ErrorField{
				Message: fmt.Sprintf("expected *BifrostChatRequest, got %T", request),
			},
		}
	}
	return c.makeReq(ctx, chatRequest)
}

Also applies to: 125-128, 136-137


159-270: Significant code duplication between Chat and Responses response builders.

createChatResponseWithExecutedToolsAndNonAutoExecutableCalls and createResponsesResponseWithExecutedToolsAndNonAutoExecutableCalls share nearly identical logic for building toolCallIDToName map, extracting tool results, and formatting contentText. Consider extracting the common logic into a shared helper function.

Also applies to: 373-529

core/mcp/toolmanager.go (3)

52-68: Consider validating clientManager parameter.

The constructor validates config for nil but doesn't validate clientManager. If clientManager is nil, the manager will be created successfully but will panic on first use when methods like GetAvailableTools call m.clientManager.GetToolPerClient(ctx).

🔎 Proposed validation
 func NewToolsManager(config *schemas.MCPToolManagerConfig, clientManager ClientManager, fetchNewRequestIDFunc func(ctx context.Context) string) *ToolsManager {
+	if clientManager == nil {
+		logger.Error(fmt.Sprintf("%s clientManager cannot be nil", MCPLogPrefix))
+		return nil
+	}
 	if config == nil {
 		config = &schemas.MCPToolManagerConfig{

203-216: Redundant length check.

The check if len(availableTools) > 0 on line 216 is redundant since the function already returns early on lines 203-205 when len(availableTools) == 0.

🔎 Proposed simplification
 	if len(availableTools) == 0 {
 		return req
 	}

 	// Get integration user agent for duplicate checking
 	var integrationUserAgentStr string
 	integrationUserAgent := ctx.Value(schemas.BifrostContextKey("integration-user-agent"))
 	if integrationUserAgent != nil {
 		if str, ok := integrationUserAgent.(string); ok {
 			integrationUserAgentStr = str
 		}
 	}

-	if len(availableTools) > 0 {
-		switch req.RequestType {
+	switch req.RequestType {

And remove the corresponding closing brace before return req.


460-474: Log message may be misleading when partial updates occur.

The log message on line 473 logs config.ToolExecutionTimeout and config.MaxAgentDepth, but if either value is <= 0, it won't be updated. This could log misleading values (e.g., logging 0 for timeout when the actual stored value remains unchanged).

🔎 Proposed fix to log actual stored values
 func (m *ToolsManager) UpdateConfig(config *schemas.MCPToolManagerConfig) {
 	if config == nil {
 		return
 	}
 	if config.ToolExecutionTimeout > 0 {
 		m.toolExecutionTimeout.Store(config.ToolExecutionTimeout)
 	}
 	if config.MaxAgentDepth > 0 {
 		m.maxAgentDepth.Store(int32(config.MaxAgentDepth))
 	}

-	logger.Info(fmt.Sprintf("%s tool manager configuration updated with tool execution timeout: %v and max agent depth: %d", MCPLogPrefix, config.ToolExecutionTimeout, config.MaxAgentDepth))
+	logger.Info(fmt.Sprintf("%s tool manager configuration updated with tool execution timeout: %v and max agent depth: %d", MCPLogPrefix, m.toolExecutionTimeout.Load(), m.maxAgentDepth.Load()))
 }

Comment on lines +109 to +123
func (c *chatAPIAdapter) createNewRequest(conversation []interface{}) interface{} {
// Convert conversation back to ChatMessage slice
chatMessages := make([]schemas.ChatMessage, 0, len(conversation))
for _, msg := range conversation {
chatMessages = append(chatMessages, msg.(schemas.ChatMessage))
}

return &schemas.BifrostChatRequest{
Provider: c.originalReq.Provider,
Model: c.originalReq.Model,
Fallbacks: c.originalReq.Fallbacks,
Params: c.originalReq.Params,
Input: chatMessages,
}
}
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 | 🟡 Minor

Type assertion in createNewRequest could panic on conversation type mismatch.

At line 113, msg.(schemas.ChatMessage) assumes all items in conversation are of type ChatMessage. If a non-ChatMessage type is accidentally added to the conversation slice earlier, this will panic. The same issue exists in responsesAPIAdapter.createNewRequest at line 326.

🔎 Suggested defensive check
 func (c *chatAPIAdapter) createNewRequest(conversation []interface{}) interface{} {
 	// Convert conversation back to ChatMessage slice
 	chatMessages := make([]schemas.ChatMessage, 0, len(conversation))
 	for _, msg := range conversation {
-		chatMessages = append(chatMessages, msg.(schemas.ChatMessage))
+		if chatMsg, ok := msg.(schemas.ChatMessage); ok {
+			chatMessages = append(chatMessages, chatMsg)
+		} else {
+			logger.Warn(fmt.Sprintf("Unexpected message type in conversation: %T", msg))
+		}
 	}
 
 	return &schemas.BifrostChatRequest{
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (c *chatAPIAdapter) createNewRequest(conversation []interface{}) interface{} {
// Convert conversation back to ChatMessage slice
chatMessages := make([]schemas.ChatMessage, 0, len(conversation))
for _, msg := range conversation {
chatMessages = append(chatMessages, msg.(schemas.ChatMessage))
}
return &schemas.BifrostChatRequest{
Provider: c.originalReq.Provider,
Model: c.originalReq.Model,
Fallbacks: c.originalReq.Fallbacks,
Params: c.originalReq.Params,
Input: chatMessages,
}
}
func (c *chatAPIAdapter) createNewRequest(conversation []interface{}) interface{} {
// Convert conversation back to ChatMessage slice
chatMessages := make([]schemas.ChatMessage, 0, len(conversation))
for _, msg := range conversation {
if chatMsg, ok := msg.(schemas.ChatMessage); ok {
chatMessages = append(chatMessages, chatMsg)
} else {
logger.Warn(fmt.Sprintf("Unexpected message type in conversation: %T", msg))
}
}
return &schemas.BifrostChatRequest{
Provider: c.originalReq.Provider,
Model: c.originalReq.Model,
Fallbacks: c.originalReq.Fallbacks,
Params: c.originalReq.Params,
Input: chatMessages,
}
}
🤖 Prompt for AI Agents
In core/mcp/agent_adaptors.go around lines 109 to 123 (and similarly in
responsesAPIAdapter.createNewRequest at ~line 326), the code uses a direct type
assertion msg.(schemas.ChatMessage) which can panic if an element has a
different type; replace the direct assertion with a safe check (v, ok :=
msg.(schemas.ChatMessage)) and handle the non-matching case (e.g., skip the item
and log/debug a warning, or return an error upstream) so the function does not
panic; ensure the resulting chatMessages slice only includes valid ChatMessage
values and preserve existing behavior for valid inputs.

Comment on lines +344 to +348
response, err := adapter.makeLLMCall(*ctx, newReq)
if err != nil {
logger.Error("Agent mode: LLM request failed: %v", err)
return nil, err
}
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

Use Errorf() for format string logging.

At line 346, logger.Error is called with a format string containing %v, but Error() expects variadic arguments (like fmt.Print). Use logger.Errorf instead to properly handle format strings (like fmt.Printf).

🤖 Prompt for AI Agents
In core/mcp/agent.go around lines 344 to 348, the call to logger.Error uses a
printf-style format string with %v which Error() does not process correctly;
change the call to logger.Errorf("Agent mode: LLM request failed: %v", err) so
the format string is interpreted and the error is included in the log (leave the
surrounding logic and return unchanged).

Comment on lines +28 to +32
// Function to fetch a new request ID for each tool call result message in agent mode,
// this is used to ensure that the tool call result messages are unique and can be tracked in plugins or by the user.
// This id is attached to ctx.Value(schemas.BifrostContextKeyRequestID) in the agent mode.
// If not provider, same request ID is used for all tool call result messages without any overrides.
fetchNewRequestIDFunc func(ctx context.Context) string
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 | 🟡 Minor

Typo in documentation comment.

Line 31 contains a typo: "If not provider" should be "If not provided".

🔎 Proposed fix
 	// Function to fetch a new request ID for each tool call result message in agent mode,
 	// this is used to ensure that the tool call result messages are unique and can be tracked in plugins or by the user.
 	// This id is attached to ctx.Value(schemas.BifrostContextKeyRequestID) in the agent mode.
-	// If not provider, same request ID is used for all tool call result messages without any overrides.
+	// If not provided, same request ID is used for all tool call result messages without any overrides.
 	fetchNewRequestIDFunc func(ctx context.Context) string
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Function to fetch a new request ID for each tool call result message in agent mode,
// this is used to ensure that the tool call result messages are unique and can be tracked in plugins or by the user.
// This id is attached to ctx.Value(schemas.BifrostContextKeyRequestID) in the agent mode.
// If not provider, same request ID is used for all tool call result messages without any overrides.
fetchNewRequestIDFunc func(ctx context.Context) string
// Function to fetch a new request ID for each tool call result message in agent mode,
// this is used to ensure that the tool call result messages are unique and can be tracked in plugins or by the user.
// This id is attached to ctx.Value(schemas.BifrostContextKeyRequestID) in the agent mode.
// If not provided, same request ID is used for all tool call result messages without any overrides.
fetchNewRequestIDFunc func(ctx context.Context) string
🤖 Prompt for AI Agents
In core/mcp/toolmanager.go around lines 28 to 32, the documentation comment has
a typo: change "If not provider" to "If not provided" in the sentence describing
behavior when the fetchNewRequestIDFunc is absent; update the comment text
accordingly to read "If not provided, the same request ID is used for all tool
call result messages without any overrides."

Comment on lines +127 to +138
// buildIntegrationDuplicateCheckMap builds a map of tool names to check for duplicates
// based on the integration user agent. This includes both direct tool names and
// integration-specific naming patterns from existing tools in the request.
//
// Parameters:
// - existingTools: List of existing tools in the request
// - integrationUserAgent: Integration user agent string (e.g., "claude-cli")
// - availableToolsPerClient: Map of client names to their available tools (for reverse pattern matching)
//
// Returns:
// - map[string]bool: Map of tool names/patterns to check against
func buildIntegrationDuplicateCheckMap(existingTools []schemas.ChatTool, integrationUserAgent string) map[string]bool {
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 | 🟡 Minor

Outdated parameter documentation.

Line 134 documents availableToolsPerClient as a parameter, but this parameter doesn't exist in the function signature. The function only takes existingTools and integrationUserAgent.

🔎 Proposed fix
 // buildIntegrationDuplicateCheckMap builds a map of tool names to check for duplicates
 // based on the integration user agent. This includes both direct tool names and
 // integration-specific naming patterns from existing tools in the request.
 //
 // Parameters:
 //   - existingTools: List of existing tools in the request
 //   - integrationUserAgent: Integration user agent string (e.g., "claude-cli")
-//   - availableToolsPerClient: Map of client names to their available tools (for reverse pattern matching)
 //
 // Returns:
 //   - map[string]bool: Map of tool names/patterns to check against
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// buildIntegrationDuplicateCheckMap builds a map of tool names to check for duplicates
// based on the integration user agent. This includes both direct tool names and
// integration-specific naming patterns from existing tools in the request.
//
// Parameters:
// - existingTools: List of existing tools in the request
// - integrationUserAgent: Integration user agent string (e.g., "claude-cli")
// - availableToolsPerClient: Map of client names to their available tools (for reverse pattern matching)
//
// Returns:
// - map[string]bool: Map of tool names/patterns to check against
func buildIntegrationDuplicateCheckMap(existingTools []schemas.ChatTool, integrationUserAgent string) map[string]bool {
// buildIntegrationDuplicateCheckMap builds a map of tool names to check for duplicates
// based on the integration user agent. This includes both direct tool names and
// integration-specific naming patterns from existing tools in the request.
//
// Parameters:
// - existingTools: List of existing tools in the request
// - integrationUserAgent: Integration user agent string (e.g., "claude-cli")
//
// Returns:
// - map[string]bool: Map of tool names/patterns to check against
func buildIntegrationDuplicateCheckMap(existingTools []schemas.ChatTool, integrationUserAgent string) map[string]bool {
🤖 Prompt for AI Agents
In core/mcp/toolmanager.go around lines 127 to 138, the function comment
documents a non-existent parameter `availableToolsPerClient`; update the
docblock to match the actual signature by removing the `availableToolsPerClient`
parameter line and ensuring only `existingTools` and `integrationUserAgent` are
described (and keep the return description as-is). Make the parameter list
accurate and concise so comments reflect the real function signature.

Comment on lines +269 to +270
updatedConfig.MCPAgentDepth = payload.ClientConfig.MCPAgentDepth
updatedConfig.MCPToolExecutionTimeout = payload.ClientConfig.MCPToolExecutionTimeout
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 | 🟡 Minor

Unconditional assignment may override change detection.

Lines 269-270 unconditionally assign the MCP config values to updatedConfig, which happens after the change detection logic (lines 234-242). This could override the conditional updates and cause unnecessary config updates.

These assignments should only happen within the change detection blocks (lines 235 and 240) where you've already set these values conditionally.

🔎 Proposed fix
 	updatedConfig.AllowDirectKeys = payload.ClientConfig.AllowDirectKeys
 	updatedConfig.MaxRequestBodySizeMB = payload.ClientConfig.MaxRequestBodySizeMB
 	updatedConfig.EnableLiteLLMFallbacks = payload.ClientConfig.EnableLiteLLMFallbacks
-	updatedConfig.MCPAgentDepth = payload.ClientConfig.MCPAgentDepth
-	updatedConfig.MCPToolExecutionTimeout = payload.ClientConfig.MCPToolExecutionTimeout
 	// Validate LogRetentionDays
 	if payload.ClientConfig.LogRetentionDays < 1 {
🤖 Prompt for AI Agents
In transports/bifrost-http/handlers/config.go around lines 269-270, the
unconditional assignments setting updatedConfig.MCPAgentDepth and
updatedConfig.MCPToolExecutionTimeout are overwriting the earlier
change-detection logic (around lines 234-242) and can force unnecessary updates;
move these two assignments into the existing conditional branches where you
detect changes (the blocks around lines 235 and 240) so they are only set when
the payload values differ from the current config, preserving the intended
conditional update behavior.

Comment on lines +24 to +25
mcp_agent_depth: 10,
mcp_tool_execution_timeout: 30,
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

MCP config fields misplaced in Governance view.

The mcp_agent_depth and mcp_tool_execution_timeout fields are MCP tool execution parameters and don't belong in the Governance configuration view. This view is focused on governance enable/disable, not MCP tool management.

These fields should be managed in a dedicated MCP or Performance Tuning view.

🤖 Prompt for AI Agents
In ui/app/workspace/config/views/governanceView.tsx around lines 24-25, the
mcp_agent_depth and mcp_tool_execution_timeout entries are misplaced in the
Governance view; remove these MCP tool execution parameters from this file and
any UI rendering here, and instead add them to the dedicated MCP or Performance
Tuning view (or create one if it doesn't exist). Update the relevant config
type/interface and default config file so the fields live under the
MCP/performance section, adjust any form/state handling to read/write from the
new location, and run/update tests that reference these keys.

Comment on lines +27 to +28
mcp_agent_depth: 10,
mcp_tool_execution_timeout: 30,
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

MCP config fields misplaced in Observability view.

The mcp_agent_depth and mcp_tool_execution_timeout fields don't belong in the Observability configuration view. This view is focused on monitoring/metrics settings (Prometheus labels), not MCP tool execution configuration.

These fields should be managed in a dedicated MCP configuration view.

Comment on lines +158 to +163
const requestBodyJson = JSON.stringify(requestBody, null, 2);
navigator.clipboard.writeText(requestBodyJson);
toast.success("Request body copied to clipboard");
} catch (error) {
toast.error("Failed to copy request body");
}
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 | 🟡 Minor

Await the clipboard write operation.

navigator.clipboard.writeText() returns a Promise. Without awaiting it, the success toast may fire before the clipboard operation completes, and any clipboard errors won't be caught by the try/catch block.

🔎 Proposed fix
-			navigator.clipboard.writeText(requestBodyJson);
-			toast.success("Request body copied to clipboard");
+			await navigator.clipboard.writeText(requestBodyJson);
+			toast.success("Request body copied to clipboard");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const requestBodyJson = JSON.stringify(requestBody, null, 2);
navigator.clipboard.writeText(requestBodyJson);
toast.success("Request body copied to clipboard");
} catch (error) {
toast.error("Failed to copy request body");
}
const requestBodyJson = JSON.stringify(requestBody, null, 2);
await navigator.clipboard.writeText(requestBodyJson);
toast.success("Request body copied to clipboard");
} catch (error) {
toast.error("Failed to copy request body");
}
🤖 Prompt for AI Agents
In ui/app/workspace/logs/views/logDetailsSheet.tsx around lines 158 to 163, the
code calls navigator.clipboard.writeText() without awaiting it so the success
toast can fire before the clipboard write completes and clipboard errors won't
be caught; modify the try block to await
navigator.clipboard.writeText(requestBodyJson) (ensuring the enclosing function
is async), then only call toast.success after the await so any errors from
writeText are caught by the catch and trigger toast.error.

Pratham-Mishra04 and others added 5 commits December 22, 2025 15:42
## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (Next.js)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

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

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

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## 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
## Summary

Briefly explain the purpose of this PR and the problem it solves.

## Changes

- What was changed and why
- Any notable design decisions or trade-offs

## Type of change

- [ ] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [ ] Core (Go)
- [ ] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (Next.js)
- [ ] Docs

## How to test

Describe the steps to validate this change. Include commands and expected outcomes.

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

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

If adding new configs or environment variables, document them here.

## Screenshots/Recordings

If UI changes, add before/after screenshots or short clips.

## Breaking changes

- [ ] Yes
- [ ] No

If yes, describe impact and migration instructions.

## Related issues

Link related issues and discussions. Example: Closes #123

## Security considerations

Note any security implications (auth, secrets, PII, sandboxing, etc.).

## 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
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: 6

🧹 Nitpick comments (12)
.editorconfig (1)

8-10: Appropriate addition of Go-specific EditorConfig settings.

The new Go section aligns with the PR's introduction of Go code for MCP integration. Go standard practice is to use tabs for indentation (enforced by gofmt), so indent_style = tab is correct.

Consider removing indent_size = 4 from this section, as it is non-standard with tab indentation and has no practical effect in Go tooling (which ignores EditorConfig). Alternatively, if you intend this to indicate visual tab display width, you may want to document this intent.

🔎 Optional: Remove or clarify `indent_size`
 [*.go]
 indent_style = tab
-indent_size = 4

This keeps EditorConfig focused on what it can effectively control for Go files. Go tooling is indifferent to EditorConfig and will enforce its own standards.

plugins/governance/config_update_sync_test.go (1)

14-235: Consider using WaitForCondition consistently for async sync verification.

This test uses time.Sleep(500 * time.Millisecond) at lines 95, 148, and 201 for async sync, while TestTeamBudgetUpdateSyncToMemory demonstrates a more robust pattern using WaitForCondition. The fixed sleeps may cause flaky tests under load.

That said, the test logic is correct and covers the important scenario of usage reset when the new limit is lower than current usage.

plugins/governance/test_utils.go (1)

143-152: rand.Seed is deprecated in Go 1.20+.

Consider using the newer approach with a local random source for better concurrency safety:

🔎 Suggested fix
+var rng = rand.New(rand.NewSource(time.Now().UnixNano()))
+
 // generateRandomID generates a random ID for test resources
 func generateRandomID() string {
-	rand.Seed(time.Now().UnixNano())
 	const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
 	b := make([]byte, 8)
 	for i := range b {
-		b[i] = letters[rand.Intn(len(letters))]
+		b[i] = letters[rng.Intn(len(letters))]
 	}
 	return string(b)
 }
plugins/governance/team_budget_test.go (1)

78-95: Consider extracting the long prompt string to a shared constant.

This longPrompt string is duplicated across multiple test files (team_budget_test.go, edge_cases_test.go, provider_budget_test.go, advanced_scenarios_test.go). Extracting it to a constant in test_utils.go would improve maintainability.

// In test_utils.go
const LongTestPrompt = "Please provide a comprehensive and detailed response to the following question. " +
    "I need extensive information covering all aspects of the topic. " +
    // ... rest of the string
plugins/governance/store_test.go (1)

200-207: Direct sync.Map manipulation couples test to implementation details.

Directly accessing store.budgets (the internal sync.Map) to modify budget state ties the test to the store's internal implementation. If the internal data structure changes, this test will break.

Consider exposing a test helper method or using the public UpdateBudgetUsageInMemory method if available.

plugins/governance/tracker_test.go (1)

80-81: Remove no-op assertion.

assert.True(t, true) always passes and provides no value. The comment "Just verify it doesn't crash" is valid intent, but if execution reaches this point without panicking, the test already succeeded.

🔎 Suggested fix
 	time.Sleep(100 * time.Millisecond)
-	// Just verify it doesn't crash
-	assert.True(t, true)
+	// Test passes if execution reaches here without panicking
 }
plugins/governance/customer_budget_test.go (1)

66-160: Consider extracting shared test logic to reduce duplication.

Both TestCustomerBudgetExceededWithMultipleVKs and TestCustomerBudgetExceededWithMultipleTeams share nearly identical request/response handling logic (lines 66-160 and 241-334). Consider extracting a helper function like runBudgetExhaustionLoop(t, vkValues []string, customerBudget float64) to reduce duplication.

This is optional since the current structure provides good readability and each test is self-contained.

Also applies to: 241-334

plugins/governance/rate_limit_enforcement_test.go (1)

489-491: Minor: string(rune('0'+i)) only works correctly for single digits.

For i >= 10, string(rune('0'+i)) produces unexpected characters (e.g., ':' for 10, ';' for 11). Since the loop only goes to 5 iterations, this works correctly here, but using strconv.Itoa(i) would be more robust.

Suggested fix
-				Content: "Request " + string(rune('0'+i)) + " to test both limits.",
+				Content: "Request " + strconv.Itoa(i) + " to test both limits.",

Add "strconv" to imports.

framework/configstore/rdb.go (3)

749-763: Clarify the contract for missing client configuration.

The code returns default values when TableClientConfig is not found, with a comment stating "This will never happen, but just in case." This creates ambiguity:

  • If this truly never happens, consider removing the defensive code and letting the error propagate
  • If this can happen in valid scenarios (e.g., fresh installation), update the comment to explain when and why
  • If this is a critical invariant, consider using a panic or fatal error instead of silently returning defaults

The current implementation masks a potentially unexpected state, making debugging harder.


1393-1468: Consider consolidating cascade deletion logic.

The cascade deletion logic is manually implemented in multiple places (DeleteVirtualKey, DeleteVirtualKeyProviderConfig, DeleteTeam, DeleteCustomer). While the pattern is consistent, the duplication increases maintenance burden and the risk of inconsistencies.

Based on learnings, budgets and rate limits maintain 1:1 relationships with their parent entities, so the manual deletion ensures no cross-entity sharing occurs.

Consider:

  1. Extracting common deletion logic into helper functions (e.g., deleteBudgetIfExists, deleteRateLimitIfExists)
  2. Documenting why manual cascade is used instead of database CASCADE constraints
  3. Adding tests to verify the cascade behavior remains correct

The current implementation is functionally correct based on the 1:1 relationship invariant from learnings, but could be more maintainable.

Also applies to: 1549-1577, 1693-1729, 1781-1821


2177-2202: Well-implemented retry helper with minor suggestions.

The RetryOnNotFound function correctly handles retries for not-found scenarios with proper context cancellation support.

Consider these optional enhancements:

  1. Add debug logging for retry attempts to aid troubleshooting
  2. Consider making this a package-level function since it doesn't use any receiver state
Optional: Add retry attempt logging
 func (s *RDBConfigStore) RetryOnNotFound(ctx context.Context, fn func(ctx context.Context) (any, error), maxRetries int, retryDelay time.Duration) (any, error) {
 	var lastErr error
 	for attempt := range maxRetries {
 		result, err := fn(ctx)
 		if err == nil {
 			return result, nil
 		}
 		if !errors.Is(err, ErrNotFound) && !errors.Is(err, gorm.ErrRecordNotFound) {
 			return nil, err
 		}
 
 		lastErr = err
+		s.logger.Debug("RetryOnNotFound: attempt %d/%d failed, retrying after %v", attempt+1, maxRetries, retryDelay)
 
 		// Don't wait after the last attempt
 		if attempt < maxRetries-1 {
 			select {
 			case <-ctx.Done():
 				return nil, ctx.Err()
 			case <-time.After(retryDelay):
 				// Continue to next retry
 			}
 		}
 	}
 	return nil, lastErr
 }
plugins/governance/store.go (1)

166-217: Improved governance checks with baseline support.

The refactored CheckBudget and CheckRateLimit methods add:

  • Multi-node baseline support for distributed usage tracking
  • Independent expiration handling for token vs request limits
  • Defensive nil checks preventing panics
  • Enhanced logging for troubleshooting

Consider adding debug logging when baselines are nil to catch potential bugs in caller code:

Optional: Log warning for nil baselines
 func (gs *LocalGovernanceStore) CheckBudget(ctx context.Context, vk *configstoreTables.TableVirtualKey, request *EvaluationRequest, baselines map[string]float64) error {
 	if vk == nil {
 		return fmt.Errorf("virtual key cannot be nil")
 	}
 
 	// This is to prevent nil pointer dereference
 	if baselines == nil {
+		gs.logger.Debug("CheckBudget: baselines is nil, using empty map")
 		baselines = map[string]float64{}
 	}

Also applies to: 220-308

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd2355b and e42e8da.

⛔ Files ignored due to path filters (11)
  • core/go.sum is excluded by !**/*.sum
  • framework/go.sum is excluded by !**/*.sum
  • plugins/governance/go.sum is excluded by !**/*.sum
  • plugins/jsonparser/go.sum is excluded by !**/*.sum
  • plugins/logging/go.sum is excluded by !**/*.sum
  • plugins/maxim/go.sum is excluded by !**/*.sum
  • plugins/mocker/go.sum is excluded by !**/*.sum
  • plugins/otel/go.sum is excluded by !**/*.sum
  • plugins/semanticcache/go.sum is excluded by !**/*.sum
  • plugins/telemetry/go.sum is excluded by !**/*.sum
  • transports/go.sum is excluded by !**/*.sum
📒 Files selected for processing (54)
  • .editorconfig
  • core/go.mod
  • core/mcp.go
  • core/providers/nebius/nebius.go
  • core/schemas/context.go
  • docs/changelogs/v1.3.47.mdx
  • docs/features/governance/virtual-keys.mdx
  • framework/configstore/rdb.go
  • framework/configstore/store.go
  • framework/configstore/tables/budget.go
  • framework/configstore/tables/mcp.go
  • framework/configstore/tables/ratelimit.go
  • framework/go.mod
  • framework/modelcatalog/main.go
  • framework/modelcatalog/sync.go
  • plugins/governance/advanced_scenarios_test.go
  • plugins/governance/changelog.md
  • plugins/governance/config_update_sync_test.go
  • plugins/governance/customer_budget_test.go
  • plugins/governance/e2e_test.go
  • plugins/governance/edge_cases_test.go
  • plugins/governance/fixtures_test.go
  • plugins/governance/go.mod
  • plugins/governance/in_memory_sync_test.go
  • plugins/governance/main.go
  • plugins/governance/provider_budget_test.go
  • plugins/governance/rate_limit_enforcement_test.go
  • plugins/governance/rate_limit_test.go
  • plugins/governance/resolver.go
  • plugins/governance/resolver_test.go
  • plugins/governance/store.go
  • plugins/governance/store_test.go
  • plugins/governance/team_budget_test.go
  • plugins/governance/test_utils.go
  • plugins/governance/tracker.go
  • plugins/governance/tracker_test.go
  • plugins/governance/usage_tracking_test.go
  • plugins/governance/utils.go
  • plugins/governance/vk_budget_test.go
  • plugins/jsonparser/go.mod
  • plugins/logging/go.mod
  • plugins/maxim/go.mod
  • plugins/mocker/go.mod
  • plugins/otel/go.mod
  • plugins/semanticcache/go.mod
  • plugins/telemetry/go.mod
  • transports/bifrost-http/handlers/governance.go
  • transports/bifrost-http/handlers/middlewares.go
  • transports/bifrost-http/lib/config.go
  • transports/bifrost-http/lib/config_test.go
  • transports/bifrost-http/lib/lib.go
  • transports/bifrost-http/server/server.go
  • transports/go.mod
  • ui/lib/constants/logs.ts
✅ Files skipped from review due to trivial changes (2)
  • docs/changelogs/v1.3.47.mdx
  • framework/go.mod
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/features/governance/virtual-keys.mdx
🧰 Additional context used
📓 Path-based instructions (1)
**

⚙️ CodeRabbit configuration file

always check the stack if there is one for the current PR. do not give localized reviews for the PR, always see all changes in the light of the whole stack of PRs (if there is a stack, if there is no stack you can continue to make localized suggestions/reviews)

Files:

  • plugins/governance/store_test.go
  • plugins/governance/provider_budget_test.go
  • plugins/governance/advanced_scenarios_test.go
  • plugins/governance/changelog.md
  • plugins/governance/team_budget_test.go
  • plugins/governance/config_update_sync_test.go
  • framework/modelcatalog/main.go
  • core/schemas/context.go
  • plugins/governance/rate_limit_enforcement_test.go
  • plugins/governance/customer_budget_test.go
  • framework/modelcatalog/sync.go
  • plugins/governance/fixtures_test.go
  • plugins/governance/resolver_test.go
  • plugins/governance/in_memory_sync_test.go
  • plugins/governance/edge_cases_test.go
  • plugins/governance/e2e_test.go
  • core/mcp.go
  • plugins/governance/tracker_test.go
  • plugins/governance/main.go
  • framework/configstore/tables/budget.go
  • framework/configstore/store.go
  • plugins/governance/go.mod
  • framework/configstore/tables/ratelimit.go
  • core/providers/nebius/nebius.go
  • framework/configstore/rdb.go
  • plugins/governance/test_utils.go
  • framework/configstore/tables/mcp.go
  • plugins/governance/resolver.go
  • plugins/governance/tracker.go
  • plugins/governance/rate_limit_test.go
  • core/go.mod
  • plugins/governance/store.go
🧠 Learnings (3)
📚 Learning: 2025-12-09T17:07:42.007Z
Learnt from: qwerty-dvorak
Repo: maximhq/bifrost PR: 1006
File: core/schemas/account.go:9-18
Timestamp: 2025-12-09T17:07:42.007Z
Learning: In core/schemas/account.go, the HuggingFaceKeyConfig field within the Key struct is currently unused and reserved for future Hugging Face inference endpoint deployments. Do not flag this field as missing from OpenAPI documentation or require its presence in the API spec until the feature is actively implemented and used. When the feature is added, update the OpenAPI docs accordingly; otherwise, treat this field as non-breaking and not part of the current API surface.

Applied to files:

  • plugins/governance/store_test.go
  • plugins/governance/provider_budget_test.go
  • plugins/governance/advanced_scenarios_test.go
  • plugins/governance/team_budget_test.go
  • plugins/governance/config_update_sync_test.go
  • framework/modelcatalog/main.go
  • core/schemas/context.go
  • plugins/governance/rate_limit_enforcement_test.go
  • plugins/governance/customer_budget_test.go
  • framework/modelcatalog/sync.go
  • plugins/governance/fixtures_test.go
  • plugins/governance/resolver_test.go
  • plugins/governance/in_memory_sync_test.go
  • plugins/governance/edge_cases_test.go
  • plugins/governance/e2e_test.go
  • core/mcp.go
  • plugins/governance/tracker_test.go
  • plugins/governance/main.go
  • framework/configstore/tables/budget.go
  • framework/configstore/store.go
  • framework/configstore/tables/ratelimit.go
  • core/providers/nebius/nebius.go
  • framework/configstore/rdb.go
  • plugins/governance/test_utils.go
  • framework/configstore/tables/mcp.go
  • plugins/governance/resolver.go
  • plugins/governance/tracker.go
  • plugins/governance/rate_limit_test.go
  • plugins/governance/store.go
📚 Learning: 2025-12-22T10:50:40.990Z
Learnt from: Pratham-Mishra04
Repo: maximhq/bifrost PR: 1154
File: plugins/governance/store.go:1165-1186
Timestamp: 2025-12-22T10:50:40.990Z
Learning: In the Bifrost governance plugin, budgets and rate limits have 1:1 relationships with their parent entities (virtual keys, teams, customers). Do not assume sharing; ensure cascade deletion logic only deletes budgets/rate limits when there are no shared references. Enforce invariants in code and add tests to verify no cross-entity sharing and that cascade deletes only remove the specific child of the parent. If a counterexample arises, adjust data model or add guards.

Applied to files:

  • plugins/governance/store_test.go
  • plugins/governance/provider_budget_test.go
  • plugins/governance/advanced_scenarios_test.go
  • plugins/governance/team_budget_test.go
  • plugins/governance/config_update_sync_test.go
  • plugins/governance/rate_limit_enforcement_test.go
  • plugins/governance/customer_budget_test.go
  • plugins/governance/fixtures_test.go
  • plugins/governance/resolver_test.go
  • plugins/governance/in_memory_sync_test.go
  • plugins/governance/edge_cases_test.go
  • plugins/governance/e2e_test.go
  • plugins/governance/tracker_test.go
  • plugins/governance/main.go
  • plugins/governance/test_utils.go
  • plugins/governance/resolver.go
  • plugins/governance/tracker.go
  • plugins/governance/rate_limit_test.go
  • plugins/governance/store.go
📚 Learning: 2025-12-19T09:26:54.961Z
Learnt from: qwerty-dvorak
Repo: maximhq/bifrost PR: 1006
File: core/providers/utils/utils.go:1050-1051
Timestamp: 2025-12-19T09:26:54.961Z
Learning: Update streaming end-marker handling so HuggingFace is treated as a non-[DONE] provider for backends that do not emit a DONE marker (e.g., meta llama on novita). In core/providers/utils/utils.go, adjust ProviderSendsDoneMarker() (or related logic) to detect providers that may not emit DONE and avoid relying on DONE as the sole end signal. Add tests to cover both DONE-emitting and non-DONE backends, with clear documentation in code comments explaining the rationale and any fallback behavior.

Applied to files:

  • core/providers/nebius/nebius.go
🧬 Code graph analysis (17)
plugins/governance/provider_budget_test.go (5)
plugins/governance/test_utils.go (11)
  • NewGlobalTestData (307-313)
  • MakeRequest (91-141)
  • APIRequest (76-81)
  • CreateVirtualKeyRequest (155-164)
  • BudgetRequest (177-180)
  • ProviderConfigRequest (167-174)
  • ExtractIDFromResponse (248-268)
  • ChatCompletionRequest (233-239)
  • ChatMessage (242-245)
  • CheckErrorMessage (272-292)
  • CalculateCost (64-73)
examples/plugins/hello-world/main.go (1)
  • Cleanup (41-44)
ui/lib/types/governance.ts (1)
  • Budget (5-11)
core/schemas/models.go (1)
  • Model (109-129)
core/providers/gemini/types.go (2)
  • Role (17-17)
  • Content (977-985)
plugins/governance/team_budget_test.go (1)
plugins/governance/test_utils.go (10)
  • MakeRequest (91-141)
  • APIRequest (76-81)
  • CreateTeamRequest (183-187)
  • BudgetRequest (177-180)
  • ExtractIDFromResponse (248-268)
  • CreateVirtualKeyRequest (155-164)
  • ChatCompletionRequest (233-239)
  • ChatMessage (242-245)
  • CheckErrorMessage (272-292)
  • CalculateCost (64-73)
plugins/governance/config_update_sync_test.go (4)
plugins/governance/test_utils.go (14)
  • NewGlobalTestData (307-313)
  • MakeRequest (91-141)
  • APIRequest (76-81)
  • CreateVirtualKeyRequest (155-164)
  • CreateRateLimitRequest (202-207)
  • UpdateVirtualKeyRequest (210-218)
  • BudgetRequest (177-180)
  • UpdateBudgetRequest (196-199)
  • ProviderConfigRequest (167-174)
  • CreateTeamRequest (183-187)
  • WaitForCondition (371-394)
  • UpdateTeamRequest (221-224)
  • CreateCustomerRequest (190-193)
  • UpdateCustomerRequest (227-230)
examples/plugins/hello-world/main.go (1)
  • Cleanup (41-44)
ui/lib/types/governance.ts (2)
  • RateLimit (13-25)
  • Budget (5-11)
core/providers/gemini/types.go (2)
  • Role (17-17)
  • Content (977-985)
framework/modelcatalog/main.go (2)
transports/bifrost-http/lib/config.go (1)
  • Config (189-220)
framework/modelcatalog/config.go (1)
  • Config (13-16)
plugins/governance/rate_limit_enforcement_test.go (3)
plugins/governance/test_utils.go (9)
  • NewGlobalTestData (307-313)
  • MakeRequest (91-141)
  • APIRequest (76-81)
  • CreateVirtualKeyRequest (155-164)
  • CreateRateLimitRequest (202-207)
  • ChatCompletionRequest (233-239)
  • ChatMessage (242-245)
  • CheckErrorMessage (272-292)
  • ProviderConfigRequest (167-174)
ui/lib/types/governance.ts (1)
  • RateLimit (13-25)
core/providers/gemini/types.go (2)
  • Role (17-17)
  • Content (977-985)
plugins/governance/resolver_test.go (5)
plugins/governance/store.go (1)
  • NewLocalGovernanceStore (75-94)
plugins/governance/resolver.go (11)
  • NewBudgetResolver (70-75)
  • DecisionAllow (18-18)
  • DecisionVirtualKeyNotFound (19-19)
  • DecisionVirtualKeyBlocked (20-20)
  • DecisionProviderBlocked (26-26)
  • DecisionModelBlocked (25-25)
  • DecisionTokenLimited (23-23)
  • DecisionRequestLimited (24-24)
  • DecisionBudgetExceeded (22-22)
  • DecisionRateLimited (21-21)
  • Decision (15-15)
ui/lib/types/governance.ts (5)
  • VirtualKey (61-80)
  • RateLimit (13-25)
  • Budget (5-11)
  • Customer (37-44)
  • Team (27-35)
core/schemas/bifrost.go (2)
  • OpenAI (35-35)
  • BifrostContextKey (117-117)
core/schemas/models.go (1)
  • Model (109-129)
plugins/governance/in_memory_sync_test.go (3)
plugins/governance/test_utils.go (3)
  • NewGlobalTestData (307-313)
  • MakeRequest (91-141)
  • APIRequest (76-81)
examples/plugins/hello-world/main.go (1)
  • Cleanup (41-44)
ui/lib/types/governance.ts (1)
  • Budget (5-11)
plugins/governance/edge_cases_test.go (1)
plugins/governance/test_utils.go (13)
  • NewGlobalTestData (307-313)
  • MakeRequest (91-141)
  • APIRequest (76-81)
  • CreateCustomerRequest (190-193)
  • BudgetRequest (177-180)
  • ExtractIDFromResponse (248-268)
  • CreateTeamRequest (183-187)
  • CreateVirtualKeyRequest (155-164)
  • ProviderConfigRequest (167-174)
  • ChatCompletionRequest (233-239)
  • ChatMessage (242-245)
  • CheckErrorMessage (272-292)
  • CalculateCost (64-73)
plugins/governance/e2e_test.go (3)
plugins/governance/test_utils.go (5)
  • MakeRequest (91-141)
  • APIRequest (76-81)
  • ExtractIDFromResponse (248-268)
  • WaitForAPICondition (398-424)
  • APIResponse (84-88)
ui/lib/types/governance.ts (2)
  • Budget (5-11)
  • RateLimit (13-25)
framework/configstore/tables/utils.go (1)
  • ParseDuration (9-43)
plugins/governance/tracker_test.go (6)
plugins/governance/fixtures_test.go (1)
  • NewMockLogger (24-32)
framework/configstore/clientconfig.go (1)
  • GovernanceConfig (720-727)
plugins/governance/resolver.go (1)
  • NewBudgetResolver (70-75)
plugins/governance/tracker.go (2)
  • NewUsageTracker (52-66)
  • UsageUpdate (17-30)
core/schemas/bifrost.go (1)
  • OpenAI (35-35)
core/utils.go (1)
  • IsFinalChunk (212-227)
plugins/governance/main.go (3)
plugins/governance/store.go (2)
  • GovernanceStore (50-72)
  • NewLocalGovernanceStore (75-94)
plugins/governance/resolver.go (2)
  • BudgetResolver (64-67)
  • NewBudgetResolver (70-75)
plugins/governance/tracker.go (2)
  • UsageTracker (33-45)
  • NewUsageTracker (52-66)
framework/configstore/tables/budget.go (1)
framework/configstore/tables/utils.go (1)
  • ParseDuration (9-43)
framework/configstore/store.go (1)
framework/configstore/tables/ratelimit.go (2)
  • TableRateLimit (11-36)
  • TableRateLimit (39-39)
plugins/governance/test_utils.go (2)
ui/lib/types/governance.ts (2)
  • Budget (5-11)
  • RateLimit (13-25)
core/schemas/models.go (1)
  • Model (109-129)
framework/configstore/tables/mcp.go (2)
core/schemas/mcp.go (1)
  • MCPStdioConfig (71-75)
ui/lib/types/mcp.ts (1)
  • MCPStdioConfig (7-11)
plugins/governance/resolver.go (4)
plugins/governance/store.go (1)
  • GovernanceStore (50-72)
framework/configstore/tables/virtualkey.go (2)
  • TableVirtualKey (95-122)
  • TableVirtualKey (125-125)
core/schemas/bifrost.go (1)
  • ModelProvider (32-32)
framework/configstore/tables/ratelimit.go (2)
  • TableRateLimit (11-36)
  • TableRateLimit (39-39)
plugins/governance/store.go (7)
framework/configstore/tables/virtualkey.go (2)
  • TableVirtualKey (95-122)
  • TableVirtualKey (125-125)
framework/configstore/tables/team.go (2)
  • TableTeam (12-38)
  • TableTeam (41-41)
framework/configstore/tables/budget.go (2)
  • TableBudget (11-27)
  • TableBudget (30-30)
framework/configstore/tables/ratelimit.go (2)
  • TableRateLimit (11-36)
  • TableRateLimit (39-39)
framework/configstore/tables/utils.go (1)
  • ParseDuration (9-43)
core/schemas/models.go (1)
  • Model (109-129)
ui/lib/types/governance.ts (4)
  • Budget (5-11)
  • RateLimit (13-25)
  • Team (27-35)
  • Customer (37-44)
🪛 GitHub Actions: PR #1153
framework/configstore/rdb.go

[error] 740-740: unknown field IsCodeModeClient in struct literal of type schemas.MCPClientConfig

🔇 Additional comments (68)
core/providers/nebius/nebius.go (1)

48-54: LGTM! Configuration plumbing is correct and consistent.

The initialization of sendBackRawRequest from config.SendBackRawRequest aligns with the existing pattern for sendBackRawResponse and properly enables configuration-driven control over whether raw requests are included in responses.

core/schemas/context.go (1)

174-176: LGTM: Good defensive programming for plugin safety.

The lazy initialization of userValues makes SetValue resilient to BifrostContext instances that might be created without using the constructor. This is particularly valuable in the new plugins architecture where external code could potentially misuse the API.

While NewBifrostContext always initializes the map (line 55), this defensive check ensures robustness against edge cases and future changes.

framework/modelcatalog/sync.go (1)

61-67: LGTM!

The optional pre-check pattern is correctly implemented with a nil guard before invoking the callback. Returning nil on cancellation is appropriate since this is an optional gating mechanism, not an error condition.

framework/modelcatalog/main.go (3)

81-86: LGTM!

Clean type definition with clear documentation explaining its optional nature and purpose.


32-33: LGTM!

Field placement is appropriate. Since the callback is set once during initialization and remains read-only, no additional synchronization is required.


88-108: All callers of modelcatalog.Init have been properly updated with the new shouldSyncPricingFunc parameter. The breaking API change is correctly handled across the codebase in all three call sites.

plugins/governance/go.mod (2)

10-10: Testify dependency is justified. The governance plugin uses github.com/stretchr/testify/assert and github.com/stretchr/testify/require across multiple test files (fixtures_test.go, tracker_test.go, store_test.go, resolver_test.go). The dependency addition is appropriate.


80-80: Governance plugin uses mcp-go indirectly; API verification applies to core layer, not governance.

The mcp-go v0.43.2 upgrade was applied to core/go.mod and transports/go.mod (direct dependencies). The governance plugin has mcp-go as an indirect dependency and does not import mark3labs packages—it only manages MCP configuration through configstore tables. No API changes to mcp-go will impact the governance plugin directly.

However, verify that core/mcp.go and transports/bifrost-http/handlers/mcp_server.go use the v0.43.2 API correctly, and note that test directories (tests/core-mcp/go.mod, tests/scripts/1millogs/go.mod) still reference v0.41.1, creating a version mismatch in the stack.

Likely an incorrect or invalid review comment.

plugins/governance/changelog.md (1)

1-3: LGTM!

The changelog entries clearly document the key changes: governance store interface extraction for extensibility, rate-limit handling improvements, and e2e test additions.

core/mcp.go (1)

1134-1151: LGTM!

The removal of the strict type assertion to *server.MCPServer correctly broadens compatibility. The validation at lines 998-1003 ensures InProcessServer is non-nil before reaching this point, and client.NewInProcessClient accepts the interface type directly.

framework/configstore/tables/budget.go (3)

24-27: LGTM!

The virtual field LastDBUsage with gorm:"-" json:"-" correctly excludes it from persistence and serialization, enabling runtime delta tracking without affecting the database schema.


32-46: LGTM!

The BeforeSave hook properly validates:

  • ResetDuration format using ParseDuration and ensures it's positive
  • MaxLimit is non-negative (zero is valid for "no budget allocated" scenarios)

This provides robust input validation before persistence.


47-52: LGTM!

The AfterFind hook correctly captures the database value into LastDBUsage after loading, enabling runtime tracking of usage deltas. This pattern is consistent with the parallel implementation in ratelimit.go.

framework/configstore/tables/ratelimit.go (2)

32-36: LGTM!

The virtual fields LastDBTokenUsage and LastDBRequestUsage with gorm:"-" json:"-" tags correctly enable runtime delta tracking without affecting persistence or serialization, consistent with the pattern in budget.go.


82-88: LGTM!

The AfterFind hook correctly captures both token and request usage values from the database into virtual fields for runtime tracking. This enables detecting in-memory usage changes since the last DB read.

plugins/governance/fixtures_test.go (3)

14-66: LGTM!

The MockLogger implementation is well-designed:

  • Thread-safe with sync.Mutex for concurrent test scenarios
  • Stores logs by category (errors, warnings, infos, debugs) for test assertions
  • Fatal appending to errors (rather than panicking) is appropriate for test contexts

68-193: LGTM!

The test data builders provide clean, composable factory functions for creating test fixtures. The consistent pattern (e.g., buildVirtualKeybuildVirtualKeyWithBudget) enables flexible test setup while maintaining DRY principles.


195-221: LGTM!

The assertion helpers appropriately use t.Helper() for accurate test failure line reporting and wrap testify assertions with domain-specific semantics.

plugins/governance/config_update_sync_test.go (3)

1-6: LGTM!

Clean package declaration and minimal imports for the test file.


686-764: Good use of WaitForCondition for reliable async verification.

This test properly uses WaitForCondition with progressive backoff for both usage updates and limit sync verification. This pattern should be preferred over fixed time.Sleep calls in other tests for consistency and reliability.


960-1123: LGTM!

The provider budget update sync test covers the complete flow: create VK with provider budget, consume budget via request, update to lower limit, and verify usage reset. The test appropriately skips when consumption fails.

plugins/governance/resolver.go (3)

64-75: LGTM!

The change from *GovernanceStore to GovernanceStore (interface type) is correct Go idiom. Interfaces should be passed by value since they're already reference types internally, and this enables dependency injection with any compatible implementation.


194-217: LGTM!

The new checkRateLimitHierarchy method consolidates rate-limit checking into a single hierarchy-aware function. It correctly:

  1. Calls CheckRateLimit on the store
  2. On error, determines the appropriate RateLimitInfo (provider-level first, then VK-level)
  3. Returns a structured EvaluationResult with the decision from the store

219-233: LGTM!

The updated checkBudgetHierarchy correctly takes the full *EvaluationRequest parameter, enabling the store to perform atomic budget checking across the VK → Team → Customer hierarchy with access to request context.

plugins/governance/test_utils.go (4)

15-73: LGTM!

The cost modeling utilities provide realistic test data for budget consumption testing. The CalculateCost function correctly calculates based on per-token pricing.


90-141: LGTM!

The MakeRequest function is well-implemented:

  • Properly handles optional body marshaling
  • Sets appropriate headers including virtual key when provided
  • Gracefully handles non-JSON responses by storing raw body

299-367: LGTM!

The GlobalTestData struct with cleanup logic is well-designed:

  • Properly tracks resources by type for ordered cleanup
  • Handles 404 responses gracefully (resource already deleted)
  • Logs cleanup summary for debugging

369-424: LGTM!

The polling utilities are well-implemented with:

  • Progressive backoff (50ms→500ms for conditions, 100ms→500ms for API)
  • Descriptive logging on timeout and multi-attempt success
  • Clean separation between generic condition checking and API-specific polling
plugins/governance/rate_limit_test.go (1)

1-991: Comprehensive rate limit test coverage looks good.

The test suite covers a wide range of rate limit scenarios including VK-level and provider-level limits for both tokens and requests. The use of t.Parallel() for concurrent test execution and proper cleanup via testData.Cleanup(t) is well implemented.

A few observations:

  • The time.Sleep(500 * time.Millisecond) calls for async sync verification are reasonable for integration tests, though they could make tests flaky under load.
  • Tests properly handle cases where rate limits might not be hit by using t.Skip rather than failing.
framework/configstore/store.go (2)

104-104: LGTM - GetRateLimits method follows existing patterns.

This method is consistent with the existing GetBudgets method at line 111, providing a clean way to retrieve all rate limits.


146-148: RetryOnNotFound properly handles context cancellation during retries.

The implementation in rdb.go (lines 2178-2202) includes a select statement that monitors ctx.Done() alongside the retry delay, immediately returning ctx.Err() if the context is cancelled. This prevents retries from continuing after context cancellation.

plugins/governance/team_budget_test.go (1)

8-159: Team budget test implementation is solid.

The test properly validates that a shared team budget is enforced across multiple VKs by:

  1. Creating a team with a restrictive budget
  2. Creating two VKs under the team with higher individual budgets
  3. Alternating requests between VKs to exhaust the shared team budget
  4. Verifying budget enforcement kicks in

The use of CalculateCost for tracking consumed budget and proper error message checking is well implemented.

plugins/governance/store_test.go (2)

68-101: Concurrent reads test is well designed.

The test launches 100 goroutines each performing 100 reads, validating that the lock-free read path handles high concurrency correctly. The use of atomic.Int64 for counters is appropriate.


1-351: Store test suite provides good coverage of governance store operations.

The tests cover key scenarios including VK retrieval, concurrent access, budget validation, rate limit updates, and reset operations. The use of builder functions for test data keeps tests readable.

plugins/governance/provider_budget_test.go (2)

57-145: Verify subtest state isolation for provider budget tests.

Both subtests (OpenAIProviderBudgetExceeded and AnthropicProviderBudgetExceeded) use the same vkValue and overall VK budget ($1.0). While provider budgets are tracked separately, requests in the OpenAI subtest will also consume from the overall VK budget.

If the first subtest consumes significant overall budget, it could affect the second subtest's behavior. Consider whether you want to:

  1. Create separate VKs for each subtest for complete isolation
  2. Document that subtests are intentionally testing shared VK behavior

1-236: Provider budget test structure is sound.

The test creates a VK with distinct provider-specific budgets and verifies that each provider's budget is independently enforced. The pattern of tracking consumed budget via CalculateCost and checking for budget-related error messages is consistent with other tests.

plugins/governance/advanced_scenarios_test.go (2)

1-1675: Comprehensive advanced scenarios test suite.

The test file provides excellent coverage of complex governance scenarios including:

  • VK switching between teams/customers after budget exhaustion
  • Hierarchical budget chain switching
  • Budget updates after exhaustion (VK, team, customer, provider config)
  • Deletion cascades for VKs, teams, and customers
  • Verification that VK entity IDs are set to nil when parent entities are deleted

The tests align well with the 1:1 relationship model between budgets/rate limits and their parent entities mentioned in the learnings.


1254-1259: The review comment is based on an incorrect premise. Rate limits are cascade-deleted when a VirtualKey is deleted, not left orphaned. The DeleteVirtualKeyInMemory function explicitly deletes both VK-level rate limits (line 1246) and provider-config-level rate limits (line 1256), using the same cascade pattern as budgets. The test comment at lines 1254–1259 suggesting that rate limits "may still exist in memory (orphaned)" is misleading and inconsistent with the actual cleanup logic.

Likely an incorrect or invalid review comment.

plugins/governance/tracker_test.go (1)

84-152: Streaming optimization test correctly validates chunk handling.

The test properly verifies that:

  1. Non-final streaming chunks update token usage but not request count
  2. Final chunks increment the request counter

This aligns with the UsageUpdate struct fields (IsStreaming, IsFinalChunk, HasUsageData) from the tracker.

plugins/governance/resolver_test.go (1)

1-551: Comprehensive test coverage for BudgetResolver - well structured.

The test suite covers essential governance scenarios including VK lifecycle, provider/model filtering, rate limits, budgets, and multi-level hierarchy. The use of table-driven tests for IsProviderAllowed and IsModelAllowed (lines 403-511) is a good pattern.

One observation on line 338: direct access to store.budgets.Store() tightly couples the test to the internal sync.Map implementation. If the storage mechanism changes, this test will break. Consider adding a method like UpdateBudgetForTest() to the store interface for controlled test manipulation, or accept this coupling as acceptable for package-internal tests.

plugins/governance/customer_budget_test.go (1)

10-160: Good integration tests for customer budget enforcement.

The tests correctly verify POST-HOC budget enforcement (the request that exceeds the limit is allowed, but subsequent requests are blocked). The shouldStop logic on lines 149-156 correctly implements this pattern.

The long lorem ipsum prompts help consume budget faster, making the tests more efficient.

plugins/governance/tracker.go (3)

47-49: Good choice for worker interval.

The 10-second interval provides a reasonable balance between responsiveness (detecting expired limits quickly) and performance (not hammering the store with resets).


128-148: Well-structured reset and dump workflow.

The three-part approach (reset rate limits → reset budgets → dump to DB) correctly separates in-memory operations from persistence. This ensures fast in-memory updates while batching DB writes for efficiency.


228-254: Selective field updates prevent config overwrites.

The selective update approach (lines 233-237) correctly updates only usage-related fields (token_current_usage, token_last_reset, request_current_usage, request_last_reset) while preserving max_limit and reset_duration. This prevents race conditions where config changes during startup could be lost.

plugins/governance/rate_limit_enforcement_test.go (1)

1-129: Well-documented POST-HOC enforcement behavior.

The test comments clearly explain that rate limit enforcement is POST-HOC: the request that exceeds the limit is ALLOWED, but subsequent requests are BLOCKED. This is important documentation for understanding the system behavior.

plugins/governance/in_memory_sync_test.go (1)

1-554: Comprehensive in-memory synchronization tests.

The tests properly verify that in-memory state is updated after CRUD operations. The pattern of verifying both the resource and its associated budget separately (e.g., lines 116-127) ensures complete coverage of the synchronization logic.

The pragmatic handling of SQLite locking issues (lines 547-551) with a warning instead of failure is a good approach for parallel test reliability.

plugins/governance/e2e_test.go (3)

766-857: Proper concurrent test implementation.

TestConcurrentRequestsToSameVK correctly uses sync.WaitGroup for goroutine synchronization and sync.Mutex for thread-safe success counting. This is the right pattern for concurrent testing.


1012-1036: Good use of polling instead of fixed sleep.

The WaitForAPICondition pattern (lines 1014-1032) is better than fixed sleeps because it adapts to actual system response time and has a clear timeout. This reduces flakiness while keeping tests responsive.


1-1543: Comprehensive e2e test coverage for governance features.

The test file covers critical governance scenarios including:

  • Multi-VK budget sharing (lines 20-183)
  • Full budget hierarchy enforcement (lines 189-354)
  • Failed requests not consuming budget (lines 360-500)
  • Rate limit reset boundaries (lines 652-760)
  • Concurrent request handling (lines 766-857)
  • Cascade deletion behavior (lines 1077-1307)
  • Provider load balancing and fallback (lines 1315-1543)

The section organization (CRITICAL, HIGH, FEATURE) makes it easy to understand test priorities.

plugins/governance/main.go (4)

40-47: Good abstraction with BaseGovernancePlugin interface.

The new interface provides a clean contract for the governance plugin, enabling easier mocking in tests and potential alternative implementations. The method signatures cover the full plugin lifecycle (hooks, interceptor, cleanup, store access).


166-224: InitFromStore enables dependency injection for testing.

The new constructor allows injecting a custom GovernanceStore implementation, which is valuable for:

  • Unit testing with mock stores
  • Integration with non-standard storage backends
  • Pre-configured store instances

The nil check on line 193-195 correctly validates the required dependency.


210-211: Context shadowing is intentional for plugin lifecycle.

Line 210 shadows the ctx parameter with a new cancellable context. This is intentional - the plugin manages its own lifecycle via cancelFunc, independent of the caller's context. This pattern ensures proper cleanup regardless of the caller's context state.


462-464: Error message change improves abstraction.

Changing from "x-bf-vk header is missing" to "virtual key is missing in headers" is a good improvement. It doesn't expose the specific header name implementation detail to API consumers, making the API more stable if header naming changes.

framework/configstore/tables/mcp.go (1)

16-35: Consistent pattern for new MCP client fields.

The new fields (IsCodeModeClient, ToolsToAutoExecuteJSON, ToolsToAutoExecute) follow the established pattern of the existing ToolsToExecute field pair. The serialization in BeforeSave (lines 63-71) and deserialization in AfterFind (lines 101-105) are symmetric and handle nil/empty cases correctly. Database migrations exist for both new columns with proper initialization of existing rows.

framework/configstore/rdb.go (4)

9-9: LGTM!

The time import is correctly added to support the new RetryOnNotFound function and MCP timeout duration handling.


45-46: Verify validation requirements for MCP configuration fields.

The new MCPAgentDepth and MCPToolExecutionTimeout fields are correctly persisted and retrieved. Consider whether these fields need validation (e.g., minimum/maximum bounds for agent depth, reasonable timeout ranges) either at the API layer or in the database layer.

Also applies to: 200-201


787-817: Blocked by schema mismatch - logic is correct.

The MCP client configuration logic properly handles:

  • Environment variable substitution with correct context (no existing headers for create, existing headers for update)
  • Explicit Select() to include zero-value boolean fields that GORM would otherwise skip

However, this code cannot function until the IsCodeModeClient field is added to schemas.MCPClientConfig (see previous comment about pipeline failure).

Note: The explicit Select() usage on lines 848-851 is the correct approach to ensure boolean fields with false values are updated in the database.

Also applies to: 820-856


1823-1830: LGTM!

The GetRateLimits method correctly retrieves all rate limits from the database, following the same pattern as other collection getters like GetBudgets.

plugins/governance/store.go (5)

17-72: Excellent interface design with clear error semantics.

The refactoring introduces a well-documented GovernanceStore interface with an explicit error contract:

  • Errors from CheckRateLimit and CheckBudget indicate policy violations, not infrastructure failures
  • This prevents inappropriate retry logic and ensures consistent behavior

The renamed LocalGovernanceStore implementation and new GovernanceData snapshot type provide clean abstractions for in-memory governance management.


96-149: LGTM!

The GetGovernanceData implementation correctly materializes an immutable snapshot from the in-memory sync.Map fields. The type-checking and nil-safety handling ensure robustness even with corrupted data.


1225-1266: Cascade deletion logic correctly maintains in-memory integrity.

The in-memory cascade deletion in DeleteVirtualKeyInMemory, DeleteTeamInMemory, and DeleteCustomerInMemory correctly:

  • Deletes associated budgets and rate limits (maintaining 1:1 relationship invariant from learnings)
  • Nullifies foreign key references in related entities
  • Maintains consistency with database cascade deletion logic in rdb.go

The implementation correctly enforces that budgets and rate limits are not shared across entities, as documented in learnings.

Also applies to: 1330-1362, 1426-1474


1576-1668: The helper functions correctly implement the budget and rate limit update semantics.

The checkAndUpdateBudget and checkAndUpdateRateLimit functions properly handle the reset logic:

  • Reset usage to 0 when current usage would exceed newly tightened limits
  • Preserve usage otherwise to avoid data loss
  • Maintain LastDB fields for multi-node baseline consistency

The equalPtr helper function referenced in checkAndUpdateRateLimit is defined in plugins/governance/utils.go and is properly accessible.


311-349: Shallow copy pattern is safe—no changes required.

The clone-first pattern correctly prevents race conditions. Verification confirms all modified fields in both TableBudget and TableRateLimit are value types (counters, timestamps) that are properly copied. Pointer fields (TokenResetDuration, RequestResetDuration) are only read via dereference, never reassigned, making shallow copying safe. Strings in Go are immutable, so shared pointer references do not create mutation races.

core/go.mod (5)

8-10: Approve patch and minor version updates to existing dependencies.

The updates to AWS SDK components (v1.32.6, v1.19.6, v1.94.0), sonic, fasthttp, oauth2, text, compress, and arch are all patch or minor version bumps that align with maintenance and security practices. No apparent compatibility concerns.

Also applies to: 12-12, 17-17, 20-22, 39-39, 45-45, 52-52, 64-66


49-49: The +incompatible tag is correct; v2.1.4+incompatible is available for potential upgrade.

The v2.1.3+incompatible version is the expected state. The +incompatible tag is necessary because go-sourcemap was originally released as v1.0.x and later had a breaking change release as v2.x; no compatible v2.1.3 variant exists. Additionally, there is no v3.x version. However, a newer patch version v2.1.4+incompatible is available in the module registry if you wish to upgrade this transitive dependency (pulled in by github.com/dop251/goja).


17-17: Version bump is compatible; agent/code-mode features are bifrost-specific implementations.

The mcp-go v0.43.2 upgrade is safe and brings improvements for JSON Schema handling and notification processing. Note that v0.43.2 does not introduce agent-mode or code-execution capabilities—these are implemented within bifrost's code. The MCP library provides the client/server foundation; bifrost's agent orchestration and code-execution features (core/mcp/agent.go, core/mcp/codemode_*.go) are independent implementations. No breaking changes exist in the v0.41.1 → v0.43.2 transition.


13-14: Dependencies are appropriately selected for production use.

Both go-typescript v0.7.0 and goja v0.0.0-20251103141225-af2ceb9156d7 are legitimate and well-maintained:

  • go-typescript v0.7.0 is a stable release (latest available version in the repo)
  • goja uses a pseudo-version because the project doesn't publish formal GitHub releases—this is standard Go practice and not indicative of instability
  • Both projects show active maintenance and no known security issues

27-27: All transitive dependencies are necessary and secure—no issues found.

The four indirect dependencies (Masterminds/semver/v3, regexp2, go-sourcemap, and pprof) are legitimate requirements pulled by the two direct dependencies go-typescript and goja, which power the new TypeScript/JavaScript execution layer.

Regarding potential optimizations:

  • regexp2 cannot be replaced with the standard library regexp package; goja (JavaScript runtime) requires ECMAScript-compatible regex semantics, not Go's syntax.
  • Masterminds/semver, go-sourcemap, and pprof have no standard library equivalents.
  • All versions are current, stable, and have no reported CVEs.

Comment on lines +60 to +82
// Create a VK with even tighter budget and provider-specific budgets
vkBudget := 0.01
createVKResp := MakeRequest(t, APIRequest{
Method: "POST",
Path: "/api/governance/virtual-keys",
Body: CreateVirtualKeyRequest{
Name: "test-vk-criss-cross-" + generateRandomID(),
TeamID: &teamID,
Budget: &BudgetRequest{
MaxLimit: vkBudget,
ResetDuration: "1h",
},
ProviderConfigs: []ProviderConfigRequest{
{
Provider: "openai",
Weight: 1.0,
Budget: &BudgetRequest{
MaxLimit: 0.08, // Even tighter provider budget
ResetDuration: "1h",
},
},
},
},
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 | 🟡 Minor

Test budget hierarchy has inconsistent expectations.

The VK budget is set to $0.01 (line 61) while the provider budget is $0.08 (line 77). The comment at line 101 states "Provider budget should be the limiting factor" but the VK budget is more restrictive.

Additionally, the loop termination condition at line 181 checks consumedBudget >= 0.08 (provider budget) but if VK budget ($0.01) is enforced first, this condition may never be reached before rejection.

Either the budget values should be swapped, or the comments and expectations should be updated to reflect VK budget being the limiting factor.

🔎 Suggested fix to align test expectations with budget hierarchy

If the intent is to test provider budget as the limiting factor, swap the values:

-	vkBudget := 0.01
+	vkBudget := 0.15  // Higher VK budget so provider is limiting
 	createVKResp := MakeRequest(t, APIRequest{
 		// ...
 		ProviderConfigs: []ProviderConfigRequest{
 			{
 				Provider: "openai",
 				Weight:   1.0,
 				Budget: &BudgetRequest{
-					MaxLimit:      0.08, // Even tighter provider budget
+					MaxLimit:      0.01, // Tighter provider budget
 					ResetDuration: "1h",
 				},
 			},
 		},
 	})

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +1083 to 1222
func (gs *LocalGovernanceStore) UpdateVirtualKeyInMemory(vk *configstoreTables.TableVirtualKey, budgetBaselines map[string]float64, rateLimitTokensBaselines map[string]int64, rateLimitRequestsBaselines map[string]int64) {
if vk == nil {
return // Nothing to update
}
gs.virtualKeys.Store(vk.Value, vk)
if budgetBaselines == nil {
budgetBaselines = make(map[string]float64)
}
if rateLimitTokensBaselines == nil {
rateLimitTokensBaselines = make(map[string]int64)
}
if rateLimitRequestsBaselines == nil {
rateLimitRequestsBaselines = make(map[string]int64)
}
// Do not update the current usage of the rate limit, as it will be updated by the usage tracker.
// But update if max limit or reset duration changes.
if existingVKValue, exists := gs.virtualKeys.Load(vk.Value); exists && existingVKValue != nil {
existingVK, ok := existingVKValue.(*configstoreTables.TableVirtualKey)
if !ok || existingVK == nil {
return // Nothing to update
}
// Create clone to avoid modifying the original
clone := *vk
// Update Budget using checkAndUpdateBudget logic (preserve usage unless currentUsage+baseline > newMaxLimit)
if clone.Budget != nil {
// Get existing budget from gs.budgets (NOT from VK.Budget which may be stale)
var existingBudget *configstoreTables.TableBudget
if existingBudgetValue, exists := gs.budgets.Load(clone.Budget.ID); exists && existingBudgetValue != nil {
if eb, ok := existingBudgetValue.(*configstoreTables.TableBudget); ok && eb != nil {
existingBudget = eb
}
}
budgetBaseline, exists := budgetBaselines[clone.Budget.ID]
if !exists {
budgetBaseline = 0.0
}
clone.Budget = checkAndUpdateBudget(clone.Budget, existingBudget, budgetBaseline)
// Update the budget in the main budgets sync.Map
if clone.Budget != nil {
gs.budgets.Store(clone.Budget.ID, clone.Budget)
}
} else if existingVK.Budget != nil {
// Budget was removed from the virtual key, delete it from memory
gs.budgets.Delete(existingVK.Budget.ID)
}
if clone.RateLimit != nil {
// Get existing rate limit from gs.rateLimits (NOT from VK.RateLimit which may be stale)
var existingRateLimit *configstoreTables.TableRateLimit
if existingRateLimitValue, exists := gs.rateLimits.Load(clone.RateLimit.ID); exists && existingRateLimitValue != nil {
if erl, ok := existingRateLimitValue.(*configstoreTables.TableRateLimit); ok && erl != nil {
existingRateLimit = erl
}
}
tokenBaseline, exists := rateLimitTokensBaselines[clone.RateLimit.ID]
if !exists {
tokenBaseline = 0
}
requestBaseline, exists := rateLimitRequestsBaselines[clone.RateLimit.ID]
if !exists {
requestBaseline = 0
}
clone.RateLimit = checkAndUpdateRateLimit(clone.RateLimit, existingRateLimit, tokenBaseline, requestBaseline)
// Update the rate limit in the main rateLimits sync.Map
if clone.RateLimit != nil {
gs.rateLimits.Store(clone.RateLimit.ID, clone.RateLimit)
}
} else if existingVK.RateLimit != nil {
// Rate limit was removed from the virtual key, delete it from memory
gs.rateLimits.Delete(existingVK.RateLimit.ID)
}
if clone.ProviderConfigs != nil {
// Create a map of existing provider configs by ID for fast lookup
existingProviderConfigs := make(map[uint]configstoreTables.TableVirtualKeyProviderConfig)
if existingVK.ProviderConfigs != nil {
for _, existingPC := range existingVK.ProviderConfigs {
existingProviderConfigs[existingPC.ID] = existingPC
}
}

// Process each new/updated provider config
for i, pc := range clone.ProviderConfigs {
if pc.RateLimit != nil {
// Get existing rate limit from gs.rateLimits (NOT from provider config which may be stale)
var existingProviderRateLimit *configstoreTables.TableRateLimit
if existingRateLimitValue, exists := gs.rateLimits.Load(pc.RateLimit.ID); exists && existingRateLimitValue != nil {
if erl, ok := existingRateLimitValue.(*configstoreTables.TableRateLimit); ok && erl != nil {
existingProviderRateLimit = erl
}
}
tokenBaseline, exists := rateLimitTokensBaselines[pc.RateLimit.ID]
if !exists {
tokenBaseline = 0
}
requestBaseline, exists := rateLimitRequestsBaselines[pc.RateLimit.ID]
if !exists {
requestBaseline = 0
}
clone.ProviderConfigs[i].RateLimit = checkAndUpdateRateLimit(pc.RateLimit, existingProviderRateLimit, tokenBaseline, requestBaseline)
// Also update the rate limit in the main rateLimits sync.Map
if clone.ProviderConfigs[i].RateLimit != nil {
gs.rateLimits.Store(clone.ProviderConfigs[i].RateLimit.ID, clone.ProviderConfigs[i].RateLimit)
}
} else {
// Rate limit was removed from provider config, delete it from memory if it existed
if existingPC, exists := existingProviderConfigs[pc.ID]; exists && existingPC.RateLimit != nil {
gs.rateLimits.Delete(existingPC.RateLimit.ID)
clone.ProviderConfigs[i].RateLimit = nil
}
}
// Update Budget for provider config (preserve usage unless currentUsage+baseline > newMaxLimit)
if pc.Budget != nil {
// Get existing budget from gs.budgets (NOT from provider config which may be stale)
var existingProviderBudget *configstoreTables.TableBudget
if existingBudgetValue, exists := gs.budgets.Load(pc.Budget.ID); exists && existingBudgetValue != nil {
if eb, ok := existingBudgetValue.(*configstoreTables.TableBudget); ok && eb != nil {
existingProviderBudget = eb
}
}
budgetBaseline, exists := budgetBaselines[pc.Budget.ID]
if !exists {
budgetBaseline = 0.0
}
clone.ProviderConfigs[i].Budget = checkAndUpdateBudget(pc.Budget, existingProviderBudget, budgetBaseline)
// Also update the budget in the main budgets sync.Map
if clone.ProviderConfigs[i].Budget != nil {
gs.budgets.Store(clone.ProviderConfigs[i].Budget.ID, clone.ProviderConfigs[i].Budget)
}
} else {
// Budget was removed from provider config, delete it from memory if it existed
if existingPC, exists := existingProviderConfigs[pc.ID]; exists && existingPC.Budget != nil {
gs.budgets.Delete(existingPC.Budget.ID)
clone.ProviderConfigs[i].Budget = nil
}
}
}
}
gs.virtualKeys.Store(vk.Value, &clone)
} else {
gs.CreateVirtualKeyInMemory(vk)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Consider refactoring high-complexity method.

UpdateVirtualKeyInMemory spans ~140 lines with high cognitive complexity, handling:

  • VK-level budget and rate limit updates
  • Provider config budget and rate limit updates
  • Cascade deletion of removed budgets/rate limits
  • Baseline-aware update decisions

While functionally correct, the complexity makes maintenance and testing challenging. Consider extracting helper methods:

func (gs *LocalGovernanceStore) updateVKBudgetInMemory(clone *VirtualKey, existingVK *VirtualKey, baselines map[string]float64) {
    // Handle VK budget update logic
}

func (gs *LocalGovernanceStore) updateVKRateLimitInMemory(clone *VirtualKey, existingVK *VirtualKey, tokenBaselines, requestBaselines map[string]int64) {
    // Handle VK rate limit update logic
}

func (gs *LocalGovernanceStore) updateProviderConfigsInMemory(clone *VirtualKey, existingVK *VirtualKey, budgetBaselines map[string]float64, tokenBaselines, requestBaselines map[string]int64) {
    // Handle provider config updates
}

Comment on lines +15 to +16
// TestUsageTracker_UpdateUsage_Successful tests successful usage tracking
func TestUsageTracker_UpdateUsage_FailedRequest(t *testing.T) {
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 | 🟡 Minor

Fix misleading comment - function tests failed requests, not successful ones.

The comment says "tests successful usage tracking" but the function is named TestUsageTracker_UpdateUsage_FailedRequest and tests that failed requests don't update budgets.

🔎 Suggested fix
-// TestUsageTracker_UpdateUsage_Successful tests successful usage tracking
+// TestUsageTracker_UpdateUsage_FailedRequest tests that failed requests do not update budget usage
 func TestUsageTracker_UpdateUsage_FailedRequest(t *testing.T) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// TestUsageTracker_UpdateUsage_Successful tests successful usage tracking
func TestUsageTracker_UpdateUsage_FailedRequest(t *testing.T) {
// TestUsageTracker_UpdateUsage_FailedRequest tests that failed requests do not update budget usage
func TestUsageTracker_UpdateUsage_FailedRequest(t *testing.T) {
🤖 Prompt for AI Agents
In plugins/governance/tracker_test.go around lines 15 to 16 the test comment is
misleading: it says "tests successful usage tracking" but the function is
TestUsageTracker_UpdateUsage_FailedRequest and verifies failed requests don't
update budgets; update the comment to accurately describe that the test verifies
failed request handling (e.g., "tests failed usage requests do not update
budgets" or similar) so the comment matches the test's intent.

Comment on lines +154 to +155
// TestUsageTracker_UpdateBudgetHierarchy tests multi-level budget updates
func TestUsageTracker_Cleanup(t *testing.T) {
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 | 🟡 Minor

Fix misleading comment - function tests cleanup, not budget hierarchy.

The comment mentions "multi-level budget updates" but the function TestUsageTracker_Cleanup only tests the cleanup operation.

🔎 Suggested fix
-// TestUsageTracker_UpdateBudgetHierarchy tests multi-level budget updates
+// TestUsageTracker_Cleanup tests that cleanup succeeds without error
 func TestUsageTracker_Cleanup(t *testing.T) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// TestUsageTracker_UpdateBudgetHierarchy tests multi-level budget updates
func TestUsageTracker_Cleanup(t *testing.T) {
// TestUsageTracker_Cleanup tests that cleanup succeeds without error
func TestUsageTracker_Cleanup(t *testing.T) {
🤖 Prompt for AI Agents
In plugins/governance/tracker_test.go around lines 154 to 155, the top-of-test
comment incorrectly says "TestUsageTracker_UpdateBudgetHierarchy tests
multi-level budget updates" but the function is TestUsageTracker_Cleanup and
tests cleanup behavior; update the comment to accurately describe this test
(e.g., "TestUsageTracker_Cleanup tests cleanup of usage tracker resources" or
"tests cleanup operation") so the comment matches the function name and purpose.

## Summary

Added a configurable code mode binding level for MCP tools, allowing tools to be exposed in the virtual file system (VFS) at either server-level or tool-level granularity.

## Changes

- Added a new `CodeModeBindingLevel` configuration option with two possible values:
  - `server`: All tools for a server are exposed in a single file (e.g., `servers/calculator.d.ts`)
  - `tool`: Each tool is exposed in its own file (e.g., `servers/calculator/add.d.ts`)
- Enhanced the `listToolFiles` and `readToolFile` tools to support both binding levels
- Implemented a tree-based VFS structure for better organization of tool files
- Added UI configuration in the MCP Gateway settings page with visual examples
- Updated database schema and migrations to store the new configuration

## Type of change

- [x] Feature
- [x] UI (Next.js)

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [x] UI (Next.js)

## How to test

1. Configure the code mode binding level in the UI:
   - Navigate to Workspace → Config → MCP Gateway
   - Select either "Server-Level" or "Tool-Level" binding
   - Save changes

2. Test with code mode in a chat:
   ```typescript
   // First list available tools
   await listToolFiles()
   
   // Read tool definitions based on binding level
   // For server-level:
   await readToolFile({ fileName: "servers/calculator.d.ts" })
   
   // For tool-level:
   await readToolFile({ fileName: "servers/calculator/add.d.ts" })
   
   // Execute tool code as usual
   await executeToolCode(`
     const result = await calculator.add({ a: 5, b: 3 });
     return result;
   `)
   ```

## Screenshots/Recordings

N/A

## Breaking changes

- [x] No

## Related issues

N/A

## Security considerations

No additional security implications. This change only affects how tools are organized and presented in the virtual file system.

## Checklist

- [x] I added/updated tests where appropriate
- [x] I updated documentation where needed
- [x] I verified builds succeed (Go and UI)
## Summary

Add support for tool execution in the Responses API, enabling agent mode and tool execution for both Chat Completions and Responses APIs with full feature parity.

## Changes

- Renamed `ExecuteMCPTool` to `ExecuteChatMCPTool` to clarify it works with Chat API format
- Added `ExecuteResponsesMCPTool` to support tool execution with Responses API format
- Implemented format converters between Chat and Responses API tool formats
- Added adapter pattern to make agent execution API-agnostic
- Updated HTTP endpoints to support both Chat and Responses API tool execution
- Added comprehensive tests for format conversion and tool execution

## Type of change

- [ ] Bug fix
- [x] Feature
- [ ] Refactor
- [ ] Documentation
- [ ] Chore/CI

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)
- [ ] Providers/Integrations
- [ ] Plugins
- [ ] UI (Next.js)
- [ ] Docs

## How to test

Test tool execution with both Chat and Responses APIs:

```sh
# Run all tests
go test ./...

# Run specific MCP tests
go test ./core/mcp -v

# Test HTTP endpoints
curl -X POST http://localhost:8080/v1/mcp/tool/chat/execute -d '{"id":"call123","type":"function","function":{"name":"calculator","arguments":"{\"x\":10,\"y\":5}"}}'

curl -X POST http://localhost:8080/v1/mcp/tool/responses/execute -d '{"call_id":"call123","name":"calculator","arguments":"{\"x\":10,\"y\":5}"}'
```

## Breaking changes

- [x] Yes
- [ ] No

The `ExecuteMCPTool` method has been renamed to `ExecuteChatMCPTool`. Any code directly calling this method will need to be updated. The HTTP endpoint has also changed from `/v1/mcp/tool/execute` to `/v1/mcp/tool/chat/execute`.

## Related issues

Enables Responses API to have full feature parity with Chat API for tool execution and agent mode.

## Security considerations

No new security implications. Tool execution permissions and sandboxing remain unchanged.

## Checklist

- [x] I added/updated tests where appropriate
- [x] I verified builds succeed (Go and UI)
- [x] I verified the CI pipeline passes locally if applicable
## Summary

Implement health monitoring for MCP clients to detect and handle connection state changes automatically.

## Changes

- Added a new `health_monitor.go` file with a health monitoring system for MCP clients
- Implemented `ClientHealthMonitor` to periodically check client health via ping
- Created `HealthMonitorManager` to manage multiple client health monitors
- Updated client state tracking to use the actual connection state instead of inferring it
- Added connection lost detection for SSE connections
- Improved client cleanup by stopping health monitoring when removing clients
- Enhanced logging with more descriptive messages for connection events

## Type of change

- [x] Feature
- [x] Bug fix

## Affected areas

- [x] Core (Go)
- [x] Transports (HTTP)

## How to test

1. Configure multiple MCP clients with different connection types
2. Start Bifrost and verify clients connect properly
3. Disconnect a client (e.g., stop an external MCP server)
4. Verify the client state changes to "disconnected" after several failed health checks
5. Reconnect the client and verify state returns to "connected"
6. Test SSE connections specifically to verify connection lost detection

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

## Breaking changes

- [x] No

## Related issues

Improves reliability of MCP client connections and provides better visibility into connection states.

## Security considerations

No significant security implications. The health monitoring system only interacts with already established connections.

## Checklist

- [x] I added/updated tests where appropriate
- [x] I verified builds succeed (Go and UI)
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
framework/configstore/migrations.go (1)

1803-1844: Include MCP fields in ClientConfig hash backfill

In migrationAddAdditionalConfigHashColumns, the ClientConfig struct used to backfill hashes is missing the MCP fields that are now part of GenerateClientConfigHash:

clientConfig := ClientConfig{
  DropExcessRequests:      cc.DropExcessRequests,
  InitialPoolSize:         cc.InitialPoolSize,
  PrometheusLabels:        cc.PrometheusLabels,
  EnableLogging:           cc.EnableLogging,
  DisableContentLogging:   cc.DisableContentLogging,
  LogRetentionDays:        cc.LogRetentionDays,
  EnableGovernance:        cc.EnableGovernance,
  EnforceGovernanceHeader: cc.EnforceGovernanceHeader,
  AllowDirectKeys:         cc.AllowDirectKeys,
  AllowedOrigins:          cc.AllowedOrigins,
  MaxRequestBodySizeMB:    cc.MaxRequestBodySizeMB,
  EnableLiteLLMFallbacks:  cc.EnableLiteLLMFallbacks,
}

Add the three MCP fields that are now hashed:

MCPAgentDepth:           cc.MCPAgentDepth,
MCPToolExecutionTimeout: cc.MCPToolExecutionTimeout,
MCPCodeModeBindingLevel: cc.MCPCodeModeBindingLevel,

Without these, the backfilled hashes won't match the complete configuration state, causing spurious resync mismatches later.

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

139-153: Type assertion could panic on conversation type mismatch.

At line 143, msg.(schemas.ChatMessage) assumes all items in conversation are of type ChatMessage. If a non-ChatMessage type is accidentally added to the conversation slice, this will panic. The same issue exists in responsesAPIAdapter.createNewRequest at line 356.

This was previously flagged. Consider adding a defensive type check with a safe assertion pattern.

core/mcp/toolmanager.go (2)

29-33: Typo in documentation comment.

Line 32 contains a typo: "If not provider" should be "If not provided".

This was previously flagged.


134-145: Outdated parameter documentation.

Line 141 documents availableToolsPerClient as a parameter, but this parameter doesn't exist in the function signature. The function only takes existingTools and integrationUserAgent.

This was previously flagged.

🧹 Nitpick comments (11)
core/chatbot_test.go (1)

531-588: Propagate a request-scoped context into ExecuteChatMCPTool instead of context.Background()

ExecuteChatMCPTool is currently always invoked with context.Background(), so cancellation/timeouts from the outer chat request cannot affect tool execution. If you have (or later add) per-request contexts with deadlines or governance metadata, consider threading that through handleToolCalls and into ExecuteChatMCPTool to avoid orphaned tool runs.

core/schemas/mux.go (2)

402-543: Function-call outputs are now routed via Output instead of Content; verify all call sites expect this

The changes in ChatMessage.ToResponsesMessages for ChatMessageRoleTool:

  • set Type=function_call_output and intentionally omit Role,
  • avoid populating rm.Content for these messages, and
  • populate ResponsesToolMessage.Output.ResponsesToolCallOutputStr from cm.Content.ContentStr.

This matches the Responses API’s function_call_output shape and aligns with the reverse conversion in ToChatMessages, which reconstructs Content from Output.

Just confirm that any existing code reading Responses-style tool outputs is looking at ResponsesToolMessage.Output rather than Content, especially for newly introduced MCP / code-mode paths.

#!/bin/bash
# Look for places that read function_call_output messages and ensure they use .Output, not .Content.
rg -n "function_call_output" core

152-207: ToResponsesToolMessage does drop non-text fields from content blocks for tool outputs

When ToResponsesToolMessage converts ContentBlocks (lines 191–202), it only copies Type and Text, discarding ImageURLStruct, InputAudio, File, and CacheControl. While the underlying ResponsesToolMessageOutputStruct does support structured output via ResponsesFunctionToolCallOutputBlocks, the conversion logic is incomplete compared to the field-preserving logic in ToResponsesMessages for other message types.

In practice, tool outputs in the codebase are created exclusively with ContentStr, never ContentBlocks, so this limitation is not currently manifested. However, documenting that tool outputs are text-only, or aligning the block-conversion logic with the richer handling in ToResponsesMessages, would clarify intent and prevent potential data loss if tool output generation changes.

core/mcp/codemode_listfiles.go (1)

69-118: Handle the rare case where code-mode servers exist but no virtual files are emitted

handleListToolFiles returns helpful messages when no MCP clients are connected or when no clients are configured for code mode. For tool-level binding, it's possible (though unlikely) to have code-mode servers where none of the tools have a non-empty Function.Name, resulting in codeModeServerCount > 0 but an empty files slice. In that case, buildVFSTree returns an empty string and the tool response is blank.

Consider adding a check for len(files) == 0 after the codeModeServerCount check to emit a clearer message like "Code-mode servers are connected but none expose function tools, so there are no virtual .d.ts files."

framework/configstore/clientconfig.go (1)

35-54: Add test coverage for new MCP config fields in hash generation

The hash implementation for MCPAgentDepth, MCPToolExecutionTimeout, and MCPCodeModeBindingLevel is correct and deterministic. All three fields always hash: integer fields hash as their actual value (or 0 if unset), and the string field hashes as its value or defaults to "server" when empty. Config changes from unset to explicit default will correctly produce different hashes.

However, the existing test suite (TestGenerateClientConfigHash and TestGenerateClientConfigHash_RuntimeVsMigrationParity) contains no test cases for these new MCP fields. Add explicit tests to verify hash behavior when these fields change.

core/mcp/codemode_readfile.go (2)

294-431: Property names and enum values in TypeScript generation need careful handling

The generated TypeScript code has two limitations worth addressing:

  1. Property names containing illegal TS identifier characters: Properties like foo-bar or some prop are emitted verbatim at line 399 and will produce syntactically invalid TypeScript. These should be quoted (e.g., "foo-bar": string;).

  2. Enum values always converted to strings: The jsonSchemaToTypeScript function uses fmt.Sprintf("%q", e) for all enum values, converting numeric and boolean enums to quoted strings (e.g., 1 | 2 becomes "1" | "2" instead of preserving their types).

Consider:

  • Quoting property names that contain non-identifier-safe characters
  • Preserving the original types for numeric and boolean enum values

486-503: toPascalCase will corrupt non-ASCII characters due to byte-slicing

toPascalCase uses byte indexing (part[:1], part[1:]) which breaks multi-byte UTF-8 characters. For example, if a tool name like "école" reaches this function, the byte slice corrupts the leading byte, producing invalid UTF-8 output. This is unlikely in practice since MCP tool identifiers conventionally use ASCII, but parseToolName allows non-ASCII characters through via unicode.IsLetter().

For more robust handling of any non-ASCII tool names, switch to rune-based slicing (convert part to runes, slice by rune index, then convert back). Otherwise, this pragmatic choice is acceptable given MCP naming conventions.

core/mcp/clientmanager.go (1)

567-604: Consider potential TOCTOU race in setupLocalHost double-check pattern.

The double-checked locking pattern here could allow two goroutines to both create server/client objects before the second lock acquisition. While the second check prevents duplicate assignment, the first goroutine's created objects would be discarded without explicit cleanup.

This is a minor concern since the created objects are lightweight and Go's GC will reclaim them, but for clarity you might consider holding the lock through creation or using sync.Once.

core/bifrost.go (1)

1879-1895: Thread-safe initialization in AddMCPClient has a subtle race condition.

If Init was called without MCPConfig (so mcpManager is nil and mcpInitOnce is unused), then a concurrent call to AddMCPClient from multiple goroutines could result in only one goroutine initializing the manager while others see mcpManager as nil after sync.Once completes (since only the first caller's config is used).

The current check at lines 1889-1892 handles this, but there's a subtle issue: if mcpInitOnce.Do was already executed by another AddMCPClient call, subsequent callers will skip initialization and then call AddClient on the manager initialized by the first caller, which is correct behavior. However, consider adding a comment to clarify this intentional behavior.

🔎 Suggested clarification
 func (bifrost *Bifrost) AddMCPClient(config schemas.MCPClientConfig) error {
 	if bifrost.mcpManager == nil {
 		// Use sync.Once to ensure thread-safe initialization
+		// Note: Only the first caller's config is used to initialize the manager.
+		// Subsequent callers will add their config via AddClient below.
 		bifrost.mcpInitOnce.Do(func() {
 			bifrost.mcpManager = mcp.NewMCPManager(bifrost.ctx, schemas.MCPConfig{
 				ClientConfigs: []schemas.MCPClientConfig{config},
 			}, bifrost.logger)
 		})
 	}
core/mcp/agent_adaptors.go (2)

219-270: Consider extracting duplicated tool results formatting logic.

The logic for building toolResultsMap and formatting contentText (lines 219-270) is duplicated almost identically in createResponsesResponseWithExecutedToolsAndNonAutoExecutableCalls (lines 455-506). This could be extracted into a shared helper function to reduce code duplication and maintenance burden.

🔎 Suggested helper extraction
// formatToolResultsAsText builds a formatted text representation of executed tool results.
func formatToolResultsAsText(
    executedToolResults []*schemas.ChatMessage,
    toolCallIDToName map[string]string,
) string {
    if len(executedToolResults) == 0 {
        return "Now I shall call these tools next..."
    }
    
    toolResultsMap := make(map[string]interface{})
    for _, toolResult := range executedToolResults {
        // ... extraction logic ...
    }
    
    jsonBytes, err := sonic.Marshal(toolResultsMap)
    if err != nil {
        return fmt.Sprintf("The Output from allowed tools calls is - %v\n\nNow I shall call these tools next...", toolResultsMap)
    }
    return fmt.Sprintf("The Output from allowed tools calls is - %s\n\nNow I shall call these tools next...", string(jsonBytes))
}

112-120: Type assertions in adapter methods assume correct types.

Methods like hasToolCalls, extractToolCalls, and addAssistantMessage use direct type assertions (e.g., response.(*schemas.BifrostChatResponse)) that will panic if the wrong type is passed. Since this is internal code and the adapters are instantiated with matching types, this is acceptable, but consider adding defensive checks for robustness.

🔎 Example defensive pattern
func (c *chatAPIAdapter) hasToolCalls(response interface{}) bool {
    chatResponse, ok := response.(*schemas.BifrostChatResponse)
    if !ok {
        logger.Warn(fmt.Sprintf("hasToolCalls received unexpected type: %T", response))
        return false
    }
    return hasToolCallsForChatResponse(chatResponse)
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e42e8da and bca600b.

⛔ Files ignored due to path filters (2)
  • core/go.sum is excluded by !**/*.sum
  • transports/go.sum is excluded by !**/*.sum
📒 Files selected for processing (43)
  • core/bifrost.go
  • core/changelog.md
  • core/chatbot_test.go
  • core/mcp/agent_adaptors.go
  • core/mcp/agent_test.go
  • core/mcp/clientmanager.go
  • core/mcp/codemode_listfiles.go
  • core/mcp/codemode_readfile.go
  • core/mcp/health_monitor.go
  • core/mcp/mcp.go
  • core/mcp/toolmanager.go
  • core/providers/nebius/nebius.go
  • core/providers/openai/openai.go
  • core/schemas/bifrost.go
  • core/schemas/mcp.go
  • core/schemas/mux.go
  • framework/configstore/clientconfig.go
  • framework/configstore/migrations.go
  • framework/configstore/rdb.go
  • framework/configstore/tables/clientconfig.go
  • framework/configstore/tables/mcp.go
  • plugins/governance/go.mod
  • transports/bifrost-http/handlers/config.go
  • transports/bifrost-http/handlers/inference.go
  • transports/bifrost-http/handlers/mcp.go
  • transports/bifrost-http/handlers/mcp_server.go
  • transports/bifrost-http/lib/config.go
  • transports/bifrost-http/server/server.go
  • transports/changelog.md
  • transports/go.mod
  • ui/app/workspace/config/logging/page.tsx
  • ui/app/workspace/config/mcp-gateway/page.tsx
  • ui/app/workspace/config/views/clientSettingsView.tsx
  • ui/app/workspace/config/views/governanceView.tsx
  • ui/app/workspace/config/views/loggingView.tsx
  • ui/app/workspace/config/views/mcpView.tsx
  • ui/app/workspace/config/views/observabilityView.tsx
  • ui/app/workspace/config/views/performanceTuningView.tsx
  • ui/app/workspace/config/views/securityView.tsx
  • ui/components/sidebar.tsx
  • ui/lib/constants/logs.ts
  • ui/lib/types/config.ts
  • ui/lib/types/schemas.ts
✅ Files skipped from review due to trivial changes (1)
  • core/changelog.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • core/providers/openai/openai.go
  • core/providers/nebius/nebius.go
🧰 Additional context used
📓 Path-based instructions (1)
**

⚙️ CodeRabbit configuration file

always check the stack if there is one for the current PR. do not give localized reviews for the PR, always see all changes in the light of the whole stack of PRs (if there is a stack, if there is no stack you can continue to make localized suggestions/reviews)

Files:

  • core/mcp/clientmanager.go
  • framework/configstore/migrations.go
  • core/mcp/mcp.go
  • core/mcp/agent_adaptors.go
  • core/mcp/codemode_readfile.go
  • core/chatbot_test.go
  • framework/configstore/tables/mcp.go
  • core/mcp/health_monitor.go
  • core/mcp/codemode_listfiles.go
  • core/mcp/agent_test.go
  • core/schemas/mcp.go
  • plugins/governance/go.mod
  • core/schemas/mux.go
  • framework/configstore/rdb.go
  • framework/configstore/tables/clientconfig.go
  • core/mcp/toolmanager.go
  • core/bifrost.go
  • framework/configstore/clientconfig.go
  • core/schemas/bifrost.go
🧠 Learnings (1)
📚 Learning: 2025-12-09T17:07:42.007Z
Learnt from: qwerty-dvorak
Repo: maximhq/bifrost PR: 1006
File: core/schemas/account.go:9-18
Timestamp: 2025-12-09T17:07:42.007Z
Learning: In core/schemas/account.go, the HuggingFaceKeyConfig field within the Key struct is currently unused and reserved for future Hugging Face inference endpoint deployments. Do not flag this field as missing from OpenAPI documentation or require its presence in the API spec until the feature is actively implemented and used. When the feature is added, update the OpenAPI docs accordingly; otherwise, treat this field as non-breaking and not part of the current API surface.

Applied to files:

  • core/mcp/clientmanager.go
  • framework/configstore/migrations.go
  • core/mcp/mcp.go
  • core/mcp/agent_adaptors.go
  • core/mcp/codemode_readfile.go
  • core/chatbot_test.go
  • framework/configstore/tables/mcp.go
  • core/mcp/health_monitor.go
  • core/mcp/codemode_listfiles.go
  • core/mcp/agent_test.go
  • core/schemas/mcp.go
  • core/schemas/mux.go
  • framework/configstore/rdb.go
  • framework/configstore/tables/clientconfig.go
  • core/mcp/toolmanager.go
  • core/bifrost.go
  • framework/configstore/clientconfig.go
  • core/schemas/bifrost.go
🧬 Code graph analysis (10)
core/mcp/clientmanager.go (4)
core/mcp/mcp.go (5)
  • MCPManager (41-49)
  • MCPLogPrefix (23-23)
  • BifrostMCPClientKey (22-22)
  • MCPClientConnectionEstablishTimeout (24-24)
  • BifrostMCPClientName (21-21)
core/mcp.go (6)
  • MCPManager (49-56)
  • MCPLogPrefix (31-31)
  • BifrostMCPClientKey (30-30)
  • MCPClientConnectionInfo (69-73)
  • MCPClientConnectionEstablishTimeout (32-32)
  • BifrostMCPClientName (29-29)
core/schemas/mcp.go (9)
  • MCPClientState (96-104)
  • MCPClientConfig (45-67)
  • MCPClientConnectionInfo (107-111)
  • MCPConnectionTypeHTTP (73-73)
  • MCPConnectionTypeSTDIO (74-74)
  • MCPConnectionTypeSSE (75-75)
  • MCPConnectionTypeInProcess (76-76)
  • MCPConnectionStateConnected (89-89)
  • MCPConnectionStateDisconnected (90-90)
core/mcp/health_monitor.go (2)
  • NewClientHealthMonitor (35-53)
  • DefaultHealthCheckInterval (14-14)
core/mcp/mcp.go (4)
core/mcp/toolmanager.go (2)
  • ToolsManager (22-34)
  • NewToolsManager (53-82)
core/schemas/mcp.go (5)
  • MCPClientState (96-104)
  • MCPConfig (14-23)
  • MCPToolManagerConfig (25-29)
  • DefaultToolExecutionTimeout (33-33)
  • DefaultMaxAgentDepth (32-32)
core/mcp/health_monitor.go (2)
  • HealthMonitorManager (185-188)
  • NewHealthMonitorManager (191-195)
core/mcp/init.go (1)
  • SetLogger (7-9)
core/mcp/agent_adaptors.go (2)
core/schemas/chatcompletions.go (5)
  • ChatAssistantMessageToolCall (719-725)
  • BifrostChatRequest (12-19)
  • BifrostChatResponse (27-42)
  • BifrostResponseChoice (745-753)
  • ChatAssistantMessage (658-665)
core/schemas/responses.go (5)
  • BifrostResponsesRequest (32-39)
  • BifrostResponsesResponse (45-85)
  • ResponsesMessageTypeMessage (297-297)
  • ResponsesInputMessageRoleAssistant (339-339)
  • ResponsesMessageTypeFunctionCall (302-302)
framework/configstore/tables/mcp.go (2)
core/schemas/mcp.go (1)
  • MCPStdioConfig (80-84)
ui/lib/types/mcp.ts (1)
  • MCPStdioConfig (7-11)
core/mcp/health_monitor.go (2)
core/mcp/mcp.go (2)
  • MCPManager (41-49)
  • MCPLogPrefix (23-23)
core/schemas/mcp.go (3)
  • MCPConnectionStateDisconnected (90-90)
  • MCPConnectionStateConnected (89-89)
  • MCPConnectionState (86-86)
core/mcp/agent_test.go (6)
core/schemas/chatcompletions.go (4)
  • BifrostChatResponse (27-42)
  • BifrostChatRequest (12-19)
  • ChatTool (261-266)
  • BifrostResponseChoice (745-753)
core/schemas/responses.go (7)
  • BifrostResponsesResponse (45-85)
  • BifrostResponsesRequest (32-39)
  • ResponsesMessageTypeFunctionCall (302-302)
  • ResponsesMessageTypeMessage (297-297)
  • ResponsesInputMessageRoleAssistant (339-339)
  • ResponsesInputMessageRoleUser (340-340)
  • ResponsesMessageTypeFunctionCallOutput (303-303)
core/schemas/bifrost.go (4)
  • BifrostError (482-491)
  • ErrorField (500-507)
  • ResponsesRequest (96-96)
  • OpenAI (35-35)
core/schemas/utils.go (1)
  • Ptr (16-18)
transports/bifrost-http/server/server.go (1)
  • SetLogger (116-118)
core/mcp/init.go (1)
  • SetLogger (7-9)
core/schemas/mcp.go (2)
ui/lib/types/mcp.ts (3)
  • MCPConnectionType (3-3)
  • MCPStdioConfig (7-11)
  • MCPConnectionState (5-5)
core/mcp.go (1)
  • MCPClientConnectionInfo (69-73)
core/schemas/mux.go (3)
core/schemas/responses.go (6)
  • ResponsesToolMessage (472-492)
  • ResponsesMessage (321-334)
  • ResponsesMessageTypeFunctionCallOutput (303-303)
  • ResponsesToolMessageOutputStruct (541-545)
  • ResponsesMessageContentBlock (406-421)
  • ResponsesMessageContent (346-351)
core/schemas/chatcompletions.go (4)
  • ChatAssistantMessageToolCall (719-725)
  • ChatAssistantMessageToolCallFunction (728-731)
  • ChatMessage (487-496)
  • ChatToolMessage (653-655)
core/schemas/utils.go (1)
  • Ptr (16-18)
core/mcp/toolmanager.go (7)
core/schemas/mcp.go (4)
  • MCPToolManagerConfig (25-29)
  • DefaultToolExecutionTimeout (33-33)
  • CodeModeBindingLevel (37-37)
  • CodeModeBindingLevelServer (40-40)
core/schemas/chatcompletions.go (6)
  • ChatTool (261-266)
  • ChatParameters (156-186)
  • ChatToolFunction (269-274)
  • ChatAssistantMessageToolCall (719-725)
  • BifrostChatRequest (12-19)
  • BifrostChatResponse (27-42)
core/mcp/mcp.go (1)
  • MCPLogPrefix (23-23)
core/mcp.go (1)
  • MCPLogPrefix (31-31)
core/schemas/bifrost.go (1)
  • BifrostError (482-491)
core/schemas/responses.go (1)
  • ResponsesParameters (87-114)
core/mcp/agent.go (1)
  • ExecuteAgentForChatRequest (30-64)
core/bifrost.go (3)
core/mcp/mcp.go (2)
  • MCPManager (41-49)
  • NewMCPManager (68-94)
core/mcp.go (2)
  • MCPManager (49-56)
  • MCPClient (59-66)
core/schemas/mcp.go (4)
  • MCPConfig (14-23)
  • MCPClient (116-120)
  • MCPClientConfig (45-67)
  • MCPToolManagerConfig (25-29)
🪛 GitHub Actions: PR #1153
framework/configstore/rdb.go

[error] 742-742: unknown field IsCodeModeClient in struct literal of type schemas.MCPClientConfig

🔇 Additional comments (48)
plugins/governance/go.mod (1)

10-10: ✓ Dependency additions look good — previous duplicate issue resolved.

The critical duplicate dependency issue from the previous review has been fixed. The new dependency additions (testify and its transitive dependencies go-spew and go-difflib) are standard testing utilities appropriate for the governance plugin, with versions properly pinned to specific commits.

Also applies to: 42-42, 85-85

core/schemas/bifrost.go (2)

120-143: Context key additions and “DO NOT SET” comments look consistent

The new BifrostMCPAgentOriginalRequestID context key and the strengthened comments for internal-only keys align with the existing context-key conventions and should help prevent misuse from plugins or callers.


476-491: No action needed. BifrostError.ExtraFields already uses json:"extra_fields" (without omitempty), consistent with all other response struct types throughout the codebase (BifrostChatResponse, BifrostTextCompletionResponse, BifrostResponsesResponse, etc.). The test in plugins/governance/e2e_test.go (lines 1406–1407) directly accesses extra_fields from the error JSON, confirming the field is expected to be always present. This is not a behavioral change—it's the established pattern.

framework/configstore/migrations.go (2)

1092-1158: New MCP client columns migrations look safe

The migrations that add tools_to_auto_execute_json and is_code_mode_client to the config_mcp_clients table properly:

  • Check for column existence before adding
  • Initialize existing rows with sensible defaults ('[]' for tools_to_auto_execute_json, false for is_code_mode_client)
  • Provide rollback support
  • Follow idempotent patterns suitable for both SQLite and PostgreSQL

1351-1482: MCP client name normalization is necessary and correctly implemented

The migration addresses a backward-compatibility issue: current validation (validateMCPClientName) rejects names with hyphens and leading digits, but existing configs may have them. The normalization logic:

  • Converts hyphens/spaces to underscores and strips leading digits, making old configs valid
  • Leaves already-safe names unchanged (no unnecessary modifications)
  • Handles collisions robustly using an in-memory assignedNames map plus DB checks
  • Applies numeric suffixes (_2, _3, …) only to normalized names that collide

The in-memory map combined with database lookups ensures correctness during the transaction, preventing race conditions and accurately detecting duplicates across the migration. The GetMCPClientByName lookups and tool execution paths that depend on the name field will work correctly after normalization.

core/schemas/mux.go (1)

116-151: Tool-call conversion from Responses → Chat is correct; arguments defaulting is sensible

ResponsesToolMessage.ToChatAssistantMessageToolCall correctly maps CallID, Name, and Arguments into a ChatAssistantMessageToolCall, and defaulting Arguments to "{}" ensures JSON unmarshaling won't blow up when the field is absent. The method is used exclusively for generic function-style tool execution in ExecuteResponsesTool(), with comprehensive test coverage including round-trip conversion validation.

framework/configstore/tables/mcp.go (3)

13-35: LGTM! New MCP client fields properly integrated.

The new IsCodeModeClient and ToolsToAutoExecute/ToolsToAutoExecuteJSON fields follow the established patterns for JSON serialization in this table. The struct correctly separates persisted JSON columns from runtime fields using the gorm:"-" tag.


63-71: Serialization logic for ToolsToAutoExecute is correct.

The implementation properly handles both nil and non-nil cases, defaulting to "[]" when nil to ensure valid JSON is always stored. This matches the existing ToolsToExecute pattern.


102-106: Deserialization logic correctly mirrors the serialization.

The AfterFind hook properly deserializes ToolsToAutoExecuteJSON back to the runtime slice, consistent with the existing ToolsToExecuteJSON handling.

core/mcp/clientmanager.go (6)

21-36: LGTM! GetClients creates proper defensive copies.

Using maps.Copy to create a shallow copy of the ToolMap prevents data races when callers iterate over returned clients while the manager modifies internal state.


47-64: ReconnectClient correctly handles lock/unlock pattern for external calls.

The method properly releases the lock before calling connectToMCPClient (which handles its own locking), avoiding potential deadlocks. The config is copied before releasing the lock.


75-110: AddClient implements proper placeholder pattern with cleanup on failure.

The two-phase approach (create placeholder → unlock → connect → cleanup on failure) prevents holding locks during potentially slow network operations while ensuring consistency.


134-165: removeClientUnsafe properly cleans up all resources.

The method correctly stops health monitoring, cancels SSE contexts, closes connections, and clears the tool map before removing from the client map. Logging errors without failing allows cleanup to continue.


231-288: RegisterTool validates tool naming and prevents overwrites.

The validation for hyphens, spaces, and numeric prefixes enforces naming conventions. Checking for existing tools prevents silent overwrites which could cause confusion.


296-452: connectToMCPClient uses proper two-phase locking pattern.

The method correctly:

  1. Acquires lock to initialize/validate client entry
  2. Releases lock for heavy I/O operations (transport creation, initialization)
  3. Re-acquires lock to update final state
  4. Handles cleanup if client was removed during connection setup
  5. Registers SSE connection-lost callback for state updates
framework/configstore/rdb.go (6)

45-47: LGTM! New MCP tool manager config fields properly persisted.

The MCPAgentDepth, MCPToolExecutionTimeout, and MCPCodeModeBindingLevel fields are correctly mapped in both UpdateClientConfig and GetClientConfig.


739-773: GetMCPConfig properly constructs ToolManagerConfig with defaults.

The method correctly handles the case where no client config exists by providing sensible defaults (30s timeout, depth 10). The duration conversion from seconds stored in DB to time.Duration is correct.


803-813: CreateMCPClientConfig correctly handles new fields.

The IsCodeModeClient and ToolsToAutoExecute fields are properly included in the database record creation.


846-854: UpdateMCPClientConfig uses Select to handle zero-value boolean.

Good use of Select() to explicitly include is_code_mode_client even when false, since GORM's Updates() skips zero values by default.


1397-1470: DeleteVirtualKey implements thorough cascading cleanup.

The method properly:

  1. Collects budget/rate limit IDs from provider configs
  2. Deletes join table entries
  3. Deletes provider configs before their associated budgets/rate limits
  4. Deletes MCP configs
  5. Deletes the virtual key itself
  6. Finally deletes the key's own budget/rate limit

This ordering respects foreign key dependencies.


2180-2205: RetryOnNotFound is a useful resilience pattern.

The implementation correctly:

  • Respects context cancellation
  • Only retries on ErrNotFound/gorm.ErrRecordNotFound
  • Avoids sleeping after the last attempt
  • Returns the last error on exhaustion
core/mcp/agent_test.go (6)

11-47: MockLLMCaller properly implements call tracking for tests.

The mock correctly maintains separate counters for chat and responses APIs, and returns an error when responses are exhausted. This enables tests to verify expected call counts.


75-149: TestHasToolCallsForChatResponse covers key edge cases.

The test correctly validates:

  • Nil response handling
  • Empty choices
  • tool_calls finish reason detection
  • Tool calls presence in message
  • stop finish reason should return false even with tool calls present

201-248: TestExecuteAgentForChatRequest validates no-op path.

The test confirms that responses without tool calls are returned unchanged without additional LLM calls, which is the expected behavior.


250-318: TestExecuteAgentForChatRequest_WithNonAutoExecutableTools validates early return.

Good test case verifying that non-auto-executable tools don't trigger additional LLM calls and are returned to the caller for handling.


487-538: Conversion tests verify round-trip integrity.

The tests for ToChatAssistantMessageToolCall and ToResponsesToolMessage properly validate that data survives format conversions, including edge cases like nil arguments defaulting to "{}".


613-681: TestExecuteAgentForResponsesRequest_ConversionRoundTrip is thorough.

This end-to-end conversion test validates that tool call IDs, names, arguments, and result outputs are preserved through the full Responses → Chat → Responses conversion cycle.

core/mcp/health_monitor.go (5)

12-17: Health check constants are reasonable defaults.

10-second interval, 5-second timeout, and 5 consecutive failures before marking unhealthy provide a good balance between responsiveness and tolerance for transient issues.


55-70: Start method properly initializes monitoring.

The method correctly checks if already monitoring, creates a cancellable context, initializes the ticker, and launches the background goroutine.


103-139: performHealthCheck has correct failure counting logic.

The method properly:

  1. Stops monitoring if client no longer exists
  2. Marks disconnected immediately if connection is nil
  3. Uses timeout context for ping
  4. Only marks disconnected after reaching max consecutive failures
  5. Resets failure count on successful ping

141-161: updateClientState avoids redundant state change logs.

Good optimization to only log when state actually changes, reducing log noise during normal operation.


197-220: HealthMonitorManager correctly handles monitor lifecycle.

StartMonitoring properly stops any existing monitor before starting a new one for the same client, preventing resource leaks. StopMonitoring cleans up and removes from the map.

core/schemas/mcp.go (4)

14-23: MCPConfig properly extended with tool manager config and request ID hook.

The ToolManagerConfig field enables runtime configuration of tool execution parameters. The FetchNewRequestIDFunc hook allows tracking individual tool call results in agent mode, which is useful for observability and plugin integration.


25-42: MCPToolManagerConfig and CodeModeBindingLevel provide clear configuration options.

The struct captures essential tool execution parameters. The CodeModeBindingLevel enum with "server" and "tool" values provides flexibility in how tools are exposed in the VFS for code execution.


44-67: MCPClientConfig enhancements are well-documented.

The added IsCodeModeClient, ToolsToAutoExecute, and updated InProcessServer fields extend the client configuration capabilities. The semantics comments for ToolsToAutoExecute clearly explain the wildcard, empty, and specific tool behaviors.


94-111: MCPClientState and MCPClientConnectionInfo provide complete client tracking.

These types properly capture:

  • Connection state and active client reference
  • Tool filtering configuration
  • Available tools mapping
  • Connection metadata for management UI
  • Cancel function for SSE cleanup (correctly excluded from JSON)
core/mcp/mcp.go (5)

18-32: Constants provide clear configuration and context key definitions.

The versioning, naming, and timeout constants establish consistent behavior. The context keys with documented semantics enable per-request filtering of clients and tools.


38-49: MCPManager struct properly encapsulates MCP integration state.

The struct includes all necessary components: context, tools manager, local server, client map with mutex protection, and health monitor manager. The separation of concerns is clear.


68-94: NewMCPManager properly initializes with defaults and error handling.

The constructor:

  1. Sets logger for the package
  2. Applies default tool manager config if not provided
  3. Initializes client map and health monitor manager
  4. Creates tools manager with config
  5. Adds configured clients (logging warnings on failure without blocking)

180-201: CheckAndExecuteAgentForChatRequest validates input and delegates appropriately.

The method correctly:

  1. Validates that makeReq is provided
  2. Short-circuits if no tool calls are present
  3. Delegates to the tools manager for actual agent execution

261-288: Cleanup method is thorough and handles all resource types.

The cleanup properly:

  1. Stops all health monitors first
  2. Acquires lock for client cleanup
  3. Removes all clients (which handles SSE cancel, connection close)
  4. Clears client map
  5. Clears local server reference

The order ensures health monitors don't try to access clients being cleaned up.

core/bifrost.go (1)

183-188: LGTM - Thread-safe MCP initialization.

The sync.Once pattern correctly ensures the MCP manager is initialized only once, even if called concurrently. The initialization is properly guarded and logs success.

core/mcp/agent_adaptors.go (1)

25-64: Well-designed adapter interface for API-agnostic agent execution.

The agentAPIAdapter interface cleanly abstracts the differences between Chat and Responses APIs, enabling the agent loop to remain format-neutral. The documentation clearly explains the conversion boundaries and design rationale.

core/mcp/toolmanager.go (5)

386-398: Good timeout handling for tool execution.

The code correctly creates a timeout context for tool execution and distinguishes between timeout errors (context.DeadlineExceeded) and other execution errors. The defer pattern ensures the context is cancelled properly.


84-132: LGTM - Tool aggregation with proper deduplication.

GetAvailableTools correctly aggregates tools from all clients, deduplicates by name, and conditionally adds code-mode tools when a code-mode client is present. The seenToolNames map ensures no duplicate tools are included.


338-356: Tool permission validation ensures security.

The code correctly validates that the requested tool is available and permitted before execution. The nested loop could be optimized to O(1) with a pre-built map, but this is acceptable for typical tool counts.


481-497: Agent execution wires dependencies correctly.

The ExecuteAgentForChatRequest method properly delegates to the shared agent execution logic, passing the manager's configuration (maxAgentDepth), request ID function, tool executor, and client manager. The pointer-to-context pattern was addressed in previous commits.


530-557: Thread-safe configuration management.

The UpdateConfig method correctly uses atomic operations for thread-safe updates, and GetCodeModeBindingLevel includes a nil check for robustness. The logging provides good observability for configuration changes.

Comment on lines +592 to +599
if bifrost.mcpManager != nil {
return bifrost.mcpManager.CheckAndExecuteAgentForChatRequest(
&ctx,
req,
response,
bifrost.makeChatCompletionRequest,
)
}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Examine the code at lines 592-599 in core/bifrost.go
sed -n '585,605p' core/bifrost.go

Repository: maximhq/bifrost

Length of output: 655


🏁 Script executed:

# Find the CheckAndExecuteAgentForChatRequest function definition
rg -A 15 "func.*CheckAndExecuteAgentForChatRequest" --type go

Repository: maximhq/bifrost

Length of output: 925


🏁 Script executed:

# Check the full context of the bifrost.mcpManager calls
sed -n '585,605p' core/bifrost.go
sed -n '682,702p' core/bifrost.go

Repository: maximhq/bifrost

Length of output: 1252


🏁 Script executed:

# Check how the ctx pointer parameter is used inside CheckAndExecuteAgentForChatRequest
rg -A 50 "func.*CheckAndExecuteAgentForChatRequest" core/mcp/mcp.go | head -80

Repository: maximhq/bifrost

Length of output: 2423


🏁 Script executed:

# Look for context dereferences (*ctx) in the mcp.go file
rg "\*ctx" core/mcp/mcp.go | head -20

Repository: maximhq/bifrost

Length of output: 41


🏁 Script executed:

# Check if context is modified (WithValue, WithCancel, etc.) in this function
rg "context\.With" core/mcp/mcp.go

Repository: maximhq/bifrost

Length of output: 41


🏁 Script executed:

# Check the ExecuteAgentForChatRequest signature to see what parameter type it expects
rg -B 2 -A 15 "func.*ExecuteAgentForChatRequest" core/mcp/ --type go

Repository: maximhq/bifrost

Length of output: 5543


🏁 Script executed:

# Check if the toolsManager methods dereference or modify context
rg "\*ctx|context\.With" core/mcp/tools_manager.go 2>/dev/null || echo "File not found, searching recursively"
rg "\*ctx|context\.With" core/mcp/ --type go

Repository: maximhq/bifrost

Length of output: 1497


Remove pointer-to-context pattern; it is an anti-pattern that does not actually propagate updates back to the caller.

The code passes &ctx to CheckAndExecuteAgentForChatRequest, which dereferences it as *ctx = context.WithValue(...) in core/mcp/agent.go. However, context.WithValue() returns a new context instance, not a modification of the existing one. Reassigning via the pointer only affects the dereferenced value within the function's call chain—it does not propagate changes back to the original caller's context variable. This pattern creates a false impression of context sharing and violates Go's immutable context semantics.

Refactor to use standard context patterns: pass ctx by value and return the modified context, or use a context that is already initialized with the necessary values before being passed.

🤖 Prompt for AI Agents
In core/bifrost.go around lines 592 to 599, the code is passing &ctx (a
pointer-to-context) into CheckAndExecuteAgentForChatRequest which is an
anti-pattern because context.WithValue returns a new context and changes won't
propagate back via pointer; instead change the call to pass ctx by value and
update CheckAndExecuteAgentForChatRequest to return the modified context (or
require the context to be pre-initialized) so the caller can capture the
returned context, remove the pointer parameter from the function signature and
all call sites, and update the implementation to accept and return
context.Context (or modify callers to set up context values before calling).

Comment on lines +88 to +200
func (m *ToolsManager) handleReadToolFile(ctx context.Context, toolCall schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, error) {
// Parse tool arguments
var arguments map[string]interface{}
if err := json.Unmarshal([]byte(toolCall.Function.Arguments), &arguments); err != nil {
return nil, fmt.Errorf("failed to parse tool arguments: %v", err)
}

fileName, ok := arguments["fileName"].(string)
if !ok || fileName == "" {
return nil, fmt.Errorf("fileName parameter is required and must be a string")
}

// Parse the file path to extract server name and optional tool name
serverName, toolName, isToolLevel := parseVFSFilePath(fileName)

// Get available tools per client
availableToolsPerClient := m.clientManager.GetToolPerClient(ctx)

// Find matching client
var matchedClientName string
var matchedTools []schemas.ChatTool
matchCount := 0

for clientName, tools := range availableToolsPerClient {
client := m.clientManager.GetClientByName(clientName)
if client == nil {
logger.Warn(fmt.Sprintf("%s Client %s not found, skipping", MCPLogPrefix, clientName))
continue
}
if !client.ExecutionConfig.IsCodeModeClient || len(tools) == 0 {
continue
}

clientNameLower := strings.ToLower(clientName)
serverNameLower := strings.ToLower(serverName)

if clientNameLower == serverNameLower {
matchCount++
if matchCount > 1 {
// Multiple matches found
errorMsg := fmt.Sprintf("Multiple servers match filename '%s':\n", fileName)
for name := range availableToolsPerClient {
if strings.ToLower(name) == serverNameLower {
errorMsg += fmt.Sprintf(" - %s\n", name)
}
}
errorMsg += "\nPlease use a more specific filename. Use the exact display name from listToolFiles to avoid ambiguity."
return createToolResponseMessage(toolCall, errorMsg), nil
}

matchedClientName = clientName

if isToolLevel {
// Tool-level: filter to specific tool
var foundTool *schemas.ChatTool
toolNameLower := strings.ToLower(toolName)
for i, tool := range tools {
if tool.Function != nil && strings.ToLower(tool.Function.Name) == toolNameLower {
foundTool = &tools[i]
break
}
}

if foundTool == nil {
availableTools := make([]string, 0)
for _, tool := range tools {
if tool.Function != nil {
availableTools = append(availableTools, tool.Function.Name)
}
}
errorMsg := fmt.Sprintf("Tool '%s' not found in server '%s'. Available tools in this server are:\n", toolName, clientName)
for _, t := range availableTools {
errorMsg += fmt.Sprintf(" - %s/%s.d.ts\n", clientName, t)
}
return createToolResponseMessage(toolCall, errorMsg), nil
}

matchedTools = []schemas.ChatTool{*foundTool}
} else {
// Server-level: use all tools
matchedTools = tools
}
}
}

if matchedClientName == "" {
// Build helpful error message with available files
bindingLevel := m.GetCodeModeBindingLevel()
var availableFiles []string

for name := range availableToolsPerClient {
if bindingLevel == schemas.CodeModeBindingLevelServer {
availableFiles = append(availableFiles, fmt.Sprintf("%s.d.ts", name))
} else {
client := m.clientManager.GetClientByName(name)
if client != nil && client.ExecutionConfig.IsCodeModeClient {
if tools, ok := availableToolsPerClient[name]; ok {
for _, tool := range tools {
if tool.Function != nil {
availableFiles = append(availableFiles, fmt.Sprintf("%s/%s.d.ts", name, tool.Function.Name))
}
}
}
}
}
}

errorMsg := fmt.Sprintf("No server found matching '%s'. Available virtual files are:\n", serverName)
for _, f := range availableFiles {
errorMsg += fmt.Sprintf(" - %s\n", f)
}
return createToolResponseMessage(toolCall, errorMsg), nil
}
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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and get its size
wc -l core/mcp/codemode_readfile.go

Repository: maximhq/bifrost

Length of output: 174


🏁 Script executed:

# Read the section around lines 228-233 where the error message is mentioned
sed -n '220,240p' core/mcp/codemode_readfile.go

Repository: maximhq/bifrost

Length of output: 174


🏁 Script executed:

# Search for the handleReadToolFile function and see the full error handling
sed -n '88,280p' core/mcp/codemode_readfile.go

Repository: maximhq/bifrost

Length of output: 6694


🏁 Script executed:

# Find where available files are listed in the function
rg -n "availableFiles" core/mcp/codemode_readfile.go -A 3 -B 3

Repository: maximhq/bifrost

Length of output: 1132


🏁 Script executed:

# Check listToolFiles implementation to see how it formats file paths
rg -n "listToolFiles" core/mcp/ -A 10 | head -100

Repository: maximhq/bifrost

Length of output: 9002


Pointer format and missing servers/ prefix in error messages

Two UX issues worth tightening up:

  1. endLine pointer prints as memory address instead of value

At line 228, the error message formats endLine with %v where it's a *int pointer, so it will display the pointer address (e.g., 0xc0001234) rather than the numeric value. Dereference it or format as a friendly placeholder like "<unset>" when nil.

  1. Available-files list omits servers/ prefix documented in tool descriptions

When no server matches (lines 180, 187), the available files are listed as calculator.d.ts or calculator/add.d.ts, but the tool descriptions (lines 25–27, 36–38) specify the format should be servers/<serverName>.d.ts or servers/<serverName>/<toolName>.d.ts. Include the prefix in error messages so suggested filenames match what the model sees from listToolFiles.

🤖 Prompt for AI Agents
core/mcp/codemode_readfile.go lines ~88-200: The error messages format an *int
pointer endLine using %v (showing address) and build available file names
without the required "servers/" prefix; fix by dereferencing endLine when
non-nil (format its int value) and use a friendly placeholder like "<unset>"
when nil, and prepend "servers/" to all suggested filenames (both server-level
and tool-level entries) so the output matches the documented
"servers/<serverName>.d.ts" and "servers/<serverName>/<toolName>.d.ts" formats.

Comment on lines 16 to +27
InitialPoolSize int `gorm:"default:300" json:"initial_pool_size"`
EnableLogging bool `gorm:"" json:"enable_logging"`
DisableContentLogging bool `gorm:"default:false" json:"disable_content_logging"` // DisableContentLogging controls whether sensitive content (inputs, outputs, embeddings, etc.) is logged
LogRetentionDays int `gorm:"default:365" json:"log_retention_days" validate:"min=1"` // Number of days to retain logs (minimum 1 day)
DisableContentLogging bool `gorm:"default:false" json:"disable_content_logging"` // DisableContentLogging controls whether sensitive content (inputs, outputs, embeddings, etc.) is logged
LogRetentionDays int `gorm:"default:365" json:"log_retention_days" validate:"min=1"` // Number of days to retain logs (minimum 1 day)
EnableGovernance bool `gorm:"" json:"enable_governance"`
EnforceGovernanceHeader bool `gorm:"" json:"enforce_governance_header"`
AllowDirectKeys bool `gorm:"" json:"allow_direct_keys"`
MaxRequestBodySizeMB int `gorm:"default:100" json:"max_request_body_size_mb"`
MCPAgentDepth int `gorm:"default:10" json:"mcp_agent_depth"`
MCPToolExecutionTimeout int `gorm:"default:30" json:"mcp_tool_execution_timeout"` // Timeout for individual tool execution in seconds (default: 30)
MCPCodeModeBindingLevel string `gorm:"default:server" json:"mcp_code_mode_binding_level"` // How tools are exposed in VFS: "server" or "tool"

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the full clientconfig.go file to understand the structure
cat -n framework/configstore/tables/clientconfig.go

Repository: maximhq/bifrost

Length of output: 4023


🏁 Script executed:

# Search for TableClientConfig usages
rg -n "TableClientConfig" --type go -A 3 -B 1 | head -100

Repository: maximhq/bifrost

Length of output: 6593


🏁 Script executed:

# Search for MCP field migrations
rg -n "MCPAgentDepth|MCPToolExecutionTimeout|MCPCodeModeBindingLevel" --type go -B 3 -A 3

Repository: maximhq/bifrost

Length of output: 15687


🏁 Script executed:

# Look at the actual migration implementation for MCP fields
sed -n '1283,1340p' framework/configstore/migrations.go

Repository: maximhq/bifrost

Length of output: 2325


🏁 Script executed:

# Search for UPDATE statements related to MCP fields in migrations
rg -n "UPDATE|backfill" framework/configstore/migrations.go | grep -i mcp

Repository: maximhq/bifrost

Length of output: 460


🏁 Script executed:

# Check how AddColumn handles defaults and look for any SQL DEFAULT statements
rg -n "AddColumn" framework/configstore/migrations.go -A 2 | grep -E "(AddColumn|default|DEFAULT)" | head -30

Repository: maximhq/bifrost

Length of output: 3059


🏁 Script executed:

# Check the runtime code that handles zero values in detail
sed -n '230,240p' transports/bifrost-http/lib/config.go

Repository: maximhq/bifrost

Length of output: 448


🏁 Script executed:

# Search for documentation or examples of how GORM defaults work with AddColumn
rg -n "gorm.*default" framework/configstore/tables/clientconfig.go | head -15

Repository: maximhq/bifrost

Length of output: 1169


🏁 Script executed:

# Look for where the fetched config is used in the MCP tool manager setup
rg -n "ToolExecutionTimeout|MaxAgentDepth|CodeModeBindingLevel" framework/configstore/rdb.go -B 2 -A 2

Repository: maximhq/bifrost

Length of output: 1286


🏁 Script executed:

# Look at the full context around where the toolManagerConfig is created
sed -n '755,775p' framework/configstore/rdb.go

Repository: maximhq/bifrost

Length of output: 829


🏁 Script executed:

# Look for where values might be validated or adjusted after reading from DB
rg -n "if.*MCPAgentDepth.*== 0|if.*MCPToolExecutionTimeout.*== 0|if.*MCPCodeModeBindingLevel.*==" --type go

Repository: maximhq/bifrost

Length of output: 487


🏁 Script executed:

# Look for any validation or default application when using the fetched config
sed -n '745,780p' framework/configstore/rdb.go

Repository: maximhq/bifrost

Length of output: 1585


Add backfill or explicit runtime defaults for MCP fields in existing database rows

The GORM defaults (default:10, default:30, default:server) apply only to new rows created after the migration. Existing rows from before this PR will have zero values (NULL/0/empty string) for MCPAgentDepth, MCPToolExecutionTimeout, and MCPCodeModeBindingLevel.

The code path in rdb.GetMCPConfig() (lines 767-769) uses these values directly without checking for zero values:

ToolExecutionTimeout: time.Duration(clientConfig.MCPToolExecutionTimeout) * time.Second,
MaxAgentDepth:        clientConfig.MCPAgentDepth,
CodeModeBindingLevel: schemas.CodeModeBindingLevel(clientConfig.MCPCodeModeBindingLevel),

While the config sync logic in transports/bifrost-http/lib/config.go (lines 507-515) does check for zero values and applies defaults, this is not guaranteed to execute in all scenarios. Either:

  • Add an explicit backfill in the migration to set these columns on existing rows, or
  • Add zero-value handling in rdb.GetMCPConfig() similar to the config sync logic
🤖 Prompt for AI Agents
In framework/configstore/tables/clientconfig.go around lines 16–27, the GORM tag
defaults for MCPAgentDepth, MCPToolExecutionTimeout and MCPCodeModeBindingLevel
only affect new rows; existing DB rows will be zero/empty and rdb.GetMCPConfig()
reads these values directly. Fix by either (A) adding a migration/backfill that
updates existing rows to set MCPAgentDepth=10, MCPToolExecutionTimeout=30,
MCPCodeModeBindingLevel='server' (update NULL/0/'' values), or (B) add defensive
zero-value handling in rdb.GetMCPConfig() to replace zero/empty values with the
defaults (AgentDepth=10, ToolExecutionTimeout=30s,
CodeModeBindingLevel="server") before returning the config. Ensure types are
converted (int -> time.Duration seconds) when applying the timeout default.

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.

3 participants