Skip to content

feat: plugin schema extension for mcp plugins#1378

Merged
akshaydeo merged 7 commits intomainfrom
01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins
Jan 29, 2026
Merged

feat: plugin schema extension for mcp plugins#1378
akshaydeo merged 7 commits intomainfrom
01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins

Conversation

@Pratham-Mishra04
Copy link
Collaborator

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.

# 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
Collaborator Author

Pratham-Mishra04 commented Jan 19, 2026

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

Important

Review skipped

Too many files!

This PR contains 238 files, which is 38 over the limit of 200.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
📝 Walkthrough

Walkthrough

Introduces per-type plugin separation (LLMPlugin, MCPPlugin) replacing unified Plugin interface, adds MCP tool execution APIs with request/response wrapping, integrates MCP manager with plugin hooks, expands logging for MCP tools, and refactors plugin system with BasePlugin foundation. Updates terminology from PreHook/PostHook to PreLLMHook/PostLLMHook for LLM and adds PreMCPHook/PostMCPHook for MCP flows.

Changes

Cohort / File(s) Summary
Core Plugin Architecture & Types
core/schemas/plugin.go, core/schemas/plugin_native.go, core/schemas/plugin_wasm.go, framework/plugins/loader.go, framework/plugins/main.go, framework/plugins/soloader.go, framework/plugins/soplugin.go
Replaced unified Plugin interface with BasePlugin, LLMPlugin, MCPPlugin, HTTPTransportPlugin, and ObservabilityPlugin. Added PluginType enum and LLMPluginShortCircuit/MCPPluginShortCircuit types. Updated plugin loader to return BasePlugin and added VerifyBasePlugin.
Bifrost Core MCP Integration
core/bifrost.go, core/schemas/bifrost.go, core/schemas/mcp.go
Added ExecuteChatMCPTool, ExecuteResponsesMCPTool APIs. Introduced llmPlugins/mcpPlugins fields, BifrostMCPRequest/BifrostMCPResponse types. Added RunLLMPreHooks/RunMCPPreHooks/RunLLMPostHooks/RunMCPPostHooks. Per-type plugin pipeline configuration and MCP tool execution hooks.
MCP Manager & Tool Execution
core/mcp/agent.go, core/mcp/mcp.go, core/mcp/toolmanager.go, core/mcp/clientmanager.go, core/mcp/codemodeexecutecode.go, core/mcp/codemodereadfile.go, core/mcp/utils.go
Updated ExecuteAgentForChatRequest/ExecuteAgentForResponsesRequest to use BifrostMCPRequest/Response callbacks. Added ExecuteToolCall on MCPManager. Tool name normalization (dash-based formatting). Enhanced code-mode execution with plugin pipeline support. Updated tool filtering and discovery.
Governance & Logging Plugins
plugins/governance/main.go, plugins/governance/resolver.go, plugins/governance/utils.go, plugins/logging/main.go, plugins/logging/utils.go, plugins/logging/operations.go
Added PreMCPHook/PostMCPHook to governance. Updated PreHook→PreLLMHook and PostHook→PostLLMHook. Added mcpCatalog dependency for MCP cost tracking. Extended LoggerPlugin with MCP tool log callbacks. Added LogStore methods for MCP tool logs.
Catalog & Framework Storage
framework/mcpcatalog/main.go, framework/logstore/migrations.go, framework/logstore/rdb.go, framework/logstore/store.go, framework/logstore/tables.go, framework/configstore/migrations.go, framework/configstore/rdb.go, framework/configstore/tables/mcp.go
Introduced MCPCatalog for pricing management. Added MCPToolLog table and search infrastructure. Extended ConfigStore/LogStore with MCP-specific CRUD and migration methods. Added tool_pricing_json column and normalization migrations.
Plugin Examples (LLM/MCP/HTTP/Multi-interface)
examples/plugins/llm-only/*, examples/plugins/mcp-only/*, examples/plugins/http-transport-only/*, examples/plugins/multi-interface/*
Added four new plugin examples demonstrating specialized plugin types. Each includes Makefile, README, go.mod, and main.go implementing appropriate hooks (PreLLMHook/PostLLMHook for LLM, PreMCPHook/PostMCPHook for MCP, HTTPTransportPreHook/PostHook for HTTP, multi-interface combining all).
Test Infrastructure & Package Rename
core/internal/llmtests/* (renamed from testutil), core/internal/mcptests/*, core/chatbot_test.go
Renamed all files in core/internal/llmtests from package testutil to llmtests. Removed chatbot_test.go. Added comprehensive MCP test suite with 15+ test files covering agent modes, code-mode, filtering, concurrency, error handling, and state transitions.
Documentation & API Specs
docs/architecture/core/plugins.mdx, docs/architecture/framework/streaming.mdx, docs/plugins/getting-started.mdx, docs/plugins/writing-go-plugin.mdx, docs/openapi/openapi.json, docs/openapi/openapi.yaml, docs/openapi/schemas/management/*, docs/openapi/paths/management/*, docs/mcp/filtering.mdx, docs/architecture/core/mcp.mdx
Updated all hook references from PreHook/PostHook to PreLLMHook/PostLLMHook. Added MCP Logs API endpoints and schemas (MCPToolLogEntry, MCPToolLogSearchFilters, MCPToolLogStats, SearchMCPLogsResponse). Updated MCP client schemas with client_id, is_code_mode_client, tool_pricing, tools_to_auto_execute. Updated tool filtering format from slash to dash-delimiter (clientName-toolName).
Build & Configuration
Makefile, .gitignore, core/go.mod
Added setup-mcp-tests and test-mcp targets. Added ignore patterns for dist, tmp, bin directories. Updated dependencies: removed goja/go-typescript, added go.starlark.net, added google.golang.org/protobuf.
In-Place Plugin Updates (Existing Examples)
examples/plugins/hello-world/*, plugins/mocker/main.go, plugins/jsonparser/main.go, plugins/litellmcompat/main.go, plugins/maxim/main.go, plugins/logging/main.go
Updated all existing plugins from PreHook/PostHook to PreLLMHook/PostLLMHook. Updated return types from PluginShortCircuit to LLMPluginShortCircuit. Updated GetName() to return lowercase identifiers per plugin system conventions.
Test Updates (Existing Tests)
examples/plugins/**/plugin_test.go, plugins/**/plugin_test.go, plugins/mocker/benchmark_test.go, plugins/governance/model_provider_governance_test.go, plugins/governance/resolver_test.go
Updated all test files to use LLMPlugins instead of Plugins field in BifrostConfig. Updated hook calls to PreLLMHook/PostLLMHook. Updated EvaluateModelAndProviderRequest/EvaluateVirtualKeyRequest signatures.
Core Utilities & Utils
core/utils.go
Added IsCodemodeTool(toolName string) bool helper function to identify code-mode tools (list_tool_files, read_tool_file, execute_code).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Bifrost
    participant PluginPipeline
    participant MCPManager
    participant MCPToolsManager

    Client->>Bifrost: ExecuteChatMCPTool(ctx, toolCall)
    activate Bifrost
    Bifrost->>PluginPipeline: RunMCPPreHooks(ctx, mcpRequest)
    activate PluginPipeline
    PluginPipeline->>PluginPipeline: Execute PreMCPHook plugins
    PluginPipeline-->>Bifrost: mcpRequest (possibly modified)
    deactivate PluginPipeline
    
    Bifrost->>MCPManager: ExecuteToolCall(ctx, mcpRequest)
    activate MCPManager
    MCPManager->>MCPToolsManager: ExecuteTool(ctx, mcpRequest)
    activate MCPToolsManager
    MCPToolsManager->>MCPToolsManager: Route by MCPRequestType
    MCPToolsManager->>MCPToolsManager: Resolve tool & client
    MCPToolsManager->>MCPToolsManager: Execute tool
    MCPToolsManager-->>MCPManager: mcpResponse
    deactivate MCPToolsManager
    MCPManager-->>Bifrost: mcpResponse
    deactivate MCPManager
    
    Bifrost->>PluginPipeline: RunMCPPostHooks(ctx, mcpResponse)
    activate PluginPipeline
    PluginPipeline->>PluginPipeline: Execute PostMCPHook plugins
    PluginPipeline-->>Bifrost: mcpResponse (possibly modified)
    deactivate PluginPipeline
    
    Bifrost-->>Client: ChatMessage or ResponsesMessage
    deactivate Bifrost
Loading
sequenceDiagram
    participant LLMRequest as Client<br/>(LLM Request)
    participant Bifrost
    participant LLMPlugins
    participant LLMProvider
    participant MCPToolCall as MCP Tool Call<br/>(Auto-Execute)

    LLMRequest->>Bifrost: ChatRequest/ResponsesRequest
    activate Bifrost
    
    Bifrost->>LLMPlugins: RunLLMPreHooks(ctx, request)
    activate LLMPlugins
    LLMPlugins->>LLMPlugins: Execute PreLLMHook plugins
    LLMPlugins-->>Bifrost: request (possibly modified)
    deactivate LLMPlugins
    
    Bifrost->>LLMProvider: Call LLM
    activate LLMProvider
    LLMProvider-->>Bifrost: response (with tool_calls)
    deactivate LLMProvider
    
    alt Tool Call Auto-Execution
        Bifrost->>MCPToolCall: ExecuteToolCall(ctx, mcpRequest)
        activate MCPToolCall
        MCPToolCall-->>Bifrost: mcpResponse
        deactivate MCPToolCall
        Bifrost->>LLMProvider: Continue with tool result
    end
    
    Bifrost->>LLMPlugins: RunLLMPostHooks(ctx, response)
    activate LLMPlugins
    LLMPlugins->>LLMPlugins: Execute PostLLMHook plugins
    LLMPlugins-->>Bifrost: response (possibly modified)
    deactivate LLMPlugins
    
    Bifrost-->>LLMRequest: Final response
    deactivate Bifrost
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • v1.4.0 #1153: Adds MCP tool execution APIs and plugin pipeline integration to core/bifrost.go with ExecuteChatMCPTool/ExecuteResponsesMCPTool — directly related to MCP tool execution wiring in this PR.

Suggested reviewers

  • danpiths
  • roroghost17

Poem

🐰 Hops with joy through plugin lands so bright,
LLM and MCP now split just right,
PreHooks and PostHooks dance in their place,
Each plugin type finds its own embrace!
From tests renamed to schemas anew,
The bifrost magic shines right through! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 4
❌ Failed checks (3 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning PR description is incomplete; it contains only the template with placeholder text and no concrete information about changes, rationale, testing approach, or breaking changes. Fill in all template sections with actual details: explain the MCP plugin schema changes, implementation decisions, affected components, specific test steps, and any breaking changes introduced.
Out of Scope Changes check ⚠️ Warning Extensive changes present beyond MCP plugin schema: chatbot test removal (core/chatbot_test.go deletion), test package renames (testutil → llmtests), new MCP test suites (1300+ lines), plugin loader refactoring, governance changes, and database migrations—significant scope expansion. Clarify PR scope: consolidate into focused commits addressing MCP plugin schema separately from test reorganization, governance updates, and infrastructure changes; or document why all changes are necessary for this feature.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive Linked issue #123 concerns Files API support, but the changeset focuses on MCP plugin schema refactoring, per-type plugin management, and code-mode execution—unrelated to file upload APIs. Verify whether the linked issue is correct or if the PR should link to issues related to MCP plugin architecture, agent execution, or plugin lifecycle management instead.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly describes the main change: adding MCP plugins to the plugin schema, which aligns with the extensive modifications throughout the codebase.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins

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

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 72da89d to 63f961e Compare January 19, 2026 09:51
@coderabbitai coderabbitai bot requested a review from roroghost17 January 19, 2026 09:51
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: 8

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (5)
plugins/mocker/main.go (2)

800-869: Fix nil deref when overriding model for ResponsesRequest.
If content.Model is set and the request is a ResponsesRequest, ChatResponse is nil and this panics. Guard the override and update extra fields instead.

🐛 Proposed fix
-	} else {
-		// Use a static string to avoid allocation
-		static := "stop"
-		finishReason = &static
-	}
+	} else {
+		// Use a static string to avoid allocation
+		finishReason = bifrost.Ptr("stop")
+	}
@@
-	// Override model if specified
-	if content.Model != nil {
-		mockResponse.ChatResponse.Model = *content.Model
-	}
+	// Override model if specified
+	if content.Model != nil {
+		if mockResponse.ChatResponse != nil {
+			mockResponse.ChatResponse.Model = *content.Model
+		}
+		extraFields := mockResponse.GetExtraFields()
+		extraFields.ModelRequested = *content.Model
+	}
Based on learnings, prefer `bifrost.Ptr(...)` over `&value` in this repo.

1001-1043: Default success should honor ResponsesRequest shape.
For ResponsesRequest, this returns a chat response, which mismatches the request type. Branch by request type and return a ResponsesResponse when needed.

🐛 Proposed fix
 	case DefaultBehaviorSuccess:
-		finishReason := "stop"
-		return req, &schemas.LLMPluginShortCircuit{
-			Response: &schemas.BifrostResponse{
-				ChatResponse: &schemas.BifrostChatResponse{
-					Model: model,
-					Usage: &schemas.BifrostLLMUsage{
-						PromptTokens:     5,
-						CompletionTokens: 10,
-						TotalTokens:      15,
-					},
-					Choices: []schemas.BifrostResponseChoice{
-						{
-							Index: 0,
-							ChatNonStreamResponseChoice: &schemas.ChatNonStreamResponseChoice{
-								Message: &schemas.ChatMessage{
-									Role: schemas.ChatMessageRoleAssistant,
-									Content: &schemas.ChatMessageContent{
-										ContentStr: bifrost.Ptr("Mock plugin default response"),
-									},
-								},
-							},
-							FinishReason: &finishReason,
-						},
-					},
-					ExtraFields: schemas.BifrostResponseExtraFields{
-						RequestType:    schemas.ChatCompletionRequest,
-						Provider:       provider,
-						ModelRequested: model,
-					},
-				},
-			},
-		}, nil
+		finishReason := bifrost.Ptr("stop")
+		if req.RequestType == schemas.ResponsesRequest {
+			return req, &schemas.LLMPluginShortCircuit{
+				Response: &schemas.BifrostResponse{
+					ResponsesResponse: &schemas.BifrostResponsesResponse{
+						CreatedAt: int(time.Now().Unix()),
+						Output: []schemas.ResponsesMessage{
+							{
+								Role: bifrost.Ptr(schemas.ResponsesInputMessageRoleAssistant),
+								Content: &schemas.ResponsesMessageContent{
+									ContentStr: bifrost.Ptr("Mock plugin default response"),
+								},
+								Type: bifrost.Ptr(schemas.ResponsesMessageTypeMessage),
+							},
+						},
+						Usage: &schemas.ResponsesResponseUsage{
+							InputTokens:  5,
+							OutputTokens: 10,
+							TotalTokens:  15,
+						},
+						ExtraFields: schemas.BifrostResponseExtraFields{
+							RequestType:    schemas.ResponsesRequest,
+							Provider:       provider,
+							ModelRequested: model,
+						},
+					},
+				},
+			}, nil
+		}
+		return req, &schemas.LLMPluginShortCircuit{
+			Response: &schemas.BifrostResponse{
+				ChatResponse: &schemas.BifrostChatResponse{
+					Model: model,
+					Usage: &schemas.BifrostLLMUsage{
+						PromptTokens:     5,
+						CompletionTokens: 10,
+						TotalTokens:      15,
+					},
+					Choices: []schemas.BifrostResponseChoice{
+						{
+							Index: 0,
+							ChatNonStreamResponseChoice: &schemas.ChatNonStreamResponseChoice{
+								Message: &schemas.ChatMessage{
+									Role: schemas.ChatMessageRoleAssistant,
+									Content: &schemas.ChatMessageContent{
+										ContentStr: bifrost.Ptr("Mock plugin default response"),
+									},
+								},
+							},
+							FinishReason: finishReason,
+						},
+					},
+					ExtraFields: schemas.BifrostResponseExtraFields{
+						RequestType:    schemas.ChatCompletionRequest,
+						Provider:       provider,
+						ModelRequested: model,
+					},
+				},
+			},
+		}, nil
Based on learnings, prefer `bifrost.Ptr(...)` over `&value` in this repo.
core/mcp/mcp.go (1)

187-209: Guard against nil executeTool callbacks.
Unlike makeReq, executeTool is currently unchecked; a nil callback will panic when tool execution begins. Add a defensive check mirroring makeReq.

🐛 Proposed fix
 func (m *MCPManager) CheckAndExecuteAgentForChatRequest(
 	ctx *schemas.BifrostContext,
 	req *schemas.BifrostChatRequest,
 	response *schemas.BifrostChatResponse,
 	makeReq func(ctx *schemas.BifrostContext, req *schemas.BifrostChatRequest) (*schemas.BifrostChatResponse, *schemas.BifrostError),
 	executeTool func(ctx *schemas.BifrostContext, request *schemas.BifrostMCPRequest) (*schemas.BifrostMCPResponse, error),
 ) (*schemas.BifrostChatResponse, *schemas.BifrostError) {
 	if makeReq == nil {
 		return nil, &schemas.BifrostError{
 			IsBifrostError: false,
 			Error: &schemas.ErrorField{
 				Message: "makeReq is required to execute agent mode",
 			},
 		}
 	}
+	if executeTool == nil {
+		return nil, &schemas.BifrostError{
+			IsBifrostError: false,
+			Error: &schemas.ErrorField{
+				Message: "executeTool is required to execute agent mode",
+			},
+		}
+	}
 	// Check if initial response has tool calls
 	if !hasToolCallsForChatResponse(response) {
 		logger.Debug("No tool calls detected, returning response")
 		return response, nil
 	}
 	// Execute agent mode
 	return m.toolsManager.ExecuteAgentForChatRequest(ctx, req, response, makeReq, executeTool)
 }
 
 func (m *MCPManager) CheckAndExecuteAgentForResponsesRequest(
 	ctx *schemas.BifrostContext,
 	req *schemas.BifrostResponsesRequest,
 	response *schemas.BifrostResponsesResponse,
 	makeReq func(ctx *schemas.BifrostContext, req *schemas.BifrostResponsesRequest) (*schemas.BifrostResponsesResponse, *schemas.BifrostError),
 	executeTool func(ctx *schemas.BifrostContext, request *schemas.BifrostMCPRequest) (*schemas.BifrostMCPResponse, error),
 ) (*schemas.BifrostResponsesResponse, *schemas.BifrostError) {
 	if makeReq == nil {
 		return nil, &schemas.BifrostError{
 			IsBifrostError: false,
 			Error: &schemas.ErrorField{
 				Message: "makeReq is required to execute agent mode",
 			},
 		}
 	}
+	if executeTool == nil {
+		return nil, &schemas.BifrostError{
+			IsBifrostError: false,
+			Error: &schemas.ErrorField{
+				Message: "executeTool is required to execute agent mode",
+			},
+		}
+	}
 	// Check if initial response has tool calls
 	if !hasToolCallsForResponsesResponse(response) {
 		logger.Debug("No tool calls detected, returning response")
 		return response, nil
 	}
 	// Execute agent mode
 	return m.toolsManager.ExecuteAgentForResponsesRequest(ctx, req, response, makeReq, executeTool)
 }

Also applies to: 239-261

transports/bifrost-http/server/server.go (1)

833-841: Guard against nil proxy config to avoid panic.
Line 839 dereferences config without a nil check; if the proxy config is cleared/unset, this will panic at runtime.

🛠️ Suggested fix
 func (s *BifrostHTTPServer) ReloadProxyConfig(ctx context.Context, config *configstoreTables.GlobalProxyConfig) error {
 	if s.Config == nil {
 		return fmt.Errorf("config not found")
 	}
 	// Store the proxy config in memory for use by components that need it
 	s.Config.ProxyConfig = config
-	logger.Info("proxy configuration reloaded: enabled=%t, type=%s", config.Enabled, config.Type)
+	if config == nil {
+		logger.Info("proxy configuration cleared")
+		return nil
+	}
+	logger.Info("proxy configuration reloaded: enabled=%t, type=%s", config.Enabled, config.Type)
 	return nil
 }
transports/bifrost-http/lib/config.go (1)

1573-1601: Store pricing data appears to be unused.

At line 1573, mcpPricingConfig.PricingData is populated from the store via buildMCPPricingDataFromStore. However, mcpPricingConfig is never used afterward. Instead, at lines 1595-1597, the MCPCatalog is initialized with buildMCPPricingDataFromFile(ctx, configData), completely ignoring the store data.

This appears to be a bug where database-persisted pricing is not loaded when a config file exists.

🐛 Suggested fix to merge or prioritize pricing data
 	config.ModelCatalog = pricingManager

 	// Initialize MCP catalog
+	// Merge pricing data: store takes priority, then file
+	mcpPricingFromFile := buildMCPPricingDataFromFile(ctx, configData)
+	mcpPricingFromStore := mcpPricingConfig.PricingData
+	// Merge: store entries override file entries
+	for key, entry := range mcpPricingFromStore {
+		mcpPricingFromFile[key] = entry
+	}
 	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
-		PricingData: buildMCPPricingDataFromFile(ctx, configData),
+		PricingData: mcpPricingFromFile,
 	}, logger)

Alternatively, if store should completely override file when present:

+	var mcpPricingData mcpcatalog.MCPPricingData
+	if config.ConfigStore != nil && len(mcpPricingConfig.PricingData) > 0 {
+		mcpPricingData = mcpPricingConfig.PricingData
+	} else {
+		mcpPricingData = buildMCPPricingDataFromFile(ctx, configData)
+	}
 	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
-		PricingData: buildMCPPricingDataFromFile(ctx, configData),
+		PricingData: mcpPricingData,
 	}, logger)
🤖 Fix all issues with AI agents
In `@core/mcp/utils.go`:
- Around line 133-145: After sanitizing the tool name in retrieveExternalTools
(sanitizedToolName), immediately call
validateNormalizedToolName(sanitizedToolName) and handle any returned error
before using it to build prefixedToolName or storing into the tools map; if
validation fails, skip adding the tool (or return/log the error) so no names
containing '/' or '..' reach convertMCPToolToBifrostSchema, prefixedToolName,
bifrostTool.Function.Name, or tools[prefixedToolName].

In `@core/schemas/bifrost.go`:
- Around line 19-22: The test setup still initializes BifrostConfig using the
removed Plugins field; update the BifrostConfig literal in
tests/core-mcp/setup.go to set the new fields LLMPlugins and MCPPlugins instead
(replace Plugins: nil with LLMPlugins: nil and MCPPlugins: nil in the
BifrostConfig initialization), ensuring the struct literal matches the new
BifrostConfig definition and compiles.

In `@examples/plugins/hello-world/main.go`:
- Around line 38-43: The example's PreLLMHook references an undefined type
schemas.LLMPluginShortCircuit causing build failures; update the PreLLMHook
signature in examples/plugins/hello-world/main.go to use the currently exported
short-circuit type from core/schemas (or, if the new LLMPluginShortCircuit type
is intended, add that type definition to core/schemas) so the example compiles.
Locate the PreLLMHook function and either replace schemas.LLMPluginShortCircuit
with the existing exported type name used elsewhere in schemas, or add the
LLMPluginShortCircuit type to core/schemas with the appropriate fields and
export it so the reference in PreLLMHook is valid.

In `@framework/mcpcatalog/main.go`:
- Around line 31-45: Init assigns config.PricingData directly into the
MCPCatalog causing potential races from external mutation; instead make a
defensive deep copy of config.PricingData before storing it in the MCPCatalog's
pricingData field. Locate Init and the types MCPPricingData and MCPCatalog and
implement a copy (construct a new MCPPricingData and copy primitive fields and
explicitly deep-copy any slices, maps or nested structs) from config.PricingData
into pricingData so the catalog holds an independent copy.

In `@plugins/logging/main.go`:
- Around line 803-812: The callback is being invoked while holding p.mu (see the
block after p.store.CreateMCPToolLog) which can block or deadlock; fix by
following the logCallback pattern: under p.mu copy p.mcpToolLogCallback to a
local variable (e.g., cb := p.mcpToolLogCallback) and then release the lock
before calling cb(entry), ensuring no DB reads or callback invocation occurs
while locked; apply the same change to the other identical callback invocation
site (the second MCP hook path) so both call sites copy the callback under p.mu
and invoke it afterwards.

In `@plugins/logging/utils.go`:
- Around line 129-143: SearchMCPToolLogs and GetMCPToolLogStats currently call
p.plugin.store without a nil guard which can panic if the plugin isn't
initialized; add the same nil checks used in DeleteMCPToolLogs to both functions
(verify p.plugin != nil and p.plugin.store != nil) and return a descriptive
error (e.g., "plugin or store is not initialized") instead of proceeding to call
p.plugin.store.SearchMCPToolLogs or p.plugin.store.GetMCPToolLogStats; keep the
rest of the logic intact so the functions delegate to the store only when the
guard passes.

In `@transports/bifrost-http/handlers/mcp.go`:
- Around line 16-25: AddMCPClient currently converts the parsed TableMCPClient
to schemas.MCPClientConfig which drops table-only fields (e.g., tool_pricing)
that EditMCPClient preserves; change the create path to accept and persist the
full configstoreTables.TableMCPClient (or ensure conversion preserves all
table-only columns) so table-only columns aren’t lost, update the
MCPManager.AddMCPClient implementations to accept/handle TableMCPClient (or a
wrapper that includes those fields), and verify the new behavior matches prior
Graphite-stack PRs for field alignment.

In `@ui/app/workspace/mcp-gateway/views/mcpClientsTable.tsx`:
- Around line 64-70: The frontend is reading client.config.client_id but the Go
struct MCPClientConfig currently JSON-serializes the field as "id"
(MCPClientConfig.ID), causing undefined; either change the Go struct tag on
MCPClientConfig.ID to `json:"client_id"` so responses include "client_id", or
update the TypeScript interface in ui/lib/types/mcp.ts to expect `id` instead of
`client_id`; ensure the client code using client.config.client_id (in
mcpClientsTable.tsx) and any other usages are updated to match the chosen
canonical field name.
♻️ Duplicate comments (8)
docs/openapi/openapi.json (8)

113260-113270: Duplicate MCP client config block.

Same client_id rename concern as the earlier MCP client configuration block.


113317-113337: Duplicate tools/pricing schema block.

Same constraints suggestion as the earlier tools/pricing block.


113558-113569: Duplicate MCP client config block.

Same client_id rename concern as the earlier MCP client configuration block.


113615-113636: Duplicate tools/pricing schema block.

Same constraints suggestion as the earlier tools/pricing block.


138609-138620: Duplicate MCP client config block.

Same client_id rename concern as the earlier MCP client configuration block.


138666-138686: Duplicate tools/pricing schema block.

Same constraints suggestion as the earlier tools/pricing block.


138724-138735: Duplicate MCP client config block.

Same client_id rename concern as the earlier MCP client configuration block.


138781-138801: Duplicate tools/pricing schema block.

Same constraints suggestion as the earlier tools/pricing block.

🟡 Minor comments (16)
docs/features/governance/virtual-keys.mdx-510-514 (1)

510-514: Update navigation path for consistency with other documentation.

The label "Enforce Virtual Keys" and screenshot are correct, but the navigation path should be Workspace → Config → Security instead of just Config → Security to match the pattern used in other docs (e.g., quickstart/gateway/setting-up-auth.mdx).

framework/mcpcatalog/main.go-83-88 (1)

83-88: Cleanup leaves a nil map (Update will panic).
If any caller updates after cleanup, the nil map will panic. Consider resetting to an empty map instead.

🔧 Proposed fix
-	mc.pricingData = nil
+	mc.pricingData = make(MCPPricingData)
core/schemas/plugin_wasm.go-5-7 (1)

5-7: Clarify streaming short-circuit wording.

The comment says streaming short-circuits are possible, but this struct doesn’t expose a stream field and the next line says streams aren’t supported in WASM. This is internally inconsistent—suggest trimming the streaming mention or explicitly stating it’s not available here.

✏️ Suggested wording fix
-// It can contain either a response (success short-circuit), a stream (streaming short-circuit), or an error (error short-circuit).
-// Streams are not supported in WASM plugins.
+// It can contain either a response (success short-circuit) or an error (error short-circuit).
+// Streams are not supported in WASM plugins, so streaming short-circuits are not exposed here.
framework/configstore/migrations.go-2490-2515 (1)

2490-2515: Missing collision detection for VK MCP configs.

The governance_virtual_key_mcp_configs processing lacks the collision detection present in the config_mcp_clients section. If two tools in a VK config have the same unprefixed name (e.g., server1_add and add), they would both be added without warning.

🔧 Add collision detection for consistency
 			if len(vkConfig.ToolsToExecute) > 0 {
 				updatedTools := make([]string, 0, len(vkConfig.ToolsToExecute))
+				seenTools := make(map[string]bool)
 				for _, tool := range vkConfig.ToolsToExecute {
 					prefix := clientName + "_"
 					if strings.HasPrefix(tool, prefix) {
 						unprefixedTool := strings.TrimPrefix(tool, prefix)
+						if seenTools[unprefixedTool] {
+							log.Printf("Collision detected when stripping prefix from VK MCP config tool '%s': unprefixed name '%s' already exists. Skipping.", tool, unprefixedTool)
+							needsUpdate = true
+							continue
+						}
+						seenTools[unprefixedTool] = true
 						updatedTools = append(updatedTools, unprefixedTool)
 						needsUpdate = true
 					} else {
+						if seenTools[tool] {
+							log.Printf("Duplicate tool name '%s' found in VK MCP config. Keeping first occurrence.", tool)
+							continue
+						}
+						seenTools[tool] = true
 						updatedTools = append(updatedTools, tool)
 					}
 				}
ui/app/workspace/mcp-logs/views/filters.tsx-18-20 (1)

18-20: Remove unused fetchLogs and fetchStats props from MCPLogFilters component.

These props are passed from the parent but never invoked within the component. Since the actual refetch logic is managed in the parent page component via useEffect hooks and the live toggle callback, these props can be safely removed from the interface and component signature.

docs/openapi/schemas/management/logging.yaml-90-93 (1)

90-93: OpenAPI schema missing "cancelled" status for MCPToolLogEntry.

The OpenAPI schema defines MCPToolLogEntry status enum as ["processing", "success", "error"] (lines 90-93), but the shared UI Statuses constant includes "cancelled". While the backend doesn't actually return "cancelled" for MCP logs and the UI has defensive fallback logic (getValidatedStatus()) that handles unknown statuses, this creates a schema contract violation.

For consistency with the actual API contract, either update the OpenAPI schema to reflect what the UI tries to use, or adjust the UI constant to match only the supported statuses for MCP logs. The defensive coding mitigates immediate risk, but the schema and implementation should align.

core/mcp/utils.go-187-190 (1)

187-190: Update prefix examples to use '-' (not '/').

The comments on Line 187–190 and Line 219–222 still show the old calculator/add style. Tool names are now stored as clientName-toolName, so the examples should match to avoid confusion.

✏️ Comment fix
-		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// Tool names in config are stored without prefix (e.g., "add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")
...
-		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// Tool names in config are stored without prefix (e.g., "add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")

Also applies to: 219-222

ui/lib/store/apis/mcpLogsApi.ts-46-49 (1)

46-49: Send zero-valued latency filters.

Lines 48-49 and 85-86 use truthy checks that will omit 0 values, preventing users from setting min_latency=0 or max_latency=0. Use explicit undefined checks instead.

♻️ Proposed fix
-				if (filters.min_latency) params.min_latency = filters.min_latency;
-				if (filters.max_latency) params.max_latency = filters.max_latency;
+				if (filters.min_latency !== undefined) params.min_latency = filters.min_latency;
+				if (filters.max_latency !== undefined) params.max_latency = filters.max_latency;
...
-				if (filters.min_latency) params.min_latency = filters.min_latency;
-				if (filters.max_latency) params.max_latency = filters.max_latency;
+				if (filters.min_latency !== undefined) params.min_latency = filters.min_latency;
+				if (filters.max_latency !== undefined) params.max_latency = filters.max_latency;
core/mcp/codemodeexecutecode.go-852-866 (1)

852-866: Short‑circuit post‑hook errors are swallowed.
The short‑circuit response path ignores RunMCPPostHooks errors, which can hide plugin failures. Handle finalErr the same way as the main path.

🔧 Proposed fix
-			finalResp, _ := pipeline.RunMCPPostHooks(nestedCtx, shortCircuit.Response, nil, preCount)
+			finalResp, finalErr := pipeline.RunMCPPostHooks(nestedCtx, shortCircuit.Response, nil, preCount)
+			if finalErr != nil {
+				if finalErr.Error != nil {
+					return nil, fmt.Errorf("%s", finalErr.Error.Message)
+				}
+				return nil, fmt.Errorf("plugin post-hook error")
+			}
 			if finalResp != nil && finalResp.ChatMessage != nil {
 				return extractResultFromChatMessage(finalResp.ChatMessage), nil
 			}
core/mcp/codemodeexecutecode.go-926-927 (1)

926-927: Pass originalToolName to extractTextFromMCPResponse for consistency.
The MCP server is called with originalToolName, and the response structure also uses originalToolName. For consistency, pass the unprefixed name to extractTextFromMCPResponse at lines 926-927 and 1010-1012.

🔧 Proposed fix
-	rawResult := extractTextFromMCPResponse(toolResponse, toolName)
+	rawResult := extractTextFromMCPResponse(toolResponse, originalToolName)

Also applies to: 1010-1012

ui/app/workspace/mcp-logs/page.tsx-170-174 (1)

170-174: Clear the selected log when it's deleted.
If the user deletes the currently selected log, the detail sheet stays open showing stale data. Add setSelectedLog((prev) => (prev?.id === log.id ? null : prev)) to the delete handler to close the sheet automatically.

🔧 Proposed fix
			try {
				await deleteLogs({ ids: [log.id] }).unwrap();
				setLogs((prevLogs) => prevLogs.filter((l) => l.id !== log.id));
				setTotalItems((prev) => prev - 1);
+				setSelectedLog((prev) => (prev?.id === log.id ? null : prev));
			} catch (err) {
core/mcp/codemodeexecutecode.go-885-888 (1)

885-888: Enforce the pre-hook tool name mutation contract.

The code documents that pre-hooks should not modify tool names (only arguments), but this contract is not enforced. While lines 869-876 extract the full toolCall object from preReq, only the arguments are used—the Function.Name is silently ignored. A plugin that modifies toolCall.Function.Name will have its changes discarded without warning or error. Either validate that preReq.ChatAssistantMessageToolCall.Function.Name matches the original tool name and raise an error if modified, or add a warning log when a mismatch is detected.

ui/app/workspace/mcp-logs/page.tsx-282-305 (1)

282-305: Guard stats updates against double-counting on repeated completion messages.

Stats are incremented whenever a log reaches success or error status, but there's no check for whether the log was already completed. If the same log receives multiple "update" messages with a terminal status (due to retries, duplicates, or re-delivery), stats will be incremented multiple times for a single execution.

Update only when transitioning to a completed state:

🔧 Proposed fix
				// Update stats for completed requests
-				if (log.status === "success" || log.status === "error") {
+				const prevLog = logs.find((existingLog) => existingLog.id === log.id);
+				const wasCompleted = prevLog && (prevLog.status === "success" || prevLog.status === "error");
+				const isCompleted = log.status === "success" || log.status === "error";
+				if (isCompleted && !wasCompleted) {
 					setStats((prevStats) => {
 						if (!prevStats) return prevStats;
core/bifrost.go-3430-3430 (1)

3430-3430: Same plugin count inconsistency.

The postHookRunner closure should capture the pipeline's plugin count at creation time rather than loading fresh from bifrost.llmPlugins.

Suggested fix
+			pluginCount := len(pipeline.llmPlugins)
 			postHookRunner = func(ctx *schemas.BifrostContext, result *schemas.BifrostResponse, err *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError) {
-				resp, bifrostErr := pipeline.RunPostHooks(ctx, result, err, len(*bifrost.llmPlugins.Load()))
+				resp, bifrostErr := pipeline.RunPostHooks(ctx, result, err, pluginCount)
 				if bifrostErr != nil {
 					return nil, bifrostErr
 				}
 				return resp, nil
 			}
core/bifrost.go-2974-2974 (1)

2974-2974: Potential inconsistency between pipeline plugins and plugin count.

The plugin count at Line 2974 is fetched via bifrost.llmPlugins.Load(), but the pipeline object captured its plugins slice earlier at getPluginPipeline() (Line 2899). If plugins are reloaded between these two points, the count passed to RunPostHooks could mismatch the actual plugins in the pipeline.

Consider using len(pipeline.llmPlugins) instead to ensure consistency:

Suggested fix
-	pluginCount := len(*bifrost.llmPlugins.Load())
+	pluginCount := len(pipeline.llmPlugins)
core/bifrost.go-3175-3175 (1)

3175-3175: Same inconsistency as noted above.

This line has the same issue - use len(pipeline.llmPlugins) for consistency with the captured pipeline.

Suggested fix
-		recoveredResp, recoveredErr := pipeline.RunPostHooks(ctx, nil, &bifrostErrVal, len(*bifrost.llmPlugins.Load()))
+		recoveredResp, recoveredErr := pipeline.RunPostHooks(ctx, nil, &bifrostErrVal, len(pipeline.llmPlugins))
🧹 Nitpick comments (35)
plugins/mocker/benchmark_test.go (1)

12-13: Consider renaming benchmark functions to reflect PreLLMHook.

The benchmark function names still reference PreHook (e.g., BenchmarkMockerPlugin_PreHook_SimpleRule) while the code now calls PreLLMHook. This naming inconsistency could cause confusion when running benchmarks or reading test output.

♻️ Suggested renames
-func BenchmarkMockerPlugin_PreHook_SimpleRule(b *testing.B) {
+func BenchmarkMockerPlugin_PreLLMHook_SimpleRule(b *testing.B) {
-func BenchmarkMockerPlugin_PreHook_RegexRule(b *testing.B) {
+func BenchmarkMockerPlugin_PreLLMHook_RegexRule(b *testing.B) {
-func BenchmarkMockerPlugin_PreHook_MultipleRules(b *testing.B) {
+func BenchmarkMockerPlugin_PreLLMHook_MultipleRules(b *testing.B) {
-func BenchmarkMockerPlugin_PreHook_NoMatch(b *testing.B) {
+func BenchmarkMockerPlugin_PreLLMHook_NoMatch(b *testing.B) {
-func BenchmarkMockerPlugin_PreHook_Template(b *testing.B) {
+func BenchmarkMockerPlugin_PreLLMHook_Template(b *testing.B) {

Also applies to: 69-70, 126-127, 205-206, 263-264

ui/app/workspace/logs/page.tsx (1)

58-59: Consider caching the default time range to ensure consistency.

Calling getDefaultTimeRange() twice could theoretically produce slightly different timestamps if there's any delay between calls. While unlikely to cause issues in practice, you could cache the result for consistency.

♻️ Suggested improvement
-			start_time: parseAsInteger.withDefault(getDefaultTimeRange().startTime),
-			end_time: parseAsInteger.withDefault(getDefaultTimeRange().endTime),
+			start_time: parseAsInteger.withDefault(dateUtils.getDefaultTimeRange().startTime),
+			end_time: parseAsInteger.withDefault(dateUtils.getDefaultTimeRange().endTime),

Or if consistency is important, compute once:

// At the top of the component, before useQueryStates
const initialDefaults = useMemo(() => dateUtils.getDefaultTimeRange(), []);

// Then in useQueryStates:
start_time: parseAsInteger.withDefault(initialDefaults.startTime),
end_time: parseAsInteger.withDefault(initialDefaults.endTime),

Note: The useMemo approach would prevent defaults from refreshing on re-renders, but since you have the focus/visibility effect handling refresh logic, this should be fine.

plugins/maxim/main.go (2)

409-412: Minor: Error message is slightly misleading.

The error wraps a logger creation failure but the message says "failed to create trace". Consider updating for clarity.

💡 Suggested fix
 	logger, err := plugin.getOrCreateLogger(effectiveLogRepoID)
 	if err != nil {
-		return req, nil, fmt.Errorf("failed to create trace: %w", err)
+		return req, nil, fmt.Errorf("failed to get logger for trace creation: %w", err)
 	}

588-600: Inconsistent guards for tag addition.

Inside the hasTags block (lines 591-596), you correctly check generationID != "" and traceID != "" before adding tags. However, lines 599-600 add model tags unconditionally without these guards.

While the current flow likely ensures IDs are set when reaching this code path, adding consistent guards would make the code more defensive and easier to reason about.

💡 Suggested fix for consistency
-		logger.AddTagToGeneration(generationID, "model", string(model))
-		logger.AddTagToTrace(traceID, "model", string(model))
+		if generationID != "" {
+			logger.AddTagToGeneration(generationID, "model", string(model))
+		}
+		if traceID != "" {
+			logger.AddTagToTrace(traceID, "model", string(model))
+		}
docs/features/governance/virtual-keys.mdx (1)

556-576: Clarify that auth is disabled only for inference requests.

The heading reads “auth disabled” for disable_auth_on_inference: true, but the config example later shows auth_config.is_enabled: true, which implies auth is still on for non-inference routes. Suggest tightening the wording to “auth disabled for inference requests” to avoid confusion.

✏️ Proposed wording tweak
-**When `disable_auth_on_inference: true` (auth disabled):**
+**When `disable_auth_on_inference: true` (auth disabled for inference requests):**
transports/bifrost-http/handlers/devpprof.go (1)

526-527: Consider classifying MCP hooks as per-request goroutines.

With MCP hooks now part of request execution, these goroutines may appear as “unknown” in the profiler output. Adding MCP hook patterns keeps the per-request categorization accurate.

♻️ Suggested update
 perRequestPatterns := []string{
 	"PreLLMHook",
 	"PostLLMHook",
+	"PreMCPHook",
+	"PostMCPHook",
 	"completeAndFlushTrace",
 	"ProcessAndSend",
ui/app/workspace/mcp-logs/views/emptyState.tsx (2)

43-74: Consider adding error handling for clipboard API.

The navigator.clipboard.writeText call may fail in certain contexts (e.g., non-secure origins, permission denied). Consider wrapping in try-catch for a better user experience.

🛠️ Suggested improvement
 function CodeBlock({ code, language, onLanguageChange, showLanguageSelect = false, readonly = true }: CodeBlockProps) {
-	const copyToClipboard = () => {
-		navigator.clipboard.writeText(code);
-		toast.success("Copied to clipboard");
+	const copyToClipboard = async () => {
+		try {
+			await navigator.clipboard.writeText(code);
+			toast.success("Copied to clipboard");
+		} catch {
+			toast.error("Failed to copy to clipboard");
+		}
 	};

67-69: Add accessible label to copy button.

The copy button only has an icon without accessible text for screen readers.

♿ Accessibility improvement
-			<Button variant="ghost" size="icon" onClick={copyToClipboard}>
+			<Button variant="ghost" size="icon" onClick={copyToClipboard} aria-label="Copy code to clipboard">
 				<Copy className="size-4" />
 			</Button>
ui/lib/types/mcp.ts (1)

35-44: Add tool_pricing to CreateMCPClientRequest for API consistency.

UpdateMCPClientRequest includes tool_pricing?: Record<string, number>;, but CreateMCPClientRequest does not. Since the backend handler is explicitly designed to accept and process tool_pricing on creation (for catalog/pricing data tracking), and MCPClientConfig includes this field, adding it to the create request interface would maintain symmetry with the update operation and align the TypeScript types with the backend implementation.

transports/bifrost-http/lib/config_test.go (1)

12222-12230: Prefer bifrost.Ptr for pointer creation.
Repo convention favors bifrost.Ptr(...) over custom pointer helpers. Consider switching the weight pointer creation (or updating the helper to delegate to bifrost.Ptr) for consistency. Based on learnings, ...

♻️ Suggested adjustment
-			Weight:     ptrFloat64(1.5),
+			Weight:     bifrost.Ptr(1.5),
plugins/semanticcache/main.go (1)

548-561: Use bifrost.Ptr for InputTokens pointer creation.
Keeps pointer creation consistent across the plugin codebase.

♻️ Suggested tweak
-			extraFields.CacheDebug.InputTokens = &inputTokens
+			extraFields.CacheDebug.InputTokens = bifrost.Ptr(inputTokens)
Based on learnings, prefer `bifrost.Ptr(...)` over `&value` in this repo.
plugins/mocker/main.go (1)

491-497: Redundant Enabled check in plugin hook.
Disabled plugins are removed by the loader, so this guard is unnecessary and can be dropped.

♻️ Suggested change
-	// Skip processing if plugin is disabled
-	if !p.config.Enabled {
-		return req, nil, nil
-	}
Based on learnings, rely on the loader to exclude disabled plugins.
ui/components/sidebar.tsx (1)

366-377: Reuse shared MCPIcon to avoid duplicate SVGs.

There’s already a shared MCPIcon in ui/components/ui/icons.tsx (lines 1757-1772). Consider importing it instead of keeping a local copy so updates stay centralized.

♻️ Proposed refactor
-import { ChevronRight } from "lucide-react";
+import { ChevronRight } from "lucide-react";
+import { MCPIcon } from "@/components/ui/icons";
@@
-// Custom MCP Icon Component
-const MCPIcon = ({ className }: { className?: string }) => (
-	<svg
-		className={className}
-		fill="currentColor"
-		fillRule="evenodd"
-		height="1em"
-		style={{ flex: "none", lineHeight: 1 }}
-		viewBox="0 0 24 24"
-		width="1em"
-		xmlns="http://www.w3.org/2000/svg"
-		aria-label="MCP clients icon"
-	>
-		<title>MCP clients icon</title>
-		<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
-		<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
-	</svg>
-);
core/schemas/mcp.go (1)

26-34: Plugin pipeline provisioning mechanism looks good.

The addition of PluginPipelineProvider and ReleasePluginPipeline enables MCP tool execution to participate in the plugin pipeline (e.g., for nested tool calls in code-mode). The pool-style acquire/release pattern and json:"-" tags are appropriate.

Consider adding a brief note in the PluginPipelineProvider comment about nil-safety, since callers may need to check if this function is set before invoking it:

📝 Optional documentation enhancement
 	// PluginPipelineProvider returns a plugin pipeline for running MCP plugin hooks.
 	// Used when executeCode tool calls nested MCP tools to ensure plugins run for them.
 	// The plugin pipeline should be released back to the pool using ReleasePluginPipeline.
+	// Callers should check if this function is nil before invoking.
 	PluginPipelineProvider func() interface{} `json:"-"`
transports/bifrost-http/handlers/logging.go (1)

513-612: Optional: centralize MCP filter parsing to prevent drift.

parseMCPFiltersAndPagination and parseMCPFilters duplicate filter parsing; extracting a shared helper would reduce maintenance divergence over time.

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

232-244: Prefer bifrost.Ptr for pointer creation (consistency).

Use bifrost.Ptr(toolCall) instead of &toolCall to align with repo pointer-construction conventions.

♻️ Proposed change
-			toolMessage, err := h.toolManager.ExecuteChatMCPTool(ctx, &toolCall)
+			toolMessage, err := h.toolManager.ExecuteChatMCPTool(ctx, bifrost.Ptr(toolCall))
Based on learnings, prefer `bifrost.Ptr(...)` for pointer creation.
ui/app/workspace/mcp-gateway/views/mcpClientSheet.tsx (1)

404-434: Consider adding explicit validation for negative pricing values.

While min="0" is set on the input, browser enforcement can be bypassed. The onChange handler checks for isNaN(value) but doesn't explicitly reject negative values.

💡 Optional: Add negative value check
 onChange={(e) => {
   const value = e.target.value === "" ? undefined : parseFloat(e.target.value);
   const newPricing = { ...field.value };
-  if (value === undefined || isNaN(value)) {
+  if (value === undefined || isNaN(value) || value < 0) {
     delete newPricing[tool.name];
   } else {
     newPricing[tool.name] = value;
   }
   field.onChange(newPricing);
 }}
framework/logstore/migrations.go (1)

752-804: Consider if this migration is necessary given the struct definition.

The MCPToolLog struct in tables.go already includes the Cost field with its index. When migrationCreateMCPToolLogsTable runs CreateTable(&MCPToolLog{}), GORM will create the cost column and index automatically.

This migration would only be needed if:

  1. The cost field was added to the struct after the initial migration shipped
  2. There are existing deployments without the cost column

If both migrations always run together on fresh installs, the HasColumn and HasIndex checks will skip the redundant operations, so this is safe but slightly unnecessary overhead.

ui/app/workspace/mcp-logs/views/columns.tsx (2)

44-47: Consider using 24-hour format for consistency with log timestamps.

The timestamp format uses 12-hour time with AM/PM (hh:mm:ss A). For log exploration interfaces, 24-hour format (HH:mm:ss) is often preferred as it's more compact and commonly used in technical contexts.

Optional: Use 24-hour format
-			return <div className="text-xs">{moment(timestamp).format("YYYY-MM-DD hh:mm:ss A (Z)")}</div>;
+			return <div className="text-xs">{moment(timestamp).format("YYYY-MM-DD HH:mm:ss (Z)")}</div>;

89-98: Consider adding currency symbol for cost display.

The cost column displays raw numbers (e.g., 0.0010). Adding a currency symbol or prefix would improve clarity.

Optional: Add dollar sign prefix
-			return <div className="font-mono text-sm">{isValidNumber ? `${cost.toFixed(4)}` : "N/A"}</div>;
+			return <div className="font-mono text-sm">{isValidNumber ? `$${cost.toFixed(4)}` : "N/A"}</div>;
docs/openapi/paths/management/logging.yaml (1)

386-392: Consider adding "cost" to sort_by options.

The sort_by enum only includes [timestamp, latency], but the UI has a cost column and the MCP tool log table has a cost index. Adding cost as a sort option would provide feature parity.

Add cost to sort options
       - name: sort_by
         in: query
         description: Field to sort by
         schema:
           type: string
-          enum: [timestamp, latency]
+          enum: [timestamp, latency, cost]
           default: timestamp
ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx (1)

98-108: Error handling in delete action could leave sheet in inconsistent state.

The delete handler catches errors and shows a toast, but keeps the dialog open. Consider also providing visual feedback on the delete button (e.g., loading state) to prevent double-clicks during the async operation.

💡 Consider adding loading state
+const [isDeleting, setIsDeleting] = useState(false);
...
 <AlertDialogAction
+  disabled={isDeleting}
   onClick={async () => {
     try {
+      setIsDeleting(true);
       await handleDelete(log);
       onOpenChange(false);
     } catch (err) {
       const errorMessage = err instanceof Error ? err.message : "Failed to delete log";
       toast.error(errorMessage);
+    } finally {
+      setIsDeleting(false);
     }
   }}
 >
-  Delete
+  {isDeleting ? "Deleting..." : "Delete"}
 </AlertDialogAction>
docs/openapi/schemas/management/logging.yaml (1)

158-187: SearchMCPLogsResponse pagination structure differs from SearchLogsResponse.

The SearchMCPLogsResponse uses a nested pagination object with total_count, while the existing SearchLogsResponse (lines 267-280) uses flat total, offset, limit fields. This inconsistency may complicate frontend handling.

Consider aligning the pagination structure with the existing SearchLogsResponse for consistency, or document the intentional difference.

ui/app/workspace/mcp-logs/views/filters.tsx (1)

125-129: FILTER_OPTIONS with "Loading..." placeholder is clickable.

When filterDataLoading is true, the options show "Loading..." as a selectable item. Although isLoading check at line 199 prevents selection, the "Loading..." string appears in the list which could be confusing.

💡 Consider showing empty state or skeleton instead
 const FILTER_OPTIONS = {
   Status: Statuses,
-  "Tool Names": filterDataLoading ? ["Loading..."] : availableToolNames,
-  Servers: filterDataLoading ? ["Loading..."] : availableServerLabels,
+  "Tool Names": availableToolNames,
+  Servers: availableServerLabels,
 } as const;

Then in the render, show a loading skeleton when the category is loading and has no items:

{filterDataLoading && values.length === 0 ? (
  <CommandItem disabled>
    <div className="h-4 w-4 animate-spin rounded-full border border-t-transparent" />
    <span className="text-muted-foreground">Loading...</span>
  </CommandItem>
) : (
  values.map((value) => ...)
)}
ui/lib/types/logs.ts (2)

646-646: Consider using a typed status instead of plain string.

The status field is typed as string, but the valid values are constrained to "processing" | "success" | "error" (per OpenAPI). Using a union type would improve type safety:

status: "processing" | "success" | "error";

Alternatively, reference the existing Status type from @/lib/constants/logs if it's compatible.


733-744: getDefaultTimeRange duplicates existing utility logic.

This function replicates the logic from get24HoursAgo and getCurrentTimestamp. Consider refactoring to reuse existing utilities:

♻️ Refactor to reuse existing utilities
 getDefaultTimeRange: (): { startTime: number; endTime: number } => {
-  const endTime = Math.floor(Date.now() / 1000);
-  const date = new Date();
-  date.setHours(date.getHours() - 24);
-  const startTime = Math.floor(date.getTime() / 1000);
-  return { startTime, endTime };
+  return {
+    startTime: dateUtils.get24HoursAgo(),
+    endTime: dateUtils.getCurrentTimestamp(),
+  };
 },
ui/app/workspace/mcp-logs/views/mcpLogsTable.tsx (1)

49-56: Type assertion may cause issues if additional sortable columns are added.

The cast sort_by: id as "timestamp" | "latency" on line 53 is narrower than the Pagination type which allows "tokens" | "cost" as well. If sortable columns for tokens or cost are added later, this will cause a type mismatch.

Consider either:

  1. Updating the cast to match the full Pagination["sort_by"] type
  2. Creating a dedicated MCPLogsPagination type if MCP logs intentionally only support timestamp/latency sorting
Option 1: Match full type
 			onPaginationChange({
 				...pagination,
-				sort_by: id as "timestamp" | "latency",
+				sort_by: id as Pagination["sort_by"],
 				order: desc ? "desc" : "asc",
 			});
framework/configstore/tables/mcp.go (1)

138-143: Inconsistent JSON library usage between serialization and deserialization.

The ToolPricing field uses encoding/json for both marshaling (line 103) and unmarshaling (line 140), but other fields in AfterFind use sonic (lines 118, 124, 129, 134). While the existing code already has this mixed pattern in BeforeSave, consider using sonic consistently for performance and to avoid potential subtle behavioral differences between the two libraries.

♻️ Use sonic consistently
 	if c.ToolPricingJSON != "" {
-		if err := json.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
+		if err := sonic.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
 			return err
 		}
 	}
core/mcp/codemodereadfile.go (1)

284-308: Path traversal validation is good, but some checks are redundant.

The defensive path validation for .. is appropriate security practice. However, some checks may be redundant:

  • Line 295: strings.Contains(parts[1], "/") will always be false because parts is already the result of strings.Split(basePath, "/").
  • Line 303: Similarly, checking strings.Contains(basePath, "/") after confirming len(parts) != 2 (meaning no "/" was found) is also redundant.

The .. checks are the important security guards here.

♻️ Simplify the validation
 	parts := strings.Split(basePath, "/")
 	if len(parts) == 2 {
 		// Tool-level: "serverName/toolName"
-		// Validate that tool name doesn't contain additional path separators or traversal
-		if parts[1] == "" || strings.Contains(parts[1], "/") || strings.Contains(parts[1], "..") {
+		// Validate that tool name is not empty and doesn't contain traversal
+		if parts[1] == "" || strings.Contains(parts[1], "..") {
 			// Invalid tool name, treat as server-level
 			return parts[0], "", false
 		}
 		return parts[0], parts[1], true
 	}
 	// Server-level: "serverName"
-	// Validate server name doesn't contain path separators or traversal
-	if strings.Contains(basePath, "/") || strings.Contains(basePath, "..") {
+	// Validate server name doesn't contain traversal (no "/" since len(parts) != 2)
+	if strings.Contains(basePath, "..") {
 		// Invalid path
 		return "", "", false
 	}
framework/plugins/main.go (1)

115-131: Consider enforcing PluginConfig.Type in LoadPlugins.
PluginConfig.Type is currently unused, so a misconfigured plugin can silently load without the intended capability. A lightweight validation avoids surprises.

♻️ Suggested validation
		plugin, err := loader.LoadPlugin(pc.Path, pc.Config)
		if err != nil {
			return nil, err
		}
+		if pc.Type != "" && pc.Type != PluginTypeAuto && !ImplementsInterface(plugin, pc.Type) {
+			return nil, fmt.Errorf("plugin %s does not implement %s", pc.Name, pc.Type)
+		}
		plugins = append(plugins, plugin)
docs/openapi/openapi.json (3)

113102-113123: Add basic constraints for tool lists and pricing.

These arrays allow duplicates and pricing can be negative. Adding simple constraints improves validation and client UX.

🧩 Suggested constraints
 "tools_to_execute": {
   "type": "array",
   "items": {
     "type": "string"
   },
+  "uniqueItems": true,
   "description": "Include-only list for tools.\n[\"*\"] => all tools are included\n[] => no tools are included\n[\"tool1\", \"tool2\"] => include only the specified tools\n"
 },
 "tools_to_auto_execute": {
   "type": "array",
   "items": {
     "type": "string"
   },
+  "uniqueItems": true,
   "description": "List of tools that can be auto-executed without user approval.\nMust be a subset of tools_to_execute.\n[\"*\"] => all executable tools can be auto-executed\n[] => no tools are auto-executed\n[\"tool1\", \"tool2\"] => only specified tools can be auto-executed\n"
 },
 "tool_pricing": {
   "type": "object",
   "additionalProperties": {
     "type": "number",
-    "format": "double"
+    "format": "double",
+    "minimum": 0
   },
   "description": "Per-tool cost in USD for execution.\nKey is the tool name, value is the cost per execution.\nExample: {\"read_file\": 0.001, \"write_file\": 0.002}\n"
 }

119716-119834: Prefer array-style query params and non-negative bounds for filters.

The docs describe comma-separated lists, but the schema models them as plain strings, which hurts client generation. Consider array query params (style: form, explode: false), add enum for status, and set minimum: 0 on latency/pagination fields.

🧭 Example shape (apply similarly to other list params)
 {
   "name": "tool_names",
   "in": "query",
   "description": "Comma-separated list of tool names to filter by",
-  "schema": {
-    "type": "string"
-  }
+  "schema": {
+    "type": "array",
+    "items": { "type": "string" }
+  },
+  "style": "form",
+  "explode": false
 },
 ...
 "limit": {
   "type": "integer",
   "default": 50,
-  "maximum": 1000
+  "maximum": 1000,
+  "minimum": 1
 },
 "offset": {
   "type": "integer",
-  "default": 0
+  "default": 0,
+  "minimum": 0
 }

Also, please confirm these new endpoints match backend/stack changes so the OpenAPI stays consistent with the stacked PRs. As per coding guidelines, please verify stack alignment.


140196-140265: DRY: reference MCPToolLogEntry in SearchMCPLogsResponse.

The log entry schema is duplicated inline; using a $ref avoids drift and keeps docs consistent.

♻️ Suggested reuse
 "logs": {
   "type": "array",
   "items": {
-    "type": "object",
-    "description": "MCP tool execution log entry",
-    "properties": {
-      "id": { "type": "string", "description": "Unique identifier for the log entry" },
-      "llm_request_id": { "type": "string", "description": "Links to the LLM request that triggered this tool call" },
-      "timestamp": { "type": "string", "format": "date-time", "description": "When the tool execution started" },
-      "tool_name": { "type": "string", "description": "Name of the MCP tool that was executed" },
-      "server_label": { "type": "string", "description": "Label of the MCP server that provided the tool" },
-      "arguments": { "type": "object", "additionalProperties": true, "description": "Tool execution arguments" },
-      "result": { "type": "object", "additionalProperties": true, "description": "Tool execution result" },
-      "error_details": { "type": "object", "additionalProperties": true, "description": "Error details if execution failed" },
-      "latency": { "type": "number", "description": "Execution time in milliseconds" },
-      "cost": { "type": "number", "description": "Cost in dollars for this tool execution" },
-      "status": { "type": "string", "enum": ["processing","success","error"], "description": "Execution status" },
-      "created_at": { "type": "string", "format": "date-time", "description": "When the log entry was created" }
-    }
+    "$ref": "#/components/schemas/MCPToolLogEntry"
   }
 },
transports/bifrost-http/lib/config.go (2)

235-256: Plugin atomic pointers should be consistently initialized.

The Config struct declares multiple atomic pointers (BasePlugins, LLMPlugins, MCPPlugins, HTTPTransportPlugins), but at line 321, only LLMPlugins is initialized in LoadConfig. This inconsistency could lead to nil pointer dereferences if GetLoadedMCPPlugins() or GetLoadedHTTPTransportPlugins() are called before rebuildInterfaceCaches() runs.

Consider initializing all plugin atomic pointers consistently, or ensure that rebuildInterfaceCaches() is always called before any plugin access.

♻️ Suggested fix in LoadConfig (around line 318-322)
 config := &Config{
 	configPath: configFilePath,
 	Providers:  make(map[schemas.ModelProvider]configstore.ProviderConfig),
-	LLMPlugins: atomic.Pointer[[]schemas.LLMPlugin]{},
+	BasePlugins:          atomic.Pointer[[]schemas.BasePlugin]{},
+	LLMPlugins:           atomic.Pointer[[]schemas.LLMPlugin]{},
+	MCPPlugins:           atomic.Pointer[[]schemas.MCPPlugin]{},
+	HTTPTransportPlugins: atomic.Pointer[[]schemas.HTTPTransportPlugin]{},
 }

861-869: Consider a direct client conversion helper to reduce boilerplate.

The pattern of wrapping a single TableMCPClient in MCPConfig just to call ConvertTableMCPConfigToSchemas appears multiple times (lines 862-868, 905-911, 1840-1847). A dedicated ConvertTableMCPClientToSchemas helper would simplify this code.

♻️ Example helper function
// convertTableMCPClientToSchemas converts a single TableMCPClient to schemas.MCPClientConfig
func convertTableMCPClientToSchemas(client configstoreTables.TableMCPClient) *schemas.MCPClientConfig {
	schemasConfig := configstore.ConvertTableMCPConfigToSchemas(&configstoreTables.MCPConfig{
		ClientConfigs: []configstoreTables.TableMCPClient{client},
	})
	if schemasConfig != nil && len(schemasConfig.ClientConfigs) > 0 {
		return &schemasConfig.ClientConfigs[0]
	}
	return nil
}

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: 18

Caution

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

⚠️ Outside diff range comments (8)
ui/lib/types/mcp.ts (1)

16-27: Backend schema misalignment requires fixes before approval:

The frontend types do not properly align with the backend core schema:

  1. Field naming mismatch: Frontend client_id should map to backend schemas.MCPClientConfig.ID (not ClientID). The current mapping in the handler is correct, but the frontend comment is misleading.

  2. tool_pricing missing from core schema: tool_pricing is added to the frontend MCPClientConfig and UpdateMCPClientRequest, but it's absent from schemas.MCPClientConfig in the backend. This means:

    • tool_pricing is stored in the database (TableMCPClient.ToolPricing)
    • But it's not exposed through the runtime core schema used by the MCP manager
    • GetMCPClients() returns MCPClient.Config (core schema) without tool_pricing
    • The feature is incomplete: pricing data cannot be retrieved via the public API

Action required: Either add tool_pricing to schemas.MCPClientConfig in ./core/schemas/mcp.go and expose it through the runtime API, or remove it from the frontend types if pricing is database-only and not meant to be used at runtime.

plugins/semanticcache/main.go (1)

348-384: Update PreLLMHook return docs to reflect LLMPluginShortCircuit.

The signature now returns *schemas.LLMPluginShortCircuit, but the comment still references *schemas.BifrostResponse.

🔧 Doc fix
-//   - *schemas.BifrostResponse: Cached response if found, nil otherwise
+//   - *schemas.LLMPluginShortCircuit: Cached response/error wrapper if found, nil otherwise
plugins/mocker/main.go (1)

866-869: Add nil check for mockResponse.ChatResponse before accessing its fields.

At line 868, the code unconditionally accesses mockResponse.ChatResponse.Model for all request types. However, mockResponse.ChatResponse is only initialized when req.RequestType == schemas.ChatCompletionRequest (line 815). For ResponsesRequest types, mockResponse.ChatResponse remains nil, causing a nil pointer dereference panic when content.Model is set.

🔧 Proposed fix
-	if content.Model != nil {
-		mockResponse.ChatResponse.Model = *content.Model
-	}
+	if content.Model != nil && mockResponse.ChatResponse != nil {
+		mockResponse.ChatResponse.Model = *content.Model
+	}
framework/plugins/soplugin_test.go (1)

333-375: Avoid calling assert/require from goroutines.

testing.T isn't goroutine-safe; this can race or drop failures under -race. Collect errors in a channel and assert on the main goroutine instead.

Proposed pattern (collect errors on main goroutine)
	const numGoroutines = 10
	done := make(chan bool, numGoroutines)
+	errCh := make(chan error, numGoroutines)

	for i := 0; i < numGoroutines; i++ {
		go func(id int) {
			defer func() { done <- true }()

			// Call PreLLMHook
			pluginCtx, cancel := schemas.NewBifrostContextWithTimeout(ctx, 10*time.Second)
			defer cancel()
-			_, _, err := plugin.PreLLMHook(pluginCtx, req)
-			assert.NoError(t, err, "PreLLMHook should succeed in goroutine %d", id)
+			if _, _, err := plugin.PreLLMHook(pluginCtx, req); err != nil {
+				errCh <- fmt.Errorf("goroutine %d PreLLMHook: %w", id, err)
+			}

			// Call PostLLMHook
-			_, _, err = plugin.PostLLMHook(pluginCtx, resp, nil)
-			assert.NoError(t, err, "PostLLMHook should succeed in goroutine %d", id)
+			if _, _, err = plugin.PostLLMHook(pluginCtx, resp, nil); err != nil {
+				errCh <- fmt.Errorf("goroutine %d PostLLMHook: %w", id, err)
+			}

			// Call GetName
-			name := basePlugin.GetName()
-			assert.Equal(t, "Hello World Plugin", name, "GetName should return correct name in goroutine %d", id)
+			if name := basePlugin.GetName(); name != "Hello World Plugin" {
+				errCh <- fmt.Errorf("goroutine %d GetName mismatch: got %s", id, name)
+			}
		}(i)
	}

	// Wait for all goroutines to complete
	for i := 0; i < numGoroutines; i++ {
		<-done
	}
+	close(errCh)
+	for err := range errCh {
+		require.NoError(t, err)
+	}
plugins/semanticcache/search.go (1)

288-369: Add build tags to exclude semanticcache plugin from WASM builds, or provide a WASM variant.

buildStreamingResponseFromResult returns LLMPluginShortCircuit{Stream: ...}, but the Stream field only exists in the native build variant (plugin_native.go). The WASM variant (plugin_wasm.go) lacks this field. Since search.go has no build tags, it will attempt compilation in both native and WASM builds, causing a compilation failure in WASM builds. Add //go:build !tinygo && !wasm to the top of search.go, or create a WASM-compatible variant of this functionality.

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

879-901: Preserve store ToolManagerConfig when merging MCP config.

mergeMCPConfig overwrites config.MCPConfig with the file version and only merges ClientConfigs. If the store has ToolManagerConfig and the file omits it, those settings are dropped on startup despite the store being the source of truth. Merge the tool manager settings from the store unless the file explicitly overrides them.

🔧 Proposed fix
- tempMCPConfig := configData.MCP
- config.MCPConfig = tempMCPConfig
+ merged := *mcpConfig // start from store (authoritative when enabled)
+ if configData.MCP.ToolManagerConfig != nil {
+     merged.ToolManagerConfig = configData.MCP.ToolManagerConfig
+ }
+ config.MCPConfig = &merged
...
- config.MCPConfig.ClientConfigs = append(mcpConfig.ClientConfigs, clientConfigsToAdd...)
+ config.MCPConfig.ClientConfigs = append(mcpConfig.ClientConfigs, clientConfigsToAdd...)

1559-1601: MCPCatalog init ignores store pricing data.

buildMCPPricingDataFromStore is computed when the config store is enabled, but mcpcatalog.Init always uses file-based pricing. That discards DB updates to tool pricing after a restart. Prefer store data when available and fall back to file data otherwise.

🔧 Proposed fix
- mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
-     PricingData: buildMCPPricingDataFromFile(ctx, configData),
- }, logger)
+ mcpPricingData := buildMCPPricingDataFromFile(ctx, configData)
+ if config.ConfigStore != nil {
+     mcpPricingData = buildMCPPricingDataFromStore(ctx, config.ConfigStore)
+ }
+ mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
+     PricingData: mcpPricingData,
+ }, logger)
core/bifrost.go (1)

2902-2992: Use pre-hook count snapshot when running post-hooks.

Line 2974/3175/3430 uses len(*bifrost.llmPlugins.Load()) for RunPostHooks. If plugins are reloaded mid-flight, this can differ from the pre-hook snapshot and skip or overrun post-hooks. Use the preCount returned by RunLLMPreHooks (or len(pipeline.llmPlugins) for streaming) to keep symmetry.

🔧 Suggested fix
- pluginCount := len(*bifrost.llmPlugins.Load())
- resp, bifrostErr := pipeline.RunPostHooks(msg.Context, result, nil, pluginCount)
+ resp, bifrostErr := pipeline.RunPostHooks(msg.Context, result, nil, preCount)
- recoveredResp, recoveredErr := pipeline.RunPostHooks(ctx, nil, &bifrostErrVal, len(*bifrost.llmPlugins.Load()))
+ recoveredResp, recoveredErr := pipeline.RunPostHooks(ctx, nil, &bifrostErrVal, preCount)
- resp, bifrostErr := pipeline.RunPostHooks(ctx, result, err, len(*bifrost.llmPlugins.Load()))
+ resp, bifrostErr := pipeline.RunPostHooks(ctx, result, err, len(pipeline.llmPlugins))

Also applies to: 3029-3175, 3428-3432

🤖 Fix all issues with AI agents
In `@core/mcp/utils.go`:
- Around line 131-145: Sanitize and then validate the normalized tool name
before registering: after computing sanitizedToolName and prefixedToolName
(built from clientName and sanitizedToolName) call
validateNormalizedToolName(prefixedToolName) and if it returns false, skip
registering this tool (continue) so invalid names like "/" or ".." never get
stored in the tools map or propagate to VFS; keep the rest of the logic
(convertMCPToolToBifrostSchema, setting bifrostTool.Function.Name, and storing
in tools) unchanged for valid names.

In `@core/schemas/plugin_wasm.go`:
- Around line 5-8: The doc comment for LLMPluginShortCircuit incorrectly implies
a streaming short-circuit exists for WASM plugins; update the comment on
LLMPluginShortCircuit to state it can contain either a response (success
short-circuit) or an error (error short-circuit) and explicitly note that
streams/streaming short-circuits are not supported in the WASM plugin build so
there is no Stream field.

In `@docs/openapi/paths/management/logging.yaml`:
- Around line 335-340: The `status` query parameter is inconsistent: its
description says "Comma-separated list of statuses" but the schema's enum allows
a single value; fix both occurrences of the `status` parameter (the one in the
mcp-logs endpoint and the one in mcp-logs-stats) so they match: either remove
the enum from the `status` schema to allow comma-separated values (keep type:
string and optionally add a pattern or example) or change the description to
indicate a single allowed value and keep the enum; update the `status` parameter
definition for both endpoints (`status` query parameter) accordingly so
description and schema are consistent.

In `@docs/plugins/getting-started.mdx`:
- Around line 57-67: The function reference lists are inconsistent with the
lifecycle execution order: update the v1.3.x and v1.4.x+ function lists to use
the actual hook names used in the lifecycle (replace or add
PreLLMHook/PostLLMHook with PreHook/PostHook where applicable, and add
PreMCPHook/PostMCPHook to the v1.4.x+ list), ensuring entries for Init(config
any) error, GetName() string, TransportInterceptor(), PreHook/PreLLMHook,
PostHook/PostLLMHook, and PreMCPHook/PostMCPHook and Cleanup() error match the
lifecycle section names exactly.

In `@framework/configstore/migrations.go`:
- Around line 2490-2507: The VK config migration for vkConfig.ToolsToExecute
currently strips clientName prefixes without checking for collisions; update the
loop that builds updatedTools to detect if unprefixedTool already exists (use a
seen map or check updatedTools) and if a collision would occur, skip adding the
duplicate or remove the prefixed entry accordingly, only set needsUpdate when
the final updatedTools differs; ensure you reference vkConfig.ToolsToExecute,
clientName, updatedTools and needsUpdate so the resulting ToolsToExecute
contains no duplicate tool names after prefix stripping.

In `@framework/configstore/store.go`:
- Around line 38-43: MockConfigStore.GetMCPConfig currently returns
*schemas.MCPConfig but the Store interface requires *tables.MCPConfig; update
the mock to satisfy the interface by either storing *tables.MCPConfig internally
or converting the stored schemas.MCPConfig to *tables.MCPConfig before
returning. Modify MockConfigStore.GetMCPConfig in
transports/bifrost-http/lib/config_test.go to produce a *tables.MCPConfig
(mirror the shape used by RDBConfigStore) — you can implement a reverse of
ConvertTableMCPConfigToSchemas or construct a tables.MCPConfig from the existing
schemas.MCPConfig fields and return (*tables.MCPConfig, error) to match the
signature. Ensure the mock’s internal field type and any setup in tests are
adjusted accordingly so method signatures align.

In `@framework/mcpcatalog/main.go`:
- Around line 31-44: The Init function currently assigns config.PricingData
directly to the MCPCatalog.pricingData, which aliases the caller's map and
allows external mutations; modify Init to defensively copy the pricing map when
config != nil && config.PricingData != nil by allocating a new MCPPricingData
map and copying each key/value from config.PricingData into it before setting
MCPCatalog.pricingData; keep the existing nil/checks and ensure the new map is
used even when the source map is empty so the catalog owns its data and avoids
external races with methods that rely on internal locking.

In `@plugins/governance/main.go`:
- Around line 515-519: The code builds tool identifiers using
fmt.Sprintf("%s-%s", vkMcpConfig.MCPClient.Name, tool) which uses a dash but the
MCP parser expects clientName/toolName; update the construction of
executeOnlyTools (where executeOnlyTools is appended) to use a slash delimiter
(clientName + "/" + tool) so the exact-string matching in the MCP context parser
and handlers (e.g., mcpserver.go, config.go) will succeed; locate the append
that references vkMcpConfig.ToolsToExecute and vkMcpConfig.MCPClient.Name and
replace the "-" with "/" accordingly.

In `@plugins/logging/main.go`:
- Around line 807-811: You're holding p.mu while performing DB I/O and invoking
callbacks (p.mcpToolLogCallback and calls to FindMCPToolLog), which can
deadlock; change the pattern to capture the callback and any required state
under p.mu (e.g., store p.mcpToolLogCallback into a local variable and copy any
identifiers needed), then release the lock before calling the callback or
calling FindMCPToolLog so all DB I/O and user callback invocation happens
outside the critical section; apply the same change to the other occurrence that
touches FindMCPToolLog (the block referenced around 925-935) so no DB or
callback runs while p.mu is held.

In `@plugins/logging/utils.go`:
- Around line 129-143: SearchMCPToolLogs and GetMCPToolLogStats call
p.plugin.store without guarding for an uninitialized store and can panic; add
the same nil-check used in
DeleteLog/DeleteLogs/DeleteMCPToolLogs/GetAvailableToolNames/GetAvailableServerLabels
(if p.plugin == nil || p.plugin.store == nil) at the top of both
SearchMCPToolLogs and GetMCPToolLogStats and return an appropriate nil result
with no error (or the same error pattern those methods use) when the plugin or
store is nil so logging-disabled scenarios are safe.

In `@transports/bifrost-http/lib/config_test.go`:
- Around line 460-467: The mock's UpdateMCPClientConfig currently only appends
to m.mcpClientConfigUpdates and does not change the in-memory state, causing
GetMCPConfig to return stale data; modify MockConfigStore.UpdateMCPClientConfig
so after appending the update to m.mcpClientConfigUpdates it also finds or
creates the corresponding entry in m.mcpConfig and replaces/updates the MCP
client's config (matching the behavior used in CreateMCPClientConfig and
ensuring subsequent GetMCPConfig reads reflect the updated tables.TableMCPClient
for the given id).

In `@transports/bifrost-http/lib/config.go`:
- Around line 1495-1507: The AddMCPClient flow fetches the stored client config
from the DB but never copies its ToolPricing into the in-memory
MCPConfig.ClientConfigs, so later RemoveMCPClient iterates an empty
removedClientConfig.ToolPricing and fails to delete catalog pricing; fix by
copying storedConfig.ToolPricing into MCPConfig.ClientConfigs[<client id or
index>] (the same place you set ClientID/Name/etc after the DB read in
AddMCPClient) immediately after the DB retrieval, or alternatively change
RemoveMCPClient to load the current client row from the DB and use that row's
ToolPricing when deleting catalog pricing (reference functions: AddMCPClient,
RemoveMCPClient, MCPConfig.ClientConfigs, ToolPricing).

In `@transports/bifrost-http/server/server.go`:
- Around line 727-764: ReloadPlugin and RemovePlugin currently
replace/unregister plugin instances without calling the plugin's Cleanup method,
causing background goroutines/resources to leak; modify ReloadPlugin and
RemovePlugin to capture the existing plugin instance before registering/removing
the new one (use the plugin name lookup via s.Config or whatever returns the
current plugin), call its Cleanup() after the new registration/unregistration
completes (or before if safer for atomicity), and log a warning if Cleanup
returns an error; ensure you reference the existing symbols ReloadPlugin,
RemovePlugin, InstantiatePlugin, RegisterPlugin, reloadBifrostPlugins, and the
plugin Cleanup() method so the change is applied where the plugin instance is
swapped out.
- Around line 642-655: Reloading builds a new MCPConfig via
configstore.ConvertTableMCPConfigToSchemas but doesn't reattach runtime hooks
(like FetchNewRequestIDFunc) set during bootstrap, causing non‑unique MCP tool
call IDs after reload; after any conversion (e.g., where mcpConfig is created in
this block and in ReloadClientConfigFromConfigStore and reloadBifrostPlugins)
reattach the runtime hooks before using the MCPConfig by assigning the same
FetchNewRequestIDFunc (and any other runtime callbacks) onto the new mcpConfig —
either add a small helper attachMCPRuntimeHooks(mcpConfig) that sets
mcpConfig.FetchNewRequestIDFunc = lib.FetchNewRequestIDFunc (or the equivalent
bootstrap value) and call it right after
configstore.ConvertTableMCPConfigToSchemas, or inline the assignment in
ReloadClientConfigFromConfigStore and reloadBifrostPlugins.

In `@ui/app/workspace/config/views/securityView.tsx`:
- Around line 243-247: The external Link component instance rendering the
documentation link (the Link with
href="https://docs.getbifrost.ai/features/governance/virtual-keys" and
target="_blank" in securityView.tsx) needs a rel="noopener noreferrer" attribute
to prevent reverse-tabnabbing; update that Link invocation to include
rel="noopener noreferrer" so the attribute is forwarded to the underlying anchor
element.

In `@ui/app/workspace/mcp-logs/page.tsx`:
- Around line 282-305: The stats update blindly increments metrics on any
terminal log update causing double-counting; fix it by checking the log's
previous terminal state before mutating stats: retrieve the prior log entry
(e.g., from the same array/state where new logs are stored) and only update
setStats when the prior status was non-terminal and the incoming log.status is
terminal, otherwise if prior status was already "success" or "error" skip
increments to total_executions, success_rate, average_latency and total_cost;
use the existing symbols setStats, prevStats, newStats, log.status,
total_executions, success_rate, average_latency and total_cost to implement this
guard (or, alternatively, recalc stats from the full logs array instead of
incrementing when you detect a terminal update).

In `@ui/app/workspace/mcp-logs/views/emptyState.tsx`:
- Around line 85-247: The memoized "examples" created by useMemo calls
getExampleBaseUrl() but doesn't track its dependency; compute the base URL
outside the memo and use it in the dependency array (e.g., const baseUrl =
getExampleBaseUrl(); then useMemo(() => { ... }, [baseUrl])) or directly include
getExampleBaseUrl()'s result in the deps so the examples object updates when
window.location changes; update references in the examples generation (symbols:
examples, useMemo, getExampleBaseUrl, baseUrl) accordingly.

In `@ui/lib/store/apis/mcpLogsApi.ts`:
- Around line 46-50: The current truthy checks when building the params object
(e.g., the lines checking filters.start_time, filters.end_time,
filters.min_latency, filters.max_latency, filters.content_search) erroneously
skip valid 0 values for min_latency/max_latency; update the conditional checks
for min_latency and max_latency in both query-builder blocks (the occurrences
around the shown diff and the similar block at lines 83-87) to use strict !==
undefined (e.g., if (filters.min_latency !== undefined) params.min_latency =
filters.min_latency;) so zero can be sent while keeping the other checks
unchanged.
🧹 Nitpick comments (24)
core/mcp/clientmanager.go (1)

221-228: Shallow copy of tool.Function may cause unintended side effects.

When creating updatedTool as a value copy of tool, the Function field (which is a pointer) still references the original ChatToolFunction. Assigning updatedTool.Function.Name = newToolName mutates the original tool's function name in-place rather than creating an independent copy.

While this may work correctly in the current flow since the old ToolMap is being replaced entirely, it could cause subtle bugs if:

  1. Any code holds a reference to the old tool
  2. The iteration order or concurrent access changes

Consider creating a deep copy of the Function if isolation is intended:

♻️ Suggested fix for deep copy
 			// Update the tool's function name if it's a function tool
 			if tool.Function != nil {
 				updatedTool := tool
-				updatedTool.Function.Name = newToolName
+				// Deep copy Function to avoid mutating the original
+				funcCopy := *tool.Function
+				funcCopy.Name = newToolName
+				updatedTool.Function = &funcCopy
 				newToolMap[newToolName] = updatedTool
 			} else {
 				newToolMap[newToolName] = tool
 			}
ui/app/workspace/logs/page.tsx (2)

58-59: Avoid calling getDefaultTimeRange() twice for initial defaults.

Each call computes fresh timestamps independently, which could result in slightly different values. Compute once and destructure to ensure consistency.

♻️ Suggested refactor

You can compute the default once at the top of the component:

// At component level, before useQueryStates
const initialTimeRange = useMemo(() => dateUtils.getDefaultTimeRange(), []);

Then use it in the parser configuration:

-			start_time: parseAsInteger.withDefault(getDefaultTimeRange().startTime),
-			end_time: parseAsInteger.withDefault(getDefaultTimeRange().endTime),
+			start_time: parseAsInteger.withDefault(initialTimeRange.startTime),
+			end_time: parseAsInteger.withDefault(initialTimeRange.endTime),

Note: Since nuqs parsers are typically evaluated once, using useMemo with an empty dependency array ensures consistent initialization while still computing fresh values on mount.


73-111: Unnecessary dependency urlState.start_time in effect.

Only urlState.end_time is used in refreshDefaultsIfStale(). Including start_time causes the effect to re-register event listeners unnecessarily when only start_time changes.

♻️ Suggested refactor
-	}, [urlState.start_time, urlState.end_time, setUrlState]);
+	}, [urlState.end_time, setUrlState]);

If the linter complains about setUrlState, note that it's a stable reference from nuqs and can typically be omitted from dependencies as well.

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

532-559: Prefer bifrost.Ptr for MCP filter pointers.
The new MCP filter parsing assigns &t/&f/&val directly. Repo convention prefers bifrost.Ptr(...) for optional fields; consider applying this in both MCP parsing helpers for consistency.

♻️ Proposed refactor
 import (
 	"context"
 	"fmt"
 	"strconv"
 	"strings"
 	"time"

+	bifrost "github.com/maximhq/bifrost/core"
 	"github.com/bytedance/sonic"
 	"github.com/fasthttp/router"
 	"github.com/maximhq/bifrost/core/schemas"
 	"github.com/maximhq/bifrost/framework/configstore/tables"
 	"github.com/maximhq/bifrost/framework/logstore"
 	"github.com/maximhq/bifrost/plugins/logging"
 	"github.com/maximhq/bifrost/transports/bifrost-http/lib"
 	"github.com/valyala/fasthttp"
 )
@@
-		filters.StartTime = &t
+		filters.StartTime = bifrost.Ptr(t)
@@
-		filters.EndTime = &t
+		filters.EndTime = bifrost.Ptr(t)
@@
-		filters.MinLatency = &f
+		filters.MinLatency = bifrost.Ptr(f)
@@
-		filters.MaxLatency = &val
+		filters.MaxLatency = bifrost.Ptr(val)
@@
-		filters.StartTime = &t
+		filters.StartTime = bifrost.Ptr(t)
@@
-		filters.EndTime = &t
+		filters.EndTime = bifrost.Ptr(t)
@@
-		filters.MinLatency = &f
+		filters.MinLatency = bifrost.Ptr(f)
@@
-		filters.MaxLatency = &val
+		filters.MaxLatency = bifrost.Ptr(val)

Based on learnings, prefer bifrost.Ptr for pointer creation.

Also applies to: 633-659

examples/plugins/hello-world-wasm-go/types.go (1)

34-34: LGTM!

The type change from *schemas.PluginShortCircuit to *schemas.LLMPluginShortCircuit correctly aligns this example plugin with the platform-wide type rename. The JSON tag remains unchanged, preserving serialization compatibility.

Consider renaming the local struct types (PreHookInput, PreHookOutput, PostHookInput, PostHookOutput) to include the LLM prefix (e.g., PreLLMHookInput) for consistency with the broader hook rename from PreHook/PostHook to PreLLMHook/PostLLMHook. This could be addressed in a follow-up to maintain naming consistency across the example.

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

661-661: LGTM! Tool pricing schema addition.

The tool_pricing field correctly validates a mapping of tool names to non-negative costs. The schema is appropriately optional and follows the existing patterns in this file.

Consider adding validation to reject empty string keys if empty tool names are invalid:

🔧 Optional: Reject empty tool name keys
-	tool_pricing: z.record(z.string(), z.number().min(0, "Cost must be non-negative")).optional(),
+	tool_pricing: z.record(z.string().min(1, "Tool name cannot be empty"), z.number().min(0, "Cost must be non-negative")).optional(),
plugins/jsonparser/plugin_test.go (2)

75-77: Prefer bifrost.Ptr(account) over &account for pointer creation.

This aligns with the repo-wide pointer-construction convention and keeps tests consistent.

♻️ Suggested change
-		Account:    &account,
+		Account:    bifrost.Ptr(account),
Based on learnings, please prefer `bifrost.Ptr()` over `&` for pointers.

174-176: Use bifrost.Ptr(account) here as well for consistency.

Matches the repository convention for pointer creation.

♻️ Suggested change
-		Account:    &account,
+		Account:    bifrost.Ptr(account),
Based on learnings, please prefer `bifrost.Ptr()` over `&` for pointers.
framework/logstore/migrations.go (1)

690-735: Consider adding missing MCP tool-log indexes (timestamp/created_at).

The migration says it explicitly creates tag-declared indexes, but timestamp and created_at (both tagged as indexed) aren’t included. Add explicit indexes (or adjust the comment) to keep it consistent.

♻️ Proposed patch
@@
 			if !migrator.HasIndex(&MCPToolLog{}, "idx_mcp_logs_status") {
 				if err := migrator.CreateIndex(&MCPToolLog{}, "idx_mcp_logs_status"); err != nil {
 					return fmt.Errorf("failed to create index on status: %w", err)
 				}
 			}
+
+			if !migrator.HasIndex(&MCPToolLog{}, "Timestamp") {
+				if err := migrator.CreateIndex(&MCPToolLog{}, "Timestamp"); err != nil {
+					return fmt.Errorf("failed to create index on timestamp: %w", err)
+				}
+			}
+
+			if !migrator.HasIndex(&MCPToolLog{}, "CreatedAt") {
+				if err := migrator.CreateIndex(&MCPToolLog{}, "CreatedAt"); err != nil {
+					return fmt.Errorf("failed to create index on created_at: %w", err)
+				}
+			}

Please verify the intended index names for Timestamp/CreatedAt with GORM’s migrator behavior.

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

733-744: Consider reusing existing date utility functions.

The implementation is correct, but you could reduce duplication by reusing the existing helpers:

♻️ Optional refactor to reuse existing functions
 	getDefaultTimeRange: (): { startTime: number; endTime: number } => {
-		const endTime = Math.floor(Date.now() / 1000);
-		const date = new Date();
-		date.setHours(date.getHours() - 24);
-		const startTime = Math.floor(date.getTime() / 1000);
+		const endTime = dateUtils.getCurrentTimestamp();
+		const startTime = dateUtils.get24HoursAgo();
 		return { startTime, endTime };
 	},
ui/app/workspace/mcp-logs/views/columns.tsx (1)

11-19: Consider extracting getValidatedStatus to a shared utility.

This helper function is duplicated in mcpLogDetailsSheet.tsx. Consider extracting it to a shared location (e.g., @/lib/utils/logs.ts) to maintain consistency and reduce duplication.

ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx (1)

50-58: Duplicate getValidatedStatus helper - same observation as columns.tsx.

This is the same helper function duplicated from columns.tsx. Consider extracting to a shared utility to maintain consistency.

ui/app/workspace/mcp-logs/views/mcpLogsTable.tsx (1)

44-57: Type narrowing for sort_by could be fragile.

The cast to "timestamp" | "latency" on line 53 narrows the type, but Pagination.sort_by accepts "timestamp" | "latency" | "tokens" | "cost". If a column with id="cost" becomes sortable, this cast would incorrectly narrow the type.

♻️ Suggested fix to use the full Pagination type
 		if (newSorting.length > 0) {
 			const { id, desc } = newSorting[0];
 			onPaginationChange({
 				...pagination,
-				sort_by: id as "timestamp" | "latency",
+				sort_by: id as Pagination["sort_by"],
 				order: desc ? "desc" : "asc",
 			});
 		}
core/schemas/plugin_native.go (1)

22-27: Minor documentation inconsistency in the Error field comment.

The comment "can set AllowFallbacks field" is copied from LLMPluginShortCircuit, but fallbacks are an LLM-specific concept that may not apply to MCP tool execution. Consider updating the comment to reflect MCP-specific behavior.

Otherwise, the struct design is appropriate — omitting the Stream field makes sense since MCP tool execution is synchronous.

📝 Suggested documentation fix
 // MCPPluginShortCircuit represents a plugin's decision to short-circuit the normal flow.
 // It can contain either a response (success short-circuit), or an error (error short-circuit).
 type MCPPluginShortCircuit struct {
 	Response *BifrostMCPResponse // If set, short-circuit with this response (skips MCP call)
-	Error    *BifrostError       // If set, short-circuit with this error (can set AllowFallbacks field)
+	Error    *BifrostError       // If set, short-circuit with this error
 }
ui/app/workspace/mcp-logs/views/emptyState.tsx (1)

44-47: Consider handling clipboard API unavailability.

The navigator.clipboard.writeText call may fail in non-secure contexts or older browsers. Consider adding error handling to provide user feedback on failure.

🔧 Suggested improvement
 const copyToClipboard = () => {
-	navigator.clipboard.writeText(code);
-	toast.success("Copied to clipboard");
+	navigator.clipboard.writeText(code)
+		.then(() => toast.success("Copied to clipboard"))
+		.catch(() => toast.error("Failed to copy to clipboard"));
 };
transports/bifrost-http/handlers/mcpinference.go (1)

48-79: Consider adding nil check for client.

If h.client is nil (e.g., due to misconfiguration), ExecuteChatMCPTool would panic. While this may be caught at initialization time, a defensive check could provide a clearer error.

🛡️ Defensive nil check
 func (h *MCPInferenceHandler) executeChatMCPTool(ctx *fasthttp.RequestCtx) {
+	if h.client == nil {
+		SendError(ctx, fasthttp.StatusInternalServerError, "MCP client not initialized")
+		return
+	}
+
 	var req schemas.ChatAssistantMessageToolCall

Otherwise, the implementation correctly validates required fields, converts context with proper cleanup via defer cancel(), and uses the established error handling patterns.

plugins/mocker/main.go (2)

493-497: Redundant Enabled guard in PreLLMHook.

Disabled plugins aren’t added to the chain, so this check can be removed to simplify the flow. Based on learnings, disabled plugins are excluded before hook invocation.


801-808: Use bifrost.Ptr for string pointers (repo convention).

Prefer bifrost.Ptr(...) over address-of for string pointers to align with repository conventions. Based on learnings, pointer creation should use bifrost.Ptr for consistency.

♻️ Suggested refactor
-	static := "stop"
-	finishReason = &static
+	finishReason = bifrost.Ptr("stop")
-	finishReason := "stop"
+	finishReason := "stop"
 ...
-							FinishReason: &finishReason,
+							FinishReason: bifrost.Ptr(finishReason),

Also applies to: 1011-1033

framework/plugins/main.go (1)

8-134: Document PluginConfig.Name and Type fields or wire them through to the loader.

LoadPlugins only passes Path and Config to the loader, leaving Name and Type unused. While Type has a comment indicating it's auto-detected, Name lacks any explanation. Either wire these fields through to LoadPlugin if they should influence plugin routing, or explicitly document them as metadata-only reserved for future use.

docs/openapi/openapi.json (4)

113116-113122: Disallow negative tool pricing values.

Pricing should be non‑negative; add a lower bound to the pricing map values (and replicate across all MCP client config copies).

♻️ Proposed fix
"tool_pricing": {
  "type": "object",
  "additionalProperties": {
    "type": "number",
    "format": "double",
+   "minimum": 0
  },
  "description": "Per-tool cost in USD for execution.\nKey is the tool name, value is the cost per execution.\nExample: {\"read_file\": 0.001, \"write_file\": 0.002}\n"
}

Also applies to: 113331-113337, 113629-113635, 138680-138686, 138795-138801


119716-119747: Use array-typed query parameters for list filters.

These are documented as comma‑separated lists, but the schema is string. For generated clients, this will surface as a single string type. Consider modeling as arrays with style: form + explode: false to align with MCPToolLogSearchFilters and improve client ergonomics. Please verify expected server parsing (CSV string vs array).

♻️ Example (apply to tool_names/server_labels/status/llm_request_ids)
{
  "name": "tool_names",
  "in": "query",
  "description": "Comma-separated list of tool names to filter by",
  "schema": {
-   "type": "string"
+   "type": "array",
+   "items": { "type": "string" }
  },
+ "style": "form",
+ "explode": false
}

Also applies to: 120345-120376


120136-120148: Require at least one log ID in delete requests.

Avoid empty delete requests by adding a minItems: 1 constraint.

♻️ Proposed fix
"ids": {
  "type": "array",
  "items": { "type": "string" },
+ "minItems": 1,
  "description": "Array of log IDs to delete"
}

119842-119955: Prefer $ref reuse for MCP log entry + stats schemas.

The log entry and stats shapes are duplicated inline in the response and again in components. Consider referencing #/components/schemas/MCPToolLogEntry and #/components/schemas/MCPToolLogStats to prevent drift.

♻️ Example replacements
"logs": {
  "type": "array",
  "items": {
-   "type": "object",
-   "description": "MCP tool execution log entry",
-   "properties": { ... }
+   "$ref": "#/components/schemas/MCPToolLogEntry"
  }
},
"stats": {
- "type": "object",
- "description": "MCP tool log statistics",
- "properties": { ... }
+ "$ref": "#/components/schemas/MCPToolLogStats"
}

Also applies to: 140200-140305

transports/bifrost-http/server/server.go (1)

259-262: Avoid shadowing enterprisePlugins.

loadCustomPlugins declares a local enterprisePlugins that shadows the package‑level list, which can drift over time. Consider reusing the global list or renaming the local variable.

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: 15

Caution

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

⚠️ Outside diff range comments (6)
plugins/maxim/main.go (2)

480-488: Guard PostLLMHook against nil context to prevent panic.
ctx.Value(...) will panic when ctx is nil; add an early return.

🐛 Proposed fix
 func (plugin *Plugin) PostLLMHook(ctx *schemas.BifrostContext, result *schemas.BifrostResponse, bifrostErr *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
+	if ctx == nil {
+		return result, bifrostErr, nil
+	}
 	// Get effective log repo ID for this request
 	effectiveLogRepoID := plugin.getEffectiveLogRepoID(ctx)
 	if effectiveLogRepoID == "" {
 		return result, bifrostErr, nil
 	}

588-600: Avoid tagging when generation/trace IDs are missing.
AddTagToGeneration/Trace runs even if IDs are empty, which can create invalid log entries.

✅ Proposed fix
-		logger.AddTagToGeneration(generationID, "model", string(model))
-		logger.AddTagToTrace(traceID, "model", string(model))
+		if hasGenerationID && generationID != "" {
+			logger.AddTagToGeneration(generationID, "model", string(model))
+		}
+		if hasTraceID && traceID != "" {
+			logger.AddTagToTrace(traceID, "model", string(model))
+		}
ui/lib/types/config.ts (1)

149-176: Add mcp_tool_execution to UI handler mappings.

The new mcp_tool_execution request type was added to the RequestType union but is missing from critical handler arrays:

  • RequestTypeLabels (ui/lib/constants/logs.ts)
  • RequestTypeColors (ui/lib/constants/logs.ts)
  • RequestTypes array (ui/lib/constants/logs.ts) — if this type should be filterable in logs UI

Without these entries, logs showing mcp_tool_execution requests will display "unknown" labels and lack color styling in components like logDetailsSheet.tsx and columns.tsx.

framework/plugins/soloader.go (1)

23-31: Guard URL detection against local paths starting with "http".

strings.HasPrefix(dp.Path, "http") incorrectly treats local file paths like http-plugins/foo.so as URLs. Explicitly check for http:// or https:// schemes instead, consistent with URL validation patterns used elsewhere in the codebase.

🐛 Suggested fix
- if strings.HasPrefix(dp.Path, "http") {
+ if strings.HasPrefix(dp.Path, "http://") || strings.HasPrefix(dp.Path, "https://") {
transports/bifrost-http/handlers/plugins.go (1)

192-192: Minor typo: double space.

🔧 Suggested fix
-		SendError(ctx, 400, "Plugins creation is  not supported when configstore is disabled")
+		SendError(ctx, 400, "Plugins creation is not supported when configstore is disabled")
transports/bifrost-http/server/server.go (1)

478-486: Guard ReloadProxyConfig against nil config to avoid panic.

If the store returns nil (proxy removed/disabled), the current implementation will dereference nil. Handle nil by clearing in‑memory config and returning early.

🔒 Proposed fix
 func (s *BifrostHTTPServer) ReloadProxyConfig(ctx context.Context, config *configstoreTables.GlobalProxyConfig) error {
 	if s.Config == nil {
 		return fmt.Errorf("config not found")
 	}
+	if config == nil {
+		s.Config.ProxyConfig = nil
+		logger.Info("proxy configuration cleared")
+		return nil
+	}
 	// Store the proxy config in memory for use by components that need it
 	s.Config.ProxyConfig = config
 	logger.Info("proxy configuration reloaded: enabled=%t, type=%s", config.Enabled, config.Type)
 	return nil
 }
🤖 Fix all issues with AI agents
In `@core/mcp/clientmanager.go`:
- Around line 234-236: The code keeps two separate name fields (client.Name and
client.ExecutionConfig.Name) that drift out of sync; remove the redundant
client.Name field and update all references to use client.ExecutionConfig.Name
as the single source of truth (or, if you prefer to keep both, add a helper like
syncName(client) and call it wherever names are changed to ensure client.Name
and client.ExecutionConfig.Name are updated together). Specifically, delete the
client.Name field declaration and any writes/reads of client.Name (including the
conditional update at the site updating client.Name), or implement and call a
central setter (e.g., SetClientName) that assigns both
client.ExecutionConfig.Name and client.Name so they cannot diverge. Ensure tests
and any serialization rely on ExecutionConfig.Name after the change.

In `@core/mcp/utils.go`:
- Around line 187-193: Update the inline comments that reference example tool
names to use the new "-" separator (e.g., "calculator-add" instead of
"calculator/add"); locate the comment near the check that calls
stripClientPrefix(toolName, config.Name) and the similar comment block around
lines corresponding to the later check (referenced in the review as also
applying to 219-226) and replace the example strings so they reflect the current
ToolMap naming convention using "-" and ensure any explanatory text matches that
format.

In `@docs/openapi/openapi.json`:
- Around line 113045-113055: The MCP client config schema is missing a required
list, so generators treat client_id and name as optional; update the MCP client
config object used by the POST /api/mcp/client request schema and the GET
response schemas to include "required": ["client_id","name","connection_type"]
so that client_id, name and connection_type are enforced as mandatory fields
(apply this change to the MCP client config schema object referenced by the POST
/api/mcp/client request and corresponding GET response definitions).

In `@docs/openapi/schemas/management/logging.yaml`:
- Around line 158-187: The OpenAPI schema places total_count in pagination but
the backend puts it in stats (stats.total_executions); fix by aligning model or
schema: either add TotalCount to the PaginationOptions struct
(framework/logstore/tables.go -> PaginationOptions) and have SearchMCPToolLogs
(rdb.go) populate that field with the computed totalCount, or change the OpenAPI
SearchMCPLogsResponse schema (SearchMCPLogsResponse) to remove
pagination.total_count and instead reference stats.total_executions as the
source of truth; pick one approach and update relevant code or schema so
total_count is consistently stored and returned.

In `@docs/openapi/schemas/management/mcp.yaml`:
- Around line 35-37: The OpenAPI schema field name client_id mismatches the Go
struct tag json:"id" on MCPClientConfig, causing deserialization failures; fix
this by updating the MCPClientConfig struct's JSON tag from "id" to "client_id"
(or alternatively change the OpenAPI schema field to id if you prefer
schema-driven naming) so the JSON key and Go tag match, then run unit tests and
go vet to ensure no other code expects the old "id" tag and update any consumers
accordingly.

In `@examples/plugins/http-transport-only/README.md`:
- Around line 124-126: Reword the three bullets in the Notes list to avoid
repeating the word “only”; specifically update the lines that read "This plugin
operates at the HTTP transport layer only", "Works only when using bifrost-http,
not when using Bifrost as a Go SDK", and "Rate limiter is in-memory (resets on
restart)" so they flow better and remove redundant “only” (e.g., "This plugin
operates at the HTTP transport layer", "Works with bifrost-http; not available
when using Bifrost as a Go SDK", and keep "Rate limiter is in-memory (resets on
restart)" or similar concise phrasing).

In `@examples/plugins/multi-interface/main.go`:
- Around line 96-117: The HTTPTransportPreHook writes into req.Headers but
req.Headers can be nil and cause a panic; in HTTPTransportPreHook check if
req.Headers is nil and initialize it to an empty map before assigning the custom
header (e.g., if req.Headers == nil then set req.Headers =
make(map[string]string)); keep the rest of the logic (tracking, ctx.SetValue,
logging) unchanged and only add this nil-check/initialization prior to using
req.Headers.
- Around line 119-152: The HTTPTransportPostHook must guard against a nil resp
and nil resp.Headers before mutating them: in HTTPTransportPostHook check if
resp is nil and return nil (or create a new schemas.HTTPResponse if appropriate
for your flow), and ensure resp.Headers is initialized (e.g., set resp.Headers =
make(map[string]string) when resp.Headers == nil) before writing the Duration
and Interfaces headers; update all places that write resp.Headers (the Duration
and Interfaces assignments) to run after these nil checks/initializations.
- Around line 37-39: The requestCount field is not protected for concurrent
access by hooks; change all increments to use atomic.AddInt64(&requestCount, 1)
and all reads to use atomic.LoadInt64(&requestCount), and add the import for
sync/atomic, or alternatively add a sync.Mutex field and wrap increments/reads
of requestCount with Lock/Unlock; locate usages of requestCount in hook handler
functions and replace direct reads/writes with the atomic or mutex operations to
avoid data races.

In `@framework/logstore/rdb.go`:
- Around line 448-518: SearchMCPToolLogs currently ignores "cost" sorting
because the switch on pagination.SortBy lacks a "cost" case; update the switch
in SearchMCPToolLogs to handle pagination.SortBy == "cost" by setting
orderClause = "cost " + direction (alongside existing "timestamp" and "latency"
cases) so cost-sorted queries use the correct ORDER BY; ensure the column name
"cost" matches the MCPToolLog model/DB schema and leave the rest of the
pagination logic unchanged.

In `@framework/plugins/main.go`:
- Around line 8-22: The local PluginType type and its constants (PluginType,
PluginTypeAuto, PluginTypeLLM, PluginTypeMCP, PluginTypeHTTPTransport,
PluginTypeObservability) should be removed and replaced by the centralized
schemas.PluginType; add the missing constants PluginTypeAuto,
PluginTypeHTTPTransport, and PluginTypeObservability to core/schemas/plugin.go
alongside the existing PluginTypeLLM and PluginTypeMCP, then update this package
to import and use schemas.PluginType (and the new constants) everywhere the
local type/consts were referenced (e.g., places using PluginTypeAuto,
PluginTypeHTTPTransport, PluginTypeObservability, PluginTypeLLM, PluginTypeMCP).

In `@framework/plugins/soplugin.go`:
- Around line 40-48: The methods GetName and Cleanup should defensively check
the underlying exported symbol pointers before calling them: in GetName, verify
dp.getName != nil and return a safe default (e.g., empty string) if nil; in
Cleanup, verify dp.cleanup != nil and return a safe default (e.g., nil error) or
a descriptive error if you prefer, instead of calling dp.getName()/dp.cleanup()
unconditionally; update the DynamicPlugin.GetName and DynamicPlugin.Cleanup
implementations to perform these nil checks to avoid panics when required
symbols weren't exported.

In `@transports/bifrost-http/server/plugins.go`:
- Around line 165-175: When InstantiatePlugin(ctx, cfg.Name, cfg.Path,
cfg.Config, s.Config) returns an error, plugin may be nil so calling
plugin.GetName() causes a nil-pointer panic; modify the error path in the
InstantiatePlugin handling to avoid dereferencing plugin—either use cfg.Name (or
another safe identifier) when calling s.Config.UpdatePluginOverallStatus instead
of plugin.GetName(), or add a nil-check before calling plugin.GetName(); ensure
the UpdatePluginOverallStatus call and the logger.Error still execute with a
safe plugin name and that enterprisePlugins, cfg.Name, InstantiatePlugin,
plugin.GetName, and s.Config.UpdatePluginOverallStatus are the referenced
symbols to locate the change.

In `@ui/app/workspace/mcp-logs/views/mcpLogsTable.tsx`:
- Around line 44-57: Change the component to use an MCP-specific pagination/sort
type and validate sort values before sending to the API: introduce a narrowed
type (e.g., allow only "timestamp" | "latency") for pagination.sort_by used by
this component, initialize useState<SortingState> with a value derived from that
narrowed type (not the generic Pagination), update handleSortingChange to
enforce/transform id to the narrowed type before calling onPaginationChange, and
ensure any call to mcpLogsApi uses the narrowed pagination or runtime-guards to
prevent "tokens" or "cost" from being sent.

In `@ui/lib/types/plugins.ts`:
- Around line 12-16: The frontend expects Plugin.actualName but the getPlugin
handler returns a raw TablePlugin without that field; update the server-side
getPlugin handler to return the same shape as getPlugins (i.e., a
PluginResponse) by wrapping the TablePlugin into PluginResponse and populating
actualName (use the TablePlugin's internal name or the route param as
appropriate) so the response matches the Plugin interface; alternatively, if you
cannot change the handler, make actualName optional on the UI-side Plugin
interface, but prefer changing getPlugin to return PluginResponse to keep
consistency with getPlugins and the Plugin type.
♻️ Duplicate comments (22)
core/schemas/plugin_wasm.go (1)

5-7: Docstring still implies streaming short-circuit in WASM.
This is already covered in prior review feedback; re-raising for visibility.

plugins/logging/main.go (2)

803-812: Don't hold p.mu while invoking callbacks.

The callback at lines 809-810 is invoked while holding p.mu (via defer p.mu.Unlock() at line 808). This can cause deadlocks if the callback re-enters the plugin or performs blocking operations. Copy the callback under lock and invoke outside.

🔧 Suggested fix
 		if err := p.store.CreateMCPToolLog(p.ctx, entry); err != nil {
 			p.logger.Warn("Failed to insert initial MCP tool log entry for request %s: %v", requestID, err)
 		} else {
 			// Call callback for initial log creation
 			p.mu.Lock()
-			defer p.mu.Unlock()
-			if p.mcpToolLogCallback != nil {
-				p.mcpToolLogCallback(entry)
+			callback := p.mcpToolLogCallback
+			p.mu.Unlock()
+			if callback != nil {
+				callback(entry)
 			}
 		}

925-935: Don't hold p.mu while doing DB I/O or invoking callbacks.

Lines 926-934 hold p.mu while calling p.store.FindMCPToolLog and invoking the callback. This can block other goroutines and risk deadlocks. Capture the callback under lock, release the lock, then perform DB I/O and invoke the callback.

🔧 Suggested fix
 		} else {
 			// Call callback for log update
 			p.mu.Lock()
-			if p.mcpToolLogCallback != nil {
+			callback := p.mcpToolLogCallback
+			p.mu.Unlock()
+			if callback != nil {
 				if updatedEntry, getErr := p.store.FindMCPToolLog(p.ctx, requestID); getErr == nil {
-					p.mcpToolLogCallback(updatedEntry)
+					callback(updatedEntry)
 				} else {
 					p.logger.Warn("failed to find updated entry for callback: %v", getErr)
 				}
 			}
-			p.mu.Unlock()
 		}
docs/openapi/openapi.json (4)

113260-113270: Duplicate of requiredness check for client_id/name.

Same concern as earlier MCP client config block.


113558-113569: Duplicate of requiredness check for client_id/name.

Same concern as earlier MCP client config block.


138609-138619: Duplicate of requiredness check for client_id/name.

Same concern as earlier MCP client config block.


138724-138734: Duplicate of requiredness check for client_id/name.

Same concern as earlier MCP client config block.

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

2832-2853: Incomplete MCP catalog pricing cleanup on client removal.

This issue was flagged in a previous review: removedClientConfig.ToolPricing is read from the in-memory config, but AddMCPClient retrieves ToolPricing from the database (lines 2791-2798) and never syncs it back to MCPConfig.ClientConfigs. Therefore, removedClientConfig.ToolPricing is always empty, and the catalog pricing entries are never deleted.

To fix, either:

  1. Sync ToolPricing from DB back to in-memory config in AddMCPClient, or
  2. Retrieve the client config from DB in RemoveMCPClient before deletion
ui/app/workspace/mcp-logs/views/emptyState.tsx (1)

85-247: Add baseUrl to the useMemo dependency array.

The useMemo calls getExampleBaseUrl() which depends on window.location. While typically stable, extracting the base URL outside the memo and adding it to the dependency array ensures correctness if the location changes.

♻️ Suggested fix
 export function MCPEmptyState({ isSocketConnected, error }: MCPEmptyStateProps) {
 	const [language, setLanguage] = useState<Language>("python");

+	const baseUrl = getExampleBaseUrl();
+
 	// Generate examples dynamically using the port utility
-	const examples: Examples = useMemo(() => {
-		const baseUrl = getExampleBaseUrl();
-
+	const examples: Examples = useMemo(() => {
 		return {
 			// ... examples using baseUrl
 		};
-	}, []);
+	}, [baseUrl]);
docs/openapi/paths/management/logging.yaml (1)

335-340: Inconsistency between description and schema for status parameter.

The description says "Comma-separated list of statuses" but the schema defines an enum which typically allows only a single value. This same inconsistency exists at lines 454-459 for mcp-logs-stats.

Either remove the enum to allow comma-separated values, or update the description to reflect single-value behavior.

framework/configstore/migrations.go (1)

2490-2507: Inconsistent collision handling between MCP clients and VK configs.

The MCP clients migration (lines 2330-2356) includes collision detection to avoid duplicate tool names after prefix stripping, but the VK MCP configs migration lacks this logic. If a VK config has both calculator_add and add in its tools_to_execute, stripping the prefix from the first would create a duplicate.

🔧 Suggested fix to add collision detection
 			if len(vkConfig.ToolsToExecute) > 0 {
 				updatedTools := make([]string, 0, len(vkConfig.ToolsToExecute))
+				seenTools := make(map[string]bool)
 				for _, tool := range vkConfig.ToolsToExecute {
 					prefix := clientName + "_"
 					if strings.HasPrefix(tool, prefix) {
 						unprefixedTool := strings.TrimPrefix(tool, prefix)
+						if seenTools[unprefixedTool] {
+							log.Printf("Collision detected when stripping prefix from VK config tool '%s': unprefixed name '%s' already exists. Keeping first occurrence.", tool, unprefixedTool)
+							needsUpdate = true
+							continue
+						}
+						seenTools[unprefixedTool] = true
 						updatedTools = append(updatedTools, unprefixedTool)
 						needsUpdate = true
 					} else {
+						if seenTools[tool] {
+							log.Printf("Duplicate tool name '%s' found in VK config. Keeping first occurrence.", tool)
+							continue
+						}
+						seenTools[tool] = true
 						updatedTools = append(updatedTools, tool)
 					}
 				}
docs/plugins/getting-started.mdx (1)

57-66: Hook lists still diverge from lifecycle wording.

The v1.3.x function list now uses PreLLMHook/PostLLMHook, but the lifecycle section still references PreHook/PostHook. The v1.4.x+ lifecycle lists PreMCPHook/PostMCPHook while the function list omits them. Please align the lists with the lifecycle naming (or vice‑versa) for a single, consistent API surface. If another PR in the Graphite stack already reconciles this, please merge the wording accordingly.

Also applies to: 86-96

examples/plugins/hello-world/main.go (1)

41-46: Fix undefined LLMPluginShortCircuit in the example (build fails).

The pipeline reports schemas.LLMPluginShortCircuit as undefined. Either export the type in core/schemas for the build tags used here, or keep the example on the currently exported short‑circuit type so it compiles.

🐛 Possible fix if the type is not exported yet
-func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
+func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {

Run this to verify the type definition and any build tags that might exclude it:

#!/bin/bash
rg -n "type\\s+LLMPluginShortCircuit" core/schemas -C 2
rg -n "//go:build|// \\+build" core/schemas -C 1
core/mcp/utils.go (1)

133-145: Validate sanitized tool names before registration.

You added validateNormalizedToolName, but it isn’t called here. That still allows / or .. to propagate into the tool map and VFS paths. Validate immediately after sanitization and skip invalid tools.

🛡️ Proposed fix
		// Sanitize the original tool name: replace any '-' with '_' to prevent conflicts with our separator
		sanitizedToolName := strings.ReplaceAll(mcpTool.Name, "-", "_")
+		if err := validateNormalizedToolName(sanitizedToolName); err != nil {
+			logger.Warn(fmt.Sprintf("%s Skipping MCP tool %q: %v", MCPLogPrefix, mcpTool.Name, err))
+			continue
+		}
plugins/logging/utils.go (1)

129-143: Missing nil guards for store access - potential panic.

SearchMCPToolLogs and GetMCPToolLogStats call p.plugin.store without nil checks, unlike the other new methods (GetAvailableToolNames, GetAvailableServerLabels, DeleteMCPToolLogs) which properly guard against uninitialized state.

🔧 Proposed fix
 func (p *PluginLogManager) SearchMCPToolLogs(ctx context.Context, filters *logstore.MCPToolLogSearchFilters, pagination *logstore.PaginationOptions) (*logstore.MCPToolLogSearchResult, error) {
 	if filters == nil || pagination == nil {
 		return nil, fmt.Errorf("filters and pagination cannot be nil")
 	}
+	if p.plugin == nil || p.plugin.store == nil {
+		return nil, fmt.Errorf("log store not initialized")
+	}
 	return p.plugin.store.SearchMCPToolLogs(ctx, *filters, *pagination)
 }

 func (p *PluginLogManager) GetMCPToolLogStats(ctx context.Context, filters *logstore.MCPToolLogSearchFilters) (*logstore.MCPToolLogStats, error) {
 	if filters == nil {
 		return nil, fmt.Errorf("filters cannot be nil")
 	}
+	if p.plugin == nil || p.plugin.store == nil {
+		return nil, fmt.Errorf("log store not initialized")
+	}
 	return p.plugin.store.GetMCPToolLogStats(ctx, *filters)
 }
transports/bifrost-http/server/server.go (2)

369-372: Reattach MCP runtime hooks after MCPConfig conversion.

The reload paths still rebuild MCPConfig via ConvertTableMCPConfigToSchemas without reapplying runtime hooks (e.g., FetchNewRequestIDFunc). This causes non‑unique MCP tool call IDs after reload.

Also applies to: 436-439


551-593: Call Cleanup() on replaced/removed plugins to avoid leaks.

Reloading or removing plugins without invoking Cleanup() can leave goroutines/resources running.

Also applies to: 595-632

framework/configstore/store.go (1)

38-43: Ensure all ConfigStore implementations/mocks match the new MCP signatures.

Mocks/tests may still return schema types; please update any remaining implementations to return *tables.MCPConfig and accept tables.TableMCPClient.

ui/app/workspace/mcp-logs/page.tsx (1)

282-305: Guard stats updates against double‑counting on repeated terminal updates.

The stats increment logic still updates totals on every terminal log.status update, which can double‑count when a completed log receives multiple updates.

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

182-196: AddMCPClient still drops ToolPricing field during conversion.

The conversion from TableMCPClient to schemas.MCPClientConfig (lines 184-194) does not include ToolPricing, while the edit path (line 248) passes the full TableMCPClient. This asymmetry means newly created clients won't have their tool pricing preserved, but edited clients will.

ui/lib/store/apis/mcpLogsApi.ts (1)

46-50: Zero latency filters are still blocked by truthy checks.

The truthy checks on lines 48-49 will skip valid 0 values for min_latency/max_latency. This prevents users from filtering on zero latency.

plugins/governance/main.go (1)

489-525: Use slash-delimited tool identifiers for MCP include-tools.

The MCP tool parser expects clientName/toolName identifiers; using - will cause filtering to miss tools.

🔧 Proposed fix
-					executeOnlyTools = append(executeOnlyTools, fmt.Sprintf("%s-%s", vkMcpConfig.MCPClient.Name, tool))
+					executeOnlyTools = append(executeOnlyTools, fmt.Sprintf("%s/%s", vkMcpConfig.MCPClient.Name, tool))
🧹 Nitpick comments (25)
ui/app/workspace/logs/page.tsx (2)

45-59: Potential issue: getDefaultTimeRange() called during render may cause stale or inconsistent defaults.

The getDefaultTimeRange() function is invoked inline within the useQueryStates parser definitions (lines 58-59). Since useQueryStates configuration is evaluated on every render, this means:

  1. Each render computes a slightly different endTime (current timestamp shifts by milliseconds/seconds).
  2. The withDefault() values only apply when the URL lacks those parameters, but the parser object itself is recreated each render.

While nuqs likely handles this gracefully by only using defaults on initial parse, calling Date.now() during render is generally discouraged as it makes the component non-deterministic.

Consider memoizing the initial defaults:

Suggested improvement
-	// Get fresh default time range
-	const getDefaultTimeRange = () => dateUtils.getDefaultTimeRange();
+	// Memoize initial default time range to avoid recomputing on every render
+	const initialTimeRange = useMemo(() => dateUtils.getDefaultTimeRange(), []);

 	// URL state management with nuqs - all filters and pagination in URL
 	const [urlState, setUrlState] = useQueryStates(
 		{
 			// ... other parsers
-			start_time: parseAsInteger.withDefault(getDefaultTimeRange().startTime),
-			end_time: parseAsInteger.withDefault(getDefaultTimeRange().endTime),
+			start_time: parseAsInteger.withDefault(initialTimeRange.startTime),
+			end_time: parseAsInteger.withDefault(initialTimeRange.endTime),

The refreshDefaultsIfStale logic in the visibility handler will still refresh stale values when needed.


42-43: Add a brief comment explaining the flag's lifecycle.

The userModifiedTimeRange ref is a session-scoped flag that prevents auto-refresh once set. A brief comment would clarify this behavior for maintainers.

Suggested documentation
-	// Track if user has manually modified the time range
+	// Track if user has manually modified the time range.
+	// Once true, auto-refresh on focus/visibility is disabled for the session.
 	const userModifiedTimeRange = useRef<boolean>(false);
plugins/mocker/benchmark_test.go (1)

58-65: Consider renaming benchmark helpers to PreLLMHook for clarity.

The call site now uses PreLLMHook, but the benchmark names/comments still read “PreHook,” which can be confusing in go test -bench output. Renaming the benchmark functions/comments would align nomenclature with the new hook name.

ui/components/sidebar.tsx (1)

72-88: Remove duplicate MCPIcon and import from ui/components/ui/icons.tsx.

This component duplicates the MCPIcon already exported from ui/components/ui/icons.tsx. Import and reuse the existing component instead.

♻️ Proposed fix

Remove lines 71-88 and add to the imports:

 import { BooksIcon, DiscordLogoIcon, GithubLogoIcon } from "@phosphor-icons/react";
+import { MCPIcon } from "@/components/ui/icons";
-// Custom MCP Icon Component
-const MCPIcon = ({ className }: { className?: string }) => (
-	<svg
-		className={className}
-		fill="currentColor"
-		fillRule="evenodd"
-		height="1em"
-		style={{ flex: "none", lineHeight: 1 }}
-		viewBox="0 0 24 24"
-		width="1em"
-		xmlns="http://www.w3.org/2000/svg"
-		aria-label="MCP clients icon"
-	>
-		<title>MCP clients icon</title>
-		<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
-		<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
-	</svg>
-);
plugins/maxim/main.go (1)

404-407: Use bifrost.Ptr for string pointers to match repo convention.
This keeps pointer creation consistent across the codebase.

♻️ Suggested update
-		traceConfig.SessionId = &sessionID
+		traceConfig.SessionId = bifrost.Ptr(sessionID)
...
-		generationConfig.Name = &generationName
+		generationConfig.Name = bifrost.Ptr(generationName)
...
-					Code:    &code,
-					Type:    &errorType,
+					Code:    bifrost.Ptr(code),
+					Type:    bifrost.Ptr(errorType),
Based on learnings, prefer bifrost.Ptr for pointer creation.

Also applies to: 426-428, 538-542

core/mcp/clientmanager.go (1)

192-236: Tool rename logic looks correct, but consider deep-copying the Function pointer.

The logic for updating tool prefixes when the client name changes is well-structured. However, there's a subtle issue: tool.Function is a pointer, so when you copy tool to updatedTool and modify updatedTool.Function.Name, you're mutating the shared ChatToolFunction instance. While this works because the old ToolMap is replaced entirely, it could cause unexpected behavior if any code holds references to the old tools.

Consider creating a new ChatToolFunction to ensure complete isolation:

🔧 Optional: Deep copy Function to avoid shared mutation
 			// Update the tool's function name if it's a function tool
 			if tool.Function != nil {
 				updatedTool := tool
-				updatedTool.Function.Name = newToolName
+				// Create a new Function to avoid mutating the shared pointer
+				newFunction := *tool.Function
+				newFunction.Name = newToolName
+				updatedTool.Function = &newFunction
 				newToolMap[newToolName] = updatedTool
 			} else {
 				newToolMap[newToolName] = tool
 			}
transports/bifrost-http/handlers/logging.go (1)

532-559: Prefer bifrost.Ptr for new pointer fields in MCP filter parsing.

For consistency with repo conventions, use the pointer helper for StartTime, EndTime, and latency fields in both parsing helpers. Verify that bifrost.Ptr is available in this package.

♻️ Example adjustment
 import (
 	"context"
 	"fmt"
 	"strconv"
 	"strings"
 	"time"

 	"github.com/bytedance/sonic"
 	"github.com/fasthttp/router"
+	"github.com/maximhq/bifrost/core"
 	"github.com/maximhq/bifrost/core/schemas"
 	"github.com/maximhq/bifrost/framework/configstore/tables"
 	"github.com/maximhq/bifrost/framework/logstore"
 	"github.com/maximhq/bifrost/plugins/logging"
 	"github.com/maximhq/bifrost/transports/bifrost-http/lib"
 	"github.com/valyala/fasthttp"
 )
@@
-		filters.StartTime = &t
+		filters.StartTime = bifrost.Ptr(t)
@@
-		filters.MinLatency = &f
+		filters.MinLatency = bifrost.Ptr(f)

Based on learnings, please keep pointer creation consistent with bifrost.Ptr.

Also applies to: 633-659

examples/plugins/llm-only/main.go (1)

90-95: Use bifrost.Ptr for pointer fields in the system message.

Align pointer creation with repo conventions and avoid direct address-of usage.

♻️ Example adjustment
 import (
 	"fmt"

+	"github.com/maximhq/bifrost/core"
 	"github.com/maximhq/bifrost/core/schemas"
 )
@@
 	if pluginConfig.InjectSystemMessage && req.ChatRequest != nil && req.ChatRequest.Input != nil {
 		systemMsg := schemas.ChatMessage{
 			Role:    "system",
-			Content: &schemas.ChatMessageContent{ContentStr: &pluginConfig.SystemMessageText},
+			Content: bifrost.Ptr(schemas.ChatMessageContent{
+				ContentStr: bifrost.Ptr(pluginConfig.SystemMessageText),
+			}),
 		}

Based on learnings, please keep pointer creation consistent with bifrost.Ptr.

examples/plugins/llm-only/Makefile (1)

1-12: Optional: add conventional all/test phony targets.

checkmake warns about missing standard targets; adding no-op test and all: build can quiet tooling.

🛠️ Suggested tweak
-.PHONY: build clean
+.PHONY: build clean all test
+
+all: build
+
+test:
+	`@echo` "No tests defined"
docs/openapi/openapi.json (2)

119732-119737: Tighten filter validation and delete safeguards in MCP logs APIs.

Consider restricting status to the documented values and preventing empty/duplicate delete requests for better client validation and safer usage.

Suggested schema tweaks
           {
             "name": "status",
             "in": "query",
             "description": "Comma-separated list of statuses to filter by (processing, success, error)",
             "schema": {
               "type": "string"
+              ,"enum": ["processing", "success", "error"]
             }
           }
@@
                   "ids": {
                     "type": "array",
+                    "minItems": 1,
+                    "uniqueItems": true,
                     "items": {
                       "type": "string"
                     },
                     "description": "Array of log IDs to delete"
                   }

Also, since this PR is part of a Graphite stack, please confirm the source-of-truth OpenAPI files in the stack (if any) reflect these changes to avoid regen drift. As per coding guidelines, please verify stack-wide consistency.

Also applies to: 120142-120148


140196-140305: Prefer $ref reuse for log entry and stats schemas.

SearchMCPLogsResponse duplicates MCPToolLogEntry and MCPToolLogStats. Using $ref here reduces drift risk and keeps the spec easier to maintain.

framework/plugins/main.go (2)

116-134: Inconsistent nil vs empty slice return on error.

LoadPlugins returns nil on error (line 128) but returns an empty slice []schemas.BasePlugin{} when config is nil (line 119). For consistency and to avoid nil-pointer issues in callers, consider always returning an empty slice on error as well, or document the nil-on-error behavior.

♻️ Suggested fix for consistency
 func LoadPlugins(loader PluginLoader, config *Config) ([]schemas.BasePlugin, error) {
 	plugins := []schemas.BasePlugin{}
 	if config == nil {
 		return plugins, nil
 	}

 	for _, pc := range config.Plugins {
 		if !pc.Enabled {
 			continue
 		}
 		plugin, err := loader.LoadPlugin(pc.Path, pc.Config)
 		if err != nil {
-			return nil, err
+			return plugins, err
 		}
 		plugins = append(plugins, plugin)
 	}

 	return plugins, nil
 }

200-214: PluginTypeAuto silently returns false.

When ImplementsInterface is called with PluginTypeAuto, it falls through to the default case and returns false. This could be misleading since PluginTypeAuto is meant for auto-detection. Consider either handling it explicitly (e.g., returning true if the plugin implements any interface) or documenting this behavior.

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

319-323: Initialize all atomic plugin pointers consistently.

LLMPlugins is initialized with an empty atomic.Pointer[[]schemas.LLMPlugin]{}, but MCPPlugins and HTTPTransportPlugins (defined in the struct at lines 243-244) are not initialized in LoadConfig. While this works because nil atomic pointers are safe, initializing all of them would be more consistent.

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

232-243: Prefer bifrost.Ptr when taking pointers.

Project convention favors bifrost.Ptr(...) over the address operator.

♻️ Suggested change
-			toolMessage, err := h.toolManager.ExecuteChatMCPTool(ctx, &toolCall)
+			toolMessage, err := h.toolManager.ExecuteChatMCPTool(ctx, bifrost.Ptr(toolCall))
Based on learnings, prefer `bifrost.Ptr(...)` for pointer creation.
ui/app/workspace/mcp-logs/views/columns.tsx (1)

12-19: Type narrowing in getValidatedStatus can be improved.

The type assertion status as Status before the includes check is technically unsound since status is still string at that point. While this works at runtime because includes performs a value check, TypeScript's type system isn't fully leveraged.

♻️ Suggested improvement
 const getValidatedStatus = (status: string): Status => {
-	// Check if status is a valid Status by checking against Statuses array
-	if (Statuses.includes(status as Status)) {
+	// Check if status is a valid Status
+	if ((Statuses as readonly string[]).includes(status)) {
 		return status as Status;
 	}
 	// Fallback to "processing" for unknown statuses
 	return "processing";
 };
framework/configstore/tables/mcp.go (1)

138-143: Inconsistent JSON library usage for deserialization.

ToolPricing uses json.Unmarshal (line 140) while all other fields in AfterFind use sonic.Unmarshal. For consistency and performance alignment, consider using sonic here as well.

♻️ Suggested fix for consistency
 	if c.ToolPricingJSON != "" {
-		if err := json.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
+		if err := sonic.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
 			return err
 		}
 	}
examples/plugins/http-transport-only/main.go (1)

34-70: Rate limiter has unbounded memory growth for inactive keys.

The requests map grows indefinitely as new API keys are seen, and old entries are only cleaned within a key's slice when that key is accessed again. For a long-running service with many unique keys, this could cause memory issues.

For a production plugin, consider periodic cleanup of inactive keys:

♻️ Optional: Add periodic cleanup for inactive keys
// Add a method to clean up old keys (call periodically or on each request)
func (rl *RateLimiter) Cleanup(window int) {
	rl.mu.Lock()
	defer rl.mu.Unlock()
	
	cutoff := time.Now().Add(-time.Duration(window) * time.Second)
	for key, reqs := range rl.requests {
		// Remove keys with no recent activity
		hasRecent := false
		for _, t := range reqs {
			if t.After(cutoff) {
				hasRecent = true
				break
			}
		}
		if !hasRecent {
			delete(rl.requests, key)
		}
	}
}

Since this is example code demonstrating plugin capabilities, this is acceptable as-is.

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

134-153: getPlugin returns empty struct when plugin not found (no configStore).

When configStore is nil and the plugin isn't found in the status map, an empty PluginResponse{} is returned. Consider returning a 404 error for consistency with the configStore-enabled path.

♻️ Suggested improvement
 func (h *PluginsHandler) getPlugin(ctx *fasthttp.RequestCtx) {
 	if h.configStore == nil {
 		pluginStatus := h.pluginsLoader.GetPluginStatus(ctx)
-		pluginInfo := PluginResponse{}
+		var pluginInfo *PluginResponse
 		for name, pluginStatus := range pluginStatus {
 			if pluginStatus.Name == ctx.UserValue("name") {
-				pluginInfo = PluginResponse{
+				pluginInfo = &PluginResponse{
 					Name:       pluginStatus.Name,
 					ActualName: name,
 					Enabled:    true,
 					Config:     map[string]any{},
 					IsCustom:   true,
 					Path:       nil,
 					Status:     pluginStatus,
 				}
 				break
 			}
 		}
+		if pluginInfo == nil {
+			SendError(ctx, fasthttp.StatusNotFound, "Plugin not found")
+			return
+		}
-		SendJSON(ctx, pluginInfo)
+		SendJSON(ctx, *pluginInfo)
 		return
 	}
framework/configstore/rdb.go (1)

716-722: Prefer bifrost.Ptr(...) over &literal for pointer creation.

Project convention favors bifrost.Ptr for pointer creation (including struct literals). Consider applying it here for consistency. Based on learnings, please use bifrost.Ptr(...) instead of &....

Also applies to: 731-734

transports/bifrost-http/server/plugins.go (1)

129-147: Inconsistent error handling for optional built-in plugins.

registerPluginWithStatus return values are ignored for logging (line 134) and governance (line 144), while telemetry errors are propagated. If these plugins fail to initialize, the error is silently discarded which could make debugging difficult.

Consider logging the error even if not returning it:

♻️ Proposed improvement
 	// 2. Logging (if enabled)
 	if s.Config.ClientConfig.EnableLogging && s.Config.LogsStore != nil {
 		config := &logging.Config{
 			DisableContentLogging: &s.Config.ClientConfig.DisableContentLogging,
 		}
-		s.registerPluginWithStatus(ctx, logging.PluginName, nil, config, false)
+		if err := s.registerPluginWithStatus(ctx, logging.PluginName, nil, config, false); err != nil {
+			logger.Warn("failed to register logging plugin: %v", err)
+		}
 	} else {
 		s.markPluginDisabled(logging.PluginName)
 	}

 	// 3. Governance (if enabled and not enterprise)
 	if s.Config.ClientConfig.EnableGovernance && ctx.Value(schemas.BifrostContextKeyIsEnterprise) == nil {
 		config := &governance.Config{
 			IsVkMandatory: &s.Config.ClientConfig.EnforceGovernanceHeader,
 		}
-		s.registerPluginWithStatus(ctx, governance.PluginName, nil, config, false)
+		if err := s.registerPluginWithStatus(ctx, governance.PluginName, nil, config, false); err != nil {
+			logger.Warn("failed to register governance plugin: %v", err)
+		}
 	} else {
 		s.markPluginDisabled(governance.PluginName)
 	}
core/mcp/mcp.go (1)

83-101: Type assertion failure returns nil without logging.

If config.PluginPipelineProvider() returns a non-nil value that doesn't implement PluginPipeline, the type assertion on line 90 silently returns nil. This could cause unexpected behavior if the provider is misconfigured.

♻️ Proposed improvement
 	if config.PluginPipelineProvider != nil && config.ReleasePluginPipeline != nil {
 		pluginPipelineProvider = func() PluginPipeline {
 			if pipeline := config.PluginPipelineProvider(); pipeline != nil {
 				if pp, ok := pipeline.(PluginPipeline); ok {
 					return pp
+				} else {
+					logger.Warn("%s Plugin pipeline does not implement expected interface", MCPLogPrefix)
 				}
 			}
 			return nil
 		}
examples/plugins/multi-interface/main.go (1)

158-189: Use pointer helper for ContentStr for consistency.

Prefer the shared pointer helper instead of &content to match repo conventions.

♻️ Suggested tweak
 import (
 	"context"
 	"encoding/json"
 	"fmt"
 	"time"

+	bifrost "github.com/maximhq/bifrost/core"
 	"github.com/maximhq/bifrost/core/schemas"
 )
@@
 		systemMsg := schemas.ChatMessage{
 			Role:    "system",
-			Content: &schemas.ChatMessageContent{ContentStr: &content},
+			Content: &schemas.ChatMessageContent{ContentStr: bifrost.Ptr(content)},
 		}

Based on learnings, prefer bifrost.Ptr(...) over address-of for pointer values.

core/bifrost.go (2)

226-239: Consider logging when pipeline type assertion fails.

The ReleasePluginPipeline callback silently ignores invalid types. While this is defensive, logging a warning would help detect integration issues during development.

🔧 Suggested improvement
 			mcpConfig.ReleasePluginPipeline = func(pipeline interface{}) {
 				if pp, ok := pipeline.(*PluginPipeline); ok {
 					bifrost.releasePluginPipeline(pp)
+				} else if pipeline != nil {
+					bifrost.logger.Warn("ReleasePluginPipeline received unexpected type: %T", pipeline)
 				}
 			}

2207-2219: Consider extracting duplicated PluginPipelineProvider setup.

The PluginPipelineProvider and ReleasePluginPipeline callback setup (lines 2207-2218) duplicates the pattern from Init (lines 226-235). Extracting this to a helper method would reduce duplication and ensure consistency.

♻️ Suggested helper extraction
// setupMCPPluginPipeline configures the MCP config with plugin pipeline callbacks
func (bifrost *Bifrost) setupMCPPluginPipeline(mcpConfig *schemas.MCPConfig) {
    mcpConfig.PluginPipelineProvider = func() interface{} {
        return bifrost.getPluginPipeline()
    }
    mcpConfig.ReleasePluginPipeline = func(pipeline interface{}) {
        if pp, ok := pipeline.(*PluginPipeline); ok {
            bifrost.releasePluginPipeline(pp)
        }
    }
}

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 2d00677 to 6cc2685 Compare January 20, 2026 14:38
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: 12

Caution

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

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

741-774: Fix MockConfigStore.GetMCPConfig return type mismatch in tests.

The MockConfigStore.GetMCPConfig method in transports/bifrost-http/lib/config_test.go:441 returns (*schemas.MCPConfig, error), but the interface in framework/configstore/store.go:41 defines the return type as (*tables.MCPConfig, error). This type mismatch breaks the interface contract and will cause test compilation failures. Update the mock to return *tables.MCPConfig instead.

All production call sites in config.go and server.go correctly handle the *tables.MCPConfig type, using ConvertTableMCPConfigToSchemas where schema types are needed.

docs/features/observability/default.mdx (1)

146-187: Update Go SDK snippet to new logging.Init signature

Lines 174-187: logging.Init now requires a *logging.Config and an MCP catalog parameter. The snippet should reflect the new signature (even if passing nil for MCP catalog).

📝 Suggested update
-    // Initialize logging plugin
-    loggingPlugin, err := logging.Init(ctx, logger, store, pricingManager)
+    // Initialize logging plugin
+    loggingPlugin, err := logging.Init(ctx, &logging.Config{}, logger, store, pricingManager, nil)
plugins/mocker/main.go (1)

866-869: Guard ChatResponse access when overriding the model.

mockResponse.ChatResponse is nil for ResponsesRequest, so this panics if content.Model is set.

🐛 Suggested fix
-	if content.Model != nil {
-		mockResponse.ChatResponse.Model = *content.Model
-	}
+	if content.Model != nil && mockResponse.ChatResponse != nil {
+		mockResponse.ChatResponse.Model = *content.Model
+	}
transports/bifrost-http/handlers/plugins.go (2)

141-160: Missing 404 response when plugin not found.

When configStore is nil and no matching plugin is found in the status map, line 159 returns an empty PluginResponse{} instead of a 404 error. This could confuse API consumers.

🐛 Proposed fix
 func (h *PluginsHandler) getPlugin(ctx *fasthttp.RequestCtx) {
 	if h.configStore == nil {
 		pluginStatus := h.pluginsLoader.GetPluginStatus(ctx)
-		pluginInfo := PluginResponse{}
+		var pluginInfo *PluginResponse
 		for name, pluginStatus := range pluginStatus {
 			if pluginStatus.Name == ctx.UserValue("name") {
-				pluginInfo = PluginResponse{
+				pluginInfo = &PluginResponse{
 					Name:       pluginStatus.Name,
 					ActualName: name,
 					Enabled:    true,
 					Config:     map[string]any{},
 					IsCustom:   true,
 					Path:       nil,
 					Status:     pluginStatus,
 				}
 				break
 			}
 		}
+		if pluginInfo == nil {
+			SendError(ctx, fasthttp.StatusNotFound, "Plugin not found")
+			return
+		}
 		SendJSON(ctx, pluginInfo)
 		return
 	}

219-239: Plugin reload before DB write may cause inconsistent state.

The plugin is reloaded (lines 221-225) before the database entry is created (line 229). If the reload succeeds but the DB write fails, the plugin will be running in memory but not persisted. Consider reversing the order or implementing rollback logic.

🐛 Proposed fix - create DB entry first, then reload
-	// We reload the plugin if its enabled
-	if request.Enabled {
-		if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, request.Path, request.Config); err != nil {
-			logger.Error("failed to load plugin: %v", err)
-			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin created in database but failed to load: %v", err))
-			return
-		}
-	}
-
-	// Create a DB entry if plugin loading was successful
+	// Create DB entry first
 	if err := h.configStore.CreatePlugin(ctx, &configstoreTables.TablePlugin{
 		Name:     request.Name,
 		Enabled:  request.Enabled,
 		Config:   request.Config,
 		Path:     request.Path,
 		IsCustom: true,
 	}); err != nil {
 		logger.Error("failed to create plugin: %v", err)
 		SendError(ctx, 500, "Failed to create plugin")
 		return
 	}
+
+	// Reload the plugin if enabled
+	if request.Enabled {
+		if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, request.Path, request.Config); err != nil {
+			logger.Error("failed to load plugin: %v", err)
+			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin created in database but failed to load: %v", err))
+			return
+		}
+	}
transports/bifrost-http/server/server.go (1)

580-589: Guard against nil proxy config to avoid panic.

ReloadProxyConfig dereferences config unconditionally; a nil config (e.g., clearing proxy) would panic.

🔧 Suggested fix
 func (s *BifrostHTTPServer) ReloadProxyConfig(ctx context.Context, config *configstoreTables.GlobalProxyConfig) error {
 	if s.Config == nil {
 		return fmt.Errorf("config not found")
 	}
 	// Store the proxy config in memory for use by components that need it
 	s.Config.ProxyConfig = config
-	logger.Info("proxy configuration reloaded: enabled=%t, type=%s", config.Enabled, config.Type)
+	if config == nil {
+		logger.Info("proxy configuration cleared")
+		return nil
+	}
+	logger.Info("proxy configuration reloaded: enabled=%t, type=%s", config.Enabled, config.Type)
 	return nil
 }
core/bifrost.go (1)

2972-2991: Post-hook count should match the pre-hook run set.

Using len(*bifrost.llmPlugins.Load()) can diverge from the set of plugins that actually ran pre-hooks if plugins are reloaded between pre- and post-hook phases. This can skip necessary post-hooks or run extra ones.

🔧 Suggested fix (non-streaming path)
-	pluginCount := len(*bifrost.llmPlugins.Load())
 	select {
 	case result = <-msg.Response:
-		resp, bifrostErr := pipeline.RunPostHooks(msg.Context, result, nil, pluginCount)
+		resp, bifrostErr := pipeline.RunPostHooks(msg.Context, result, nil, preCount)
...
 	case bifrostErrVal := <-msg.Err:
 		bifrostErrPtr := &bifrostErrVal
-		resp, bifrostErrPtr = pipeline.RunPostHooks(msg.Context, nil, bifrostErrPtr, pluginCount)
+		resp, bifrostErrPtr = pipeline.RunPostHooks(msg.Context, nil, bifrostErrPtr, preCount)

Consider carrying preCount into the streaming/error paths (e.g., via context or a field on the queued message) so post-hooks always align with the pre-hook set.

Also applies to: 3173-3176, 3428-3434

🤖 Fix all issues with AI agents
In `@core/mcp/codemodeexecutecode.go`:
- Around line 852-859: The short-circuit/post-hook handling in callMCPTool only
accepts ChatMessage (checks around shortCircuit.Response and after
pipeline.RunMCPPostHooks) causing ResponsesMessage from MCP plugins to be
treated as invalid; update both handling sites in callMCPTool to also detect
when the returned message is a ResponsesMessage, extract the tool result or
error from it (analogous to extractResultFromChatMessage — add or reuse a helper
like extractResultFromResponsesMessage if needed), and return the extracted
result or surface any tool error found instead of returning "plugin
short-circuit returned invalid response".

In `@core/schemas/bifrost.go`:
- Around line 383-391: GetToolName can panic by dereferencing nil pointers;
update it to safely check nested pointers before dereferencing: in
BifrostMCPRequest.GetToolName ensure r.ChatAssistantMessageToolCall is non-nil
AND r.ChatAssistantMessageToolCall.Function is non-nil AND
r.ChatAssistantMessageToolCall.Function.Name is non-nil before returning
*r.ChatAssistantMessageToolCall.Function.Name, and likewise ensure
r.ResponsesToolMessage is non-nil and r.ResponsesToolMessage.Name is non-nil
before returning *r.ResponsesToolMessage.Name; if checks fail return an empty
string.

In `@docs/openapi/paths/management/plugins.yaml`:
- Around line 5-9: The path parameter naming for plugin endpoints is
inconsistent: GET /plugins/{name} calls it “display name” while PUT/DELETE
describe it as “Plugin name”; decide whether the API expects the display name or
the actual/internal name and update all endpoint parameter descriptions to match
(or add a one-line clarifying note). Specifically, align the path param
descriptions for GET /plugins/{name}, PUT /plugins/{name}, and DELETE
/plugins/{name} to consistently refer to either the display name (the config
field `name`) or the internal key (`actualName` from GetName()), and mention the
distinction between `actualName` and `name` where relevant in the response docs.

In `@docs/openapi/schemas/management/logging.yaml`:
- Around line 263-277: The schema for virtual_keys.value in the virtual_keys
array is inconsistent with implementation: the description says "redacted if
applicable" but the code currently returns an empty string; either update the
schema to state the field is always an empty string in filtered responses or
change the implementation to perform explicit redaction to match
selected_keys.value (e.g., return "***" or another redaction token) and ensure
the OpenAPI schema reflects that behavior; locate and modify the
virtual_keys.value description in the YAML (and add omitempty or appropriate
nullable/allowEmptyValue if you choose to keep empty-string behavior) or
implement the redaction logic where virtual_keys are produced so
virtual_keys.value matches selected_keys.value.

In `@docs/quickstart/gateway/cli-agents.mdx`:
- Around line 385-395: Update the "Using Virtual Key Authentication" section to
clarify that virtual keys only grant access to MCP tools allowed by the virtual
key's configuration: locate the "Using Virtual Key Authentication" heading and
the example JSON command (the claude mcp add-json bifrost line) and replace the
sentence that currently reads "This gives Claude Code access to all configured
MCP tools in Bifrost without any additional setup." with a sentence stating that
Claude Code will only have access to the specific MCP tools permitted by the
virtual key's configuration, and optionally add a short note instructing users
to verify or modify the virtual key's permissions if additional tool access is
required.

In `@examples/plugins/http-transport-only/main.go`:
- Around line 176-182: The code writes into req.Headers without ensuring it's
initialized, which can panic when nil; before assigning keys to req.Headers in
the example (the block that sets "X-Plugin-Processed" and "X-Request-Time"), add
a guard that checks if req.Headers is nil and, if so, initialize it (e.g.,
make(map[string]string) or the appropriate header map type) so subsequent
assignments to req.Headers["..."] are safe; keep the
ctx.SetValue(schemas.BifrostContextKey("http-plugin-start-time"), ...) line
unchanged.
- Around line 187-210: In HTTPTransportPostHook, guard against resp or
resp.Headers being nil before mutating: check that resp (type
*schemas.HTTPResponse) is not nil, then if resp.Headers is nil allocate a new
map and assign it, and only then set headers like "X-Request-Duration-Ms", CORS
and security headers; also ensure the duration header logic only runs when ctx
value extraction succeeded and resp is non-nil.

In `@examples/plugins/mcp-only/main.go`:
- Around line 114-118: The code reads the request id using
ctx.Value(schemas.BifrostContextKey("request_id")) which will be nil because the
canonical key constant is schemas.BifrostContextKeyRequestID; update the read to
use schemas.BifrostContextKeyRequestID (e.g.,
ctx.Value(schemas.BifrostContextKeyRequestID)) and ensure you consistently use
the canonical key when constructing the audit message (leave the mcp-audit-trail
key as-is if intended to be a separate key).

In `@examples/plugins/multi-interface/Makefile`:
- Line 1: Add missing phony targets to the Makefile by declaring "all" and
"test" in the .PHONY list and implementing them as simple aliases: make "all"
should depend on or invoke the existing "build" target and "test" should run the
project's test target or be a placeholder that returns success; update the
.PHONY line to include all, test, build, clean so checkmake/minphony warnings
are resolved and conventions are followed.

In `@framework/configstore/migrations.go`:
- Around line 2312-2727: migrationRemoveServerPrefixFromMCPTools fails to strip
legacy prefixes because client.Name was normalized earlier by
migrationNormalizeMCPClientNames; update the prefix-detection logic inside
migrationRemoveServerPrefixFromMCPTools (the loops that build prefix :=
clientName + "_" for tools_to_execute_json, tools_to_auto_execute_json, and
tool_pricing_json and the VK logic) to also detect and strip legacy prefixed
forms by comparing a normalized form of the tool's prefix to client.Name: for
each tool, if it contains '_' get the substring before '_' (toolPrefix),
normalize toolPrefix the same way migrationNormalizeMCPClientNames does
(lowercase, remove non-alphanumerics/normalize dashes), and if
normalized(toolPrefix) == clientName then treat it as a match and strip
toolPrefix + "_" — apply the same normalization-based detection for keys in
tool_pricing_json and for vkConfig.ToolsToExecute so both current and
pre-normalized prefixes are removed.

In `@transports/bifrost-http/lib/config_test.go`:
- Around line 460-499: Update UpdateMCPClientConfig to initialize m.mcpConfig
when it is nil (same pattern as CreateMCPClientConfig) so updates are applied to
the in-memory state returned by GetMCPConfig; inside UpdateMCPClientConfig, if
m.mcpConfig == nil set m.mcpConfig = &schemas.MCPConfig{ClientConfigs:
[]schemas.MCPClientConfig{}} (or equivalent) before iterating/append to
m.mcpConfig.ClientConfigs, and then proceed to locate by ID or append the new
schemas.MCPClientConfig constructed from the incoming clientConfig (use the same
field mappings as the existing append block).

In `@ui/app/workspace/logs/page.tsx`:
- Around line 77-115: The focus/visibility refresh currently only checks
userModifiedTimeRange, which misses shared URLs with non-default
start_time/end_time; update refreshDefaultsIfStale to only reset when the
current urlState range matches the original defaults captured on mount: capture
initialDefaults = getDefaultTimeRange() in a ref when the component mounts, then
in refreshDefaultsIfStale compare urlState.start_time and urlState.end_time
against initialDefaults (with the same small tolerance used now) and only call
setUrlState to rolling defaults if they match the initialDefaults and
userModifiedTimeRange.current is false; reference the functions/vars
refreshDefaultsIfStale, getDefaultTimeRange, userModifiedTimeRange, urlState,
setUrlState to locate and change the logic.
♻️ Duplicate comments (13)
examples/plugins/http-transport-only/README.md (1)

124-126: Tighten wording to avoid repeated “only”.

Minor readability nit; rephrase to avoid the double “only.”

✏️ Suggested tweak
-- This plugin operates at the HTTP transport layer only
-- Works only when using bifrost-http, not when using Bifrost as a Go SDK
+- This plugin operates at the HTTP transport layer
+- Works with bifrost-http, not when using Bifrost as a Go SDK
core/schemas/plugin_wasm.go (1)

5-8: Doc comment still mentions streaming short-circuit for WASM build.

The comment on line 6 states the type "can contain either a response (success short-circuit), a stream (streaming short-circuit), or an error" but the WASM struct has no Stream field. Line 7 correctly notes streams aren't supported, but line 6 creates confusion by listing it as a capability.

Suggested comment fix
 // LLMPluginShortCircuit represents a plugin's decision to short-circuit the normal flow.
-// It can contain either a response (success short-circuit), a stream (streaming short-circuit), or an error (error short-circuit).
+// In WASM plugins, it can contain either a response (success short-circuit) or an error (error short-circuit).
 // Streams are not supported in WASM plugins.
examples/plugins/hello-world/main.go (1)

41-46: Build failure: schemas.LLMPluginShortCircuit is undefined (Line 41).

The pipeline reports undefined: schemas.LLMPluginShortCircuit. Ensure the type is exported for this build (or update the example to the currently exported short‑circuit type) so the example compiles.

🐛 Possible fix (if the new type isn’t exported in this build)
-func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
+func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {
examples/plugins/multi-interface/main.go (3)

37-39: Protect requestCount against concurrent access.

Hooks run concurrently; unsynchronized increments/reads can race and corrupt counts.

🐛 Proposed fix (atomic)
 import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"sync/atomic"
 	"time"
 
 	"github.com/maximhq/bifrost/core/schemas"
 )
@@
-	requestCount int64
+	requestCount atomic.Int64
 	startTime    time.Time
 )
@@
-		requestCount++
-		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
+		current := requestCount.Add(1)
+		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", current)
@@
-			content = fmt.Sprintf("Processing request #%d. Server uptime: %v", requestCount, time.Since(startTime))
+			content = fmt.Sprintf("Processing request #%d. Server uptime: %v", requestCount.Load(), time.Since(startTime))
@@
-		fmt.Printf("[Multi-Interface Plugin] Cleanup called - processed %d requests over %v\n",
-			requestCount, uptime)
+		fmt.Printf("[Multi-Interface Plugin] Cleanup called - processed %d requests over %v\n",
+			requestCount.Load(), uptime)

106-110: Initialize req.Headers before writing.

req.Headers can be nil and will panic on assignment.

🐛 Proposed fix
 	// Add request tracking (configurable)
 	if pluginConfig.TrackRequests {
+		if req.Headers == nil {
+			req.Headers = map[string]string{}
+		}
 		requestCount++
 		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
 	}

129-133: Guard resp / resp.Headers before mutation.

Post hooks may receive nil response or nil headers.

🐛 Proposed fix
 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	if !pluginConfig.EnableHTTPHooks {
 		return nil
 	}
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = map[string]string{}
+	}
docs/openapi/paths/management/logging.yaml (1)

335-340: Status filter description vs. enum is still inconsistent.

This is the same issue already flagged earlier (comma-separated list vs. single-value enum).

Also applies to: 460-465

ui/app/workspace/mcp-logs/views/mcpLogsTable.tsx (1)

44-57: Type safety concern: initial sorting state can receive invalid sort_by values.

The Pagination type allows sort_by: "timestamp" | "latency" | "tokens" | "cost", but MCP logs only support "timestamp" and "latency". While handleSortingChange (line 53) correctly narrows the type when the user changes sorting, the initial state on line 44 directly uses pagination.sort_by without validation.

Consider adding runtime validation or using an MCP-specific pagination type to ensure only valid sort values are used.

core/mcp/utils.go (1)

224-230: Update comment examples to use "-" separator.

Same issue as above - the comment on line 226 shows "calculator/add" but should reflect the - separator convention.

ui/app/workspace/mcp-logs/page.tsx (1)

292-317: Stats double-counting risk on repeated terminal log updates.

The stats update increments counters whenever log.status is "success" or "error", but doesn't check if the log previously had a terminal status. If a completed log receives multiple updates (e.g., cost backfill), stats will be inflated.

🔧 Proposed fix: Guard against double-counting
 // Update stats for completed requests
-if (log.status === "success" || log.status === "error") {
+const existingLog = logs.find((l) => l.id === log.id);
+const wasTerminal = existingLog?.status === "success" || existingLog?.status === "error";
+const isTerminal = log.status === "success" || log.status === "error";
+
+// Only update stats if transitioning to terminal state
+if (isTerminal && !wasTerminal) {
   setStats((prevStats) => {
     if (!prevStats) return prevStats;
     // ... rest of stats update logic
   });
 }
docs/openapi/schemas/management/logging.yaml (1)

218-247: Existing issue: total_count placement mismatch between schema and backend.

This was flagged in a previous review—the OpenAPI schema defines pagination.total_count as required, but the backend stores this value in stats.total_executions. Either add TotalCount to the PaginationOptions struct and populate it in the handler, or move total_count under stats in this schema.

framework/logstore/rdb.go (1)

452-536: LGTM! Cost sorting now supported.

The switch statement at lines 473-482 correctly handles cost sorting, addressing the previous review feedback. The pagination, virtual key population, and hasLogs fallback logic are all properly implemented.

plugins/governance/main.go (1)

511-519: Tool delimiter uses dash but MCP parser may expect slash.

Lines 511 and 518 use fmt.Sprintf("%s-*", ...) and fmt.Sprintf("%s-%s", ...) respectively, using a dash delimiter. A previous review flagged that the MCP context parser expects clientName/toolName format (slash delimiter) based on core/mcp/mcp.go conventions.

🔧 Proposed fix
- executeOnlyTools = append(executeOnlyTools, fmt.Sprintf("%s-*", vkMcpConfig.MCPClient.Name))
+ executeOnlyTools = append(executeOnlyTools, fmt.Sprintf("%s/*", vkMcpConfig.MCPClient.Name))
- executeOnlyTools = append(executeOnlyTools, fmt.Sprintf("%s-%s", vkMcpConfig.MCPClient.Name, tool))
+ executeOnlyTools = append(executeOnlyTools, fmt.Sprintf("%s/%s", vkMcpConfig.MCPClient.Name, tool))
#!/bin/bash
# Verify the expected delimiter format in MCP parsing logic
echo "=== Checking MCP tool delimiter patterns ==="
rg -n "clientName.*toolName|tool.*split|shouldSkipTool" -g '*.go' -C 3 | head -80

echo -e "\n=== Checking include-tools header parsing ==="
rg -n "include-tools|MCPContextKeyIncludeTools" -g '*.go' -C 2 | head -40
🧹 Nitpick comments (21)
ui/app/workspace/logs/views/logDetailsSheet.tsx (1)

659-659: Consider adding whitespace-pre-wrap for consistent error display.

The break-words addition improves text wrapping for long error messages. However, the error details block at line 668 uses break-words whitespace-pre-wrap which preserves line breaks and whitespace. For consistency in error display formatting, consider applying the same treatment here.

Optional: Match styling with error details block
-<div className="px-6 py-2 font-mono text-xs break-words">{log.error_details.error.message}</div>
+<div className="px-6 py-2 font-mono text-xs break-words whitespace-pre-wrap">{log.error_details.error.message}</div>
plugins/mocker/benchmark_test.go (1)

12-13: Consider renaming benchmark functions for consistency.

The benchmark function names still reference PreHook (e.g., BenchmarkMockerPlugin_PreHook_SimpleRule) while the actual method calls have been updated to PreLLMHook. Consider updating function names to match (e.g., BenchmarkMockerPlugin_PreLLMHook_SimpleRule) for clarity and grep-ability.

♻️ Suggested rename for consistency
-// BenchmarkMockerPlugin_PreHook_SimpleRule benchmarks simple rule matching
-func BenchmarkMockerPlugin_PreHook_SimpleRule(b *testing.B) {
+// BenchmarkMockerPlugin_PreLLMHook_SimpleRule benchmarks simple rule matching
+func BenchmarkMockerPlugin_PreLLMHook_SimpleRule(b *testing.B) {

Apply similar renames to:

  • BenchmarkMockerPlugin_PreHook_RegexRuleBenchmarkMockerPlugin_PreLLMHook_RegexRule
  • BenchmarkMockerPlugin_PreHook_MultipleRulesBenchmarkMockerPlugin_PreLLMHook_MultipleRules
  • BenchmarkMockerPlugin_PreHook_NoMatchBenchmarkMockerPlugin_PreLLMHook_NoMatch
  • BenchmarkMockerPlugin_PreHook_TemplateBenchmarkMockerPlugin_PreLLMHook_Template
ui/components/sidebar.tsx (1)

71-88: Duplicate MCPIcon component - consider importing from icons.tsx.

This MCPIcon component duplicates the one already exported from ui/components/ui/icons.tsx (lines 1757-1772). The implementations are nearly identical. Consider importing the existing icon to reduce duplication.

♻️ Import existing icon instead
-// Custom MCP Icon Component
-const MCPIcon = ({ className }: { className?: string }) => (
-	<svg
-		className={className}
-		fill="currentColor"
-		fillRule="evenodd"
-		height="1em"
-		style={{ flex: "none", lineHeight: 1 }}
-		viewBox="0 0 24 24"
-		width="1em"
-		xmlns="http://www.w3.org/2000/svg"
-		aria-label="MCP clients icon"
-	>
-		<title>MCP clients icon</title>
-		<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
-		<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
-	</svg>
-);

Then add to imports:

import { MCPIcon } from "@/components/ui/icons";
framework/configstore/tables/mcp.go (1)

139-143: Inconsistent JSON library usage - use sonic.Unmarshal for consistency.

The AfterFind hook uses sonic.Unmarshal for all other fields (StdioConfig, ToolsToExecute, ToolsToAutoExecute, Headers) but uses standard json.Unmarshal for ToolPricing. Use sonic consistently for better performance and uniformity.

♻️ Use sonic.Unmarshal for consistency
 	if c.ToolPricingJSON != "" {
-		if err := json.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
+		if err := sonic.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
 			return err
 		}
 	}
transports/bifrost-http/handlers/websocket.go (1)

205-242: Consider extracting a shared broadcast helper.

BroadcastLogUpdate and BroadcastMCPLogUpdate now share the same flow; a small helper could reduce drift over time.

examples/plugins/multi-interface/main.go (1)

173-185: Prefer bifrost.Ptr for pointer fields.

♻️ Suggested change
 import (
 	"context"
 	"encoding/json"
 	"fmt"
 	"time"
 
+	bifrost "github.com/maximhq/bifrost/core"
 	"github.com/maximhq/bifrost/core/schemas"
 )
@@
 		systemMsg := schemas.ChatMessage{
 			Role:    "system",
-			Content: &schemas.ChatMessageContent{ContentStr: &content},
+			Content: &schemas.ChatMessageContent{ContentStr: bifrost.Ptr(content)},
 		}

Based on learnings, prefer bifrost.Ptr(...) over taking addresses directly for pointer fields.

plugins/logging/main.go (2)

743-765: Remove empty tool-name guard per upstream guarantees

Lines 743-765: upstream validation guarantees non‑empty tool names here, so the fullToolName != "" guard is unnecessary and can mask unexpected cases. Consider removing it and adding a brief comment about the invariant.

♻️ Suggested adjustment
-	// Extract server label from tool name (format: {client}-{tool_name})
-	// The first part before hyphen is the client/server label
-	if fullToolName != "" {
-		if idx := strings.Index(fullToolName, "-"); idx > 0 {
-			serverLabel = fullToolName[:idx]
-			toolName = fullToolName[idx+1:]
-		} else {
-			toolName = fullToolName
-		}
-		switch toolName {
-		case mcp.ToolTypeListToolFiles, mcp.ToolTypeReadToolFile, mcp.ToolTypeExecuteToolCode:
-			if serverLabel == "" {
-				serverLabel = "codemode"
-			}
-		}
-	}
+	// Extract server label from tool name (format: {client}-{tool_name})
+	// Upstream validation guarantees tool name is non-empty.
+	if idx := strings.Index(fullToolName, "-"); idx > 0 {
+		serverLabel = fullToolName[:idx]
+		toolName = fullToolName[idx+1:]
+	} else {
+		toolName = fullToolName
+	}
+	switch toolName {
+	case mcp.ToolTypeListToolFiles, mcp.ToolTypeReadToolFile, mcp.ToolTypeExecuteToolCode:
+		if serverLabel == "" {
+			serverLabel = "codemode"
+		}
+	}

Based on learnings, this aligns with the non-empty tool-name invariant in this file.


781-790: Use bifrost.Ptr for string pointers

Lines 781-790: prefer bifrost.Ptr(...) over &value for consistency with repo conventions.

♻️ Suggested adjustment
-		if parentRequestID != "" {
-			entry.LLMRequestID = &parentRequestID
-		}
+		if parentRequestID != "" {
+			entry.LLMRequestID = bifrost.Ptr(parentRequestID)
+		}
 
-		if virtualKeyID != "" {
-			entry.VirtualKeyID = &virtualKeyID
-		}
-		if virtualKeyName != "" {
-			entry.VirtualKeyName = &virtualKeyName
-		}
+		if virtualKeyID != "" {
+			entry.VirtualKeyID = bifrost.Ptr(virtualKeyID)
+		}
+		if virtualKeyName != "" {
+			entry.VirtualKeyName = bifrost.Ptr(virtualKeyName)
+		}

Based on learnings, this keeps pointer creation consistent across the repo.

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

2142-2156: Align cache rebuild with As*Plugin helpers

Lines 2147-2156: direct type assertions will include DynamicPlugin instances even when their hook pointers are nil. To keep cache behavior consistent with framework/plugins helpers, consider switching to plugins.AsLLMPlugin/AsMCPPlugin/AsHTTPTransportPlugin.

♻️ Suggested adjustment
-	for _, p := range *basePlugins {
-		if llmPlugin, ok := p.(schemas.LLMPlugin); ok {
-			llm = append(llm, llmPlugin)
-		}
-		if mcpPlugin, ok := p.(schemas.MCPPlugin); ok {
-			mcp = append(mcp, mcpPlugin)
-		}
-		if httpPlugin, ok := p.(schemas.HTTPTransportPlugin); ok {
-			httpTransport = append(httpTransport, httpPlugin)
-		}
-	}
+	for _, p := range *basePlugins {
+		if llmPlugin := plugins.AsLLMPlugin(p); llmPlugin != nil {
+			llm = append(llm, llmPlugin)
+		}
+		if mcpPlugin := plugins.AsMCPPlugin(p); mcpPlugin != nil {
+			mcp = append(mcp, mcpPlugin)
+		}
+		if httpPlugin := plugins.AsHTTPTransportPlugin(p); httpPlugin != nil {
+			httpTransport = append(httpTransport, httpPlugin)
+		}
+	}
plugins/maxim/plugin_test.go (1)

98-100: Prefer bifrost.Ptr for Account pointer creation.

Align with the repo’s pointer-construction convention for consistency.

♻️ Proposed change
-		Account:    &account,
+		Account:    bifrost.Ptr(account),
Based on learnings, prefer bifrost.Ptr() over the address operator for pointer creation.
core/mcp/utils.go (1)

192-198: Update comment examples to use "-" separator.

The comment on line 194 still shows "calculator/add" as the example, but tool names are now stored with the - separator (e.g., "calculator-add").

📝 Suggested fix
-		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// Tool names in config are stored without prefix (e.g., "add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")
transports/bifrost-http/server/utils.go (2)

22-30: Consider using a map or set for O(1) lookup.

The current implementation is fine for a small number of built-in plugins, but as the list grows, a map lookup would be more efficient and easier to maintain.

♻️ Optional refactor using a map
+var builtinPlugins = map[string]struct{}{
+	telemetry.PluginName:     {},
+	logging.PluginName:       {},
+	governance.PluginName:    {},
+	litellmcompat.PluginName: {},
+	maxim.PluginName:         {},
+	semanticcache.PluginName: {},
+	otel.PluginName:          {},
+}
+
 // isBuiltinPlugin checks if a plugin is a built-in plugin
 func isBuiltinPlugin(name string) bool {
-	return name == telemetry.PluginName ||
-		name == logging.PluginName ||
-		name == governance.PluginName ||
-		name == litellmcompat.PluginName ||
-		name == maxim.PluginName ||
-		name == semanticcache.PluginName ||
-		name == otel.PluginName
+	_, ok := builtinPlugins[name]
+	return ok
 }

123-150: Handle nil source input in MarshalPluginConfig.

The function returns an error for invalid types but doesn't explicitly handle nil input, which would fall through to the final error case. This is acceptable behavior, but documenting it or adding an explicit nil check would improve clarity.

♻️ Optional: Add explicit nil handling
 // MarshalPluginConfig marshals the plugin configuration
 func MarshalPluginConfig[T any](source any) (*T, error) {
+	if source == nil {
+		return nil, fmt.Errorf("config source is nil")
+	}
 	// If its a *T, then we will confirm
 	if config, ok := source.(*T); ok {
 		return config, nil
 	}
framework/plugins/soloader.go (2)

15-31: openPlugin mutates DynamicPlugin as a side effect.

The function modifies dp.Path (line 23) and dp.plugin (line 29) as side effects. While this works, consider returning the modified path or making the mutation intent more explicit in the function name (e.g., openAndConfigurePlugin).


128-158: VerifyBasePlugin duplicates symbol lookup logic from LoadPlugin.

Consider extracting the common GetName/Cleanup verification into a shared helper to reduce duplication and ensure consistency.

♻️ Proposed refactor to reduce duplication
// verifyRequiredSymbols checks for GetName and Cleanup symbols and returns the plugin name
func verifyRequiredSymbols(pluginObj *plugin.Plugin, dp *DynamicPlugin) (string, error) {
	getNameSym, err := pluginObj.Lookup("GetName")
	if err != nil {
		return "", fmt.Errorf("required symbol GetName not found: %w", err)
	}
	var ok bool
	if dp.getName, ok = getNameSym.(func() string); !ok {
		return "", fmt.Errorf("failed to cast GetName to func() string")
	}

	cleanupSym, err := pluginObj.Lookup("Cleanup")
	if err != nil {
		return "", fmt.Errorf("required symbol Cleanup not found: %w", err)
	}
	if dp.cleanup, ok = cleanupSym.(func() error); !ok {
		return "", fmt.Errorf("failed to cast Cleanup to func() error")
	}

	return dp.getName(), nil
}

Then use this in both LoadPlugin and VerifyBasePlugin.

core/mcp/agent.go (1)

292-316: Consider extracting goroutine logic for readability.

The inline goroutine with multiple conditional branches could be extracted into a named helper function for improved testability and readability.

♻️ Optional: Extract to helper function
func executeToolCallAsync(
    ctx *schemas.BifrostContext,
    toolCall schemas.ChatAssistantMessageToolCall,
    executeToolFunc func(*schemas.BifrostContext, *schemas.BifrostMCPRequest) (*schemas.BifrostMCPResponse, error),
) *schemas.ChatMessage {
    mcpRequest := &schemas.BifrostMCPRequest{
        RequestType:                  schemas.MCPRequestTypeChatToolCall,
        ChatAssistantMessageToolCall: &toolCall,
    }
    
    mcpResponse, toolErr := executeToolFunc(ctx, mcpRequest)
    if toolErr != nil {
        logger.Warn(fmt.Sprintf("Tool execution failed: %v", toolErr))
        return createToolResultMessage(toolCall, "", toolErr)
    }
    if mcpResponse != nil && mcpResponse.ChatMessage != nil {
        return mcpResponse.ChatMessage
    }
    return createToolResultMessage(toolCall, "", nil)
}
plugins/mocker/main.go (1)

494-497: Drop the redundant Enabled check in PreLLMHook.

Disabled plugins are filtered by the loader, so this branch is dead and can be removed to simplify the hot path.

♻️ Suggested cleanup
-	// Skip processing if plugin is disabled
-	if !p.config.Enabled {
-		return req, nil, nil
-	}

Based on learnings, disabled plugins aren’t added to the chain, so internal Enabled checks are unnecessary.

transports/bifrost-http/server/plugins.go (1)

146-164: Ignored error returns from registerPluginWithStatus.

Lines 151 and 161 call s.registerPluginWithStatus() but ignore the returned error (unlike line 142 which returns on error). If registration failure for logging or governance plugins should be non-fatal, consider at least logging the error rather than silently ignoring it.

♻️ Suggested improvement
 	// 2. Logging (if enabled)
 	if s.Config.ClientConfig.EnableLogging && s.Config.LogsStore != nil {
 		config := &logging.Config{
 			DisableContentLogging: &s.Config.ClientConfig.DisableContentLogging,
 		}
-		s.registerPluginWithStatus(ctx, logging.PluginName, nil, config, false)
+		if err := s.registerPluginWithStatus(ctx, logging.PluginName, nil, config, false); err != nil {
+			logger.Warn("failed to register logging plugin: %v", err)
+		}
 	} else {
 		s.markPluginDisabled(logging.PluginName)
 	}

 	// 3. Governance (if enabled and not enterprise)
 	if s.Config.ClientConfig.EnableGovernance && ctx.Value(schemas.BifrostContextKeyIsEnterprise) == nil {
 		config := &governance.Config{
 			IsVkMandatory: &s.Config.ClientConfig.EnforceGovernanceHeader,
 		}
-		s.registerPluginWithStatus(ctx, governance.PluginName, nil, config, false)
+		if err := s.registerPluginWithStatus(ctx, governance.PluginName, nil, config, false); err != nil {
+			logger.Warn("failed to register governance plugin: %v", err)
+		}
 	} else {
 		s.markPluginDisabled(governance.PluginName)
 	}
plugins/semanticcache/search.go (1)

297-309: Redundant context writes in streaming goroutine.

Lines 308-309 inside the goroutine duplicate the context writes already performed at lines 297-299 before the goroutine starts. While not harmful, this is unnecessary.

♻️ Optional cleanup
 	// Mark cache-hit once to avoid concurrent ctx writes
 	ctx.SetValue(isCacheHitKey, true)
 	ctx.SetValue(cacheHitTypeKey, cacheType)

 	// Create stream channel
 	streamChan := make(chan *schemas.BifrostStream)

 	go func() {
 		defer close(streamChan)

-		// Set cache-hit markers inside the streaming goroutine to avoid races
-		ctx.SetValue(isCacheHitKey, true)
-		ctx.SetValue(cacheHitTypeKey, cacheType)
-
 		// Process each stream chunk
 		for i, chunkData := range streamArray {
framework/logstore/rdb.go (1)

660-672: Consider returning a dedicated struct instead of []MCPToolLog.

GetAvailableMCPVirtualKeys returns []MCPToolLog but only uses VirtualKeyID and VirtualKeyName fields. This could be confusing for callers and wastes memory on unused fields.

♻️ Suggested improvement

Consider returning a lightweight struct:

type MCPVirtualKeyInfo struct {
    ID   string
    Name string
}

func (s *RDBLogStore) GetAvailableMCPVirtualKeys(ctx context.Context) ([]MCPVirtualKeyInfo, error) {
    var results []MCPVirtualKeyInfo
    result := s.db.WithContext(ctx).
        Model(&MCPToolLog{}).
        Select("DISTINCT virtual_key_id as id, virtual_key_name as name").
        Where("virtual_key_id IS NOT NULL AND virtual_key_id != '' AND virtual_key_name IS NOT NULL AND virtual_key_name != ''").
        Find(&results)
    // ...
}
core/bifrost.go (1)

2147-2159: Prefer full struct copy to avoid missing future fields.

The manual field copy can drift if schemas.ChatToolFunction grows. Consider copying the struct value and then mutating only Name.

♻️ Suggested refactor
-				toolFunction := schemas.ChatToolFunction{}
-				toolFunction.Name = tool.Function.Name
-				toolFunction.Description = tool.Function.Description
-				toolFunction.Parameters = tool.Function.Parameters
-				toolFunction.Strict = tool.Function.Strict
+				toolFunction := *tool.Function
 				// Remove the client prefix from the tool name
 				toolFunction.Name = strings.TrimPrefix(toolFunction.Name, client.ExecutionConfig.Name+"-")

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 6cc2685 to 53614f1 Compare January 20, 2026 15:38
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: 7

Caution

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

⚠️ Outside diff range comments (6)
framework/modelcatalog/main.go (1)

1-2: Package documentation is inconsistent with the terminology changes.

The comment and log message updates correctly align terminology to "model catalog". However, the package documentation on line 1 still says "provides a pricing manager for the framework", which is now inconsistent.

Suggested fix
-// Package modelcatalog provides a pricing manager for the framework.
+// Package modelcatalog provides a model catalog for the framework.
 package modelcatalog

Also applies to: 94-94, 118-118, 163-163

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

524-537: Add MCP hook patterns to properly categorize per-request goroutines.

The rename from PreHook/PostHook to PreLLMHook/PostLLMHook is correct and aligns with this PR's hook refactoring. However, PreMCPHook and PostMCPHook are now active in the codebase (defined in plugins, called via RunMCPPreHooks/RunMCPPostHooks in the core) but are missing from the perRequestPatterns list. These MCP hook patterns should be added alongside the LLM hook patterns to ensure MCP-related per-request goroutines are properly detected for leak detection.

Consider adding:

"PreMCPHook",
"PostMCPHook",
ui/app/workspace/logs/page.tsx (1)

158-173: Time‑range “modified” flag is too eager.

Any filter update that carries the existing time range will disable auto‑refresh. Only flip the flag when start/end actually change.

🛠️ Suggested fix
-			if (newFilters.start_time !== undefined || newFilters.end_time !== undefined) {
-				userModifiedTimeRange.current = true;
-			}
+			const startChanged = newFilters.start_time !== filters.start_time;
+			const endChanged = newFilters.end_time !== filters.end_time;
+			if (startChanged || endChanged) {
+				userModifiedTimeRange.current = true;
+			}
plugins/semanticcache/main.go (1)

356-360: Update PreLLMHook docblock return type to LLMPluginShortCircuit.

The comment still says it returns *schemas.BifrostResponse, but the signature now returns *schemas.LLMPluginShortCircuit. This is a quick doc fix to prevent confusion.

Proposed fix
-//   - *schemas.BifrostResponse: Cached response if found, nil otherwise
+//   - *schemas.LLMPluginShortCircuit: Cached response/stream/error if found, nil otherwise
transports/bifrost-http/handlers/plugins.go (1)

219-239: Rollback loaded plugin if DB create fails to avoid inconsistent state.
Loading before the DB insert can leave a running plugin without a persisted record if the insert fails. Consider unloading on error (or swapping the order with a delete-on-load-failure strategy).

🛠️ Proposed fix (rollback on DB insert failure)
 	if err := h.configStore.CreatePlugin(ctx, &configstoreTables.TablePlugin{
 		Name:     request.Name,
 		Enabled:  request.Enabled,
 		Config:   request.Config,
 		Path:     request.Path,
 		IsCustom: true,
 	}); err != nil {
 		logger.Error("failed to create plugin: %v", err)
+		if request.Enabled {
+			if removeErr := h.pluginsLoader.RemovePlugin(ctx, request.Name); removeErr != nil {
+				logger.Warn("failed to rollback loaded plugin after DB create error: %v", removeErr)
+			}
+		}
 		SendError(ctx, 500, "Failed to create plugin")
 		return
 	}
transports/bifrost-http/server/server.go (1)

581-589: Guard against nil proxy config to prevent panic.
ReloadProxyConfig dereferences config without a nil check; a nil config would crash the server.

🛠️ Proposed fix
 func (s *BifrostHTTPServer) ReloadProxyConfig(ctx context.Context, config *configstoreTables.GlobalProxyConfig) error {
 	if s.Config == nil {
 		return fmt.Errorf("config not found")
 	}
+	if config == nil {
+		return fmt.Errorf("proxy config is nil")
+	}
 	// Store the proxy config in memory for use by components that need it
 	s.Config.ProxyConfig = config
 	logger.Info("proxy configuration reloaded: enabled=%t, type=%s", config.Enabled, config.Type)
 	return nil
 }
🤖 Fix all issues with AI agents
In `@framework/configstore/rdb.go`:
- Around line 776-798: ConvertTableMCPConfigToSchemas currently drops callback
fields from tables.MCPConfig; update it to copy FetchNewRequestIDFunc,
PluginPipelineProvider, and ReleasePluginPipeline into the returned
*schemas.MCPConfig so those callbacks are preserved. In the
ConvertTableMCPConfigToSchemas function, after building clientConfigs and before
returning, set the corresponding fields on schemas.MCPConfig (using the same
names: FetchNewRequestIDFunc, PluginPipelineProvider, ReleasePluginPipeline)
from tableConfig so the schema object retains the callbacks.

In `@transports/bifrost-http/handlers/mcpserver.go`:
- Around line 329-333: Update the misleading comment next to the loop that
builds executeOnlyTools (symbols: vkMcpConfig.ToolsToExecute, executeOnlyTools,
fmt.Sprintf call) so it accurately reflects the actual separator/wildcard format
used by the code — change the comment from saying "wildcard uses '/'" to
indicate the wildcard uses the hyphen-based pattern (e.g., "-*" after the client
name) to match the fmt.Sprintf("%s-%s", vkMcpConfig.MCPClient.Name, tool)
behavior.

In `@transports/bifrost-http/lib/config_test.go`:
- Around line 472-519: UpdateMCPClientConfig currently reconstructs
TableMCPClient entries and can drop the stable internal ID or leave ClientID
empty; modify the update logic in UpdateMCPClientConfig so that when you match
an existing entry in m.mcpConfig.ClientConfigs you preserve its existing
internal ID field and set ClientID to the passed-in id when
clientConfig.ClientID is empty, and when appending a new entry ensure you
populate ClientID with id and carry over any existing clientConfig.ID (or set a
stable ID if none) so GetMCPConfig remains consistent for tests that rely on
IDs.

In `@transports/bifrost-http/lib/config.go`:
- Around line 2851-2858: convertSchemasMCPClientConfigToTable currently omits
ToolPricing so c.MCPConfig.ClientConfigs entries never contain pricing; update
the conversion to populate the ToolPricing field and/or, inside AddMCPClient
after fetching pricing from the DB (where ToolPricing is obtained), assign that
fetched map back into the in-memory entry
(c.MCPConfig.ClientConfigs[i].ToolPricing) so that
RemoveMCPClient/removedClientConfig sees the real ToolPricing and the loop
calling c.MCPCatalog.DeletePricingData(iterated toolName) will execute
correctly. Ensure you update references in convertSchemasMCPClientConfigToTable
and the AddMCPClient flow that appends/updates c.MCPConfig.ClientConfigs to keep
ToolPricing in sync.

In `@ui/app/workspace/logs/page.tsx`:
- Around line 88-105: Auto‑refresh stops because after the first refresh
urlState no longer equals the original initialDefaults.current; when you call
setUrlState in the focus handler (the block using urlState,
initialDefaults.current, getDefaultTimeRange and setUrlState), also update the
baseline by assigning initialDefaults.current.startTime and
initialDefaults.current.endTime to the new defaults returned from
getDefaultTimeRange so subsequent focus events compare against the refreshed
baseline and allow further auto‑refreshes.

In `@ui/app/workspace/mcp-logs/views/emptyState.tsx`:
- Around line 67-69: The icon-only Copy button lacks an accessible name; update
the Button where onClick={copyToClipboard} renders the <Copy /> icon to include
an accessible label (e.g., add aria-label="Copy logs" or aria-label="Copy to
clipboard") or include visually-hidden text inside the Button so screen readers
announce its purpose; ensure the change is applied to the Button instance that
wraps the <Copy /> icon and keep the existing copyToClipboard handler unchanged.

In `@ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx`:
- Around line 120-133: The latency check treats 0 as falsy and shows "NA";
update the two usages of log.latency in the LogEntryDetailsView block to use
nullish checks so zero is preserved: replace .add(log.latency || 0, "ms") with
.add(log.latency ?? 0, "ms") and change the Latency value expression from
log.latency ? `${log.latency.toFixed(2)}ms` : "NA" to a null/undefined check
such as log.latency != null ? `${log.latency.toFixed(2)}ms` : "NA" (or use ??
consistently).
♻️ Duplicate comments (18)
examples/plugins/multi-interface/Makefile (1)

1-1: Add all/test phony targets to satisfy checkmake.

This was already raised in a prior review and still applies.

🛠️ Proposed update
-.PHONY: build clean
+.PHONY: build clean all test
+
+all: build
+
+test:
+	`@echo` "No tests configured for this example"
ui/app/workspace/mcp-logs/views/emptyState.tsx (1)

85-247: Track the getExampleBaseUrl() dependency in the memoized examples.

examples can become stale if the base URL changes. This was already flagged earlier; reiterating to ensure it’s fixed.

🩹 Proposed fix
 export function MCPEmptyState({ isSocketConnected, error }: MCPEmptyStateProps) {
 	const [language, setLanguage] = useState<Language>("python");
 
 	// Generate examples dynamically using the port utility
-	const examples: Examples = useMemo(() => {
-		const baseUrl = getExampleBaseUrl();
+	const baseUrl = getExampleBaseUrl();
+	const examples: Examples = useMemo(() => {
 
 		return {
 			manual: {
 				python: `import openai
@@
-	}, []);
+	}, [baseUrl]);
core/mcp/utils.go (1)

192-197: Fix example tool-name separator in comments (use “-”, not “/”).

The example strings still show calculator/add, but tool names are now prefixed as calculator-add. Please update both comment blocks to avoid confusion.

Proposed fix
-// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")

Also applies to: 224-227

ui/app/workspace/mcp-logs/page.tsx (1)

292-315: Prevent stats double-counting on repeated terminal updates.

If a completed log receives multiple updates, these increments can inflate totals. Track the previous status before incrementing.

🐛 Proposed fix
-				// Update stats for completed requests
-				if (log.status === "success" || log.status === "error") {
+				// Update stats only when transitioning into a terminal status
+				const prevLog = logs.find((existingLog) => existingLog.id === log.id);
+				const wasTerminal = prevLog && (prevLog.status === "success" || prevLog.status === "error");
+				if ((log.status === "success" || log.status === "error") && !wasTerminal) {
 					setStats((prevStats) => {
 						if (!prevStats) return prevStats;
examples/plugins/http-transport-only/main.go (2)

176-178: Guard req.Headers before mutation.

req.Headers can be nil and will panic when writing.

🐛 Proposed fix
 	// Example 4: Add custom headers
+	if req.Headers == nil {
+		req.Headers = map[string]string{}
+	}
 	req.Headers["X-Plugin-Processed"] = "true"
 	req.Headers["X-Request-Time"] = time.Now().Format(time.RFC3339)

189-210: Guard resp / resp.Headers before mutation.

Post hooks can receive a nil response or nil headers.

🐛 Proposed fix
 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	fmt.Println("[HTTP-Transport-Only Plugin] HTTPTransportPostHook called")
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = map[string]string{}
+	}
 
 	// Calculate request duration
 	startTime := ctx.Value(schemas.BifrostContextKey("http-plugin-start-time"))
examples/plugins/http-transport-only/README.md (1)

124-126: Address repeated "only" for better readability.

The static analysis correctly identifies the double use of "only" in consecutive lines. This was also flagged in a past review.

✏️ Suggested fix
 ## Notes
 
-- This plugin operates at the HTTP transport layer only
-- Works only when using bifrost-http, not when using Bifrost as a Go SDK
+- This plugin operates at the HTTP transport layer
+- Works with bifrost-http; not available when using Bifrost as a Go SDK
 - Rate limiter is in-memory (resets on restart)
core/schemas/plugin_wasm.go (1)

5-8: Clarify WASM short‑circuit comment (streaming not supported).
Line 6 mentions streaming even though WASM short-circuits do not support streams.

Proposed doc tweak
-// LLMPluginShortCircuit represents a plugin's decision to short-circuit the normal flow.
-// It can contain either a response (success short-circuit), a stream (streaming short-circuit), or an error (error short-circuit).
-// Streams are not supported in WASM plugins.
+// LLMPluginShortCircuit represents a plugin's decision to short-circuit the normal flow.
+// In WASM plugins, it can contain either a response (success short-circuit) or an error (error short-circuit).
+// Streams are not supported in WASM plugins.
docs/openapi/paths/management/logging.yaml (1)

335-340: Inconsistency between description and schema for status parameter.

The description says "Comma-separated list of statuses to filter by" but the schema has an enum which restricts to a single value. This same issue exists at lines 459-464 for mcp-logs-stats.

docs/plugins/getting-started.mdx (1)

52-59: Add PreMCPHook/PostMCPHook to the v1.4.x+ function list.

The lifecycle section (Line 94) references PreMCPHook/PostMCPHook, but the v1.4.x+ exported function list doesn’t include them, which is confusing for implementers.

📚 Suggested doc fix
-    - `PreLLMHook()` - Intercept requests before they reach providers
-    - `PostLLMHook()` - Process responses after provider calls
+    - `PreLLMHook()` - Intercept requests before they reach providers
+    - `PreMCPHook()` - Intercept MCP tool requests before execution
+    - `PostLLMHook()` - Process responses after provider calls
+    - `PostMCPHook()` - Process MCP tool responses after execution

Based on learnings, please cross-check against the stacked PRs to ensure the hook list matches the implemented interface.

examples/plugins/hello-world/main.go (1)

41-46: Fix undefined LLMPluginShortCircuit (build currently fails).

Line 41 still references schemas.LLMPluginShortCircuit, and CI reports it as undefined. Either add the type to core/schemas or use the existing short‑circuit type so the example compiles.

🐛 Minimal fix (if LLMPluginShortCircuit isn’t exported yet)
-func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
+func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {
examples/plugins/multi-interface/main.go (3)

37-39: Race condition on requestCount remains unaddressed.

The requestCount variable is accessed concurrently by HTTPTransportPreHook (line 108), PreLLMHook (line 177), and Cleanup (line 309) without synchronization. Use atomic.Int64 or a mutex.


106-110: Initialize req.Headers before writing.

req.Headers may be nil; writing to it will panic.


129-149: Guard resp and resp.Headers before mutation.

Post hooks may receive a nil response or nil headers map, causing a panic on write at lines 132 and 149.

examples/plugins/mcp-only/main.go (1)

114-121: Use the canonical request-id context key.

The audit trail reads request_id using a custom key, but the canonical key is schemas.BifrostContextKeyRequestID, so this will always resolve to nil.

🔧 Proposed fix
-		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKey("request_id")))
+		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKeyRequestID))
docs/openapi/schemas/management/logging.yaml (1)

263-277: Clarify value field behavior in virtual_keys.

The value field description states "redacted if applicable" (line 276), but per past review findings, the implementation returns an empty string without explicit redaction. Consider either:

  1. Updating the description to clarify the field is always empty in filter responses, or
  2. Adding omitempty behavior documentation if the field should be omitted entirely

This is a documentation accuracy concern, not a functional issue.

transports/bifrost-http/server/server.go (2)

654-735: Call Cleanup on replaced/removed plugins to avoid leaks.
ReloadPlugin and RemovePlugin replace/unregister plugin instances without invoking Cleanup, so background resources may linger.

🛠️ Suggested pattern (cleanup old instance)
 func (s *BifrostHTTPServer) ReloadPlugin(ctx context.Context, name string, path *string, pluginConfig any) error {
 	logger.Debug("reloading plugin %s", name)
+	oldPlugin, _ := s.Config.FindPluginByName(name)
@@
 	// 2. Register (replaces old version atomically)
 	if err := s.Config.RegisterPlugin(plugin); err != nil {
 		return updateError("registering", err)
 	}
+	if oldPlugin != nil {
+		if err := oldPlugin.Cleanup(); err != nil {
+			logger.Warn("failed to cleanup old plugin %s: %v", name, err)
+		}
+	}
@@
 func (s *BifrostHTTPServer) RemovePlugin(ctx context.Context, displayName string) error {
@@
-	// 1. Unregister from config
+	oldPlugin, _ := s.Config.FindPluginByName(name)
+	// 1. Unregister from config
 	if err := s.Config.UnregisterPlugin(name); err != nil {
 		return err
 	}
+	if oldPlugin != nil {
+		if err := oldPlugin.Cleanup(); err != nil {
+			logger.Warn("failed to cleanup plugin %s: %v", name, err)
+		}
+	}

19-22: Duplicate import of configstore/tables will not compile.
Go forbids importing the same path twice; keep a single alias and update all references accordingly.

Additionally, ReloadProxyConfig accesses the config parameter without a nil guard at line 587 (accessing config.Enabled and config.Type), which will panic if a nil config is passed. Add a nil check before line 587.

🛠️ Proposed fixes
 import (
 	"context"
 	"embed"
 	"errors"
 	"fmt"
 	"net"
 	"os"
 	"os/signal"
 	"syscall"
 	"time"

 	"github.com/fasthttp/router"
 	"github.com/google/uuid"
 	bifrost "github.com/maximhq/bifrost/core"
 	"github.com/maximhq/bifrost/core/schemas"
 	"github.com/maximhq/bifrost/framework/configstore"
-	"github.com/maximhq/bifrost/framework/configstore/tables"
 	configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables"
-	ReloadModelConfig(ctx context.Context, id string) (*tables.TableModelConfig, error)
+	ReloadModelConfig(ctx context.Context, id string) (*configstoreTables.TableModelConfig, error)
-	ReloadProvider(ctx context.Context, name string) (*tables.TableProvider, error)
+	ReloadProvider(ctx context.Context, name string) (*configstoreTables.TableProvider, error)
 func (s *BifrostHTTPServer) ReloadProxyConfig(ctx context.Context, config *configstoreTables.GlobalProxyConfig) error {
 	if s.Config == nil {
 		return fmt.Errorf("config not found")
 	}
+	if config == nil {
+		return fmt.Errorf("proxy config not found")
+	}
 	// Store the proxy config in memory for use by components that need it
 	s.Config.ProxyConfig = config
🧹 Nitpick comments (19)
examples/plugins/multi-interface/README.md (1)

176-178: Call out concurrency safety for shared state.

The Notes section mentions tracking state across requests, but doesn’t warn that counters/timestamps must be guarded (mutex/atomic) to avoid data races in Go plugins. Consider adding a short note.

ui/components/sidebar.tsx (1)

71-88: Prefer the shared MCPIcon to avoid duplication.

You already have MCPIcon in ui/components/ui/icons.tsx; reusing it avoids divergence and reduces maintenance.

♻️ Suggested refactor
@@
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
 import { Separator } from "@/components/ui/separator";
+import { MCPIcon } from "@/components/ui/icons";
@@
-// Custom MCP Icon Component
-const MCPIcon = ({ className }: { className?: string }) => (
-	<svg
-		className={className}
-		fill="currentColor"
-		fillRule="evenodd"
-		height="1em"
-		style={{ flex: "none", lineHeight: 1 }}
-		viewBox="0 0 24 24"
-		width="1em"
-		xmlns="http://www.w3.org/2000/svg"
-		aria-label="MCP clients icon"
-	>
-		<title>MCP clients icon</title>
-		<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
-		<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
-	</svg>
-);
ui/app/workspace/mcp-logs/views/emptyState.tsx (1)

76-78: Avoid socket/streaming-aware UI state in this component.

This component currently renders UI based on isSocketConnected, which couples it to live connection state. Consider lifting the socket-aware logic to the parent and passing down a precomputed status element/string so the component renders data-only UI. Based on learnings, avoid streaming-aware logic in ui/ components.

Also applies to: 269-277

core/mcp/codemodeexecutecode_test.go (1)

248-259: Consider strengthening the truncation assertion.

The test only logs when truncation occurs but doesn't assert the expected behavior. If truncation is broken, the test will still pass.

💡 Suggested improvement
 	t.Run("Truncate long result", func(t *testing.T) {
 		longString := ""
 		for i := 0; i < 300; i++ {
 			longString += "a"
 		}

 		result := formatResultForLog(longString)
-		if len(result) > 200 {
-			// Should be truncated to around 200 chars (plus quotes and ellipsis)
-			t.Logf("Result length: %d (truncated as expected)", len(result))
+		// Should be truncated to around 200 chars (plus quotes and ellipsis indicator)
+		if len(result) >= len(longString) {
+			t.Errorf("Expected result to be truncated, got length %d", len(result))
 		}
 	})
framework/configstore/tables/mcp.go (2)

12-18: Consider using typed function signatures for plugin pipeline callbacks.

The PluginPipelineProvider and ReleasePluginPipeline fields use interface{}, which loses type safety. If these are meant to work with a specific pipeline type, consider using a concrete type or a defined interface to catch type mismatches at compile time rather than runtime.


139-143: Inconsistent JSON library usage between BeforeSave and AfterFind.

BeforeSave uses encoding/json for marshaling ToolPricing (line 103), but AfterFind uses encoding/json for unmarshaling while other fields use sonic.Unmarshal. This inconsistency could lead to subtle behavioral differences. Consider using sonic consistently for all (de)serialization in this file, matching the pattern used for other fields.

♻️ Suggested fix for consistency
 	if c.ToolPricingJSON != "" {
-		if err := json.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
+		if err := sonic.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
 			return err
 		}
 	}
transports/bifrost-http/server/utils.go (2)

22-30: Consider using a set or map for maintainability.

The current implementation uses chained comparisons, which will require modification each time a new built-in plugin is added. A map-based lookup would be more maintainable.

♻️ Optional refactor using a set
+var builtinPlugins = map[string]struct{}{
+	telemetry.PluginName:     {},
+	logging.PluginName:       {},
+	governance.PluginName:    {},
+	litellmcompat.PluginName: {},
+	maxim.PluginName:         {},
+	semanticcache.PluginName: {},
+	otel.PluginName:          {},
+}
+
 // isBuiltinPlugin checks if a plugin is a built-in plugin
 func isBuiltinPlugin(name string) bool {
-	return name == telemetry.PluginName ||
-		name == logging.PluginName ||
-		name == governance.PluginName ||
-		name == litellmcompat.PluginName ||
-		name == maxim.PluginName ||
-		name == semanticcache.PluginName ||
-		name == otel.PluginName
+	_, exists := builtinPlugins[name]
+	return exists
 }

123-150: Redundant byte slice conversion in map marshaling path.

On line 133, sonic.Marshal already returns []byte, so the cast on line 137 ([]byte(configString)) is unnecessary since configString is already []byte.

♻️ Remove redundant conversion
 	// If its a map[string]any, then we will JSON parse and confirm
 	if configMap, ok := source.(map[string]any); ok {
-		configString, err := sonic.Marshal(configMap)
+		configBytes, err := sonic.Marshal(configMap)
 		if err != nil {
 			return nil, err
 		}
-		if err := sonic.Unmarshal([]byte(configString), config); err != nil {
+		if err := sonic.Unmarshal(configBytes, config); err != nil {
 			return nil, err
 		}
 		return config, nil
 	}
core/mcp/codemodereadfile.go (1)

284-308: Path traversal validation is good, but contains redundant checks.

The .. check on line 285 covers the entire path, making the subsequent .. checks on lines 295 and 303 redundant since they would already be caught.

Additionally, the check on line 295 for strings.Contains(parts[1], "/") is logically unreachable since parts[1] was derived from splitting on /.

♻️ Simplify redundant checks
 	// Defensive validation: reject paths with path traversal attempts
 	if strings.Contains(basePath, "..") {
 		// Return empty to indicate invalid path
 		return "", "", false
 	}

 	// Check for path separator
 	parts := strings.Split(basePath, "/")
 	if len(parts) == 2 {
 		// Tool-level: "serverName/toolName"
-		// Validate that tool name doesn't contain additional path separators or traversal
-		if parts[1] == "" || strings.Contains(parts[1], "/") || strings.Contains(parts[1], "..") {
+		// Validate that tool name is not empty
+		if parts[1] == "" {
 			// Invalid tool name, treat as server-level
 			return parts[0], "", false
 		}
 		return parts[0], parts[1], true
 	}
 	// Server-level: "serverName"
-	// Validate server name doesn't contain path separators or traversal
-	if strings.Contains(basePath, "/") || strings.Contains(basePath, "..") {
-		// Invalid path
-		return "", "", false
-	}
+	// Note: If we reach here with len(parts) != 2, basePath has no "/" 
+	// and ".." was already checked above
 	return basePath, "", false
core/mcp/codemodeexecutecode.go (1)

882-891: Consider logging when pre-hook argument parsing fails.

When sonic.Unmarshal fails for modified tool arguments, the code logs a warning but continues with original arguments. This is reasonable fallback behavior, though you might want to track this metric for debugging plugin issues.

framework/logstore/store.go (1)

50-50: Align store interface with plugin layer's lighter return type.

The GetAvailableMCPVirtualKeys method returns []MCPToolLog, but the database query only selects virtual_key_id and virtual_key_name fields. The plugin layer already demonstrates the optimal pattern by converting this to []KeyPair (containing just ID and Name). Updating the store interface to return []KeyPair directly would eliminate unnecessary overhead and align the lower layer with what the upper layer is already doing.

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

224-228: Prefer shared MCPContextKeyIncludeTools constant.
Line 227/339: the literal key duplicates core’s constant; reusing it prevents drift across the stack.

♻️ Suggested refactor
 import (
 	"bufio"
 	"context"
 	"fmt"
 	"slices"
 	"strings"
 	"sync"

 	"github.com/bytedance/sonic"
+	coremcp "github.com/maximhq/bifrost/core/mcp"
 	"github.com/fasthttp/router"
 	"github.com/mark3labs/mcp-go/mcp"
 	"github.com/mark3labs/mcp-go/server"
 	bifrost "github.com/maximhq/bifrost/core"
 	"github.com/maximhq/bifrost/core/schemas"
-				ctx = context.WithValue(ctx, schemas.BifrostContextKey("mcp-include-tools"), toolFilter)
+				ctx = context.WithValue(ctx, coremcp.MCPContextKeyIncludeTools, toolFilter)
-	ctx = context.WithValue(ctx, schemas.BifrostContextKey("mcp-include-tools"), executeOnlyTools)
+	ctx = context.WithValue(ctx, coremcp.MCPContextKeyIncludeTools, executeOnlyTools)

Also applies to: 338-340

framework/plugins/soplugin_test.go (1)

649-651: Use bifrost.Ptr for string pointers.
Line 651: the helper still uses &; the repo prefers bifrost.Ptr for pointer creation. Based on learnings, prefer bifrost.Ptr here.

♻️ Suggested refactor
 import (
 	"context"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"runtime"
 	"strings"
 	"testing"
 	"time"

+	bifrost "github.com/maximhq/bifrost/core"
 	"github.com/maximhq/bifrost/core/schemas"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
 func stringPtr(s string) *string {
-	return &s
+	return bifrost.Ptr(s)
 }
plugins/logging/main.go (1)

781-790: Use bifrost.Ptr for MCP log pointer fields.
Line 781-789: prefer bifrost.Ptr for LLMRequestID/virtual key pointers to match repo style. Based on learnings, use bifrost.Ptr instead of &.

♻️ Suggested refactor
-		if parentRequestID != "" {
-			entry.LLMRequestID = &parentRequestID
-		}
-
-		if virtualKeyID != "" {
-			entry.VirtualKeyID = &virtualKeyID
-		}
-		if virtualKeyName != "" {
-			entry.VirtualKeyName = &virtualKeyName
-		}
+		if parentRequestID != "" {
+			entry.LLMRequestID = bifrost.Ptr(parentRequestID)
+		}
+
+		if virtualKeyID != "" {
+			entry.VirtualKeyID = bifrost.Ptr(virtualKeyID)
+		}
+		if virtualKeyName != "" {
+			entry.VirtualKeyName = bifrost.Ptr(virtualKeyName)
+		}
framework/logstore/tables.go (1)

536-557: Silent error handling in DeserializeFields may mask data issues.

The deserialization silently swallows JSON unmarshal errors by setting fields to nil. While this prevents query failures on corrupted data, it could mask data integrity issues.

Consider adding debug-level logging for unmarshal failures to aid troubleshooting, similar to the pattern used in other deserialize functions that have comments like "// Log error but don't fail the operation".

framework/logstore/rdb.go (1)

660-672: Consider returning a dedicated type instead of []MCPToolLog.

GetAvailableMCPVirtualKeys returns []MCPToolLog but only populates VirtualKeyID and VirtualKeyName fields. A dedicated struct or returning []tables.TableVirtualKey directly would better communicate the intent and reduce confusion.

💡 Alternative approach
// Option 1: Return a slice of simpler structs
type MCPVirtualKeyInfo struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

func (s *RDBLogStore) GetAvailableMCPVirtualKeys(ctx context.Context) ([]MCPVirtualKeyInfo, error) {
    var keys []MCPVirtualKeyInfo
    result := s.db.WithContext(ctx).
        Model(&MCPToolLog{}).
        Select("DISTINCT virtual_key_id as id, virtual_key_name as name").
        Where("virtual_key_id IS NOT NULL AND virtual_key_id != '' AND virtual_key_name IS NOT NULL AND virtual_key_name != ''").
        Find(&keys)
    // ...
}
core/bifrost.go (3)

292-310: Clarify nil vs empty slice semantics in documentation.

The nil check if config.LLMPlugins != nil creates a subtle API distinction:

  • nil → preserve existing plugins
  • []schemas.LLMPlugin{} (empty) → clear all plugins

This is a valid design pattern but could benefit from a doc comment explaining this behavior, especially since callers may unintentionally pass empty slices expecting no change.

📝 Suggested documentation
 // ReloadConfig reloads the config from DB
-// Currently we update account, drop excess requests, and plugin lists
+// Currently we update account, drop excess requests, and plugin lists.
+// Note: For plugin lists, nil means "preserve existing", while an empty slice means "clear all".
 // We will keep on adding other aspects as required
 func (bifrost *Bifrost) ReloadConfig(config schemas.BifrostConfig) error {

3792-3819: Consider preserving error context in executeMCPToolWithHooks.

The error wrapping at line 3817 loses the structured BifrostError information:

return nil, fmt.Errorf("%s", GetErrorMessage(bifrostErr))

While this matches the error return type expected by the agent interface, it discards potentially useful context like StatusCode, IsBifrostError, and ExtraFields.

♻️ Consider implementing the error interface on BifrostError

If BifrostError implements the error interface, you could return it directly:

// In schemas/errors.go (if not already present)
func (e *BifrostError) Error() string {
    if e.Error != nil && e.Error.Message != "" {
        return e.Error.Message
    }
    return "bifrost error"
}

Then in executeMCPToolWithHooks:

-    return nil, fmt.Errorf("%s", GetErrorMessage(bifrostErr))
+    return nil, bifrostErr

This preserves the structured error for callers that want to type-assert, while still satisfying the error interface.


2207-2219: Consider extracting common MCP config setup to reduce duplication.

The plugin pipeline provider setup (lines 2207-2219) duplicates the same logic from Init (lines 226-236). If one is modified, the other could be forgotten.

♻️ Extract helper for MCP config setup
// setupMCPConfigWithPipelineProviders configures the MCP config with plugin pipeline functions
func (bifrost *Bifrost) setupMCPConfigWithPipelineProviders(config *schemas.MCPConfig) {
    config.PluginPipelineProvider = func() interface{} {
        return bifrost.getPluginPipeline()
    }
    config.ReleasePluginPipeline = func(pipeline interface{}) {
        if pp, ok := pipeline.(*PluginPipeline); ok {
            bifrost.releasePluginPipeline(pp)
        }
    }
}

Then use in both Init and AddMCPClient:

mcpConfig := *config.MCPConfig
bifrost.setupMCPConfigWithPipelineProviders(&mcpConfig)
bifrost.mcpManager = mcp.NewMCPManager(bifrostCtx, mcpConfig, bifrost.logger)

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 53614f1 to cdbcb7c Compare January 20, 2026 21:15
@Pratham-Mishra04 Pratham-Mishra04 mentioned this pull request Jan 20, 2026
18 tasks
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: 9

Caution

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

⚠️ Outside diff range comments (9)
plugins/maxim/main.go (1)

588-602: Add guards before adding model tags to generation/trace.

Lines 599-600 call AddTagToGeneration and AddTagToTrace unconditionally, but generationID or traceID may be empty strings if hasGenerationID or hasTraceID were false. This is inconsistent with the guarded pattern used in the tags loop above (lines 591-597).

Proposed fix
 		if hasTags {
 			for key, value := range tags {
 				if generationID != "" {
 					logger.AddTagToGeneration(generationID, key, value)
 				}
 				if traceID != "" {
 					logger.AddTagToTrace(traceID, key, value)
 				}
 			}
 		}
-		logger.AddTagToGeneration(generationID, "model", string(model))
-		logger.AddTagToTrace(traceID, "model", string(model))
+		if generationID != "" {
+			logger.AddTagToGeneration(generationID, "model", string(model))
+		}
+		if traceID != "" {
+			logger.AddTagToTrace(traceID, "model", string(model))
+		}
 		// Flush only the effective logger that was used for this request
 		logger.Flush()
plugins/telemetry/main.go (1)

301-408: Nil dereference and high-cardinality metric explosion risk on line 406.

At line 406, bifrostErr.Error.Message is accessed without a nil check on the Error pointer, which will panic if Error is nil. Additionally, using error messages as Prometheus labels creates unbounded cardinality and explodes the series count with each unique message from external providers.

Use bounded fields like bifrostErr.Type or bifrostErr.StatusCode instead, with a safe fallback:

✅ Suggested fix
-			errorPromLabelValues := make([]string, 0, len(promLabelValues)+1)
-			errorPromLabelValues = append(errorPromLabelValues, promLabelValues[:len(p.defaultBifrostLabels)]...) // all default labels
-			errorPromLabelValues = append(errorPromLabelValues, bifrostErr.Error.Message)                         // reason
-			errorPromLabelValues = append(errorPromLabelValues, promLabelValues[len(p.defaultBifrostLabels):]...) // then custom labels
+			reason := "unknown"
+			if bifrostErr.Type != nil {
+				reason = *bifrostErr.Type
+			} else if bifrostErr.StatusCode != nil {
+				reason = strconv.Itoa(*bifrostErr.StatusCode)
+			}
+			errorPromLabelValues := make([]string, 0, len(promLabelValues)+1)
+			errorPromLabelValues = append(errorPromLabelValues, promLabelValues[:len(p.defaultBifrostLabels)]...) // all default labels
+			errorPromLabelValues = append(errorPromLabelValues, reason)                                           // reason
+			errorPromLabelValues = append(errorPromLabelValues, promLabelValues[len(p.defaultBifrostLabels):]...) // then custom labels
docs/plugins/getting-started.mdx (1)

99-104: v1.3.x lifecycle section uses old hook names.

The v1.3.x execution order section (lines 101, 103) still references PreHook and PostHook, but the function list (lines 65-66) was updated to PreLLMHook/PostLLMHook. Update the lifecycle section for consistency:

   <Tab title="v1.3.x">
     1. `TransportInterceptor` - Modifies raw HTTP requests (HTTP transport only)
-    2. `PreHook` - Executes in registration order, can short-circuit requests
+    2. `PreLLMHook` - Executes in registration order, can short-circuit requests
     3. Provider call (if not short-circuited)
-    4. `PostHook` - Executes in reverse order of PreHooks
+    4. `PostLLMHook` - Executes in reverse order of PreLLMHooks
   </Tab>
transports/bifrost-http/handlers/plugins.go (1)

219-239: Plugin reload before DB write can leave orphaned runtime state.

If ReloadPlugin succeeds (line 221) but CreatePlugin fails (line 229), the plugin will be loaded in memory but not persisted. Consider adding cleanup on DB write failure or reversing the order (DB write first, then reload).

🐛 Suggested fix: cleanup on failure or reorder

Option 1: Cleanup on DB failure

 	// We reload the plugin if its enabled
 	if request.Enabled {
 		if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, request.Path, request.Config); err != nil {
 			logger.Error("failed to load plugin: %v", err)
 			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin created in database but failed to load: %v", err))
 			return
 		}
 	}

 	// Create a DB entry if plugin loading was successful
 	if err := h.configStore.CreatePlugin(ctx, &configstoreTables.TablePlugin{
 		Name:     request.Name,
 		Enabled:  request.Enabled,
 		Config:   request.Config,
 		Path:     request.Path,
 		IsCustom: true,
 	}); err != nil {
+		// Cleanup: remove the loaded plugin since DB write failed
+		if request.Enabled {
+			_ = h.pluginsLoader.RemovePlugin(ctx, request.Name)
+		}
 		logger.Error("failed to create plugin: %v", err)
 		SendError(ctx, 500, "Failed to create plugin")
 		return
 	}

Option 2: DB write first, then reload (preferred)

-	// We reload the plugin if its enabled
-	if request.Enabled {
-		if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, request.Path, request.Config); err != nil {
-			logger.Error("failed to load plugin: %v", err)
-			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin created in database but failed to load: %v", err))
-			return
-		}
-	}

-	// Create a DB entry if plugin loading was successful
 	if err := h.configStore.CreatePlugin(ctx, &configstoreTables.TablePlugin{
 		Name:     request.Name,
 		Enabled:  request.Enabled,
 		Config:   request.Config,
 		Path:     request.Path,
 		IsCustom: true,
 	}); err != nil {
 		logger.Error("failed to create plugin: %v", err)
 		SendError(ctx, 500, "Failed to create plugin")
 		return
 	}

+	// Reload the plugin if enabled
+	if request.Enabled {
+		if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, request.Path, request.Config); err != nil {
+			logger.Error("failed to load plugin: %v", err)
+			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin created in database but failed to load: %v", err))
+			return
+		}
+	}
plugins/mocker/main.go (2)

866-870: Potential nil pointer dereference when overriding model.

If req.RequestType == schemas.ResponsesRequest, mockResponse.ChatResponse will be nil, but line 868 unconditionally accesses mockResponse.ChatResponse.Model.

🔧 Proposed fix
 	// Override model if specified
-	if content.Model != nil {
-		mockResponse.ChatResponse.Model = *content.Model
+	if content.Model != nil && mockResponse.ChatResponse != nil {
+		mockResponse.ChatResponse.Model = *content.Model
+	} else if content.Model != nil && mockResponse.ResponsesResponse != nil {
+		mockResponse.ResponsesResponse.Model = content.Model
 	}

998-1046: handleDefaultBehavior's success case only returns ChatResponse format.

When DefaultBehaviorSuccess is configured and a ResponsesRequest reaches handleDefaultBehavior (no matching rules), the mock response creates a ChatResponse structure and sets ExtraFields.RequestType to ChatCompletionRequest regardless of the actual request type. This creates a mismatch where the response format and metadata don't match the request type.

The generateSuccessShortCircuit function properly handles both request types with conditional logic—mirror this approach in handleDefaultBehavior to support ResponsesRequest by creating a ResponsesResponse when needed.

transports/bifrost-http/server/server.go (1)

582-589: Guard nil proxy config to avoid panic on disable/clear.

If a caller reloads with nil (e.g., proxy disabled), the log line dereferences config. Consider a nil guard.

🛠️ Suggested fix
 func (s *BifrostHTTPServer) ReloadProxyConfig(ctx context.Context, config *configstoreTables.GlobalProxyConfig) error {
 	if s.Config == nil {
 		return fmt.Errorf("config not found")
 	}
 	// Store the proxy config in memory for use by components that need it
 	s.Config.ProxyConfig = config
-	logger.Info("proxy configuration reloaded: enabled=%t, type=%s", config.Enabled, config.Type)
+	if config == nil {
+		logger.Info("proxy configuration cleared")
+		return nil
+	}
+	logger.Info("proxy configuration reloaded: enabled=%t, type=%s", config.Enabled, config.Type)
 	return nil
 }
transports/bifrost-http/lib/config.go (1)

1506-1551: Fix unused mcpPricingConfig and store pricing override.

mcpPricingConfig is declared but never used (compile error), and MCP catalog pricing always comes from file even when the store is enabled. This likely drops DB pricing on startup.

🛠️ Proposed fix
 func initFrameworkConfigFromFile(ctx context.Context, config *Config, configData *ConfigData) {
 	pricingConfig := &modelcatalog.Config{}
-	mcpPricingConfig := &mcpcatalog.Config{}
+	mcpPricingData := buildMCPPricingDataFromFile(ctx, configData)
 	if config.ConfigStore != nil {
@@
-		mcpPricingConfig.PricingData = buildMCPPricingDataFromStore(ctx, config.ConfigStore)
+		mcpPricingData = buildMCPPricingDataFromStore(ctx, config.ConfigStore)
 	} else if configData.FrameworkConfig != nil && configData.FrameworkConfig.Pricing != nil {
@@
-	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
-		PricingData: buildMCPPricingDataFromFile(ctx, configData),
-	}, logger)
+	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
+		PricingData: mcpPricingData,
+	}, logger)
core/bifrost.go (1)

1823-1863: Same issue in reloadMCPPlugin: Silently returns when plugin list is nil.

The same pattern exists here at lines 1828-1831. Consider applying a similar fix as suggested for reloadLLMPlugin.

🤖 Fix all issues with AI agents
In `@core/bifrost.go`:
- Around line 1781-1821: In reloadLLMPlugin, don't silently return when
oldPlugins == nil; instead initialize a new slice and proceed to add the
provided plugin and perform the same compare-and-swap flow. Specifically, when
bifrost.llmPlugins.Load() returns nil, create a newPlugins slice (e.g.,
zero-length []schemas.LLMPlugin), append the incoming plugin, then attempt
bifrost.llmPlugins.CompareAndSwap(nil, &newPlugins) and handle success/failure
exactly like the existing path (including logging "adding new LLM plugin" and
retry loop); this ensures reloadLLMPlugin properly handles the case where no
plugins were previously configured.

In `@core/mcp/clientmanager.go`:
- Around line 221-228: The code shallow-copies tool into updatedTool then
mutates updatedTool.Function.Name, which also mutates the original tool.Function
because Function is a pointer; fix by performing a deep copy of the Function
before mutating—create a new ChatToolFunction instance copying fields from
tool.Function, assign it to updatedTool.Function, set updatedTool.Function.Name
= newToolName, and then put updatedTool into newToolMap; reference symbols:
tool.Function, updatedTool, newToolMap, client.ToolMap, ChatToolFunction.

In `@docs/features/governance/virtual-keys.mdx`:
- Around line 545-585: Update the wording to clarify that "auth disabled" refers
specifically to the inference-level toggle disable_auth_on_inference, not global
authentication: replace phrasing like "When `disable_auth_on_inference: true`
(auth disabled)" and "When `disable_auth_on_inference: false` (auth enabled)"
with explicit terms such as "When authentication is disabled for inference
(`disable_auth_on_inference: true`)" and "When authentication remains enabled
for inference (`disable_auth_on_inference: false`)", and add a short note under
the Authentication and Virtual Keys section stating that this flag only bypasses
auth for inference requests (routing/governance still apply) so readers aren’t
confused about global auth settings; keep the examples and header usage (e.g.,
x-bf-vk and Authorization) unchanged.

In `@docs/openapi/schemas/management/logging.yaml`:
- Around line 159-200: The MCPToolLogSearchFilters schema in the OpenAPI YAML
definition is missing the virtual_key_ids filter property that exists in the
backend implementation at framework/logstore/tables.go. Add a new property
called virtual_key_ids to the MCPToolLogSearchFilters object with type array
containing string items and an appropriate description for filtering by virtual
key identifiers, placing it among the other filter properties to maintain
consistency with the backend search filters.

In `@docs/openapi/schemas/management/mcp.yaml`:
- Around line 31-75: Add a required array to the MCPClientCreateRequest schema
listing name and connection_type as mandatory, keep client_id optional, and
express the conditional requirement that connection_string is required when
connection_type is HTTP or SSE and stdio_config is required when connection_type
is STDIO (use OpenAPI composition like oneOf/allOf with a discriminator on
connection_type or explicitly document the conditional requirement in the field
descriptions); update MCPClientCreateRequest, and reference the fields name,
connection_type, connection_string, stdio_config, and client_id when making the
change so the schema and API contract match the backend validation in
core/mcp/utils.go.

In `@examples/plugins/http-transport-only/main.go`:
- Around line 95-99: When parsing rate_window from configMap in main.go, guard
against zero or negative values so rate limiting math and Retry-After remain
valid: after reading configMap["rate_window"] into pluginConfig.RateWindow (the
symbol to update), check if the int value is <= 0 and if so replace it with a
sane default (e.g., 60) or clamp it to a minimum, and log the adjustment; ensure
downstream code that relies on pluginConfig.RateWindow uses the clamped value.

In `@framework/configstore/migrations.go`:
- Around line 2321-2347: hasClientPrefix fails when clientName contains
underscores because it always splits toolName at the first underscore; change
the logic in hasClientPrefix to detect prefixes via HasPrefix/TrimPrefix rather
than SplitN: first check if strings.HasPrefix(toolName, clientName+"_") and if
so return true with strings.TrimPrefix(toolName, clientName+"_"); then compute
normalizedClient := normalizeMCPClientName(clientName) and check
strings.HasPrefix(toolName, normalizedClient+"_") and if so return true with
strings.TrimPrefix(toolName, normalizedClient+"_"); remove reliance on splitting
at the first underscore so client names with underscores are matched correctly.

In `@plugins/governance/main.go`:
- Around line 228-233: The warning for modelCatalog being nil is inconsistent
with the message in Init; update the logger.Warn call that checks modelCatalog
to use the same phrasing as Init (e.g., "governance plugin requires model
catalog to calculate cost, all LLM cost calculations will be skipped.") so
messages are consistent; locate the modelCatalog nil check in
plugins/governance/main.go and change the logger.Warn string (the logger.Warn
call referencing modelCatalog) to match the Init message exactly.

In `@transports/bifrost-http/server/server.go`:
- Around line 20-22: The file imports the same package twice as both
"github.com/maximhq/bifrost/framework/configstore/tables" and as the alias
"configstoreTables", causing a compile error; remove the duplicate import and
use a single import name consistently (pick one, e.g., keep the alias
configstoreTables). Update all references in this file that use the other
identifier (instances of tables.* or configstoreTables.*) so they all use the
chosen identifier, and repeat the same cleanup for the other duplicate import
blocks in this file.
♻️ Duplicate comments (17)
examples/plugins/http-transport-only/README.md (1)

124-126: Wording nit: repeated “only” in Notes list.
Already raised in a prior review; reword if still pending.

✏️ Suggested tweak
- - This plugin operates at the HTTP transport layer only
- - Works only when using bifrost-http, not when using Bifrost as a Go SDK
+ - This plugin operates at the HTTP transport layer
+ - Works with bifrost-http, not when using Bifrost as a Go SDK
examples/plugins/http-transport-only/main.go (2)

176-178: Guard req.Headers before writing.
Same issue flagged ранее — writing to a nil map will panic.


192-210: Guard resp / resp.Headers before writing.
Same issue flagged earlier — nil response or headers map will panic.

examples/plugins/multi-interface/Makefile (1)

1-12: Add all and test phony targets to satisfy checkmake.

Static analysis flags missing required phony targets all and test. While this is an example Makefile, adding these targets maintains consistency.

🛠️ Suggested fix
-.PHONY: build clean
+.PHONY: all build clean test
+
+all: build
 
 build:
 	`@echo` "Building Multi-Interface plugin..."
 	`@mkdir` -p build
 	`@go` build -buildmode=plugin -o build/multi-interface.so main.go
 	`@echo` "Plugin built successfully: build/multi-interface.so"
 
 clean:
 	`@echo` "Cleaning build directory..."
 	`@rm` -rf build
 	`@echo` "Clean complete"
+
+test:
+	`@echo` "No tests configured for this example"
core/schemas/plugin_wasm.go (1)

5-7: Doc comment still implies streaming in WASM.

The comment says a stream short-circuit is possible, but the WASM build has no Stream field and explicitly says streams aren’t supported. Please align the wording.

📝 Suggested doc tweak
-// It can contain either a response (success short-circuit), a stream (streaming short-circuit), or an error (error short-circuit).
+// In WASM plugins, it can contain either a response (success short-circuit) or an error (error short-circuit).
ui/app/workspace/mcp-logs/views/emptyState.tsx (1)

85-247: The useMemo has an empty dependency array but calls getExampleBaseUrl().

This issue was previously flagged. The baseUrl from getExampleBaseUrl() should be computed outside the memo and included in the dependency array to ensure the examples update if the base URL changes.

docs/plugins/getting-started.mdx (1)

57-59: v1.4.x+ function list is missing PreMCPHook/PostMCPHook.

The function reference list for v1.4.x+ (lines 57-58) only mentions PreLLMHook() and PostLLMHook(), but the execution order section (lines 94, 96) explicitly references PreLLMHook/PreMCPHook and PostLLMHook/PostMCPHook. Add the MCP hooks to the function list for consistency:

     - `PreLLMHook()` - Intercept requests before they reach providers
     - `PostLLMHook()` - Process responses after provider calls
+    - `PreMCPHook()` - Intercept MCP tool requests before execution
+    - `PostMCPHook()` - Process MCP tool responses after execution
     - `Cleanup() error` - Clean up resources on shutdown
core/mcp/utils.go (2)

192-198: Comment still references old path format.

The comment on line 194 says "calculator/add" but tool names now use - as separator (e.g., "calculator-add").

📝 Suggested fix
 		// Strip client prefix from tool name before checking
 		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")
 		unprefixedToolName := stripClientPrefix(toolName, config.Name)

224-230: Same comment issue - update path format example.

📝 Suggested fix
 		// Strip client prefix from tool name before checking
 		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")
 		unprefixedToolName := stripClientPrefix(toolName, config.Name)
ui/lib/store/apis/mcpLogsApi.ts (1)

51-52: Allow zero latency filters to be sent.

The truthy checks skip valid 0 values for min_latency/max_latency. Use !== undefined instead.

-				if (filters.min_latency) params.min_latency = filters.min_latency;
-				if (filters.max_latency) params.max_latency = filters.max_latency;
+				if (filters.min_latency !== undefined) params.min_latency = filters.min_latency;
+				if (filters.max_latency !== undefined) params.max_latency = filters.max_latency;

Also applies to lines 91-92.

examples/plugins/mcp-only/main.go (1)

114-118: Use the canonical request-id context key.

The audit trail reads request_id using a string-based key, but the canonical key is schemas.BifrostContextKeyRequestID, so this will always resolve to nil.

-		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKey("request_id")))
+		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKeyRequestID))
examples/plugins/multi-interface/main.go (3)

37-39: Protect requestCount against concurrent access.

Hooks run concurrently; unsynchronized increments/reads can race and corrupt counts. Use atomic.Int64 for thread-safe access.

🔧 Proposed fix
 import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"sync/atomic"
 	"time"

 	"github.com/maximhq/bifrost/core/schemas"
 )
@@
-	requestCount int64
+	requestCount atomic.Int64

Then update usages:

  • Line 108: current := requestCount.Add(1)
  • Line 177: requestCount.Load()
  • Line 309: requestCount.Load()

106-110: Initialize req.Headers before writing.

req.Headers can be nil; assigning into it will panic.

 	if pluginConfig.TrackRequests {
+		if req.Headers == nil {
+			req.Headers = make(map[string]string)
+		}
 		requestCount++
 		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
 	}

119-152: Guard resp and resp.Headers before mutation.

Post hooks may receive a nil response or nil headers map; both will panic on write.

 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	if !pluginConfig.EnableHTTPHooks {
 		return nil
 	}
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = make(map[string]string)
+	}
transports/bifrost-http/lib/config_test.go (1)

472-519: Normalize ClientID on update to prevent stale identifiers.

UpdateMCPClientConfig writes clientConfig.ClientID directly; if it’s empty or mismatched with id, in-memory reads can drift. Align ClientID with id (and preserve any stable ID if present).

🔧 Suggested fix
-	// Update the in-memory state to ensure GetMCPConfig returns updated data
-	for i := range m.mcpConfig.ClientConfigs {
-		if m.mcpConfig.ClientConfigs[i].ClientID == id {
-			// Found the entry, update it with the new config
-			m.mcpConfig.ClientConfigs[i] = tables.TableMCPClient{
-				ClientID:           clientConfig.ClientID,
-				Name:               clientConfig.Name,
-				IsCodeModeClient:   clientConfig.IsCodeModeClient,
-				ConnectionType:     clientConfig.ConnectionType,
-				ConnectionString:   clientConfig.ConnectionString,
-				StdioConfig:        clientConfig.StdioConfig,
-				Headers:            clientConfig.Headers,
-				ToolsToExecute:     clientConfig.ToolsToExecute,
-				ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-			}
-			return nil
-		}
-	}
-	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
-	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, tables.TableMCPClient{
-		ClientID:           clientConfig.ClientID,
-		Name:               clientConfig.Name,
-		IsCodeModeClient:   clientConfig.IsCodeModeClient,
-		ConnectionType:     clientConfig.ConnectionType,
-		ConnectionString:   clientConfig.ConnectionString,
-		StdioConfig:        clientConfig.StdioConfig,
-		Headers:            clientConfig.Headers,
-		ToolsToExecute:     clientConfig.ToolsToExecute,
-		ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-	})
+	updated := clientConfig
+	updated.ClientID = id
+	// Update the in-memory state to ensure GetMCPConfig returns updated data
+	for i := range m.mcpConfig.ClientConfigs {
+		if m.mcpConfig.ClientConfigs[i].ClientID == id {
+			updated.ID = m.mcpConfig.ClientConfigs[i].ID
+			m.mcpConfig.ClientConfigs[i] = updated
+			return nil
+		}
+	}
+	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
+	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, updated)
docs/openapi/schemas/management/logging.yaml (1)

249-277: Clarify/align virtual key value redaction in MCP filter data.

virtual_keys.value says “redacted if applicable,” but ensure the response actually redacts or update the description to match the real behavior (e.g., empty string).

transports/bifrost-http/server/server.go (1)

654-735: Call Cleanup on plugin reload/remove to avoid leaks.

Old plugin instances aren’t cleaned up; background goroutines/resources may persist after replace/remove. Please invoke Cleanup() on the previous instance.

🧹 Nitpick comments (17)
examples/plugins/llm-only/Makefile (1)

1-12: LGTM — functional Makefile for the example plugin.

The build and clean targets work correctly for the plugin's purpose.

Two optional improvements if you want to align with Makefile conventions:

  1. Add an all target (common default): You could add all: build as the first target.
  2. Add file dependency: The build target could depend on main.go to enable incremental rebuilds when the source changes.

These are minor polish items for example code — fine to skip.

♻️ Optional: Add conventional targets and dependency
-.PHONY: build clean
+.PHONY: all build clean
+
+all: build
 
-build:
+build: main.go
 	`@echo` "Building LLM-Only plugin..."
 	`@mkdir` -p build
 	`@go` build -buildmode=plugin -o build/llm-only.so main.go
 	`@echo` "Plugin built successfully: build/llm-only.so"
ui/lib/types/schemas.ts (1)

661-661: Consider coercing pricing inputs + rejecting Infinity.

If tool_pricing values are coming from form inputs, they’re likely strings and will fail z.number() validation; also Infinity would pass. Consider z.coerce.number().finite().min(0) or confirm upstream conversion. Also, please verify that the rest of the PR stack expects the same shape.

Proposed adjustment
-	tool_pricing: z.record(z.string(), z.number().min(0, "Cost must be non-negative")).optional(),
+	tool_pricing: z
+		.record(z.string(), z.coerce.number().finite().min(0, "Cost must be non-negative"))
+		.optional(),
plugins/mocker/plugin_test.go (1)

66-68: Prefer bifrost.Ptr(...) for Account pointers in these configs.

For consistency with repo conventions, consider using bifrost.Ptr(account) instead of &account in these BifrostConfig literals (apply across this file).

♻️ Example change
-        Account:    &account,
+        Account:    bifrost.Ptr(account),

Based on learnings, prefer using bifrost.Ptr(...) for pointer creation.

Also applies to: 109-112, 185-188, 264-267, 327-330, 397-400

ui/app/workspace/mcp-logs/views/filters.tsx (2)

199-202: Consider memoizing the selected count to avoid duplicate computation.

getSelectedCount() is called twice in the render. While the impact is minimal, you could memoize this value.

♻️ Optional refactor
+	const selectedCount = getSelectedCount();
+
 	return (
 		<div className="flex items-center justify-between space-x-2">
 			...
-						{getSelectedCount() > 0 && (
+						{selectedCount > 0 && (
 							<span className="bg-primary/10 flex h-6 w-6 items-center justify-center rounded-full text-xs font-normal">
-								{getSelectedCount()}
+								{selectedCount}
 							</span>
 						)}

217-220: Simplify the redundant loading check.

Since filterDataLoading is checked for all three categories, you can simplify this condition.

♻️ Suggested simplification
-											const isLoading =
-												(category === "Tool Names" && filterDataLoading) ||
-												(category === "Servers" && filterDataLoading) ||
-												(category === "Virtual Keys" && filterDataLoading);
+											const isLoading =
+												filterDataLoading && ["Tool Names", "Servers", "Virtual Keys"].includes(category);
framework/configstore/tables/mcp.go (1)

138-143: Inconsistent JSON library: uses json.Unmarshal while other fields use sonic.Unmarshal.

The ToolPricing deserialization uses json.Unmarshal (line 140), but other fields in AfterFind use sonic.Unmarshal (lines 118, 124, 129, 134). While functionally equivalent, this inconsistency could cause confusion and slight performance differences.

♻️ Suggested fix for consistency
 	if c.ToolPricingJSON != "" {
-		if err := json.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
+		if err := sonic.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
 			return err
 		}
 	}
ui/app/workspace/mcp-logs/views/mcpLogsTable.tsx (1)

44-57: Add runtime validation to prevent invalid sort values from reaching the API.

The type cast on line 53 (id as "timestamp" | "latency") only provides compile-time type safety. If a user manually edits the URL to include an invalid sort_by value (e.g., ?sort_by=tokens), it will pass through to the backend API which only supports "timestamp" and "latency" for MCP logs.

♻️ Suggested fix with runtime validation
 	const handleSortingChange = (updaterOrValue: SortingState | ((old: SortingState) => SortingState)) => {
 		const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue;
 		setSorting(newSorting);
 		if (newSorting.length > 0) {
 			const { id, desc } = newSorting[0];
+			// Runtime validation: only "timestamp" and "latency" are valid for MCP logs
+			const validSortBy = (id === "timestamp" || id === "latency") ? id : "timestamp";
 			onPaginationChange({
 				...pagination,
-				sort_by: id as "timestamp" | "latency",
+				sort_by: validSortBy,
 				order: desc ? "desc" : "asc",
 			});
 		}
 	};
transports/bifrost-http/server/utils.go (1)

22-30: Consider using a registry pattern instead of hardcoded plugin names.

The isBuiltinPlugin function hardcodes plugin names, requiring updates to this function whenever a new builtin plugin is added. This could lead to maintenance drift.

♻️ Alternative: use a set for easier maintenance
+var builtinPluginNames = map[string]struct{}{
+	telemetry.PluginName:     {},
+	logging.PluginName:       {},
+	governance.PluginName:    {},
+	litellmcompat.PluginName: {},
+	maxim.PluginName:         {},
+	semanticcache.PluginName: {},
+	otel.PluginName:          {},
+}
+
 // isBuiltinPlugin checks if a plugin is a built-in plugin
 func isBuiltinPlugin(name string) bool {
-	return name == telemetry.PluginName ||
-		name == logging.PluginName ||
-		name == governance.PluginName ||
-		name == litellmcompat.PluginName ||
-		name == maxim.PluginName ||
-		name == semanticcache.PluginName ||
-		name == otel.PluginName
+	_, ok := builtinPluginNames[name]
+	return ok
 }
plugins/governance/model_provider_governance_test.go (1)

1664-1664: Section header "End-to-End Tests - PostHook Integration" should be updated.

For consistency with the PreLLMHook section rename, consider updating this to "PostLLMHook Integration" to match the new naming convention used elsewhere in the PR.

transports/bifrost-http/handlers/logging.go (2)

513-615: Consider extracting shared filter parsing logic to reduce duplication.

parseMCPFiltersAndPagination and parseMCPFilters share identical filter parsing code (lines 520-565 vs 623-668). Consider extracting the filter parsing into a shared helper:

♻️ Suggested refactor
// parseMCPFiltersCore extracts MCP filter fields from query parameters
func parseMCPFiltersCore(ctx *fasthttp.RequestCtx) (*logstore.MCPToolLogSearchFilters, error) {
    filters := &logstore.MCPToolLogSearchFilters{}
    // ... shared filter parsing logic ...
    return filters, nil
}

func parseMCPFiltersAndPagination(ctx *fasthttp.RequestCtx) (*logstore.MCPToolLogSearchFilters, *logstore.PaginationOptions, error) {
    filters, err := parseMCPFiltersCore(ctx)
    if err != nil {
        return nil, nil, err
    }
    pagination := &logstore.PaginationOptions{}
    // ... pagination-specific logic ...
    return filters, pagination, nil
}

func parseMCPFilters(ctx *fasthttp.RequestCtx) (*logstore.MCPToolLogSearchFilters, error) {
    return parseMCPFiltersCore(ctx)
}

Also applies to: 619-671


698-707: Helper function toSlice is duplicated.

The toSlice helper converting map[string]struct{} to []string is defined inline in both getLogs (lines 192-201) and getMCPLogs (lines 698-707). Consider extracting it as a package-level helper.

core/mcp/utils.go (1)

610-682: Consider extracting repeated recursion logic to reduce duplication.

The FixArraySchemas function has duplicated recursion patterns for handling nested arrays within the main loop and within anyOf/oneOf/allOf blocks. While functional, this could be simplified.

♻️ Optional: Extract helper for items recursion
// recurseIntoItems handles recursion into items schema
func recurseIntoItems(itemsMap map[string]interface{}) {
	itemsType, _ := itemsMap["type"].(string)
	if itemsType == "array" {
		FixArraySchemas(map[string]interface{}{"": itemsMap})
	} else if itemsType == "object" {
		if itemsProps, ok := itemsMap["properties"].(map[string]interface{}); ok {
			FixArraySchemas(itemsProps)
		}
	}
}

This would reduce the repeated if itemsType == "array" { ... } else if itemsType == "object" { ... } blocks.

transports/bifrost-http/handlers/plugins.go (2)

72-93: Missing deterministic ordering in the no-configStore branch.

When configStore == nil, the plugins are iterated from a map without sorting, resulting in non-deterministic ordering. The configStore branch (lines 129-131) sorts by Name. Consider adding sorting here for consistency.

♻️ Add sorting for consistency
 		for name, pluginStatus := range pluginStatus {
 			finalPlugins = append(finalPlugins, PluginResponse{
 				Name:       pluginStatus.Name,
 				ActualName: name,
 				Enabled:    true,
 				Config:     map[string]any{},
 				IsCustom:   true,
 				Path:       nil,
 				Status:     pluginStatus,
 			})
 		}
+		// Sort plugins by Name for deterministic ordering
+		sort.Slice(finalPlugins, func(i, j int) bool {
+			return finalPlugins[i].Name < finalPlugins[j].Name
+		})
 		SendJSON(ctx, map[string]any{

140-161: getPlugin (no-configStore) returns empty struct if not found.

When the plugin is not found in the loop, pluginInfo remains an empty PluginResponse{} which is then returned. Consider returning a 404 error instead for consistency.

♻️ Return 404 when plugin not found
 		pluginInfo := PluginResponse{}
+		found := false
 		for name, pluginStatus := range pluginStatus {
 			if pluginStatus.Name == ctx.UserValue("name") {
 				pluginInfo = PluginResponse{
 					Name:       pluginStatus.Name,
 					ActualName: name,
 					Enabled:    true,
 					Config:     map[string]any{},
 					IsCustom:   true,
 					Path:       nil,
 					Status:     pluginStatus,
 				}
+				found = true
 				break
 			}
 		}
+		if !found {
+			SendError(ctx, fasthttp.StatusNotFound, "Plugin not found")
+			return
+		}
 		SendJSON(ctx, pluginInfo)
framework/logstore/migrations.go (1)

693-753: Consider enabling transactions for table creation migration.

The migrationCreateMCPToolLogsTable uses migrator.DefaultOptions (no transaction), while other migrations in this file use UseTransaction = true. For consistency and safety during partial failures, consider wrapping this migration in a transaction.

-	m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
+	opts := *migrator.DefaultOptions
+	opts.UseTransaction = true
+	m := migrator.New(db, &opts, []*migrator.Migration{{
plugins/semanticcache/main.go (1)

429-611: Use bifrost.Ptr for InputTokens consistency.
Keeps pointer creation aligned with repo conventions. Based on learnings, please prefer bifrost.Ptr for pointers.

♻️ Suggested tweak
-			extraFields.CacheDebug.InputTokens = &inputTokens
+			extraFields.CacheDebug.InputTokens = bifrost.Ptr(inputTokens)
plugins/logging/utils.go (1)

148-169: Remove redundant nil guards for MCP filter helpers.

Given the construction guarantee for PluginLogManager, these nil guards add inconsistency and can be dropped to match the rest of the file’s behavior.

Proposed cleanup
 func (p *PluginLogManager) GetAvailableToolNames(ctx context.Context) ([]string, error) {
-	if p == nil || p.plugin == nil || p.plugin.store == nil {
-		return []string{}, nil
-	}
 	return p.plugin.store.GetAvailableToolNames(ctx)
 }
@@
 func (p *PluginLogManager) GetAvailableServerLabels(ctx context.Context) ([]string, error) {
-	if p == nil || p.plugin == nil || p.plugin.store == nil {
-		return []string{}, nil
-	}
 	return p.plugin.store.GetAvailableServerLabels(ctx)
 }
@@
 func (p *PluginLogManager) GetAvailableMCPVirtualKeys(ctx context.Context) []KeyPair {
-	if p == nil || p.plugin == nil {
-		return []KeyPair{}
-	}
 	return p.plugin.GetAvailableMCPVirtualKeys(ctx)
 }

Based on learnings, the store is always initialized in this package.

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from cdbcb7c to 540e8c0 Compare January 23, 2026 09:32
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (7)
plugins/jsonparser/main.go (1)

119-129: Guard against nil result before calling GetExtraFields().

result.GetExtraFields() will panic if result is nil. Add an early nil check before that call.

🛠️ Proposed fix
 func (p *JsonParserPlugin) PostLLMHook(ctx *schemas.BifrostContext, result *schemas.BifrostResponse, err *schemas.BifrostError) (*schemas.BifrostResponse, *schemas.BifrostError, error) {
 	// If there's an error, don't process
 	if err != nil {
 		return result, err, nil
 	}
 
+	if result == nil {
+		return result, err, nil
+	}
+
 	extraFields := result.GetExtraFields()
 
 	// Check if plugin should run based on usage type
 	if !p.shouldRun(ctx, extraFields.RequestType) {
 		return result, err, nil
 	}
transports/bifrost-http/server/server.go (1)

582-589: Guard nil proxy config before logging fields.

If config is nil (e.g., clearing settings), config.Enabled will panic.

🛠️ Proposed fix
 func (s *BifrostHTTPServer) ReloadProxyConfig(ctx context.Context, config *configstoreTables.GlobalProxyConfig) error {
 	if s.Config == nil {
 		return fmt.Errorf("config not found")
 	}
+	if config == nil {
+		s.Config.ProxyConfig = nil
+		logger.Info("proxy configuration cleared")
+		return nil
+	}
 	// Store the proxy config in memory for use by components that need it
 	s.Config.ProxyConfig = config
 	logger.Info("proxy configuration reloaded: enabled=%t, type=%s", config.Enabled, config.Type)
 	return nil
 }
transports/bifrost-http/handlers/plugins.go (3)

141-161: Returns empty PluginResponse instead of 404 when plugin not found.

When configStore == nil and the requested plugin is not found in the status map, the handler returns an empty PluginResponse{} with all zero/nil values instead of a 404 error. This is inconsistent with the configStore != nil path (line 186) which correctly returns a 404.

🐛 Suggested fix to return 404 when plugin not found
 func (h *PluginsHandler) getPlugin(ctx *fasthttp.RequestCtx) {
 	if h.configStore == nil {
 		pluginStatus := h.pluginsLoader.GetPluginStatus(ctx)
-		pluginInfo := PluginResponse{}
+		var pluginInfo *PluginResponse
 		for name, pluginStatus := range pluginStatus {
 			if pluginStatus.Name == ctx.UserValue("name") {
-				pluginInfo = PluginResponse{
+				pluginInfo = &PluginResponse{
 					Name:       pluginStatus.Name,
 					ActualName: name,
 					Enabled:    true,
 					Config:     map[string]any{},
 					IsCustom:   true,
 					Path:       nil,
 					Status:     pluginStatus,
 				}
 				break
 			}
 		}
-		SendJSON(ctx, pluginInfo)
+		if pluginInfo == nil {
+			SendError(ctx, fasthttp.StatusNotFound, "Plugin not found")
+			return
+		}
+		SendJSON(ctx, pluginInfo)
 		return
 	}

199-199: Typo: double space in error message.

Minor typo: "is not" should be "is not".

✏️ Fix typo
-		SendError(ctx, 400, "Plugins creation is  not supported when configstore is disabled")
+		SendError(ctx, 400, "Plugins creation is not supported when configstore is disabled")

219-239: Plugin loaded before DB entry created—partial state on DB failure.

The plugin is loaded into memory first (lines 221-226), and only then is the DB entry created (lines 229-239). If DB creation fails, the plugin remains loaded in memory but has no corresponding DB record, leading to an inconsistent state.

Consider either:

  1. Creating the DB entry first (original order), then loading the plugin
  2. Or unloading the plugin if DB creation fails
🐛 Suggested fix to unload plugin on DB failure
 	// We reload the plugin if its enabled
 	if request.Enabled {
 		if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, request.Path, request.Config); err != nil {
 			logger.Error("failed to load plugin: %v", err)
 			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin created in database but failed to load: %v", err))
 			return
 		}
 	}

 	// Create a DB entry if plugin loading was successful
 	if err := h.configStore.CreatePlugin(ctx, &configstoreTables.TablePlugin{
 		Name:     request.Name,
 		Enabled:  request.Enabled,
 		Config:   request.Config,
 		Path:     request.Path,
 		IsCustom: true,
 	}); err != nil {
 		logger.Error("failed to create plugin: %v", err)
+		// Unload the plugin since DB entry creation failed
+		if request.Enabled {
+			if removeErr := h.pluginsLoader.RemovePlugin(ctx, request.Name); removeErr != nil {
+				logger.Warn("failed to unload plugin after DB creation failure: %v", removeErr)
+			}
+		}
 		SendError(ctx, 500, "Failed to create plugin")
 		return
 	}
core/mcp/toolmanager.go (1)

420-467: Avoid a possible nil deref in executeToolInternal.

executeToolInternal assumes toolCall.Function.Name is non-nil. If any direct caller bypasses ExecuteTool, this can panic. A small guard here makes the helper safe.

🛡️ Suggested guard
 func (m *ToolsManager) executeToolInternal(ctx *schemas.BifrostContext, toolCall *schemas.ChatAssistantMessageToolCall) (*schemas.ChatMessage, string, string, error) {
-	toolName := *toolCall.Function.Name
+	if toolCall == nil || toolCall.Function.Name == nil {
+		return nil, "", "", fmt.Errorf("tool call missing function name")
+	}
+	toolName := *toolCall.Function.Name
#!/bin/bash
# Verify all call sites validate toolCall before calling executeToolInternal.
rg -n "executeToolInternal\\(" --glob '*.go'
transports/bifrost-http/lib/config.go (1)

1511-1553: MCPCatalog init ignores config-store pricing data.

When the config store is enabled, buildMCPPricingDataFromStore is computed but never used; MCPCatalog is initialized from the file data only. This drops persisted pricing on startup when config.json lacks tool_pricing. Use store data when available (or merge file+store if intended). Line 1547 initializes MCPCatalog with file data unconditionally.

🐛 Proposed fix
-	mcpPricingConfig := &mcpcatalog.Config{}
+	mcpPricingConfig := &mcpcatalog.Config{}
 	if config.ConfigStore != nil {
 		frameworkConfig, err := config.ConfigStore.GetFrameworkConfig(ctx)
 		if err != nil {
 			logger.Warn("failed to get framework config from store: %v", err)
 		}
@@
-		mcpPricingConfig.PricingData = buildMCPPricingDataFromStore(ctx, config.ConfigStore)
+		mcpPricingConfig.PricingData = buildMCPPricingDataFromStore(ctx, config.ConfigStore)
 	} else if configData.FrameworkConfig != nil && configData.FrameworkConfig.Pricing != nil {
 		pricingConfig.PricingURL = configData.FrameworkConfig.Pricing.PricingURL
 		syncDuration := time.Duration(*configData.FrameworkConfig.Pricing.PricingSyncInterval) * time.Second
 		pricingConfig.PricingSyncInterval = &syncDuration
 	}
@@
-	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
-		PricingData: buildMCPPricingDataFromFile(ctx, configData),
-	}, logger)
+	// Prefer store pricing when available; fall back to file pricing
+	if mcpPricingConfig.PricingData == nil {
+		mcpPricingConfig.PricingData = buildMCPPricingDataFromFile(ctx, configData)
+	}
+	mcpCatalog, err := mcpcatalog.Init(ctx, mcpPricingConfig, logger)
🤖 Fix all issues with AI agents
In `@framework/mcpcatalog/main.go`:
- Around line 32-45: Init currently calls logger.Info without guarding against
logger being nil; add a nil-check for the logger in Init (or require non-nil by
panicking/returning error) similar to how config is defensively handled: ensure
the function (Init) checks if logger == nil and either sets a no-op logger or
returns an error before calling logger.Info, and store the validated logger into
the returned MCPCatalog.logger field so subsequent methods can assume non-nil;
reference the Init function, the logger parameter, and the MCPCatalog.logger
field when making the change.

In `@transports/bifrost-http/lib/config.go`:
- Around line 1471-1478: When iterating mcpConfig.ClientConfigs after calling
configStore.GetMCPClientByName, add a nil guard for the returned dbClientConfig
before accessing dbClientConfig.ToolPricing: if dbClientConfig is nil, log or
warn (using logger) and continue so the loop does not dereference a nil pointer;
update the block around GetMCPClientByName and the subsequent for toolName,
costPerExecution := range dbClientConfig.ToolPricing to check dbClientConfig !=
nil first and skip when nil.

In `@ui/app/workspace/logs/page.tsx`:
- Around line 181-184: The code marks manual time-range edits via
userModifiedTimeRange.current inside the setFilters path but does not
re-baseline auto-refresh when a user triggers "reset zoom"; update the
reset-zoom handler (e.g., the function handling the reset zoom UI action) to
clear userModifiedTimeRange.current (set to false) and update the baseline
defaults (initialDefaults) to the restored filter values so auto-refresh/focus
refresh resumes; ensure this uses the same filter object applied by setFilters
so the state is consistent and future setFilters calls respect the reset
baseline.

In `@ui/app/workspace/mcp-logs/page.tsx`:
- Around line 219-221: The current filter lets logs without virtual_key_id
through when filters.virtual_key_ids is set; update the condition in page.tsx so
that when filters.virtual_key_ids?.length is truthy you reject logs that either
lack log.virtual_key_id or whose log.virtual_key_id is not included—i.e., change
the check to return false when filters.virtual_key_ids?.length &&
(!log.virtual_key_id || !filters.virtual_key_ids.includes(log.virtual_key_id))
to ensure logs missing virtual_key_id are excluded.
♻️ Duplicate comments (24)
examples/plugins/http-transport-only/main.go (3)

95-99: Clamp non-positive rate_window values to avoid disabling limits.

A zero/negative window makes the limiter ineffective and can yield invalid Retry-After behavior.

🔧 Suggested fix
 	// Parse rate_window
 	if rateWindow, ok := configMap["rate_window"].(float64); ok {
 		pluginConfig.RateWindow = int(rateWindow)
-		fmt.Printf("[HTTP-Transport-Only Plugin] Rate window: %d seconds\n", pluginConfig.RateWindow)
+		if pluginConfig.RateWindow <= 0 {
+			pluginConfig.RateWindow = 60
+			fmt.Println("[HTTP-Transport-Only Plugin] Invalid rate window; defaulting to 60 seconds")
+		} else {
+			fmt.Printf("[HTTP-Transport-Only Plugin] Rate window: %d seconds\n", pluginConfig.RateWindow)
+		}
 	}

176-178: Guard req.Headers before mutation to avoid panic.

Writing into a nil map will panic in Go.

🐛 Proposed fix
 	// Example 4: Add custom headers
+	if req.Headers == nil {
+		req.Headers = map[string]string{}
+	}
 	req.Headers["X-Plugin-Processed"] = "true"
 	req.Headers["X-Request-Time"] = time.Now().Format(time.RFC3339)

192-210: Guard resp / resp.Headers before mutation.

Post hooks can receive a nil response or nil headers map; both will panic on write.

🐛 Proposed fix
 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	fmt.Println("[HTTP-Transport-Only Plugin] HTTPTransportPostHook called")
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = map[string]string{}
+	}
 
 	// Calculate request duration
 	startTime := ctx.Value(schemas.BifrostContextKey("http-plugin-start-time"))
examples/plugins/http-transport-only/README.md (1)

124-126: Tighten wording to avoid repeated "only".

The double use of "only" in consecutive bullets reduces readability.

✏️ Suggested tweak
- - This plugin operates at the HTTP transport layer only
- - Works only when using bifrost-http, not when using Bifrost as a Go SDK
+ - This plugin operates at the HTTP transport layer
+ - Works with bifrost-http, not when using Bifrost as a Go SDK
ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx (1)

120-132: Latency 0 shows as “NA”.
A latency of 0 is valid; use nullish checks so zero renders correctly.

🛠️ Proposed fix
-								value={moment(log.timestamp)
-									.add(log.latency || 0, "ms")
-									.format("YYYY-MM-DD HH:mm:ss A")}
+								value={moment(log.timestamp)
+									.add(log.latency ?? 0, "ms")
+									.format("YYYY-MM-DD HH:mm:ss A")}
@@
-							<LogEntryDetailsView className="w-full" label="Latency" value={log.latency ? `${log.latency.toFixed(2)}ms` : "NA"} />
+							<LogEntryDetailsView
+								className="w-full"
+								label="Latency"
+								value={log.latency != null ? `${log.latency.toFixed(2)}ms` : "NA"}
+							/>
framework/configstore/migrations.go (1)

2321-2347: Prefix stripping fails for client names containing underscores.
SplitN only captures text before the first underscore, so my_client_add won’t match clientName = "my_client" and the prefix remains. This breaks tool normalization for those clients.

🛠️ Proposed fix
-	hasClientPrefix := func(toolName, clientName string) (bool, string) {
-		if !strings.Contains(toolName, "_") {
-			return false, ""
-		}
-
-		// Get the prefix before the first underscore
-		parts := strings.SplitN(toolName, "_", 2)
-		if len(parts) != 2 {
-			return false, ""
-		}
-		toolPrefix := parts[0]
-		unprefixedPart := parts[1]
-
-		// Check exact match first
-		if toolPrefix == clientName {
-			return true, unprefixedPart
-		}
-
-		// Check normalized match (for legacy prefixes that existed before normalization)
-		normalizedToolPrefix := normalizeMCPClientName(toolPrefix)
-		if normalizedToolPrefix == clientName {
-			return true, unprefixedPart
-		}
-
-		return false, ""
-	}
+	hasClientPrefix := func(toolName, clientName string) (bool, string) {
+		prefix := clientName + "_"
+		if strings.HasPrefix(toolName, prefix) {
+			return true, strings.TrimPrefix(toolName, prefix)
+		}
+		if !strings.Contains(toolName, "_") {
+			return false, ""
+		}
+		parts := strings.SplitN(toolName, "_", 2)
+		if len(parts) != 2 {
+			return false, ""
+		}
+		toolPrefix := parts[0]
+		unprefixedPart := parts[1]
+		if normalizeMCPClientName(toolPrefix) == clientName {
+			return true, unprefixedPart
+		}
+		return false, ""
+	}
examples/plugins/hello-world/main.go (1)

41-46: Fix undefined schemas.LLMPluginShortCircuit (build failure).

CI still reports undefined: schemas.LLMPluginShortCircuit at Line 41. Please align this example with the actually exported short‑circuit type or add the new type to core/schemas so the example compiles.

🐛 Proposed fix (if the new type isn’t exported yet)
-func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
+func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {
#!/bin/bash
# Verify which short‑circuit types are exported in core/schemas
rg -n "type LLMPluginShortCircuit" core -g '*.go'
rg -n "type PluginShortCircuit" core -g '*.go'
ui/lib/store/apis/mcpLogsApi.ts (1)

49-53: Allow 0ms latency filters to be sent.
Truthy checks skip valid 0 values for min_latency/max_latency. Use !== undefined in both query builders.

🛠️ Proposed fix
-				if (filters.min_latency) params.min_latency = filters.min_latency;
-				if (filters.max_latency) params.max_latency = filters.max_latency;
+				if (filters.min_latency !== undefined) params.min_latency = filters.min_latency;
+				if (filters.max_latency !== undefined) params.max_latency = filters.max_latency;
-				if (filters.min_latency) params.min_latency = filters.min_latency;
-				if (filters.max_latency) params.max_latency = filters.max_latency;
+				if (filters.min_latency !== undefined) params.min_latency = filters.min_latency;
+				if (filters.max_latency !== undefined) params.max_latency = filters.max_latency;

Also applies to: 89-93

ui/app/workspace/mcp-logs/page.tsx (1)

304-327: Prevent stats double-counting on repeated terminal updates.
If a completed log is updated again, the counters are incremented again. Guard by checking the previous status before updating stats (or refetch stats).

🛠️ Possible guard
-				// Update stats for completed requests
-				if (log.status === "success" || log.status === "error") {
+				const prevLog = logs.find((existing) => existing.id === log.id);
+				const wasTerminal = prevLog && (prevLog.status === "success" || prevLog.status === "error");
+				// Update stats only on first transition to terminal
+				if (!wasTerminal && (log.status === "success" || log.status === "error")) {
 					setStats((prevStats) => {
 						if (!prevStats) return prevStats;
examples/plugins/multi-interface/main.go (3)

37-39: Make requestCount atomic across concurrent hooks.
Hooks run concurrently; unsynchronized reads/writes will race and corrupt counts.

🔧 Proposed fix (atomic)
 import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"sync/atomic"
 	"time"

 	"github.com/maximhq/bifrost/core/schemas"
 )
@@
 	// Plugin state
 	requestCount int64
 	startTime    time.Time
 )
@@
 	if pluginConfig.TrackRequests {
-		requestCount++
-		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
+		current := atomic.AddInt64(&requestCount, 1)
+		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", current)
 	}
@@
-			content = fmt.Sprintf("Processing request #%d. Server uptime: %v", requestCount, time.Since(startTime))
+			content = fmt.Sprintf("Processing request #%d. Server uptime: %v", atomic.LoadInt64(&requestCount), time.Since(startTime))
@@
-			requestCount, uptime)
+			atomic.LoadInt64(&requestCount), uptime)

Also applies to: 106-110, 174-178, 306-310


106-110: Initialize req.Headers before assignment.
req.Headers can be nil; writing into it will panic.

🛠️ Proposed fix
 	if pluginConfig.TrackRequests {
+		if req.Headers == nil {
+			req.Headers = map[string]string{}
+		}
 		requestCount++
 		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
 	}

119-149: Guard resp/resp.Headers before mutation.
resp or resp.Headers can be nil; both would panic on write.

🛠️ Proposed fix
 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	if !pluginConfig.EnableHTTPHooks {
 		return nil
 	}
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = map[string]string{}
+	}
examples/plugins/multi-interface/Makefile (1)

1-12: Add all/test phony targets to satisfy checkmake.

🛠️ Proposed update
-.PHONY: build clean
+.PHONY: build clean all test
+
+all: build
+
+test:
+	`@echo` "No tests configured for this example"
examples/plugins/mcp-only/main.go (1)

114-121: Use the canonical request-id context key.

The audit trail reads request_id using a custom context key, but the canonical key is schemas.BifrostContextKeyRequestID. This will always resolve to nil.

🐛 Suggested fix
-		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKey("request_id")))
+		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKeyRequestID))
transports/bifrost-http/server/server.go (2)

19-22: Remove duplicate tables import to avoid build failure.

Go does not allow importing the same path twice; keep one alias and standardize usages.

🛠️ Proposed fix
-	"github.com/maximhq/bifrost/framework/configstore/tables"
-	configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables"
+	configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables"
@@
-	ReloadModelConfig(ctx context.Context, id string) (*tables.TableModelConfig, error)
+	ReloadModelConfig(ctx context.Context, id string) (*configstoreTables.TableModelConfig, error)
@@
-	ReloadProvider(ctx context.Context, name string) (*tables.TableProvider, error)
+	ReloadProvider(ctx context.Context, name string) (*configstoreTables.TableProvider, error)
@@
-func (s *BifrostHTTPServer) ReloadModelConfig(ctx context.Context, id string) (*tables.TableModelConfig, error) {
+func (s *BifrostHTTPServer) ReloadModelConfig(ctx context.Context, id string) (*configstoreTables.TableModelConfig, error) {
@@
-func (s *BifrostHTTPServer) ReloadProvider(ctx context.Context, name string) (*tables.TableProvider, error) {
+func (s *BifrostHTTPServer) ReloadProvider(ctx context.Context, name string) (*configstoreTables.TableProvider, error) {

Also applies to: 71-74, 350-402


657-735: Call Cleanup on replaced/removed plugins to avoid leaks.

Reload/Remove currently swap instances without invoking Cleanup().

🛠️ Proposed fix
-	plugin, err := InstantiatePlugin(ctx, name, path, pluginConfig, s.Config)
+	oldPlugin, _ := s.Config.FindPluginByName(name)
+	plugin, err := InstantiatePlugin(ctx, name, path, pluginConfig, s.Config)
@@
 	if err := s.Config.RegisterPlugin(plugin); err != nil {
 		return updateError("registering", err)
 	}
+	if oldPlugin != nil {
+		if err := oldPlugin.Cleanup(); err != nil {
+			logger.Warn("failed to cleanup old plugin %s: %v", name, err)
+		}
+	}
@@
-	if err := s.Config.UnregisterPlugin(name); err != nil {
+	oldPlugin, _ := s.Config.FindPluginByName(name)
+	if err := s.Config.UnregisterPlugin(name); err != nil {
 		return err
 	}
+	if oldPlugin != nil {
+		if err := oldPlugin.Cleanup(); err != nil {
+			logger.Warn("failed to cleanup plugin %s: %v", name, err)
+		}
+	}
core/mcp/clientmanager.go (1)

222-226: Avoid mutating shared ChatToolFunction when renaming tools

Line 224 does updatedTool := tool and then mutates updatedTool.Function.Name. Since Function is a pointer, this also mutates the original map entry. Deep-copy the function before updating to avoid side effects during iteration or reuse.

♻️ Suggested deep copy
 			if tool.Function != nil {
 				updatedTool := tool
-				updatedTool.Function.Name = newToolName
+				functionCopy := *tool.Function
+				functionCopy.Name = newToolName
+				updatedTool.Function = &functionCopy
 				newToolMap[newToolName] = updatedTool
 			} else {
 				newToolMap[newToolName] = tool
 			}
#!/bin/bash
# Confirm ChatTool.Function is a pointer
rg -n 'type ChatTool struct' core/schemas/chatcompletions.go
docs/openapi/paths/management/logging.yaml (1)

335-340: Align status description with enum constraints

Line 335 and Line 459 describe a comma-separated list, but the enum only allows a single value. Either remove the enum to allow lists or update the description to single-value behavior for both MCP endpoints.

Also applies to: 459-465

docs/plugins/getting-started.mdx (1)

57-58: Align hook names across the function lists and lifecycle steps.

The v1.3.x tab now lists PreLLMHook/PostLLMHook, but the lifecycle still shows PreHook/PostHook. Also, the v1.4.x list doesn’t mention PreMCPHook/PostMCPHook while the execution order does. Please ensure the lists match the actual API and the lifecycle description. As per coding guidelines, please verify against the full Graphite stack before finalizing.

Also applies to: 65-66, 86-96

core/mcp/utils.go (1)

192-195: Update tool-name examples to the “-” separator.
Comments still show "calculator/add" while the code now uses clientName-toolName.

📝 Suggested update
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")
...
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")

Also applies to: 224-227

plugins/governance/main.go (1)

228-233: Inconsistent warning message for modelCatalog nil case.

The warning in InitFromStore (line 229) says "all cost calculations will be skipped" but Init (line 135) says "all LLM cost calculations will be skipped". These should be consistent for clarity.

🔧 Suggested fix
 if modelCatalog == nil {
-    logger.Warn("governance plugin requires model catalog to calculate cost, all cost calculations will be skipped.")
+    logger.Warn("governance plugin requires model catalog to calculate cost, all LLM cost calculations will be skipped.")
 }
transports/bifrost-http/lib/config_test.go (1)

472-517: Preserve stable MCP client identifiers during updates.

UpdateMCPClientConfig rebuilds entries and can drop the stable ID or leave ClientID empty if the caller doesn’t populate it, which can make subsequent GetMCPConfig reads inconsistent.

🩹 Suggested fix (preserve ClientID/ID)
-	// Update the in-memory state to ensure GetMCPConfig returns updated data
-	for i := range m.mcpConfig.ClientConfigs {
-		if m.mcpConfig.ClientConfigs[i].ClientID == id {
-			// Found the entry, update it with the new config
-			m.mcpConfig.ClientConfigs[i] = tables.TableMCPClient{
-				ClientID:           clientConfig.ClientID,
-				Name:               clientConfig.Name,
-				IsCodeModeClient:   clientConfig.IsCodeModeClient,
-				ConnectionType:     clientConfig.ConnectionType,
-				ConnectionString:   clientConfig.ConnectionString,
-				StdioConfig:        clientConfig.StdioConfig,
-				Headers:            clientConfig.Headers,
-				ToolsToExecute:     clientConfig.ToolsToExecute,
-				ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-			}
-			return nil
-		}
-	}
-	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
-	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, tables.TableMCPClient{
-		ClientID:           clientConfig.ClientID,
-		Name:               clientConfig.Name,
-		IsCodeModeClient:   clientConfig.IsCodeModeClient,
-		ConnectionType:     clientConfig.ConnectionType,
-		ConnectionString:   clientConfig.ConnectionString,
-		StdioConfig:        clientConfig.StdioConfig,
-		Headers:            clientConfig.Headers,
-		ToolsToExecute:     clientConfig.ToolsToExecute,
-		ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-	})
+	updated := clientConfig
+	if updated.ClientID == "" {
+		updated.ClientID = id
+	}
+
+	// Update the in-memory state to ensure GetMCPConfig returns updated data
+	for i := range m.mcpConfig.ClientConfigs {
+		if m.mcpConfig.ClientConfigs[i].ClientID == id {
+			if updated.ID == 0 {
+				updated.ID = m.mcpConfig.ClientConfigs[i].ID
+			}
+			m.mcpConfig.ClientConfigs[i] = updated
+			return nil
+		}
+	}
+	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
+	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, updated)
core/bifrost.go (2)

1939-1945: Same concern as previously noted: reload no-ops when plugin list is nil.

Returning immediately when the list is nil prevents adding the first plugin via reload.


1984-2008: reloadMCPPlugin drops additions when no plugins are configured.

If mcpPlugins is nil, the reload path returns early and the new plugin never gets stored. This makes “add via reload” a no-op.

🛠️ Suggested fix (mirror LLM behavior)
 	for {
 		var pluginToCleanup schemas.MCPPlugin
 		found := false
 		oldPlugins := bifrost.mcpPlugins.Load()
 		if oldPlugins == nil {
-			return nil
+			newPlugins := []schemas.MCPPlugin{plugin}
+			if bifrost.mcpPlugins.CompareAndSwap(nil, &newPlugins) {
+				bifrost.logger.Debug("adding first MCP plugin %s", plugin.GetName())
+				return nil
+			}
+			continue
 		}
🧹 Nitpick comments (21)
plugins/mocker/benchmark_test.go (5)

12-67: Consider renaming benchmark function to match the method being tested.

The function name BenchmarkMockerPlugin_PreHook_SimpleRule still references PreHook, but it's now testing PreLLMHook. This naming inconsistency could cause confusion when interpreting benchmark results or searching for relevant tests.

Suggested rename
-func BenchmarkMockerPlugin_PreHook_SimpleRule(b *testing.B) {
+func BenchmarkMockerPlugin_PreLLMHook_SimpleRule(b *testing.B) {

69-124: Function name inconsistency with tested method.

Same issue as above - function name references PreHook while testing PreLLMHook.

Suggested rename
-func BenchmarkMockerPlugin_PreHook_RegexRule(b *testing.B) {
+func BenchmarkMockerPlugin_PreLLMHook_RegexRule(b *testing.B) {

126-203: Function name inconsistency with tested method.

Same issue - consider renaming to BenchmarkMockerPlugin_PreLLMHook_MultipleRules.


205-261: Function name inconsistency with tested method.

Same issue - consider renaming to BenchmarkMockerPlugin_PreLLMHook_NoMatch.


263-316: Function name inconsistency with tested method.

Same issue - consider renaming to BenchmarkMockerPlugin_PreLLMHook_Template.

The actual code changes (method calls to PreLLMHook and updated comments) are correct and align with the PR's refactor to separate LLM vs MCP plugin paths.

ui/components/sidebar.tsx (1)

72-89: Prefer the shared MCPIcon to avoid SVG duplication.
There’s already an MCPIcon in ui/components/ui/icons.tsx; reusing it keeps the icon consistent and reduces maintenance.

♻️ Proposed refactor
-import { ChevronRight } from "lucide-react";
+import { ChevronRight } from "lucide-react";
+import { MCPIcon } from "@/components/ui/icons";
@@
-// Custom MCP Icon Component
-const MCPIcon = ({ className }: { className?: string }) => (
-	<svg
-		className={className}
-		fill="currentColor"
-		fillRule="evenodd"
-		height="1em"
-		style={{ flex: "none", lineHeight: 1 }}
-		viewBox="0 0 24 24"
-		width="1em"
-		xmlns="http://www.w3.org/2000/svg"
-		aria-label="MCP clients icon"
-	>
-		<title>MCP clients icon</title>
-		<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
-		<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
-	</svg>
-);
transports/bifrost-http/handlers/websocket.go (1)

205-242: Consider extracting a shared broadcast helper to avoid drift.
BroadcastLogUpdate and BroadcastMCPLogUpdate are nearly identical; a small helper would reduce duplication and future inconsistencies.

♻️ Suggested refactor
+func (h *WebSocketHandler) broadcastLogMessage(messageType, operationType string, payload interface{}) {
+	message := struct {
+		Type      string      `json:"type"`
+		Operation string      `json:"operation"`
+		Payload   interface{} `json:"payload"`
+	}{
+		Type:      messageType,
+		Operation: operationType,
+		Payload:   payload,
+	}
+
+	data, err := json.Marshal(message)
+	if err != nil {
+		logger.Error("failed to marshal %s entry: %v", messageType, err)
+		return
+	}
+	h.BroadcastMarshaledMessage(data)
+}
@@
-	message := struct {
-		Type      string        `json:"type"`
-		Operation string        `json:"operation"` // "create" or "update"
-		Payload   *logstore.Log `json:"payload"`
-	}{
-		Type:      "log",
-		Operation: operationType,
-		Payload:   logEntry,
-	}
-
-	data, err := json.Marshal(message)
-	if err != nil {
-		logger.Error("failed to marshal log entry: %v", err)
-		return
-	}
-
-	h.BroadcastMarshaledMessage(data)
+	h.broadcastLogMessage("log", operationType, logEntry)
@@
-	message := struct {
-		Type      string               `json:"type"`
-		Operation string               `json:"operation"` // "create" or "update"
-		Payload   *logstore.MCPToolLog `json:"payload"`
-	}{
-		Type:      "mcp_log",
-		Operation: operationType,
-		Payload:   logEntry,
-	}
-
-	data, err := json.Marshal(message)
-	if err != nil {
-		logger.Error("failed to marshal MCP log entry: %v", err)
-		return
-	}
-
-	h.BroadcastMarshaledMessage(data)
+	h.broadcastLogMessage("mcp_log", operationType, logEntry)
ui/app/workspace/mcp-logs/views/filters.tsx (1)

84-129: Reduce duplication of filterKeyMap.
The mapping is defined twice; extracting it once avoids drift.

♻️ Suggested refactor
+const FILTER_KEY_MAP: Record<string, keyof MCPToolLogFilters> = {
+	Status: "status",
+	"Tool Names": "tool_names",
+	Servers: "server_labels",
+	"Virtual Keys": "virtual_key_ids",
+};
@@
-	const filterKeyMap: Record<keyof typeof FILTER_OPTIONS, keyof MCPToolLogFilters> = {
-		Status: "status",
-		"Tool Names": "tool_names",
-		Servers: "server_labels",
-		"Virtual Keys": "virtual_key_ids",
-	};
-
-	const filterKey = filterKeyMap[category];
+	const filterKey = FILTER_KEY_MAP[category];
@@
-	const filterKeyMap: Record<keyof typeof FILTER_OPTIONS, keyof MCPToolLogFilters> = {
-		Status: "status",
-		"Tool Names": "tool_names",
-		Servers: "server_labels",
-		"Virtual Keys": "virtual_key_ids",
-	};
-
-	const filterKey = filterKeyMap[category];
+	const filterKey = FILTER_KEY_MAP[category];
plugins/logging/utils.go (1)

188-218: Consider removing inconsistent nil guards for consistency.

Based on the learning that PluginLogManager always has a valid store, the nil guards in GetAvailableToolNames and GetAvailableServerLabels (lines 190-192, 198-200) are inconsistent with SearchMCPToolLogs and GetMCPToolLogStats which don't have them. Similarly, GetAvailableMCPVirtualKeys only checks p.plugin but not p.plugin.store.

For consistency with the established invariant, consider either:

  1. Removing nil guards from all methods (since store is guaranteed), or
  2. Adding them uniformly to all methods

The current mixed approach may confuse future maintainers about when guards are needed.

plugins/governance/utils.go (1)

18-24: Optional: add a nil guard for req to avoid panic.

If callers can pass a nil request, a quick guard keeps this helper safe.

♻️ Proposed tweak
 func parseVirtualKeyFromHTTPRequest(req *schemas.HTTPRequest) *string {
+	if req == nil {
+		return nil
+	}
 	var virtualKeyValue string
 	vkHeader := req.CaseInsensitiveHeaderLookup("x-bf-vk")
framework/plugins/loader.go (1)

6-15: LGTM with minor documentation suggestion.

The interface changes correctly support the BasePlugin abstraction, enabling type assertions to specific plugin interfaces (LLMPlugin, MCPPlugin, etc.). This aligns with the broader plugin separation architecture.

Minor: The VerifyBasePlugin documentation has slight redundancy - it says both "empty string if the plugin is invalid" and "Returns an error if the plugin is invalid". Consider clarifying:

📝 Optional documentation clarification
 	// VerifyBasePlugin verifies a plugin at the given path
-	// Returns the name of the plugin or an empty string if the plugin is invalid
-	// Returns an error if the plugin is invalid
+	// Returns the plugin name on success, or an empty string with an error if validation fails
 	// This method is used to verify that the plugin is a valid base plugin and has the required symbols
 	VerifyBasePlugin(path string) (string, error)
core/mcp/clientmanager.go (1)

429-456: Add timeout context to tool discovery for SSE/STDIO connections

For SSE/STDIO connections, ctx is long-lived without a timeout. If the server hangs during tool retrieval, retrieveExternalTools(ctx, ...) will block indefinitely, stalling the Add/Reconnect operation. Apply the same timeout pattern used for initialization to prevent indefinite hangs.

Suggested timeout for tool discovery
-	tools, err := retrieveExternalTools(ctx, externalClient, config.Name)
+	toolCtx, toolCancel := context.WithTimeout(ctx, MCPClientConnectionEstablishTimeout)
+	defer toolCancel()
+	tools, err := retrieveExternalTools(toolCtx, externalClient, config.Name)
core/mcp/codemodereadfile.go (1)

284-307: Consider surfacing an explicit “invalid path” error.

parseVFSFilePath now returns empty values for invalid paths, but handleReadToolFile will treat that as “server not found” and list files, which is a bit confusing. An early explicit error would make this clearer.

💡 Optional tweak
 serverName, toolName, isToolLevel := parseVFSFilePath(fileName)
+if serverName == "" {
+    return createToolResponseMessage(toolCall, "Invalid fileName. Use a value from listToolFiles (e.g., servers/<server>/<tool>.d.ts)."), nil
+}
transports/bifrost-http/server/plugins.go (1)

146-161: Use the shared pointer helper for config fields.
Prefer the repo’s pointer helper for scalar pointer fields to keep pointer creation consistent.

♻️ Suggested tweak
-		config := &logging.Config{
-			DisableContentLogging: &s.Config.ClientConfig.DisableContentLogging,
-		}
+		config := &logging.Config{
+			DisableContentLogging: bifrost.Ptr(s.Config.ClientConfig.DisableContentLogging),
+		}
...
-		config := &governance.Config{
-			IsVkMandatory: &s.Config.ClientConfig.EnforceGovernanceHeader,
-		}
+		config := &governance.Config{
+			IsVkMandatory: bifrost.Ptr(s.Config.ClientConfig.EnforceGovernanceHeader),
+		}

Based on learnings, prefer bifrost.Ptr(...) for pointer creation.

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

74-92: Sorting not applied when configStore is nil.

In the configStore != nil path (lines 127-131), plugins are sorted by Name for deterministic ordering. However, when configStore == nil, no sorting is applied, resulting in inconsistent API behavior depending on configuration.

♻️ Suggested fix to add sorting in the nil configStore path
 	if h.configStore == nil {
 		pluginStatus := h.pluginsLoader.GetPluginStatus(ctx)
 		finalPlugins := []PluginResponse{}
 		for name, pluginStatus := range pluginStatus {
 			finalPlugins = append(finalPlugins, PluginResponse{
 				Name:       pluginStatus.Name,
 				ActualName: name,
 				Enabled:    true,
 				Config:     map[string]any{},
 				IsCustom:   true,
 				Path:       nil,
 				Status:     pluginStatus,
 			})
 		}
+		// Sort plugins by Name for deterministic ordering
+		sort.Slice(finalPlugins, func(i, j int) bool {
+			return finalPlugins[i].Name < finalPlugins[j].Name
+		})
 		SendJSON(ctx, map[string]any{
 			"plugins": finalPlugins,
 			"count":   len(finalPlugins),
 		})
 		return
 	}
plugins/logging/main.go (1)

746-765: Redundant codemode tool check.

The codemode tool check at lines 746-748 returns early if bifrost.IsCodemodeTool(fullToolName) is true. However, lines 759-764 then check again for specific codemode tool types and set serverLabel = "codemode". This second check is unreachable because the early return already handled codemode tools.

♻️ Remove unreachable code
 	// Skip execution for codemode tools
 	if bifrost.IsCodemodeTool(fullToolName) {
 		return req, nil, nil
 	}

 	// Extract server label from tool name (format: {client}-{tool_name})
 	// The first part before hyphen is the client/server label
 	if fullToolName != "" {
 		if idx := strings.Index(fullToolName, "-"); idx > 0 {
 			serverLabel = fullToolName[:idx]
 			toolName = fullToolName[idx+1:]
 		} else {
 			toolName = fullToolName
 		}
-		switch toolName {
-		case mcp.ToolTypeListToolFiles, mcp.ToolTypeReadToolFile, mcp.ToolTypeExecuteToolCode:
-			if serverLabel == "" {
-				serverLabel = "codemode"
-			}
-		}
 	}
core/mcp/mcp.go (1)

83-101: Silent failure on PluginPipeline type assertion.

If config.PluginPipelineProvider() returns a non-nil value that doesn't implement PluginPipeline, the type assertion at line 90 will fail and nil is returned silently. This could lead to subtle bugs where plugin hooks are unexpectedly skipped.

Consider logging a warning when the type assertion fails.

♻️ Add logging for type assertion failure
 	if config.PluginPipelineProvider != nil && config.ReleasePluginPipeline != nil {
 		pluginPipelineProvider = func() PluginPipeline {
 			if pipeline := config.PluginPipelineProvider(); pipeline != nil {
 				if pp, ok := pipeline.(PluginPipeline); ok {
 					return pp
+				} else {
+					logger.Warn("%s Plugin pipeline provider returned incompatible type: %T", MCPLogPrefix, pipeline)
 				}
 			}
 			return nil
 		}
core/bifrost.go (3)

140-153: Consider copying plugin slices on Init for immutability.

ReloadConfig already copies slices, but Init stores the config slice headers directly. Copying avoids accidental mutation or data races from external references.

♻️ Suggested tweak
-	bifrost.llmPlugins.Store(&config.LLMPlugins)
-	bifrost.mcpPlugins.Store(&config.MCPPlugins)
+	llmPluginsCopy := append([]schemas.LLMPlugin(nil), config.LLMPlugins...)
+	mcpPluginsCopy := append([]schemas.MCPPlugin(nil), config.MCPPlugins...)
+	bifrost.llmPlugins.Store(&llmPluginsCopy)
+	bifrost.mcpPlugins.Store(&mcpPluginsCopy)

Also applies to: 295-307


2306-2314: Consider centralizing tool-name prefix stripping.

Right now the prefix removal logic is embedded inline; a shared helper would keep naming consistent with MCP discovery/sanitization rules across code paths. Based on learnings, this helps avoid accidental re-mapping of sanitized tool names.


3059-3144: Use the pre-hook count for post-hook execution.

RunLLMPreHooks returns preCount, but post-hooks currently use len(*bifrost.llmPlugins.Load()). If plugins are reloaded concurrently, this can skip post-hooks for plugins that already ran pre-hooks. Prefer preCount and (for streaming) consider propagating it to the stream post-hook runner.

🔧 Suggested adjustment (non-streaming + error path)
-	pluginCount := len(*bifrost.llmPlugins.Load())
+	pluginCount := preCount
@@
-	recoveredResp, recoveredErr := pipeline.RunPostHooks(ctx, nil, &bifrostErrVal, len(*bifrost.llmPlugins.Load()))
+	recoveredResp, recoveredErr := pipeline.RunPostHooks(ctx, nil, &bifrostErrVal, preCount)

Also applies to: 3332-3334, 3587-3589

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

2288-2297: Consider deep-copying slice fields in GetPluginStatus.

maps.Copy performs a shallow copy of schemas.PluginStatus, so the Logs and Types slices still share backing arrays with internal state. If callers mutate, it can leak into pluginStatus. A defensive deep copy avoids this.

♻️ Suggested refactor
-	result := make(map[string]schemas.PluginStatus, len(c.pluginStatus))
-	maps.Copy(result, c.pluginStatus)
+	result := make(map[string]schemas.PluginStatus, len(c.pluginStatus))
+	for name, status := range c.pluginStatus {
+		logsCopy := append([]string(nil), status.Logs...)
+		typesCopy := append([]schemas.PluginType(nil), status.Types...)
+		status.Logs = logsCopy
+		status.Types = typesCopy
+		result[name] = status
+	}
 
 	return result

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 540e8c0 to fc318bd Compare January 23, 2026 11:37
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: 10

Caution

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

⚠️ Outside diff range comments (8)
examples/plugins/hello-world-wasm-go/README.md (1)

97-122: Clarify hook naming terminology in documentation.

The export table correctly names the WASM functions as pre_hook and post_hook, but the documentation sections (lines 97–122) and benefits list (line 170) refer to them as PreLLMHook and PostLLMHook. Add a note explaining that the exported symbols are pre_hook and post_hook, while the conceptual hook names in the documentation are PreLLMHook and PostLLMHook to reflect their LLM-context use.

plugins/telemetry/main.go (1)

361-473: Snapshot result and bifrostErr fields before spawning the goroutine.

The goroutine reads result and bifrostErr while they are returned to the caller and passed to downstream hooks in the plugin chain (core/bifrost.go:4362/4379), which can mutate them. Extract all needed fields before the go func() to eliminate the data race.

plugins/mocker/main.go (2)

866-869: Model override only applies to ChatResponse, not ResponsesResponse.

When content.Model is set, the override is only applied to mockResponse.ChatResponse.Model. If the request is a ResponsesRequest, the model override is silently ignored. Additionally, if mockResponse.ChatResponse is nil (which it would be for a ResponsesRequest), this will cause a nil pointer dereference.

🐛 Proposed fix
 	// Override model if specified
 	if content.Model != nil {
-		mockResponse.ChatResponse.Model = *content.Model
+		if mockResponse.ChatResponse != nil {
+			mockResponse.ChatResponse.Model = *content.Model
+		}
+		if mockResponse.ResponsesResponse != nil {
+			mockResponse.ResponsesResponse.Model = content.Model
+		}
 	}

998-1046: handleDefaultBehavior returns only ChatResponse for DefaultBehaviorSuccess.

When no rules match and DefaultBehaviorSuccess is configured, the mock response always creates a ChatResponse. If the original request was a ResponsesRequest, this may cause type mismatches downstream. Consider mirroring the request type logic from generateSuccessShortCircuit.

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

1514-1557: Use store-derived MCP pricing when ConfigStore is enabled.
mcpPricingConfig is populated from the store but never used, so MCPCatalog is always initialized from file data, which can be stale vs DB updates. Consider using store pricing when ConfigStore is present.

🐛 Suggested fix
-	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
-		PricingData: buildMCPPricingDataFromFile(ctx, configData),
-	}, logger)
+	mcpPricingData := buildMCPPricingDataFromFile(ctx, configData)
+	if config.ConfigStore != nil {
+		mcpPricingData = buildMCPPricingDataFromStore(ctx, config.ConfigStore)
+	}
+	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
+		PricingData: mcpPricingData,
+	}, logger)
core/mcp/clientmanager.go (1)

407-456: Add timeout for tool retrieval and ensure STDIO contexts are cancelled on failure.

For SSE/STDIO, retrieveExternalTools runs on longLivedCtx without a timeout, which can cause indefinite hangs in AddClient/Reconnect. Additionally, on Start/Initialize failure only SSE cancels the long-lived context—STDIO subprocesses are never cancelled and the context is never stored for later cleanup (unlike SSE which stores it at line 476). Add a timeout-bound context for tool retrieval and extend the error-path cancellation to STDIO.

🔧 Suggested fix
 	if err := externalClient.Start(ctx); err != nil {
-		if config.ConnectionType == schemas.MCPConnectionTypeSSE {
-			cancel() // Cancel SSE context only on error
-		}
+		if config.ConnectionType == schemas.MCPConnectionTypeSSE || config.ConnectionType == schemas.MCPConnectionTypeSTDIO {
+			cancel() // Cancel long-lived context on error
+		}
 		return fmt.Errorf("failed to start MCP client transport %s: %v", config.Name, err)
 	}
@@
 	if err != nil {
-		if config.ConnectionType == schemas.MCPConnectionTypeSSE {
-			cancel() // Cancel SSE context only on error
-		}
+		if config.ConnectionType == schemas.MCPConnectionTypeSSE || config.ConnectionType == schemas.MCPConnectionTypeSTDIO {
+			cancel() // Cancel long-lived context on error
+		}
 		return fmt.Errorf("failed to initialize MCP client %s: %v", config.Name, err)
 	}
@@
-	tools, err := retrieveExternalTools(ctx, externalClient, config.Name)
+	toolsCtx := ctx
+	if config.ConnectionType == schemas.MCPConnectionTypeSSE || config.ConnectionType == schemas.MCPConnectionTypeSTDIO {
+		var toolsCancel context.CancelFunc
+		toolsCtx, toolsCancel = context.WithTimeout(longLivedCtx, MCPClientConnectionEstablishTimeout)
+		defer toolsCancel()
+	}
+	tools, err := retrieveExternalTools(toolsCtx, externalClient, config.Name)
core/mcp/codemodereadfile.go (1)

141-150: Normalize requested tool name before comparison.

toolName from the path is compared against normalized tool names (underscores). If a caller supplies a dashed name, it won’t match. Normalize toolName the same way before comparing. Based on learnings, tool names are sanitized consistently in the MCP system.

🔧 Proposed fix
-				toolNameLower := strings.ToLower(toolName)
+				toolNameLower := strings.ToLower(strings.ReplaceAll(toolName, "-", "_"))
transports/bifrost-http/handlers/plugins.go (1)

219-238: Rollback plugin load if DB create fails.

Line 219–235 loads the plugin before the DB entry is created. If CreatePlugin fails, the plugin remains loaded but isn’t persisted, which can leave the system in an inconsistent state. Consider rolling back the load and fix the error message to reflect the actual flow.

🐛 Suggested fix
 	// We reload the plugin if its enabled
 	if request.Enabled {
 		if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, request.Path, request.Config); err != nil {
 			logger.Error("failed to load plugin: %v", err)
-			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin created in database but failed to load: %v", err))
+			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Failed to load plugin: %v", err))
 			return
 		}
 	}

 	// Create a DB entry if plugin loading was successful
 	if err := h.configStore.CreatePlugin(ctx, &configstoreTables.TablePlugin{
 		Name:     request.Name,
 		Enabled:  request.Enabled,
 		Config:   request.Config,
 		Path:     request.Path,
 		IsCustom: true,
 	}); err != nil {
 		logger.Error("failed to create plugin: %v", err)
+		if request.Enabled {
+			if rollbackErr := h.pluginsLoader.RemovePlugin(ctx, request.Name); rollbackErr != nil {
+				logger.Error("failed to rollback plugin load: %v", rollbackErr)
+			}
+		}
 		SendError(ctx, 500, "Failed to create plugin")
 		return
 	}
🤖 Fix all issues with AI agents
In `@core/bifrost.go`:
- Line 3373: The post-hook uses a dynamic snapshot via bifrost.llmPlugins.Load()
which can change during hot reloads and cause post-hook count drift; replace the
ad-hoc len(*bifrost.llmPlugins.Load()) with the stable prehook snapshot
(preCount) where available or with len(pipeline.llmPlugins) to ensure the
post-hook loop uses the same plugin set as the prehook — update occurrences at
the locations referencing bifrost.llmPlugins.Load() to use preCount or
pipeline.llmPlugins respectively so post-hooks cannot be skipped due to
mid-flight reloads.
- Around line 290-307: The top comment in ReloadConfig is misleading (it
mentions updating account but ReloadConfig only updates dropExcessRequests and
plugin lists); either implement account update logic or change the comment—edit
the comment above func (bifrost *Bifrost) ReloadConfig to accurately describe
what the function does (e.g., "Update dropExcessRequests and atomically replace
LLM and MCP plugin lists") and remove the reference to "account"; references to
ReloadConfig, Bifrost, dropExcessRequests, llmPlugins, and mcpPlugins should
help you locate where to change the comment.
- Around line 2071-2080: The RemovePlugin signature now requires a pluginType
argument but the HTTP transport interfaces and call sites still use the old
two-parameter form; update the transport/server interface method signature (the
server's RemovePlugin method) and all handler call sites to pass the new third
argument, using schemas.PluginTypeLLM or schemas.PluginTypeMCP (or derive from
the request's plugin type field) as appropriate; ensure handlers that call
bifrost.RemovePlugin (the removal endpoints in plugins handlers and the config
handler) accept/parse the plugin type from the incoming request and forward it
to bifrost.RemovePlugin, and adjust any compile-time interface implementations
to match the new signature so removeLLMPlugin/removeMCPPlugin dispatch continues
to work.

In `@core/mcp/codemodeexecutecode.go`:
- Around line 852-879: The short-circuit branch currently ignores errors
returned by pipeline.RunMCPPostHooks; capture the returned error (e.g.,
finalResp, finalErr := pipeline.RunMCPPostHooks(nestedCtx,
shortCircuit.Response, nil, preCount)) and handle finalErr the same way the main
path does (return the error or wrap it) before processing finalResp; likewise
when calling pipeline.RunMCPPostHooks for shortCircuit.Error capture the
returned error and handle it (e.g., finalResp, finalErr :=
pipeline.RunMCPPostHooks(nestedCtx, nil, shortCircuit.Error, preCount)) so you
don't drop hook failures—apply this change around the existing
shortCircuit.Response and shortCircuit.Error handling in the function containing
RunMCPPostHooks.

In `@docs/openapi/schemas/management/logging.yaml`:
- Around line 112-157: The MCPToolLogEntry schema is missing the backend fields
virtual_key_id and virtual_key_name and should reference that error_details is a
serialized BifrostError; update the MCPToolLogEntry object to add properties
virtual_key_id (string) and virtual_key_name (string), and change/annotate
error_details to reflect the BifrostError shape (or reference a BifrostError
schema like $ref: "#/components/schemas/BifrostError" or include the same
properties) so clients match the backend MCPToolLog model and can deserialize
error_details correctly.

In `@framework/configstore/rdb.go`:
- Around line 857-903: UpdateMCPClientConfig currently marshals
clientConfigCopy.Headers (map[string]schemas.EnvVar) directly, which mismatches
TableMCPClient.BeforeSave; instead build a map[string]string (e.g., headersMap)
by iterating clientConfigCopy.Headers and for each schemas.EnvVar set
headersMap[key] = envVar.EnvVar if envVar.FromEnv is true else envVar.Value,
normalize nil to empty map, then json.Marshal that headersMap into headersJSON
and use that in updates; reference UpdateMCPClientConfig,
clientConfigCopy.Headers and TableMCPClient.BeforeSave when making the change.

In `@framework/logstore/tables.go`:
- Around line 456-557: The WebSocket broadcast path is emitting MCPToolLog
objects without redacting VirtualKey, unlike the HTTP logging endpoints; update
the BroadcastMCPLogUpdate() flow to apply the same redaction used by the HTTP
handlers by replacing each MCPToolLog.VirtualKey with the output of
GetAllRedactedVirtualKeys() (or by invoking the same redaction helper used in
logging.go) before serialization/broadcast so that VirtualKey.Value is never
sent over the socket; locate references to MCPToolLog.VirtualKey and the
broadcast function BroadcastMCPLogUpdate() to implement this defense-in-depth
change.

In `@transports/bifrost-http/handlers/mcp.go`:
- Around line 21-25: The MCPManager interface declares EditMCPClient with a
different type alias than another stacked definition; unify the interface so all
code uses the same concrete type alias to avoid divergence: pick one alias
(e.g., tables.TableMCPClient) and update the MCPManager interface method
signature (EditMCPClient) and all callers/implementations
(AddMCPClient/RemoveMCPClient users if needed) to reference that single symbol,
then remove the duplicate interface definition so only one MCPManager exists
across the codebase and ensure imports are adjusted accordingly.

In `@transports/bifrost-http/handlers/plugins.go`:
- Around line 339-349: The disable branch uses h.pluginsLoader.RemovePlugin(ctx,
name) but its error text incorrectly says "failed to reload"; update the error
handling so it logs and returns a consistent stop/disable message: call
logger.Error with a message like "failed to stop plugin: %v" (or similar) when
RemovePlugin returns an error, and change the SendError invocation to return
"Plugin updated in database but failed to stop: %v" instead of "reload". Ensure
you modify the code paths around h.pluginsLoader.RemovePlugin, SendError and any
existing logger usage to reflect "stop" rather than "reload".

In `@ui/app/workspace/mcp-logs/views/filters.tsx`:
- Around line 18-19: The props fetchLogs and fetchStats on the Filters component
are declared but never used; either remove them from the component props
interface or invoke them when filters change. If the intent is to refetch on
filter updates, call fetchLogs() and fetchStats() inside the onFiltersChange
handler (or trigger them from a useEffect that watches the filters state) after
updating the filters (e.g., in the function named onFiltersChange or after
setFilters); otherwise remove fetchLogs and fetchStats from the component props
and any callers to keep the interface accurate.
♻️ Duplicate comments (20)
examples/plugins/http-transport-only/README.md (1)

124-126: Tighten Notes wording to avoid repeated “only”.

Minor readability nit; consider rephrasing for flow.

✏️ Suggested tweak
- - This plugin operates at the HTTP transport layer only
- - Works only when using bifrost-http, not when using Bifrost as a Go SDK
+ - This plugin operates at the HTTP transport layer
+ - Works with bifrost-http, not when using Bifrost as a Go SDK
core/schemas/plugin_wasm.go (1)

5-8: Remove streaming mention from WASM short‑circuit docs.

The struct has no Stream field and streaming isn’t supported in WASM.

✏️ Suggested comment fix
-// It can contain either a response (success short-circuit), a stream (streaming short-circuit), or an error (error short-circuit).
-// Streams are not supported in WASM plugins.
+// In WASM plugins, it can contain either a response (success short-circuit) or an error (error short-circuit).
+// Streams are not supported in WASM plugins.
ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx (2)

50-58: getValidatedStatus is duplicated from columns.tsx.

Same helper exists in both files. Consider extracting to a shared utility as noted in the columns.tsx review.


128-133: Latency display hides valid 0ms values.

The condition log.latency ? ... : "NA" treats 0 as falsy. This was already flagged in a previous review.

framework/configstore/migrations.go (1)

2323-2349: Prefix stripping misses client names containing underscores.
Splitting on the first _ means a client like my_client won’t match my_client_add, leaving prefixes intact.

🐛 Suggested fix
-	hasClientPrefix := func(toolName, clientName string) (bool, string) {
-		if !strings.Contains(toolName, "_") {
-			return false, ""
-		}
-
-		// Get the prefix before the first underscore
-		parts := strings.SplitN(toolName, "_", 2)
-		if len(parts) != 2 {
-			return false, ""
-		}
-		toolPrefix := parts[0]
-		unprefixedPart := parts[1]
-
-		// Check exact match first
-		if toolPrefix == clientName {
-			return true, unprefixedPart
-		}
-
-		// Check normalized match (for legacy prefixes that existed before normalization)
-		normalizedToolPrefix := normalizeMCPClientName(toolPrefix)
-		if normalizedToolPrefix == clientName {
-			return true, unprefixedPart
-		}
-
-		return false, ""
-	}
+	hasClientPrefix := func(toolName, clientName string) (bool, string) {
+		prefix := clientName + "_"
+		if strings.HasPrefix(toolName, prefix) {
+			return true, strings.TrimPrefix(toolName, prefix)
+		}
+		// Legacy prefix: normalize the substring before the first underscore
+		if idx := strings.IndexByte(toolName, '_'); idx > 0 {
+			toolPrefix := toolName[:idx]
+			unprefixedPart := toolName[idx+1:]
+			if normalizeMCPClientName(toolPrefix) == clientName {
+				return true, unprefixedPart
+			}
+		}
+		return false, ""
+	}
examples/plugins/http-transport-only/main.go (3)

95-99: Clamp non‑positive rate_window to avoid bypassing limits.

🔧 Suggested fix
-	// Parse rate_window
-	if rateWindow, ok := configMap["rate_window"].(float64); ok {
-		pluginConfig.RateWindow = int(rateWindow)
-		fmt.Printf("[HTTP-Transport-Only Plugin] Rate window: %d seconds\n", pluginConfig.RateWindow)
-	}
+	// Parse rate_window
+	if rateWindow, ok := configMap["rate_window"].(float64); ok {
+		pluginConfig.RateWindow = int(rateWindow)
+		if pluginConfig.RateWindow <= 0 {
+			pluginConfig.RateWindow = 60
+			fmt.Println("[HTTP-Transport-Only Plugin] Invalid rate window; defaulting to 60 seconds")
+		} else {
+			fmt.Printf("[HTTP-Transport-Only Plugin] Rate window: %d seconds\n", pluginConfig.RateWindow)
+		}
+	}

176-178: Initialize req.Headers before writing.

🐛 Suggested fix
 	// Example 4: Add custom headers
+	if req.Headers == nil {
+		req.Headers = map[string]string{}
+	}
 	req.Headers["X-Plugin-Processed"] = "true"
 	req.Headers["X-Request-Time"] = time.Now().Format(time.RFC3339)

189-210: Guard resp/resp.Headers before mutation.

🐛 Suggested fix
 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	fmt.Println("[HTTP-Transport-Only Plugin] HTTPTransportPostHook called")
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = map[string]string{}
+	}
 
 	// Calculate request duration
 	startTime := ctx.Value(schemas.BifrostContextKey("http-plugin-start-time"))
examples/plugins/multi-interface/main.go (3)

37-39: Protect requestCount from concurrent access.

🔧 Suggested fix (atomic)
 import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"sync/atomic"
 	"time"
@@
-	requestCount int64
+	requestCount atomic.Int64
 	startTime    time.Time
 )
@@
-		requestCount++
-		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
+		current := requestCount.Add(1)
+		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", current)
@@
-			content = fmt.Sprintf("Processing request #%d. Server uptime: %v", requestCount, time.Since(startTime))
+			content = fmt.Sprintf("Processing request #%d. Server uptime: %v", requestCount.Load(), time.Since(startTime))
@@
-		fmt.Printf("[Multi-Interface Plugin] Cleanup called - processed %d requests over %v\n",
-			requestCount, uptime)
+		fmt.Printf("[Multi-Interface Plugin] Cleanup called - processed %d requests over %v\n",
+			requestCount.Load(), uptime)

Also applies to: 107-109, 176-178, 308-309


106-110: Initialize req.Headers before writing.

🐛 Suggested fix
 	// Add request tracking (configurable)
 	if pluginConfig.TrackRequests {
+		if req.Headers == nil {
+			req.Headers = map[string]string{}
+		}
 		requestCount++
 		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
 	}

129-150: Guard resp/resp.Headers before mutation.

🐛 Suggested fix
 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	if !pluginConfig.EnableHTTPHooks {
 		return nil
 	}
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = map[string]string{}
+	}
 
 	if pluginConfig.EnableLogging {
 		fmt.Println("[Multi-Interface Plugin] HTTPTransportPostHook called")
 	}
examples/plugins/multi-interface/Makefile (1)

1-12: LGTM with minor static analysis note.

The Makefile is well-structured with clear build and clean targets. The static analysis tool flags missing all and test phony targets, but this has already been addressed in a previous review comment.

transports/bifrost-http/lib/config_test.go (1)

472-517: Preserve stable ClientID/ID when updating mock MCP clients.
The update path can drop the existing internal ID and leave ClientID empty if the caller doesn’t set it, which makes subsequent reads inconsistent.

🔧 Suggested fix
-	// Update the in-memory state to ensure GetMCPConfig returns updated data
-	for i := range m.mcpConfig.ClientConfigs {
-		if m.mcpConfig.ClientConfigs[i].ClientID == id {
-			// Found the entry, update it with the new config
-			m.mcpConfig.ClientConfigs[i] = tables.TableMCPClient{
-				ClientID:           clientConfig.ClientID,
-				Name:               clientConfig.Name,
-				IsCodeModeClient:   clientConfig.IsCodeModeClient,
-				ConnectionType:     clientConfig.ConnectionType,
-				ConnectionString:   clientConfig.ConnectionString,
-				StdioConfig:        clientConfig.StdioConfig,
-				Headers:            clientConfig.Headers,
-				ToolsToExecute:     clientConfig.ToolsToExecute,
-				ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-			}
-			return nil
-		}
-	}
-	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
-	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, tables.TableMCPClient{
-		ClientID:           clientConfig.ClientID,
-		Name:               clientConfig.Name,
-		IsCodeModeClient:   clientConfig.IsCodeModeClient,
-		ConnectionType:     clientConfig.ConnectionType,
-		ConnectionString:   clientConfig.ConnectionString,
-		StdioConfig:        clientConfig.StdioConfig,
-		Headers:            clientConfig.Headers,
-		ToolsToExecute:     clientConfig.ToolsToExecute,
-		ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-	})
+	updated := clientConfig
+	updated.ClientID = id
+
+	// Update the in-memory state to ensure GetMCPConfig returns updated data
+	for i := range m.mcpConfig.ClientConfigs {
+		if m.mcpConfig.ClientConfigs[i].ClientID == id {
+			updated.ID = m.mcpConfig.ClientConfigs[i].ID
+			m.mcpConfig.ClientConfigs[i] = updated
+			return nil
+		}
+	}
+	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
+	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, updated)
docs/openapi/paths/management/logging.yaml (1)

335-340: Status filter description conflicts with enum in MCP endpoints.

The description says comma-separated values, but the enum only permits a single status. Please align both mcp-logs and mcp-logs-stats.

🔧 Suggested fix (keep enum, update description)
-        description: Comma-separated list of statuses to filter by (processing, success, error)
+        description: Status to filter by
         schema:
           type: string
           enum: [processing, success, error]
...
-        description: Comma-separated list of statuses to filter by
+        description: Status to filter by
         schema:
           type: string
           enum: [processing, success, error]

Also applies to: 459-465

docs/plugins/getting-started.mdx (1)

52-67: Hook lists still inconsistent with lifecycle section.

The v1.4.x+ function list omits PreMCPHook/PostMCPHook while the lifecycle mentions them, and the v1.3.x lifecycle still uses PreHook/PostHook while the list uses PreLLMHook/PostLLMHook. Please align these sections with the actual interface (and with the stacked PRs’ implementation). Based on learnings, ensure this matches the stack’s finalized hook naming.

✏️ Suggested doc alignment
@@
-    - `PreLLMHook()` - Intercept requests before they reach providers
-    - `PostLLMHook()` - Process responses after provider calls
+    - `PreLLMHook()` - Intercept requests before they reach providers
+    - `PostLLMHook()` - Process responses after provider calls
+    - `PreMCPHook()` - Intercept MCP tool execution requests
+    - `PostMCPHook()` - Process MCP tool execution responses
@@
-    2. `PreHook` - Executes in registration order, can short-circuit requests
+    2. `PreLLMHook` - Executes in registration order, can short-circuit requests
@@
-    4. `PostHook` - Executes in reverse order of PreHooks
+    4. `PostLLMHook` - Executes in reverse order of PreHooks

Also applies to: 92-104

ui/app/workspace/mcp-logs/page.tsx (1)

231-329: Guard stats increments on repeated terminal updates.

Line 304+ increments totals for every "success"/"error" update; repeated updates on the same log can inflate stats. Consider checking the previous log status or refetching stats.

core/mcp/utils.go (1)

192-195: Update comment examples to the new “-” separator.

The examples still show "calculator/add" even though tool names are now prefixed with "-".

📝 Suggested update
-		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// Tool names in config are stored without prefix (e.g., "add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")
...
-		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// Tool names in config are stored without prefix (e.g., "add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")

Also applies to: 224-227

examples/plugins/mcp-only/main.go (1)

114-117: Use the canonical request-id context key.

schemas.BifrostContextKey("request_id") will not match the canonical key and will read as nil. Use schemas.BifrostContextKeyRequestID instead.

🛠️ Suggested fix
-		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKey("request_id")))
+		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKeyRequestID))
transports/bifrost-http/server/server.go (1)

653-733: Call Cleanup on replaced/removed plugins to avoid leaks.

Reload/remove still replaces plugins without invoking Cleanup(), which can leave goroutines/resources running.

🛠️ Suggested fix
 // ReloadPlugin
-	plugin, err := InstantiatePlugin(ctx, name, path, pluginConfig, s.Config)
+	oldPlugin, _ := s.Config.FindPluginByName(name)
+	plugin, err := InstantiatePlugin(ctx, name, path, pluginConfig, s.Config)
@@
 	if err := s.Config.RegisterPlugin(plugin); err != nil {
 		return updateError("registering", err)
 	}
+	if oldPlugin != nil {
+		if err := oldPlugin.Cleanup(); err != nil {
+			logger.Warn("failed to cleanup old plugin %s: %v", name, err)
+		}
+	}

 // RemovePlugin
-	if err := s.Config.UnregisterPlugin(name); err != nil {
+	oldPlugin, _ := s.Config.FindPluginByName(name)
+	if err := s.Config.UnregisterPlugin(name); err != nil {
 		return err
 	}
+	if oldPlugin != nil {
+		if err := oldPlugin.Cleanup(); err != nil {
+			logger.Warn("failed to cleanup plugin %s: %v", name, err)
+		}
+	}
docs/openapi/schemas/management/logging.yaml (1)

159-199: Document virtual_key_ids in MCPToolLogSearchFilters.
The backend filter includes virtual_key_ids, but the schema omits it.

Suggested schema addition
 MCPToolLogSearchFilters:
   type: object
   description: MCP tool log search filters
   properties:
@@
     llm_request_ids:
       type: array
       items:
         type: string
       description: Filter by linked LLM request IDs
+    virtual_key_ids:
+      type: array
+      items:
+        type: string
+      description: Filter by virtual key IDs
🧹 Nitpick comments (15)
examples/plugins/http-transport-only/Makefile (1)

1-1: Add all/test targets or suppress checkmake for examples.

checkmake is warning about missing phony all and test. If you want to keep lint clean, consider adding a simple all: build and a stub test target (or explicitly exclude this example from that rule).

♻️ Example Makefile adjustment
-.PHONY: build clean
+.PHONY: all build clean test
+
+all: build
+
+test:
+	`@echo` "No tests for http-transport-only example"
ui/components/sidebar.tsx (1)

72-89: Reuse the shared MCPIcon to avoid duplication.

There’s already an MCPIcon exported from ui/components/ui/icons.tsx; importing it keeps icons consistent and avoids drift.

♻️ Suggested refactor
-import { ChevronRight } from "lucide-react";
+import { ChevronRight } from "lucide-react";
+import { MCPIcon } from "@/components/ui/icons";
...
-// Custom MCP Icon Component
-const MCPIcon = ({ className }: { className?: string }) => (
-  <svg ...>...</svg>
-);
ui/app/workspace/mcp-logs/views/columns.tsx (1)

11-19: getValidatedStatus helper is duplicated.

This helper function is also defined in mcpLogDetailsSheet.tsx. Consider extracting it to a shared utility (e.g., @/lib/utils/logs.ts) to avoid duplication and ensure consistency.

♻️ Suggested shared utility

Create a shared file like ui/lib/utils/logs.ts:

import { Status, Statuses } from "@/lib/constants/logs";

export const getValidatedStatus = (status: string): Status => {
  if (Statuses.includes(status as Status)) {
    return status as Status;
  }
  return "processing";
};

Then import from both columns.tsx and mcpLogDetailsSheet.tsx.

ui/app/workspace/mcp-logs/views/filters.tsx (2)

84-109: filterKeyMap is duplicated in handleFilterSelect and isSelected.

The mapping from category names to filter keys is repeated. Consider extracting it as a constant.

♻️ Extract filterKeyMap constant
+const FILTER_KEY_MAP: Record<string, keyof MCPToolLogFilters> = {
+  Status: "status",
+  "Tool Names": "tool_names",
+  Servers: "server_labels",
+  "Virtual Keys": "virtual_key_ids",
+} as const;
+
 const handleFilterSelect = (category: keyof typeof FILTER_OPTIONS, value: string) => {
-  const filterKeyMap: Record<keyof typeof FILTER_OPTIONS, keyof MCPToolLogFilters> = {
-    Status: "status",
-    "Tool Names": "tool_names",
-    Servers: "server_labels",
-    "Virtual Keys": "virtual_key_ids",
-  };
-  const filterKey = filterKeyMap[category];
+  const filterKey = FILTER_KEY_MAP[category];
   // ...
 };

146-151: Loading placeholders are rendered as selectable filter items.

When filterDataLoading is true, strings like "Loading..." appear in the filter options and can be selected (though disabled). Consider filtering out loading placeholders entirely or using a distinct loading state UI.

♻️ Alternative: show loading state differently
const FILTER_OPTIONS = {
  Status: Statuses,
  "Tool Names": filterDataLoading ? [] : availableToolNames,
  Servers: filterDataLoading ? [] : availableServerLabels,
  "Virtual Keys": filterDataLoading ? [] : availableVirtualKeys.map((key) => key.name),
} as const;

Then in the CommandGroup, show a loading indicator when the category is loading and has no items.

ui/app/workspace/plugins/views/pluginsView.tsx (1)

205-205: Minor: Extraneous whitespace in FormLabel.

There appears to be extra whitespace/tab inside the FormLabel tag that should be cleaned up for consistency.

🧹 Suggested cleanup
-										<FormLabel	>Enabled</FormLabel>
+										<FormLabel>Enabled</FormLabel>
framework/plugins/soloader.go (1)

128-158: Consider extracting shared GetName/Cleanup validation logic.

Both LoadPlugin and VerifyBasePlugin duplicate the same validation logic for GetName and Cleanup symbols. Consider extracting this into a helper to reduce duplication and ensure consistency.

♻️ Optional refactor to reduce duplication
// verifyRequiredSymbols validates and assigns GetName and Cleanup to dp
func verifyRequiredSymbols(pluginObj *plugin.Plugin, dp *DynamicPlugin) error {
    getNameSym, err := pluginObj.Lookup("GetName")
    if err != nil {
        return fmt.Errorf("required symbol GetName not found: %w", err)
    }
    var ok bool
    if dp.getName, ok = getNameSym.(func() string); !ok {
        return fmt.Errorf("failed to cast GetName to func() string")
    }

    cleanupSym, err := pluginObj.Lookup("Cleanup")
    if err != nil {
        return fmt.Errorf("required symbol Cleanup not found: %w", err)
    }
    if dp.cleanup, ok = cleanupSym.(func() error); !ok {
        return fmt.Errorf("failed to cast Cleanup to func() error")
    }
    return nil
}

Then use in both LoadPlugin and VerifyBasePlugin.

plugins/logging/main.go (1)

746-765: Codemode tool check after extraction may skip logging for partial tool names.

The codemode check at line 746 uses fullToolName (before extraction), which is correct. However, the second switch block at lines 759-764 that sets serverLabel = "codemode" for codemode tools will never execute because bifrost.IsCodemodeTool(fullToolName) already returns early at line 747.

This dead code can be removed for clarity.

♻️ Suggested cleanup
 	// Extract server label from tool name (format: {client}-{tool_name})
 	// The first part before hyphen is the client/server label
 	if fullToolName != "" {
 		if idx := strings.Index(fullToolName, "-"); idx > 0 {
 			serverLabel = fullToolName[:idx]
 			toolName = fullToolName[idx+1:]
 		} else {
 			toolName = fullToolName
 		}
-		switch toolName {
-		case mcp.ToolTypeListToolFiles, mcp.ToolTypeReadToolFile, mcp.ToolTypeExecuteToolCode:
-			if serverLabel == "" {
-				serverLabel = "codemode"
-			}
-		}
 	}
transports/bifrost-http/lib/config.go (1)

2292-2310: Avoid returning PluginStatus with shared slice backing.
maps.Copy shallow-copies slices; callers can mutate Logs/Types and inadvertently affect internal state. Deep-copy slices in the getters.

♻️ Suggested adjustment
 func (c *Config) GetPluginStatus() map[string]schemas.PluginStatus {
 	c.pluginStatusMu.RLock()
 	defer c.pluginStatusMu.RUnlock()
 
-	result := make(map[string]schemas.PluginStatus, len(c.pluginStatus))
-	maps.Copy(result, c.pluginStatus)
+	result := make(map[string]schemas.PluginStatus, len(c.pluginStatus))
+	for name, status := range c.pluginStatus {
+		status.Logs = slices.Clone(status.Logs)
+		status.Types = slices.Clone(status.Types)
+		result[name] = status
+	}
 
 	return result
 }
 
 func (c *Config) GetPluginStatusByName(name string) (schemas.PluginStatus, bool) {
 	c.pluginStatusMu.RLock()
 	defer c.pluginStatusMu.RUnlock()
 
 	status, ok := c.pluginStatus[name]
+	if ok {
+		status.Logs = slices.Clone(status.Logs)
+		status.Types = slices.Clone(status.Types)
+	}
 	return status, ok
 }
plugins/mocker/plugin_test.go (1)

65-68: Prefer bifrost.Ptr(...) for pointer creation in tests.
Use the repo-standard pointer helper for consistency.

♻️ Example update (apply similarly to the other occurrences)
-	Account:    &account,
+	Account:    bifrost.Ptr(account),
Based on learnings, use `bifrost.Ptr` instead of `&value` for pointer creation.

Also applies to: 108-111, 185-187, 264-266, 327-329, 397-399

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

770-900: Use bifrost.Ptr() for pointer field assignments in new MCP filter helpers to match repository conventions.

The codebase consistently uses bifrost.Ptr() for pointer creation. Update the assignments in the new MCP filter helpers (StartTime, EndTime, MinLatency, MaxLatency) to follow this pattern, and add the required import.

♻️ Proposed refactor
-        filters.StartTime = &t
+        filters.StartTime = bifrost.Ptr(t)

-        filters.EndTime = &t
+        filters.EndTime = bifrost.Ptr(t)

-        filters.MinLatency = &f
+        filters.MinLatency = bifrost.Ptr(f)

-        filters.MaxLatency = &val
+        filters.MaxLatency = bifrost.Ptr(val)
 import (
     "context"
     "fmt"
     "strconv"
     "strings"
     "time"

+    bifrost "github.com/maximhq/bifrost/core"
     "github.com/bytedance/sonic"
     "github.com/fasthttp/router"
     "github.com/maximhq/bifrost/core/schemas"
core/mcp/clientmanager.go (1)

194-202: Remove duplicate IsCodeModeClient assignment.

config.IsCodeModeClient is set twice; dropping the second write avoids confusion during future edits.

♻️ Suggested cleanup
 	config.Headers = updatedConfig.Headers
 	config.ToolsToExecute = updatedConfig.ToolsToExecute
 	config.ToolsToAutoExecute = updatedConfig.ToolsToAutoExecute
-	config.IsCodeModeClient = updatedConfig.IsCodeModeClient
ui/app/workspace/mcp-logs/views/mcpLogsTable.tsx (1)

11-25: Consider lifting live/socket state out of this table component.

Per the UI guidance, keep components streaming-agnostic; move liveEnabled/isSocketConnected and the live status rendering to the parent (or pass a pre-rendered status node) so the table renders only the data it is given. Based on learnings, please consider this refactor.

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

74-91: Consider sorting plugins even when configStore is nil.

Map iteration order is non-deterministic; sorting here keeps API output stable like the configStore branch.

♻️ Suggested tweak
 	if h.configStore == nil {
 		pluginStatus := h.pluginsLoader.GetPluginStatus(ctx)
 		finalPlugins := []PluginResponse{}
 		for name, pluginStatus := range pluginStatus {
 			finalPlugins = append(finalPlugins, PluginResponse{
 				Name:       pluginStatus.Name,
 				ActualName: name,
 				Enabled:    true,
 				Config:     map[string]any{},
 				IsCustom:   true,
 				Path:       nil,
 				Status:     pluginStatus,
 			})
 		}
+		sort.Slice(finalPlugins, func(i, j int) bool {
+			return finalPlugins[i].Name < finalPlugins[j].Name
+		})
 		SendJSON(ctx, map[string]any{
 			"plugins": finalPlugins,
 			"count":   len(finalPlugins),
 		})
 		return
 	}
core/bifrost.go (1)

140-153: Avoid storing caller-owned plugin slices directly.
Line 152–153 stores config.*Plugins by pointer; external mutation can leak into runtime state. Consider copying like you already do in ReloadConfig.

♻️ Proposed fix
-	bifrost.llmPlugins.Store(&config.LLMPlugins)
-	bifrost.mcpPlugins.Store(&config.MCPPlugins)
+	llmPluginsCopy := append([]schemas.LLMPlugin(nil), config.LLMPlugins...)
+	mcpPluginsCopy := append([]schemas.MCPPlugin(nil), config.MCPPlugins...)
+	bifrost.llmPlugins.Store(&llmPluginsCopy)
+	bifrost.mcpPlugins.Store(&mcpPluginsCopy)

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from fc318bd to 08a8856 Compare January 23, 2026 19:40
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: 8

Caution

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

⚠️ Outside diff range comments (3)
plugins/maxim/main.go (1)

491-551: Avoid using ctx inside the goroutine to prevent reuse races.

The comment notes contexts can be reused; however, GetTracerFromContext(ctx) is still called inside the goroutine. Capture tracer/trace ID before launching to avoid late reads from a potentially reused context.

🔧 Suggested fix
-	// Capture context values BEFORE goroutine to avoid race conditions
+	// Capture context values BEFORE goroutine to avoid race conditions
 	// when the same context is reused across multiple requests
 	generationID, hasGenerationID := ctx.Value(GenerationIDKey).(string)
 	traceID, hasTraceID := ctx.Value(TraceIDKey).(string)
 	tags, hasTags := ctx.Value(TagsKey).(map[string]string)

 	isFinalChunk := bifrost.IsFinalChunk(ctx)
+	requestType, _, model := bifrost.GetResponseFields(result, bifrostErr)
+	tracer, bifrostTraceID, _ := bifrost.GetTracerFromContext(ctx)

 	go func() {
-		requestType, _, model := bifrost.GetResponseFields(result, bifrostErr)
-
 		var streamResponse *streaming.ProcessedStreamResponse
 		if bifrost.IsStreamRequestType(requestType) {
-			// Use central tracer's accumulator
-			tracer, bifrostTraceID, err := bifrost.GetTracerFromContext(ctx)
-			if err == nil && tracer != nil && bifrostTraceID != "" {
+			// Use central tracer's accumulator
+			if tracer != nil && bifrostTraceID != "" {
 				accResult := tracer.ProcessStreamingChunk(bifrostTraceID, isFinalChunk, result, bifrostErr)
 				if accResult != nil {
 					streamResponse = convertAccResultToProcessedStreamResponse(accResult)
 				}
 			}
@@
-				if bifrost.IsStreamRequestType(requestType) {
-					// Cleanup via central tracer
-					tracer, bifrostTraceID, err := bifrost.GetTracerFromContext(ctx)
-					if err == nil && tracer != nil && bifrostTraceID != "" {
-						tracer.CleanupStreamAccumulator(bifrostTraceID)
-					}
-				}
+				if bifrost.IsStreamRequestType(requestType) && tracer != nil && bifrostTraceID != "" {
+					tracer.CleanupStreamAccumulator(bifrostTraceID)
+				}
@@
-				if streamResponse != nil && isFinalChunk {
-					// Cleanup via central tracer
-					tracer, bifrostTraceID, err := bifrost.GetTracerFromContext(ctx)
-					if err == nil && tracer != nil && bifrostTraceID != "" {
-						tracer.CleanupStreamAccumulator(bifrostTraceID)
-					}
-				}
+				if streamResponse != nil && isFinalChunk && tracer != nil && bifrostTraceID != "" {
+					tracer.CleanupStreamAccumulator(bifrostTraceID)
+				}
 			}
plugins/governance/main.go (1)

685-725: PostLLMHook skips non‑VK usage tracking.
The early return when virtualKey == "" prevents the usage path that the comments below explicitly intend to run for empty virtual keys. This drops provider/model usage for non‑VK requests.

🛠️ Proposed fix
-	// Skip if no virtual key
-	if virtualKey == "" {
-		return result, err, nil
-	}
-
 	// Extract request type, provider, and model
 	requestType, provider, model := bifrost.GetResponseFields(result, err)
transports/bifrost-http/lib/config.go (1)

1516-1557: MCP pricing data source inconsistency.

The mcpPricingConfig populated from the config store (line 1529) is created but never used. The actual mcpcatalog.Init call (line 1551) always uses buildMCPPricingDataFromFile, ignoring the store-based pricing data when ConfigStore is available.

This appears unintentional since:

  1. Line 1529 explicitly builds pricing from store when ConfigStore != nil
  2. But the init at line 1551-1553 unconditionally uses the file-based builder
🐛 Proposed fix
 	config.ModelCatalog = pricingManager
 
 	// Initialize MCP catalog
-	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
-		PricingData: buildMCPPricingDataFromFile(ctx, configData),
-	}, logger)
+	// Use store-based pricing if available, otherwise fall back to file
+	var mcpPricingData mcpcatalog.MCPPricingData
+	if config.ConfigStore != nil {
+		mcpPricingData = mcpPricingConfig.PricingData // Already populated from store at line 1529
+	} else {
+		mcpPricingData = buildMCPPricingDataFromFile(ctx, configData)
+	}
+	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
+		PricingData: mcpPricingData,
+	}, logger)
🤖 Fix all issues with AI agents
In `@core/mcp/clientmanager.go`:
- Around line 197-201: There is a duplicated assignment of
config.IsCodeModeClient from updatedConfig (the same line appears twice); remove
the redundant assignment so config.IsCodeModeClient =
updatedConfig.IsCodeModeClient only occurs once in the update block (the code
that sets config.Headers, config.ToolsToExecute, config.ToolsToAutoExecute,
etc.), leaving the other assignments intact.
- Around line 454-461: The tool retrieval uses the long-lived ctx (longLivedCtx)
which may block indefinitely for SSE/STDIO; change the call to
retrieveExternalTools to use the connection-establishment timeout context
(initCtx) instead. Move or create initCtx (a context with timeout) into scope
before the retrieveExternalTools call (or create a short-lived context around
that call using context.WithTimeout) and pass that context to
retrieveExternalTools while ensuring you cancel it after use; keep logging
(logger.Debug/Warn) and the fallback to an empty tools map unchanged.

In `@core/mcp/codemodeexecutecode_test.go`:
- Around line 248-259: The "Truncate long result" test doesn't fail when
truncation is broken; update the test for formatResultForLog to assert expected
truncation: replace the non-failing log with assertions that len(result) is <=
the expected max (e.g., ~205 to account for quotes/ellipsis) and that the
returned string contains the truncation marker (e.g., "..." or ends with an
ellipsis/closing quote). Use t.Fatalf or t.Errorf in the "Truncate long result"
test to fail when these conditions are not met so the test fails when
formatResultForLog does not truncate correctly.

In `@core/mcp/codemodeexecutecode.go`:
- Around line 1082-1085: The doc comment for extractResultFromResponsesMessage
incorrectly describes its return values (mentions a boolean) — update the
comment to reflect the actual signature (returns (interface{}, error)); state
that it returns the extracted result (or nil) and an error if one occurred, and
that it checks for tool errors first then extracts output from
ResponsesToolMessage; reference the function name
extractResultFromResponsesMessage and the parameter type
*schemas.ResponsesMessage in the updated comment.

In `@framework/configstore/migrations.go`:
- Around line 2455-2476: The migration's single-pass map iteration causes
nondeterministic collision outcomes; change to a two-pass approach: first
iterate toolPricing and copy all unprefixed keys into updatedPricing (marking
needsUpdate if any prefixed key was stripped or any move happens) using
hasClientPrefix to detect/unprefix, then in a second pass iterate toolPricing
again and only add prefixed entries (or their unprefixed form) when the
unprefixed key does not already exist in updatedPricing; keep the existing log
messages for collisions but remove reliance on iteration order so unprefixed
keys always win (update references to updatedPricing, toolPricing,
hasClientPrefix, and needsUpdate accordingly).

In `@transports/bifrost-http/handlers/logging.go`:
- Around line 831-837: The handler currently only allows sort_by values
"timestamp" or "latency" in the pagination logic in logging.go; update the
validation in the function handling the request (where pagination.SortBy is set
from ctx.QueryArgs().Peek("sort_by")) to also accept "cost" (i.e., allow
"timestamp", "latency" or "cost") and ensure the error message from that block
reflects the allowed values to include "cost" so sorting MCP logs by cost is
permitted.

In `@ui/app/workspace/logs/page.tsx`:
- Around line 181-185: The current logic in page.tsx flips
userModifiedTimeRange.current whenever newFilters includes start_time or
end_time, but filters.tsx always spreads those keys so presence isn't enough;
change the check in the onFiltersChange handler to compare newFilters.start_time
and newFilters.end_time against the current/previous filter values (the existing
filters state used by the component) and only set userModifiedTimeRange.current
= true if either value is actually different (including changes from/into
undefined). Reference the symbols newFilters, userModifiedTimeRange, start_time,
end_time and the onFiltersChange flow used with filters.tsx to locate and
implement the comparison.

In `@ui/app/workspace/mcp-logs/page.tsx`:
- Around line 182-200: The handleDelete callback currently removes the deleted
log from state but doesn't refresh the statistic cards; after a successful
delete (after deleteLogs.unwrap() and updating setLogs/setTotalItems) call
fetchStats() to re-fetch and update the stat cards, and add fetchStats to the
useCallback dependency array so the callback uses the latest refetch function;
ensure this call happens only on success (inside the try block, after state
updates) and keep the existing error handling unchanged.
♻️ Duplicate comments (22)
examples/plugins/http-transport-only/README.md (1)

124-126: Minor wording repetition in Notes list.

docs/features/governance/virtual-keys.mdx (1)

556-576: Clarify “auth disabled/enabled” as inference-scoped.

The phrasing can still read like a global auth toggle. Consider tightening it to “auth bypassed on inference”.

✏️ Suggested wording tweak
-**When `disable_auth_on_inference: true` (auth disabled):**
+**When `disable_auth_on_inference: true` (auth bypassed on inference):**
@@
-**When `disable_auth_on_inference: false` (auth enabled):**
+**When `disable_auth_on_inference: false` (auth enforced on inference):**
examples/plugins/hello-world/main.go (1)

41-46: Fix example build: LLMPluginShortCircuit unresolved in CodeQL.
The pipeline reports schemas.LLMPluginShortCircuit is undefined at Line 41, so this example still fails to compile in the examples module. Ensure the example’s go.mod/go.work depends on a bifrost/core version that exports LLMPluginShortCircuit, or (if targeting older core versions) keep the signature on PluginShortCircuit.

#!/bin/bash
# Verify the example module's core dependency and the type definition.
fd -a 'go\.mod$' examples/plugins/hello-world -x sed -n '1,120p' {}
rg -n "bifrost/core" examples/plugins/hello-world/go.mod
rg -n "type LLMPluginShortCircuit" -g '*.go' core/schemas
core/schemas/plugin_wasm.go (1)

5-7: Doc comment still implies stream support in WASM.

Line 6 contradicts Line 7. Clarify that WASM short-circuit supports response/error only.

✏️ Suggested doc fix
-// LLMPluginShortCircuit represents a plugin's decision to short-circuit the normal flow.
-// It can contain either a response (success short-circuit), a stream (streaming short-circuit), or an error (error short-circuit).
-// Streams are not supported in WASM plugins.
+// LLMPluginShortCircuit represents a plugin's decision to short-circuit the normal flow.
+// In WASM plugins, it can contain either a response (success short-circuit) or an error (error short-circuit).
+// Streams are not supported in WASM plugins.
examples/plugins/multi-interface/Makefile (1)

1-1: Add all/test phony targets to satisfy checkmake.

This mirrors the prior review note; still applies here.

🛠️ Suggested update
-.PHONY: build clean
+.PHONY: build clean all test
+
+all: build
+
+test:
+	`@echo` "No tests configured for this example"
ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx (1)

128-132: Latency display hides valid 0ms values.

Same issue noted previously; still present.

🛠️ Suggested fix
-									.add(log.latency || 0, "ms")
+									.add(log.latency ?? 0, "ms")
 									.format("YYYY-MM-DD HH:mm:ss A")}
 							/>
-							<LogEntryDetailsView className="w-full" label="Latency" value={log.latency ? `${log.latency.toFixed(2)}ms` : "NA"} />
+							<LogEntryDetailsView
+								className="w-full"
+								label="Latency"
+								value={typeof log.latency === "number" ? `${log.latency.toFixed(2)}ms` : "NA"}
+							/>
docs/openapi/paths/management/logging.yaml (1)

335-340: Status parameter description vs enum mismatch.

The description allows comma-separated values while the enum enforces a single value for both MCP logs endpoints.

Also applies to: 459-464

ui/app/workspace/mcp-logs/page.tsx (1)

304-327: Guard stats updates against repeated terminal updates.

Terminal updates can arrive multiple times and the current logic increments on every update. This still risks double-counting.

examples/plugins/mcp-only/main.go (1)

114-118: Use the canonical request-id context key.

The canonical key constant is schemas.BifrostContextKeyRequestID; the current key will not resolve.

🛠️ Suggested fix
-		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKey("request_id")))
+		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKeyRequestID))
core/mcp/codemodeexecutecode.go (1)

851-879: Handle post-hook errors in short-circuit paths.

Errors returned by RunMCPPostHooks are still ignored for short-circuit responses/errors, which can mask plugin failures.

🛠️ Suggested fix
-			finalResp, _ := pipeline.RunMCPPostHooks(nestedCtx, shortCircuit.Response, nil, preCount)
+			finalResp, finalErr := pipeline.RunMCPPostHooks(nestedCtx, shortCircuit.Response, nil, preCount)
+			if finalErr != nil {
+				if finalErr.Error != nil {
+					return nil, fmt.Errorf("%s", finalErr.Error.Message)
+				}
+				return nil, fmt.Errorf("plugin post-hooks returned error")
+			}
 			if finalResp != nil {
@@
-		if shortCircuit.Error != nil {
-			pipeline.RunMCPPostHooks(nestedCtx, nil, shortCircuit.Error, preCount)
+		if shortCircuit.Error != nil {
+			_, finalErr := pipeline.RunMCPPostHooks(nestedCtx, nil, shortCircuit.Error, preCount)
+			if finalErr != nil {
+				if finalErr.Error != nil {
+					return nil, fmt.Errorf("%s", finalErr.Error.Message)
+				}
+				return nil, fmt.Errorf("plugin post-hooks returned error")
+			}
 			if shortCircuit.Error.Error != nil {
 				return nil, fmt.Errorf("%s", shortCircuit.Error.Error.Message)
 			}
 			return nil, fmt.Errorf("plugin short-circuit error")
 		}
framework/configstore/rdb.go (1)

857-905: Headers JSON serialization still mismatches table semantics.

headers_json is built from map[string]EnvVar, which diverges from TableMCPClient.BeforeSave (string map) and can store resolved secrets. Align serialization to map[string]string.

🛠️ Suggested fix
 		if clientConfigCopy.Headers == nil {
 			clientConfigCopy.Headers = map[string]schemas.EnvVar{}
 		}
-		headersJSON, err := json.Marshal(clientConfigCopy.Headers)
+		headersToSerialize := make(map[string]string, len(clientConfigCopy.Headers))
+		for key, value := range clientConfigCopy.Headers {
+			if value.IsFromEnv() {
+				headersToSerialize[key] = value.EnvVar
+			} else {
+				headersToSerialize[key] = value.GetValue()
+			}
+		}
+		headersJSON, err := json.Marshal(headersToSerialize)
 		if err != nil {
 			return fmt.Errorf("failed to marshal headers: %w", err)
 		}
transports/bifrost-http/handlers/plugins.go (1)

345-350: Error message should say "failed to stop" instead of "failed to reload".

When disabling a plugin, the error message should reflect the stop operation rather than reload.

Suggested fix
 	} else {
 		ctx.SetUserValue("isDisabled", true)
 		if err := h.pluginsLoader.RemovePlugin(ctx, name); err != nil {
-			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin updated in database but failed to reload: %v", err))
+			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin updated in database but failed to stop: %v", err))
 			return
 		}
 	}
examples/plugins/multi-interface/main.go (3)

37-39: Race condition on requestCount remains unaddressed.

This variable is read/written from concurrent hook invocations without synchronization. Use atomic.Int64 or a mutex.


107-109: Nil check needed for req.Headers before writing.

req.Headers may be nil; writing to it will panic.


129-149: Guard resp and resp.Headers before mutation.

The post hook may receive a nil response or nil headers map, causing a panic on write.

transports/bifrost-http/lib/config_test.go (1)

472-518: Preserve stable IDs when updating mock MCP clients.

UpdateMCPClientConfig rebuilds entries and can drop ClientID/ID, which risks stale or duplicate in‑memory state for tests that read back via GetMCPConfig.

🔧 Suggested fix
-	// Update the in-memory state to ensure GetMCPConfig returns updated data
-	for i := range m.mcpConfig.ClientConfigs {
-		if m.mcpConfig.ClientConfigs[i].ClientID == id {
-			// Found the entry, update it with the new config
-			m.mcpConfig.ClientConfigs[i] = tables.TableMCPClient{
-				ClientID:           clientConfig.ClientID,
-				Name:               clientConfig.Name,
-				IsCodeModeClient:   clientConfig.IsCodeModeClient,
-				ConnectionType:     clientConfig.ConnectionType,
-				ConnectionString:   clientConfig.ConnectionString,
-				StdioConfig:        clientConfig.StdioConfig,
-				Headers:            clientConfig.Headers,
-				ToolsToExecute:     clientConfig.ToolsToExecute,
-				ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-			}
-			return nil
-		}
-	}
-	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
-	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, tables.TableMCPClient{
-		ClientID:           clientConfig.ClientID,
-		Name:               clientConfig.Name,
-		IsCodeModeClient:   clientConfig.IsCodeModeClient,
-		ConnectionType:     clientConfig.ConnectionType,
-		ConnectionString:   clientConfig.ConnectionString,
-		StdioConfig:        clientConfig.StdioConfig,
-		Headers:            clientConfig.Headers,
-		ToolsToExecute:     clientConfig.ToolsToExecute,
-		ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-	})
+	updated := clientConfig
+	if updated.ClientID == "" {
+		updated.ClientID = id
+	}
+
+	// Update the in-memory state to ensure GetMCPConfig returns updated data
+	for i := range m.mcpConfig.ClientConfigs {
+		if m.mcpConfig.ClientConfigs[i].ClientID == id {
+			updated.ID = m.mcpConfig.ClientConfigs[i].ID
+			m.mcpConfig.ClientConfigs[i] = updated
+			return nil
+		}
+	}
+	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
+	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, updated)
transports/bifrost-http/server/server.go (1)

647-734: Call Cleanup on replaced/removed plugins to avoid leaks.
Reload and removal swap plugin instances without invoking Cleanup(), so background goroutines/resources can leak.

🔧 Proposed fix
 func (s *BifrostHTTPServer) ReloadPlugin(ctx context.Context, name string, path *string, pluginConfig any) error {
 	logger.Debug("reloading plugin %s", name)
+	oldPlugin, _ := s.Config.FindPluginByName(name)
 
 	// 1. Instantiate new version
 	plugin, err := InstantiatePlugin(ctx, name, path, pluginConfig, s.Config)
@@
 	// 2. Register (replaces old version atomically)
 	if err := s.Config.RegisterPlugin(plugin); err != nil {
 		return updateError("registering", err)
 	}
+	if oldPlugin != nil {
+		if err := oldPlugin.Cleanup(); err != nil {
+			logger.Warn("failed to cleanup old plugin %s: %v", name, err)
+		}
+	}
@@
 func (s *BifrostHTTPServer) RemovePlugin(ctx context.Context, displayName string) error {
@@
-	if err := s.Config.UnregisterPlugin(name); err != nil {
+	oldPlugin, _ := s.Config.FindPluginByName(name)
+	if err := s.Config.UnregisterPlugin(name); err != nil {
 		return err
 	}
+	if oldPlugin != nil {
+		if err := oldPlugin.Cleanup(); err != nil {
+			logger.Warn("failed to cleanup plugin %s: %v", name, err)
+		}
+	}
docs/openapi/schemas/management/logging.yaml (3)

112-157: MCPToolLogEntry is missing VK fields and error_details typing.
Keep schema aligned with backend fields and error shape.

🔧 Suggested schema update
     server_label:
       type: string
       description: Label of the MCP server that provided the tool
+    virtual_key_id:
+      type: string
+      description: Virtual key ID used for the tool execution
+    virtual_key_name:
+      type: string
+      nullable: true
+      description: Virtual key name
@@
-    error_details:
-      type: object
-      additionalProperties: true
-      description: Error details if execution failed
+    error_details:
+      $ref: '../../schemas/inference/common.yaml#/BifrostError'

159-200: Add virtual_key_ids to MCPToolLogSearchFilters.

🔧 Suggested schema addition
     llm_request_ids:
       type: array
       items:
         type: string
       description: Filter by linked LLM request IDs
+    virtual_key_ids:
+      type: array
+      items:
+        type: string
+      description: Filter by virtual key IDs

249-277: Clarify redaction semantics for MCPLogsFilterDataResponse.virtual_keys.value.

🔧 Suggested doc clarification
           value:
             type: string
-            description: Virtual key value (redacted if applicable)
+            description: Virtual key value (always redacted; empty string)
core/bifrost.go (2)

290-291: ReloadConfig comment still mentions account updates.

Line 290 says account is updated, but ReloadConfig doesn’t change it—align the comment or logic.


3373-3373: Post-hook plugin count can drift on hot reloads.

Prefer the pre-hook count snapshot or pipeline.llmPlugins length so post-hooks can’t be skipped if plugins reload mid-flight.

Also applies to: 3593-3593, 3849-3849

🧹 Nitpick comments (15)
plugins/governance/utils.go (1)

36-44: Same case sensitivity issue for x-api-key and x-goog-api-key headers.

The prefix check lowercases the key value, but since you're returning the original xAPIKey/xGoogleAPIKey (not a lowercased version), this code is actually correct for these cases. However, ensure VirtualKeyPrefix itself is consistently lowercase, or use strings.ToLower(VirtualKeyPrefix) for defensive coding.

For consistency with the Authorization header fix and defensive coding:

♻️ Suggested defensive fix
 	xAPIKey := req.CaseInsensitiveHeaderLookup("x-api-key")
-	if xAPIKey != "" && strings.HasPrefix(strings.ToLower(xAPIKey), VirtualKeyPrefix) {
+	if xAPIKey != "" && strings.HasPrefix(strings.ToLower(xAPIKey), strings.ToLower(VirtualKeyPrefix)) {
 		return bifrost.Ptr(xAPIKey)
 	}
 	// Checking x-goog-api-key header
 	xGoogleAPIKey := req.CaseInsensitiveHeaderLookup("x-goog-api-key")
-	if xGoogleAPIKey != "" && strings.HasPrefix(strings.ToLower(xGoogleAPIKey), VirtualKeyPrefix) {
+	if xGoogleAPIKey != "" && strings.HasPrefix(strings.ToLower(xGoogleAPIKey), strings.ToLower(VirtualKeyPrefix)) {
 		return bifrost.Ptr(xGoogleAPIKey)
 	}
plugins/maxim/main.go (1)

399-428: Prefer bifrost.Ptr for string pointers.

Use the project-standard pointer helper for consistency and readability. Based on learnings, prefer bifrost.Ptr(...) for pointer creation.

♻️ Proposed update
-	if sessionID != "" {
-		traceConfig.SessionId = &sessionID
-	}
+	if sessionID != "" {
+		traceConfig.SessionId = bifrost.Ptr(sessionID)
+	}
@@
-	if generationName != "" {
-		generationConfig.Name = &generationName
-	}
+	if generationName != "" {
+		generationConfig.Name = bifrost.Ptr(generationName)
+	}
transports/bifrost-http/server/utils.go (1)

32-68: Consider honoring XDG_CONFIG_HOME on Unix.

This is a common convention for config paths and improves OS integration.

♻️ Suggested tweak
 	default:
 		// Linux, macOS and other Unix-like systems: ~/.config/bifrost
-		if homeDir, err := os.UserHomeDir(); err == nil {
-			configDir = filepath.Join(homeDir, ".config", "bifrost")
-		}
+		if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
+			configDir = filepath.Join(xdg, "bifrost")
+		} else if homeDir, err := os.UserHomeDir(); err == nil {
+			configDir = filepath.Join(homeDir, ".config", "bifrost")
+		}
 	}
examples/plugins/llm-only/main.go (1)

70-103: Use schema constants + bifrost.Ptr for injected messages.

Keeps examples aligned with repo conventions and avoids raw role strings. Based on learnings, prefer bifrost.Ptr(...) for pointer creation.

♻️ Proposed change
-import (
-	"fmt"
-
-	"github.com/maximhq/bifrost/core/schemas"
-)
+import (
+	"fmt"
+
+	bifrost "github.com/maximhq/bifrost/core"
+	"github.com/maximhq/bifrost/core/schemas"
+)
@@
 	systemMsg := schemas.ChatMessage{
-		Role:    "system",
-		Content: &schemas.ChatMessageContent{ContentStr: &pluginConfig.SystemMessageText},
+		Role:    schemas.ChatMessageRoleSystem,
+		Content: &schemas.ChatMessageContent{ContentStr: bifrost.Ptr(pluginConfig.SystemMessageText)},
 	}
plugins/logging/main.go (1)

713-812: Prefer bifrost.Ptr for pointer fields.
At Line 781+, these assignments use &value. The repo convention is to use bifrost.Ptr(...) for pointer creation to keep style consistent. Based on learnings, prefer bifrost.Ptr(...) here.

♻️ Suggested update
-		if parentRequestID != "" {
-			entry.LLMRequestID = &parentRequestID
-		}
+		if parentRequestID != "" {
+			entry.LLMRequestID = bifrost.Ptr(parentRequestID)
+		}
@@
-		if virtualKeyID != "" {
-			entry.VirtualKeyID = &virtualKeyID
-		}
-		if virtualKeyName != "" {
-			entry.VirtualKeyName = &virtualKeyName
-		}
+		if virtualKeyID != "" {
+			entry.VirtualKeyID = bifrost.Ptr(virtualKeyID)
+		}
+		if virtualKeyName != "" {
+			entry.VirtualKeyName = bifrost.Ptr(virtualKeyName)
+		}
examples/plugins/mcp-only/Makefile (1)

1-12: Consider adding all and test phony targets to satisfy checkmake.
The linter warns about missing required phony targets. If you want to keep checkmake quiet, add lightweight aliases.

♻️ Suggested Makefile targets
-.PHONY: build clean
+.PHONY: all build clean test
+
+all: build
+
+test:
+	`@echo` "No tests defined"
ui/app/workspace/mcp-logs/page.tsx (1)

456-498: Avoid streaming-awareness props in UI child components.

Passing isSocketConnected / liveEnabled into view components bakes in streaming state. Prefer deriving any display state in this page and keep child components data-driven. Based on learnings, avoid streaming-aware UI props in ui/ components.

examples/plugins/mcp-only/main.go (1)

3-7: Prefer bifrost.Ptr for pointer creation.

Consistent pointer helpers make the codebase more uniform.

♻️ Suggested fix
 import (
 	"fmt"

+	bifrost "github.com/maximhq/bifrost/core"
 	"github.com/maximhq/bifrost/core/schemas"
 )
@@
 					Response: &schemas.BifrostMCPResponse{
 						ResponsesMessage: &schemas.ResponsesMessage{
 							ResponsesToolMessage: &schemas.ResponsesToolMessage{
-								Error: &errorMsg,
+								Error: bifrost.Ptr(errorMsg),
 							},
 						},
 					},

Based on learnings, prefer bifrost.Ptr over address-of for pointer creation.

Also applies to: 100-106

core/mcp/utils.go (2)

192-198: Update comment examples to match the new "-" separator.

The comment on line 194 still shows "calculator/add" as an example, but tool names are now stored with the "-" separator (e.g., "calculator-add").

Suggested fix
 		// Strip client prefix from tool name before checking
 		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")
 		unprefixedToolName := stripClientPrefix(toolName, config.Name)

224-230: Update comment examples to match the new "-" separator.

Same issue as above - the comment references "calculator/add" but should be "calculator-add".

Suggested fix
 		// Strip client prefix from tool name before checking
 		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")
 		unprefixedToolName := stripClientPrefix(toolName, config.Name)
transports/bifrost-http/handlers/plugins.go (1)

74-92: Consider sorting plugins in the nil-path for consistent output.

The non-nil path (lines 129-131) sorts finalPlugins by Name for deterministic ordering, but the nil-path here doesn't sort. This could lead to inconsistent API responses depending on whether configStore is enabled.

Suggested fix
 		for name, pluginStatus := range pluginStatus {
 			finalPlugins = append(finalPlugins, PluginResponse{
 				Name:       pluginStatus.Name,
 				ActualName: name,
 				Enabled:    true,
 				Config:     map[string]any{},
 				IsCustom:   true,
 				Path:       nil,
 				Status:     pluginStatus,
 			})
 		}
+		// Sort plugins by Name for deterministic ordering
+		sort.Slice(finalPlugins, func(i, j int) bool {
+			return finalPlugins[i].Name < finalPlugins[j].Name
+		})
 		SendJSON(ctx, map[string]any{
core/mcp/mcp.go (1)

83-101: Type assertion failure returns nil without logging - consider adding a warning.

If config.PluginPipelineProvider() returns a non-nil value that doesn't implement PluginPipeline, the type assertion silently fails and returns nil. This could make debugging difficult if the pipeline isn't being applied.

Suggested fix
 	if config.PluginPipelineProvider != nil && config.ReleasePluginPipeline != nil {
 		pluginPipelineProvider = func() PluginPipeline {
 			if pipeline := config.PluginPipelineProvider(); pipeline != nil {
 				if pp, ok := pipeline.(PluginPipeline); ok {
 					return pp
+				} else {
+					logger.Warn(fmt.Sprintf("%s Plugin pipeline provider returned incompatible type: %T", MCPLogPrefix, pipeline))
 				}
 			}
 			return nil
 		}
transports/bifrost-http/server/plugins.go (1)

146-164: Consider: Consistent error handling for optional built-in plugins.

At line 151, registerPluginWithStatus is called without capturing the error for the logging plugin, while telemetry (line 142) propagates errors. If logging or governance plugin initialization fails, the error is silently ignored.

If this is intentional (non-critical plugins), consider adding a comment explaining why. If not, propagate errors or at least log them.

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

224-247: Prefer bifrost.Ptr for pointer creation to align with repo style.
This keeps pointer construction consistent across the codebase.

♻️ Proposed refactor
-			toolMessage, err := h.toolManager.ExecuteChatMCPTool(ctx, &toolCall)
+			toolMessage, err := h.toolManager.ExecuteChatMCPTool(ctx, bifrost.Ptr(toolCall))
Based on learnings, maintain pointer-creation consistency with `bifrost.Ptr()`.
core/bifrost.go (1)

139-153: Defensive copy of plugin slices before storing.

Storing pointers to the config slices allows external mutation/races. ReloadConfig already copies; consider doing the same in Init.

♻️ Suggested update
-	bifrost.llmPlugins.Store(&config.LLMPlugins)
-	bifrost.mcpPlugins.Store(&config.MCPPlugins)
+	llmPluginsCopy := append([]schemas.LLMPlugin(nil), config.LLMPlugins...)
+	mcpPluginsCopy := append([]schemas.MCPPlugin(nil), config.MCPPlugins...)
+	bifrost.llmPlugins.Store(Ptr(llmPluginsCopy))
+	bifrost.mcpPlugins.Store(Ptr(mcpPluginsCopy))

Based on learnings, prefer Ptr(...) for pointer creation.

@Pratham-Mishra04 Pratham-Mishra04 force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 08a8856 to 3289c79 Compare January 23, 2026 20:33
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: 7

Caution

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

⚠️ Outside diff range comments (4)
transports/bifrost-http/lib/config.go (1)

1513-1557: MCPCatalog init ignores store pricing when ConfigStore is enabled.
When config.ConfigStore != nil, you still initialize the MCP catalog with file pricing data, which can be empty or stale compared to the store. This drops catalog pricing after restart.

🔧 Proposed fix
 func initFrameworkConfigFromFile(ctx context.Context, config *Config, configData *ConfigData) {
 	pricingConfig := &modelcatalog.Config{}
-	mcpPricingConfig := &mcpcatalog.Config{}
+	mcpPricingData := buildMCPPricingDataFromFile(ctx, configData)
 	if config.ConfigStore != nil {
 		frameworkConfig, err := config.ConfigStore.GetFrameworkConfig(ctx)
 		if err != nil {
 			logger.Warn("failed to get framework config from store: %v", err)
 		}
 		if frameworkConfig != nil && frameworkConfig.PricingURL != nil {
 			pricingConfig.PricingURL = frameworkConfig.PricingURL
 		}
 		if frameworkConfig != nil && frameworkConfig.PricingSyncInterval != nil {
 			syncDuration := time.Duration(*frameworkConfig.PricingSyncInterval) * time.Second
 			pricingConfig.PricingSyncInterval = &syncDuration
 		}
-		mcpPricingConfig.PricingData = buildMCPPricingDataFromStore(ctx, config.ConfigStore)
+		mcpPricingData = buildMCPPricingDataFromStore(ctx, config.ConfigStore)
 	} else if configData.FrameworkConfig != nil && configData.FrameworkConfig.Pricing != nil {
 		pricingConfig.PricingURL = configData.FrameworkConfig.Pricing.PricingURL
 		syncDuration := time.Duration(*configData.FrameworkConfig.Pricing.PricingSyncInterval) * time.Second
 		pricingConfig.PricingSyncInterval = &syncDuration
 	}
@@
 	// Initialize MCP catalog
 	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
-		PricingData: buildMCPPricingDataFromFile(ctx, configData),
+		PricingData: mcpPricingData,
 	}, logger)
plugins/semanticcache/main.go (1)

356-360: Update PreLLMHook return docs to match the new short‑circuit type.

The comment still says it returns *schemas.BifrostResponse, but the signature now returns *schemas.LLMPluginShortCircuit.

📝 Suggested update
-//   - *schemas.BifrostResponse: Cached response if found, nil otherwise
+//   - *schemas.LLMPluginShortCircuit: Cached response short-circuit if found, nil otherwise
plugins/mocker/main.go (2)

866-869: Guard model override to avoid panic on Responses requests.
On schemas.ResponsesRequest, mockResponse.ChatResponse is nil, so Line 868 will panic if content.Model is set. Add a nil-check or set the model on the concrete response type you built.

🐛 Suggested fix
-	// Override model if specified
-	if content.Model != nil {
-		mockResponse.ChatResponse.Model = *content.Model
-	}
+	// Override model if specified (only when response type supports it)
+	if content.Model != nil && mockResponse.ChatResponse != nil {
+		mockResponse.ChatResponse.Model = *content.Model
+	}
+	// If ResponsesResponse supports a model field, set it there instead.

1001-1043: Default success should respect the request type.
If the incoming request is schemas.ResponsesRequest, this path still builds a ChatResponse, which yields a mismatched API shape. Consider constructing a ResponsesResponse (similar to generateSuccessShortCircuit) or passing through for unsupported request types.

🐛 Suggested fix (sketch)
 case DefaultBehaviorSuccess:
-	finishReason := "stop"
+	if req.RequestType == schemas.ResponsesRequest {
+		// Build a ResponsesResponse equivalent to the chat path
+		return req, &schemas.LLMPluginShortCircuit{
+			Response: &schemas.BifrostResponse{
+				ResponsesResponse: &schemas.BifrostResponsesResponse{
+					// populate fields ...
+				},
+			},
+		}, nil
+	}
+	finishReason := "stop"
 	return req, &schemas.LLMPluginShortCircuit{
 		Response: &schemas.BifrostResponse{
 			ChatResponse: &schemas.BifrostChatResponse{
🤖 Fix all issues with AI agents
In `@docs/openapi/schemas/management/mcp.yaml`:
- Around line 49-66: MCPClientCreateRequestBase currently references
MCPConnectionType for connection_type but MCPClientCreateRequest's
oneOf/discriminator only allows http/sse/stdio, causing a validation mismatch
for the inprocess value; either restrict the connection_type enum on
MCPClientCreateRequestBase to only the allowed create-time values (http, sse,
stdio) or add an explicit MCPClientCreateRequest variant that supports
connection_type: inprocess (and update the oneOf/discriminator to include that
variant) so the OpenAPI schema is consistent between MCPClientCreateRequestBase,
MCPConnectionType, and MCPClientCreateRequest.

In `@examples/plugins/llm-only/Makefile`:
- Line 1: The Makefile currently only declares .PHONY: build clean and is
missing standard phony targets required by checkmake; update the .PHONY line to
include all and test (e.g. .PHONY: all test build clean) and add minimal no-op
targets named all and test (or have all depend on build and test run a simple
sanity command) so the Makefile defines those targets for linters and CI; modify
the Makefile's .PHONY declaration and add the corresponding all and test target
stubs.

In `@framework/configstore/migrations.go`:
- Around line 2388-2393: The deduplication branch skips duplicate unprefixed
tools but never sets needsUpdate, so the cleaned tool list isn't persisted; in
the loop that checks seenTools[tool] (the duplicate-detection block around
seenTools, tool and clientName), set needsUpdate = true when you continue on a
duplicate (or otherwise mark the client as modified) so the deduped list for
that client is saved; update the same pattern at the other occurrence around
lines with the seenTools check to ensure the change is persisted.

In `@framework/mcpcatalog/main.go`:
- Around line 85-90: Cleanup currently sets mc.pricingData = nil which will
cause panics if UpdatePricingData or DeletePricingData write to the map
afterwards; change Cleanup (keeping the mc.mu lock/unlock) to reinitialize
mc.pricingData to an empty map of the same type as declared (instead of nil), or
alternatively add a safe initializer at the top of
UpdatePricingData/DeletePricingData that creates mc.pricingData if nil;
reference MCPCatalog.Cleanup, the mc.mu mutex, and pricingData and
UpdatePricingData/DeletePricingData when making the change.

In `@transports/bifrost-http/handlers/plugins.go`:
- Around line 219-224: The error message is misleading because the code calls
h.pluginsLoader.ReloadPlugin before persisting the plugin; update the log and
response text in the ReloadPlugin error path (inside the if request.Enabled
block where h.pluginsLoader.ReloadPlugin is called) to reflect that the plugin
failed to load/enable rather than being "created in database" (e.g., use a
message like "failed to load/enable plugin" or "plugin failed to load into
runtime" in both logger.Error and SendError).

In `@transports/bifrost-http/server/server.go`:
- Around line 183-186: The method BifrostHTTPServer.ExecuteChatMCPTool must
guard against a nil *schemas.ChatAssistantMessageToolCall to avoid downstream
panics: add an explicit nil check at the start of ExecuteChatMCPTool and, if
toolCall is nil, return (nil, a clear *schemas.BifrostError) describing the
bad/missing input instead of calling s.Client.ExecuteChatMCPTool; this ensures
BifrostHTTPServer.ExecuteChatMCPTool fails fast and returns a meaningful error
when toolCall is nil.

In `@ui/app/workspace/plugins/views/pluginsView.tsx`:
- Line 205: Remove the stray whitespace/tab inside the opening JSX tag for the
FormLabel component in pluginsView.tsx: replace
"<FormLabel	>Enabled</FormLabel>" with a standard
"<FormLabel>Enabled</FormLabel>" so the tag is correctly formatted; locate the
occurrence by searching for the FormLabel component in the pluginsView.tsx view
and update the opening tag to eliminate the extraneous whitespace.
♻️ Duplicate comments (37)
examples/plugins/http-transport-only/README.md (1)

124-126: Reword to avoid repeated “only.”
This is the same readability nit flagged previously; a small rephrase would improve flow.

ui/app/workspace/config/views/securityView.tsx (1)

263-267: LGTM!

The external link now correctly includes rel="noopener noreferrer" to prevent reverse-tabnabbing. The updated copy clearly directs users to documentation for header details.

ui/app/workspace/logs/page.tsx (2)

217-237: Chart zoom handlers don't update the user-modified tracking ref.

handleTimeRangeChange (chart zoom) and handleResetZoom should interact with userModifiedTimeRange to properly track user intent:

  • Zooming in should mark the range as user-modified
  • Resetting should clear the flag and re-baseline initialDefaults
🛠️ Suggested fix
 const handleTimeRangeChange = useCallback(
 	(startTime: number, endTime: number) => {
+		userModifiedTimeRange.current = true;
 		setUrlState({
 			start_time: startTime,
 			end_time: endTime,
 			offset: 0,
 		});
 	},
 	[setUrlState],
 );

 const handleResetZoom = useCallback(() => {
 	const now = Math.floor(Date.now() / 1000);
 	const twentyFourHoursAgo = now - 24 * 60 * 60;
+	userModifiedTimeRange.current = false;
+	initialDefaults.current = { startTime: twentyFourHoursAgo, endTime: now };
 	setUrlState({
 		start_time: twentyFourHoursAgo,
 		end_time: now,
 		offset: 0,
 	});
 }, [setUrlState]);

181-185: Time range modification detection still triggers on any filter change.

The current check newFilters.start_time !== undefined || newFilters.end_time !== undefined will always be true when filters.tsx spreads the full filter object. This causes userModifiedTimeRange to flip on non-time-range filter changes (e.g., content search).

Compare against previous values to detect actual changes:

🛠️ Suggested fix
-		if (newFilters.start_time !== undefined || newFilters.end_time !== undefined) {
-			userModifiedTimeRange.current = true;
-		}
+		const newStartTime = newFilters.start_time ? dateUtils.toUnixTimestamp(new Date(newFilters.start_time)) : undefined;
+		const newEndTime = newFilters.end_time ? dateUtils.toUnixTimestamp(new Date(newFilters.end_time)) : undefined;
+		if (newStartTime !== urlState.start_time || newEndTime !== urlState.end_time) {
+			userModifiedTimeRange.current = true;
+		}
core/mcp/codemodeexecutecode_test.go (1)

248-259: Truncation test has a weak assertion that doesn't fail when truncation is broken.

The test only logs when len(result) > 200 but doesn't fail if truncation doesn't work correctly. If len(result) <= 200, the test passes silently without verifying expected behavior.

🧪 Suggested fix to add proper assertion
 	t.Run("Truncate long result", func(t *testing.T) {
 		longString := ""
 		for i := 0; i < 300; i++ {
 			longString += "a"
 		}
 
 		result := formatResultForLog(longString)
-		if len(result) > 200 {
-			// Should be truncated to around 200 chars (plus quotes and ellipsis)
-			t.Logf("Result length: %d (truncated as expected)", len(result))
-		}
+		// Input is 300 chars, result should be truncated
+		// Untruncated would be 300 + 2 quotes = 302 chars
+		if len(result) >= 302 {
+			t.Errorf("Expected result to be truncated, got length %d", len(result))
+		}
+		// Verify truncation indicator is present
+		if !strings.Contains(result, "...") && len(result) < 302 {
+			t.Logf("Result length: %d (truncated)", len(result))
+		}
 	})
examples/plugins/http-transport-only/main.go (3)

95-99: Guard non-positive rate_window values to avoid bypassing limits.

If rate_window is 0/negative, the window math becomes invalid. Consider clamping to a sane default.


176-178: Guard req.Headers before mutation.

req.Headers can be nil; writing to it will panic.

🐛 Proposed fix
 	// Example 4: Add custom headers
+	if req.Headers == nil {
+		req.Headers = map[string]string{}
+	}
 	req.Headers["X-Plugin-Processed"] = "true"

189-214: Guard resp / resp.Headers before mutation.

Post hooks can receive a nil response or nil headers map.

🐛 Proposed fix
 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	fmt.Println("[HTTP-Transport-Only Plugin] HTTPTransportPostHook called")
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = map[string]string{}
+	}
 
 	// Calculate request duration
transports/bifrost-http/lib/config_test.go (1)

472-518: Preserve ClientID and stable ID when updating MCP clients.

UpdateMCPClientConfig rebuilds entries from clientConfig and can drop ClientID (if empty) and the existing internal ID, which makes GetMCPConfig inconsistent for tests that rely on stable identifiers. Consider normalizing ClientID from id and carrying over the existing ID.

🔧 Proposed fix
-	// Update the in-memory state to ensure GetMCPConfig returns updated data
-	for i := range m.mcpConfig.ClientConfigs {
-		if m.mcpConfig.ClientConfigs[i].ClientID == id {
-			// Found the entry, update it with the new config
-			m.mcpConfig.ClientConfigs[i] = tables.TableMCPClient{
-				ClientID:           clientConfig.ClientID,
-				Name:               clientConfig.Name,
-				IsCodeModeClient:   clientConfig.IsCodeModeClient,
-				ConnectionType:     clientConfig.ConnectionType,
-				ConnectionString:   clientConfig.ConnectionString,
-				StdioConfig:        clientConfig.StdioConfig,
-				Headers:            clientConfig.Headers,
-				ToolsToExecute:     clientConfig.ToolsToExecute,
-				ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-			}
-			return nil
-		}
-	}
-	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
-	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, tables.TableMCPClient{
-		ClientID:           clientConfig.ClientID,
-		Name:               clientConfig.Name,
-		IsCodeModeClient:   clientConfig.IsCodeModeClient,
-		ConnectionType:     clientConfig.ConnectionType,
-		ConnectionString:   clientConfig.ConnectionString,
-		StdioConfig:        clientConfig.StdioConfig,
-		Headers:            clientConfig.Headers,
-		ToolsToExecute:     clientConfig.ToolsToExecute,
-		ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-	})
+	updated := clientConfig
+	if updated.ClientID == "" {
+		updated.ClientID = id
+	}
+
+	// Update the in-memory state to ensure GetMCPConfig returns updated data
+	for i := range m.mcpConfig.ClientConfigs {
+		if m.mcpConfig.ClientConfigs[i].ClientID == id {
+			// Preserve the stable internal ID if the caller didn't set it
+			if updated.ID == 0 {
+				updated.ID = m.mcpConfig.ClientConfigs[i].ID
+			}
+			m.mcpConfig.ClientConfigs[i] = updated
+			return nil
+		}
+	}
+	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
+	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, updated)
examples/plugins/multi-interface/Makefile (1)

1-12: Consider adding all and test phony targets.

Static analysis flags missing all and test targets. While not critical for an example, adding them improves consistency with other Makefiles in the repository.

🔧 Proposed addition
-.PHONY: build clean
+.PHONY: all build clean test
+
+all: build
+
+test:
+	`@echo` "No tests configured for this example"

 build:
examples/plugins/multi-interface/main.go (3)

37-39: Race condition on requestCount - use atomic operations.

requestCount is incremented in HTTPTransportPreHook and read in multiple locations without synchronization. Since hooks run concurrently, this can cause data races.

🔧 Proposed fix
 import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"sync/atomic"
 	"time"

 	"github.com/maximhq/bifrost/core/schemas"
 )

-	requestCount int64
+	requestCount atomic.Int64

Then update usages:

  • Line 108: current := requestCount.Add(1)
  • Line 177: requestCount.Load()
  • Line 309: requestCount.Load()

106-110: Guard against nil req.Headers before writing.

req.Headers may be nil; writing to it directly will panic.

🔧 Proposed fix
 	if pluginConfig.TrackRequests {
+		if req.Headers == nil {
+			req.Headers = map[string]string{}
+		}
 		requestCount++
 		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
 	}

129-133: Guard against nil resp and resp.Headers in post hook.

HTTPTransportPostHook writes to resp.Headers without checking if resp or resp.Headers is nil, which can cause a panic.

🔧 Proposed fix
 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	if !pluginConfig.EnableHTTPHooks {
 		return nil
 	}
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = map[string]string{}
+	}
plugins/governance/main.go (1)

228-233: Inconsistent warning message for modelCatalog in InitFromStore.

Line 229 says "all cost calculations will be skipped" but Init (line 135) says "all LLM cost calculations will be skipped". These should be consistent for clarity.

🔧 Suggested fix
 if modelCatalog == nil {
-    logger.Warn("governance plugin requires model catalog to calculate cost, all cost calculations will be skipped.")
+    logger.Warn("governance plugin requires model catalog to calculate cost, all LLM cost calculations will be skipped.")
 }
core/mcp/clientmanager.go (2)

454-461: Tool retrieval may hang indefinitely for SSE/STDIO connections.

For SSE/STDIO connections, ctx at line 455 is the long-lived context without a timeout. If the MCP server doesn't respond to the tool listing request, this call could block indefinitely.

Consider using initCtx (the timeout context) for tool retrieval, as this is part of the connection establishment phase.


197-201: Duplicate IsCodeModeClient assignment.

Line 197 assigns config.IsCodeModeClient = updatedConfig.IsCodeModeClient, and line 201 repeats the identical assignment.

🔧 Suggested fix
 	config.Name = updatedConfig.Name
 	config.IsCodeModeClient = updatedConfig.IsCodeModeClient
 	config.Headers = updatedConfig.Headers
 	config.ToolsToExecute = updatedConfig.ToolsToExecute
 	config.ToolsToAutoExecute = updatedConfig.ToolsToAutoExecute
-	config.IsCodeModeClient = updatedConfig.IsCodeModeClient
ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx (1)

128-132: Latency display hides valid 0ms values.

Line 132 uses log.latency ? ... : "NA", which treats a latency of 0 as falsy and renders "NA". A latency of 0ms is a valid value that should be displayed.

🔧 Suggested fix
-							<LogEntryDetailsView className="w-full" label="Latency" value={log.latency ? `${log.latency.toFixed(2)}ms` : "NA"} />
+							<LogEntryDetailsView
+								className="w-full"
+								label="Latency"
+								value={typeof log.latency === "number" ? `${log.latency.toFixed(2)}ms` : "NA"}
+							/>
ui/app/workspace/mcp-logs/views/filters.tsx (1)

18-19: Unused fetchLogs and fetchStats props.

These props are declared in the interface and destructured but never called within the component. Either remove them if not needed, or implement the intended functionality.

docs/openapi/paths/management/logging.yaml (1)

335-340: Status filter description still conflicts with enum.

Description says comma-separated list but enum limits to a single value. Please align.

Also applies to: 459-465

docs/plugins/getting-started.mdx (1)

57-58: Clarify MCP hook exposure in the v1.4.x+ interface list.

The lifecycle section references PreMCPHook/PostMCPHook, but the v1.4.x+ function list only shows PreLLMHook/PostLLMHook. Either add MCP hook entries or explicitly note they live on a separate MCP plugin interface to avoid confusion.

📝 Possible doc adjustment
   - `HTTPTransportPostHook()` - Intercept HTTP responses after they exit Bifrost core (HTTP transport only)
   - `PreLLMHook()` - Intercept requests before they reach providers
   - `PostLLMHook()` - Process responses after provider calls
+  - `PreMCPHook()` - Intercept MCP tool calls before execution
+  - `PostMCPHook()` - Process MCP tool results after execution
   - `Cleanup() error` - Clean up resources on shutdown

Based on learnings, please confirm earlier stack PRs implement MCP hooks before finalizing docs.

Also applies to: 65-66, 86-96

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

830-837: Allow sort_by=cost for MCP log pagination.

PaginationOptions.SortBy includes cost, and MCP logs expose cost. Rejecting it blocks a valid sort option.

💡 Proposed fix
-	if sortBy == "timestamp" || sortBy == "latency" {
+	if sortBy == "timestamp" || sortBy == "latency" || sortBy == "cost" {
 		pagination.SortBy = sortBy
 	} else {
-		return nil, nil, fmt.Errorf("invalid sort_by: must be 'timestamp' or 'latency'")
+		return nil, nil, fmt.Errorf("invalid sort_by: must be 'timestamp', 'latency', or 'cost'")
 	}
framework/configstore/migrations.go (1)

2458-2478: Make tool_pricing migration deterministic when prefixed/unprefixed keys collide.

Go map iteration order is random, so the “winner” in prefixed/unprefixed collisions is nondeterministic. Prefer a two-pass approach so unprefixed keys always win.

🔧 Proposed fix (two-pass, deterministic)
-					updatedPricing := make(map[string]float64)
-					for toolName, price := range toolPricing {
-						if hasPrefix, unprefixedTool := hasClientPrefix(toolName, clientName); hasPrefix {
-							if existingPrice, exists := updatedPricing[unprefixedTool]; exists {
-								log.Printf("Collision detected when stripping prefix from pricing key '%s' for client '%s': unprefixed key '%s' already exists with price %.6f. Keeping existing unprefixed value (%.6f), discarding prefixed value (%.6f).", toolName, clientName, unprefixedTool, existingPrice, existingPrice, price)
-								needsUpdate = true
-								continue
-							}
-							updatedPricing[unprefixedTool] = price
-							needsUpdate = true
-						} else {
-							if existingPrice, exists := updatedPricing[toolName]; exists {
-								log.Printf("Collision detected for pricing key '%s' for client '%s': key already exists with price %.6f. Keeping first value (%.6f), discarding duplicate (%.6f).", toolName, clientName, existingPrice, existingPrice, price)
-								continue
-							}
-							updatedPricing[toolName] = price
-						}
-					}
+					updatedPricing := make(map[string]float64)
+					// Pass 1: keep already-unprefixed keys
+					for toolName, price := range toolPricing {
+						if hasPrefix, _ := hasClientPrefix(toolName, clientName); hasPrefix {
+							continue
+						}
+						if _, exists := updatedPricing[toolName]; !exists {
+							updatedPricing[toolName] = price
+						}
+					}
+					// Pass 2: add prefixed keys only when missing
+					for toolName, price := range toolPricing {
+						if hasPrefix, unprefixedTool := hasClientPrefix(toolName, clientName); hasPrefix {
+							if existingPrice, exists := updatedPricing[unprefixedTool]; exists {
+								log.Printf("Collision detected when stripping prefix from pricing key '%s' for client '%s': unprefixed key '%s' already exists with price %.6f. Keeping existing unprefixed value (%.6f), discarding prefixed value (%.6f).", toolName, clientName, unprefixedTool, existingPrice, existingPrice, price)
+								needsUpdate = true
+								continue
+							}
+							updatedPricing[unprefixedTool] = price
+							needsUpdate = true
+						}
+					}
ui/app/workspace/mcp-logs/page.tsx (2)

182-200: Stats may become stale after log deletion.

After deleting a log, setLogs and setTotalItems are updated, but the stat cards (total executions, success rate, avg latency, total cost) are not refreshed. This can lead to inconsistent UI where the row count doesn't match "Total Executions".

Consider calling fetchStats() after a successful delete:

🛠️ Suggested fix
 const handleDelete = useCallback(
 	async (log: MCPToolLogEntry) => {
 		if (!hasDeleteAccess) {
 			throw new Error("No delete access");
 		}
 
 		try {
 			await deleteLogs({ ids: [log.id] }).unwrap();
 			setLogs((prevLogs) => prevLogs.filter((l) => l.id !== log.id));
 			setTotalItems((prev) => prev - 1);
+			fetchStats();
 		} catch (err) {
 			const errorMessage = getErrorMessage(err);
 			setError(errorMessage);
 			throw new Error(errorMessage);
 		}
 	},
-	[deleteLogs, hasDeleteAccess],
+	[deleteLogs, hasDeleteAccess, fetchStats],
 );

304-329: Stats updates may double-count on repeated terminal log updates.

The stats update logic increments counters whenever log.status is "success" or "error", but doesn't check if the log was previously in a terminal state. If a completed log receives multiple updates (e.g., cost backfill after completion), stats will be inflated.

Consider checking the previous log's status before updating stats:

🛠️ Suggested approach
 // Update stats for completed requests
 if (log.status === "success" || log.status === "error") {
+	// Find existing log to check previous status
+	const existingLog = logs.find((l) => l.id === log.id);
+	const wasAlreadyTerminal = existingLog && (existingLog.status === "success" || existingLog.status === "error");
+	
+	// Only update execution counts if transitioning to terminal state
+	if (wasAlreadyTerminal) {
+		// Just update cost if it changed
+		if (log.cost !== existingLog.cost) {
+			setStats((prevStats) => {
+				if (!prevStats) return prevStats;
+				return {
+					...prevStats,
+					total_cost: (Number(prevStats.total_cost) || 0) - Number(existingLog.cost ?? 0) + Number(log.cost ?? 0),
+				};
+			});
+		}
+		return;
+	}
+	
 	setStats((prevStats) => {
 		// ... existing stats update logic
 	});
 }
core/mcp/codemodeexecutecode.go (2)

852-879: Handle post-hook errors in short‑circuit branches.

RunMCPPostHooks errors are ignored in the short‑circuit paths, which can mask plugin failures and diverges from the main flow’s error handling.

🛠️ Suggested fix
-			finalResp, _ := pipeline.RunMCPPostHooks(nestedCtx, shortCircuit.Response, nil, preCount)
+			finalResp, finalErr := pipeline.RunMCPPostHooks(nestedCtx, shortCircuit.Response, nil, preCount)
+			if finalErr != nil {
+				if finalErr.Error != nil {
+					return nil, fmt.Errorf("%s", finalErr.Error.Message)
+				}
+				return nil, fmt.Errorf("plugin post-hooks returned error")
+			}
 			if finalResp != nil {
 				// Try ChatMessage first
 				if finalResp.ChatMessage != nil {
 					return extractResultFromChatMessage(finalResp.ChatMessage), nil
 				}
@@
-		if shortCircuit.Error != nil {
-			pipeline.RunMCPPostHooks(nestedCtx, nil, shortCircuit.Error, preCount)
+		if shortCircuit.Error != nil {
+			_, finalErr := pipeline.RunMCPPostHooks(nestedCtx, nil, shortCircuit.Error, preCount)
+			if finalErr != nil {
+				if finalErr.Error != nil {
+					return nil, fmt.Errorf("%s", finalErr.Error.Message)
+				}
+				return nil, fmt.Errorf("plugin post-hooks returned error")
+			}
 			if shortCircuit.Error.Error != nil {
 				return nil, fmt.Errorf("%s", shortCircuit.Error.Error.Message)
 			}
 			return nil, fmt.Errorf("plugin short-circuit error")
 		}

1082-1085: Doc comment doesn’t match return signature.

The comment says a boolean is returned, but the function returns (interface{}, error).

📝 Suggested update
-// Returns the extracted result/error, and a boolean indicating if it was an error.
+// Returns the extracted result or a non-nil error when a tool error is present.
framework/configstore/rdb.go (1)

878-884: Headers JSON shape likely mismatches TableMCPClient persistence.

UpdateMCPClientConfig marshals map[string]schemas.EnvVar directly, which diverges from the BeforeSave behavior that stores env-var references or resolved values as map[string]string. This risks inconsistent JSON shape and persisting resolved secrets.

🛠️ Suggested fix
-		headersJSON, err := json.Marshal(clientConfigCopy.Headers)
+		headersToSerialize := make(map[string]string, len(clientConfigCopy.Headers))
+		for key, value := range clientConfigCopy.Headers {
+			if value.IsFromEnv() {
+				headersToSerialize[key] = value.EnvVar
+			} else {
+				headersToSerialize[key] = value.GetValue()
+			}
+		}
+		headersJSON, err := json.Marshal(headersToSerialize)
 		if err != nil {
 			return fmt.Errorf("failed to marshal headers: %w", err)
 		}
examples/plugins/hello-world/main.go (1)

41-46: Build still fails: schemas.LLMPluginShortCircuit unresolved.

Pipeline reports undefined: schemas.LLMPluginShortCircuit at Line 41. Either ensure the type is exported for this build or keep the example on the currently available short‑circuit type.

🛠️ Example fix if the type isn’t available yet
-func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.LLMPluginShortCircuit, error) {
+func PreLLMHook(ctx *schemas.BifrostContext, req *schemas.BifrostRequest) (*schemas.BifrostRequest, *schemas.PluginShortCircuit, error) {
transports/bifrost-http/handlers/plugins.go (1)

346-349: Disable path should report stop failure, not reload.

The error message still says “failed to reload” when RemovePlugin fails.

📝 Suggested fix
-			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin updated in database but failed to reload: %v", err))
+			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin updated in database but failed to stop: %v", err))
core/mcp/utils.go (2)

192-198: Update comment example to match the "-" separator.

The comment on line 194 still shows "calculator/add" as the prefixed tool name example, but the actual format uses - as separator (e.g., "calculator-add").

📝 Suggested update
 		// Strip client prefix from tool name before checking
 		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")
 		unprefixedToolName := stripClientPrefix(toolName, config.Name)

224-230: Same comment example correction needed here.

The comment at line 226 also uses the old / separator format.

📝 Suggested update
 		// Strip client prefix from tool name before checking
 		// Tool names in config are stored without prefix (e.g., "add")
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")
 		unprefixedToolName := stripClientPrefix(toolName, config.Name)
examples/plugins/mcp-only/main.go (1)

114-121: Use the canonical request-id context key.

The audit trail reads request_id using a string-based context key, but the canonical key is schemas.BifrostContextKeyRequestID. This will always resolve to nil.

🐛 Suggested fix
 	if pluginConfig.EnableAudit {
-		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKey("request_id")))
+		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKeyRequestID))
 		ctx.SetValue(schemas.BifrostContextKey("mcp-audit-trail"), auditMsg)
docs/openapi/schemas/management/logging.yaml (2)

112-143: Add virtual key fields + BifrostError ref to MCPToolLogEntry.
The backend log model includes virtual_key identifiers and error_details is a serialized BifrostError; the schema should reflect that for correct client deserialization.

📘 Suggested schema update
 MCPToolLogEntry:
   type: object
   description: MCP tool execution log entry
   properties:
@@
     server_label:
       type: string
       description: Label of the MCP server that provided the tool
+    virtual_key_id:
+      type: string
+      description: Virtual key ID used for the tool execution
+    virtual_key_name:
+      type: string
+      nullable: true
+      description: Virtual key name
@@
-    error_details:
-      type: object
-      additionalProperties: true
-      description: Error details if execution failed
+    error_details:
+      $ref: '../../schemas/inference/common.yaml#/BifrostError'

159-183: Add virtual_key_ids filter to MCPToolLogSearchFilters.
The backend search filters include virtual key IDs; the schema should expose this filter to clients.

📘 Suggested schema update
 MCPToolLogSearchFilters:
   type: object
   description: MCP tool log search filters
   properties:
@@
     llm_request_ids:
       type: array
       items:
         type: string
       description: Filter by linked LLM request IDs
+    virtual_key_ids:
+      type: array
+      items:
+        type: string
+      description: Filter by virtual key IDs
transports/bifrost-http/server/server.go (1)

653-734: Call Cleanup() on replaced/removed plugins to avoid leaks.

Reload/remove currently swaps plugin instances without invoking Cleanup(), so background goroutines/resources may leak. Please capture the old instance and clean it up after replacement/removal.

🔧 Suggested fix
 // ReloadPlugin
- plugin, err := InstantiatePlugin(ctx, name, path, pluginConfig, s.Config)
+ oldPlugin, _ := s.Config.FindPluginByName(name)
+ plugin, err := InstantiatePlugin(ctx, name, path, pluginConfig, s.Config)
 ...
- if err := s.Config.RegisterPlugin(plugin); err != nil {
+ if err := s.Config.RegisterPlugin(plugin); err != nil {
     return updateError("registering", err)
 }
+ if oldPlugin != nil {
+     if err := oldPlugin.Cleanup(); err != nil {
+         logger.Warn("failed to cleanup old plugin %s: %v", name, err)
+     }
+ }
 
 // RemovePlugin
- if err := s.Config.UnregisterPlugin(name); err != nil {
+ oldPlugin, _ := s.Config.FindPluginByName(name)
+ if err := s.Config.UnregisterPlugin(name); err != nil {
     return err
 }
+ if oldPlugin != nil {
+     if err := oldPlugin.Cleanup(); err != nil {
+         logger.Warn("failed to cleanup plugin %s: %v", name, err)
+     }
+ }
core/bifrost.go (2)

289-307: ReloadConfig comment still claims account updates.

The comment says account is updated, but the method only updates dropExcessRequests and plugin lists. Please align the comment or implement the account update.


3373-3377: Post-hook count should use the pre-hook snapshot (hot-reload drift).

These counts can change mid-flight if plugins reload; prefer preCount (where available) or len(pipeline.llmPlugins) to keep post-hooks aligned with the pre-hook set.

🔧 Suggested adjustments
-	pluginCount := len(*bifrost.llmPlugins.Load())
+	pluginCount := preCount
-	recoveredResp, recoveredErr := pipeline.RunPostHooks(ctx, nil, &bifrostErrVal, len(*bifrost.llmPlugins.Load()))
+	recoveredResp, recoveredErr := pipeline.RunPostHooks(ctx, nil, &bifrostErrVal, preCount)
-				resp, bifrostErr := pipeline.RunPostHooks(ctx, result, err, len(*bifrost.llmPlugins.Load()))
+				resp, bifrostErr := pipeline.RunPostHooks(ctx, result, err, len(pipeline.llmPlugins))

Also applies to: 3593-3594, 3849-3850

🧹 Nitpick comments (21)
ui/app/workspace/plugins/views/pluginsView.tsx (2)

186-197: Consider extracting Badge rendering for readability.

The Badge component on line 192 has multiple props making it long. Consider wrapping for better readability or extracting inline styles.

♻️ Suggested formatting improvement
 {selectedPlugin.status.types.map((type) => (
-	<Badge key={type} variant="outline" className={cn("h-5 px-2 text-xs font-medium uppercase", getPluginTypeColor(type))}>{type}</Badge>
+	<Badge
+		key={type}
+		variant="outline"
+		className={cn("h-5 px-2 text-xs font-medium uppercase", getPluginTypeColor(type))}
+	>
+		{type}
+	</Badge>
 ))}

114-116: Consider logging the error for debugging.

The caught error is not logged, which could make debugging production issues more difficult.

♻️ Suggested improvement
 } catch (error) {
+	console.error("Failed to update plugin:", error);
 	toast.error("Failed to update plugin");
 }
examples/plugins/llm-only/README.md (1)

68-68: Clarify the default value for system_message_text.

The default value shows "You are a helpful assistant..." with an ellipsis. It's unclear whether the ellipsis is:

  1. Part of the actual default string value, or
  2. Used to indicate truncation for brevity in the documentation
📝 Suggested clarification options

Option 1: If the default is exactly "You are a helpful assistant.":

-| `system_message_text` | string | `"You are a helpful assistant..."` | Custom system message to inject |
+| `system_message_text` | string | `"You are a helpful assistant."` | Custom system message to inject |

Option 2: If truncated for brevity, make it explicit:

-| `system_message_text` | string | `"You are a helpful assistant..."` | Custom system message to inject |
+| `system_message_text` | string | `"You are a helpful assistant."` (truncated) | Custom system message to inject |

Option 3: Reference the actual implementation:

-| `system_message_text` | string | `"You are a helpful assistant..."` | Custom system message to inject |
+| `system_message_text` | string | See implementation | Custom system message to inject |
ui/app/workspace/mcp-gateway/views/mcpClientSheet.tsx (1)

419-427: Consider enforcing non-negative pricing values.

The min="0" attribute provides browser-level enforcement, but parseFloat can still produce negative values if users manually edit or paste negative numbers. Consider clamping to zero:

💡 Optional fix
 onChange={(e) => {
-	const value = e.target.value === "" ? undefined : parseFloat(e.target.value);
+	const parsed = parseFloat(e.target.value);
+	const value = e.target.value === "" ? undefined : (isNaN(parsed) || parsed < 0 ? undefined : parsed);
 	const newPricing = { ...field.value };
-	if (value === undefined || isNaN(value)) {
+	if (value === undefined) {
 		delete newPricing[tool.name];
 	} else {
 		newPricing[tool.name] = value;
 	}
 	field.onChange(newPricing);
 }}
examples/plugins/http-transport-only/main.go (1)

34-70: RateLimiter has unbounded memory growth for long-lived processes.

The requests map retains entries for active keys indefinitely, only pruning old timestamps within each key. For high-cardinality keys (e.g., per-user rate limiting), this can lead to memory growth over time.

For an example plugin this is acceptable, but production implementations should consider periodic full cleanup of stale keys or an LRU eviction policy.

plugins/logging/main.go (1)

746-765: Verify codemode tool check consistency.

The PreMCPHook skips logging for codemode tools using bifrost.IsCodemodeTool(fullToolName), but then lines 759-764 still attempt to set serverLabel = "codemode" for codemode tools. Since the function returns early at line 747-748 for codemode tools, lines 759-764 are dead code for codemode tools.

This is harmless but slightly confusing. Consider either removing the codemode case in the switch (since it's unreachable) or documenting why it exists.

examples/plugins/mcp-only/Makefile (1)

1-12: Optional: add all/test phony targets to satisfy checkmake.
This keeps the example Makefile minimal while avoiding lint noise.

♻️ Suggested tweak
-.PHONY: build clean
+.PHONY: all build test clean
+
+all: build
@@
 build:
 	`@echo` "Building MCP-Only plugin..."
 	`@mkdir` -p build
 	`@go` build -buildmode=plugin -o build/mcp-only.so main.go
 	`@echo` "Plugin built successfully: build/mcp-only.so"
+
+test:
+	`@echo` "Running MCP-Only plugin tests..."
+	`@go` test ./...
framework/plugins/loader.go (1)

11-15: Clarify VerifyBasePlugin return semantics.

The comments are slightly ambiguous about the relationship between the return values:

  • "Returns the name of the plugin or an empty string if the plugin is invalid"
  • "Returns an error if the plugin is invalid"

This could be interpreted as: (1) empty string + error on invalid, (2) empty string OR error on invalid. Consider clarifying whether both are returned together on failure.

Suggested documentation clarification
 	// VerifyBasePlugin verifies a plugin at the given path
-	// Returns the name of the plugin or an empty string if the plugin is invalid
-	// Returns an error if the plugin is invalid
+	// Returns the name of the plugin on success, or an empty string with an error if the plugin is invalid.
 	// This method is used to verify that the plugin is a valid base plugin and has the required symbols
 	VerifyBasePlugin(path string) (string, error)
ui/components/sidebar.tsx (1)

72-89: Duplicate MCPIcon component — consider reusing the existing export.

A nearly identical MCPIcon is already exported from ui/components/ui/icons.tsx (lines 1757-1772). This local definition duplicates the SVG paths and introduces minor inconsistencies (different aria-label, explicit height/width vs inherited sizing).

♻️ Suggested refactor

Import and use the existing icon instead of redeclaring:

+import { MCPIcon } from "@/components/ui/icons";
-
-// Custom MCP Icon Component
-const MCPIcon = ({ className }: { className?: string }) => (
-	<svg
-		className={className}
-		fill="currentColor"
-		fillRule="evenodd"
-		height="1em"
-		style={{ flex: "none", lineHeight: 1 }}
-		viewBox="0 0 24 24"
-		width="1em"
-		xmlns="http://www.w3.org/2000/svg"
-		aria-label="MCP clients icon"
-	>
-		<title>MCP clients icon</title>
-		<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
-		<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
-	</svg>
-);

If sizing differences are needed, wrap or style the imported icon rather than duplicating.

plugins/logging/operations.go (1)

436-469: LGTM!

The new GetAvailableMCPVirtualKeys function and extractUniqueMCPKeyPairs helper follow the established patterns from existing key extraction methods. The logic correctly handles deduplication and filters out empty key pairs.

The extractUniqueMCPKeyPairs and extractUniqueKeyPairs functions share nearly identical logic, differing only in input types. If Go generics are acceptable in this codebase, these could be consolidated. However, the current approach is clear and maintainable as-is.

ui/app/workspace/mcp-logs/views/filters.tsx (1)

217-220: Consider simplifying the loading check.

The isLoading condition checks filterDataLoading separately for each dynamic category, but it's the same condition. This could be simplified:

♻️ Suggested simplification
-											const isLoading =
-												(category === "Tool Names" && filterDataLoading) ||
-												(category === "Servers" && filterDataLoading) ||
-												(category === "Virtual Keys" && filterDataLoading);
+											const isLoading =
+												filterDataLoading && ["Tool Names", "Servers", "Virtual Keys"].includes(category);
examples/plugins/llm-only/main.go (2)

29-61: Consider using a structured config parsing approach.

The Init function manually extracts each field from the config map with type assertions. While functional for an example, this pattern is verbose and error-prone if the config structure grows.

For a more maintainable approach, consider using encoding/json with marshal/unmarshal:

♻️ Optional refactor using JSON round-trip
 func Init(config any) error {
 	fmt.Println("[LLM-Only Plugin] Init called")
 
-	// Parse configuration
-	if configMap, ok := config.(map[string]interface{}); ok {
-		if injectMsg, ok := configMap["inject_system_message"].(bool); ok {
-			pluginConfig.InjectSystemMessage = injectMsg
-			fmt.Printf("[LLM-Only Plugin] System message injection: %v\n", pluginConfig.InjectSystemMessage)
-		}
-		// ... more type assertions
-	}
+	// Parse configuration using JSON round-trip
+	if configMap, ok := config.(map[string]interface{}); ok {
+		jsonBytes, err := json.Marshal(configMap)
+		if err == nil {
+			_ = json.Unmarshal(jsonBytes, pluginConfig)
+		}
+	}
 
 	fmt.Printf("[LLM-Only Plugin] Configuration loaded: %+v\n", pluginConfig)
 	return nil
 }

91-95: Use bifrost.Ptr() for pointer creation.

Based on learnings from the repository, prefer using bifrost.Ptr() to create pointers instead of the address operator for consistency.

♻️ Suggested fix
 		systemMsg := schemas.ChatMessage{
 			Role:    "system",
-			Content: &schemas.ChatMessageContent{ContentStr: &pluginConfig.SystemMessageText},
+			Content: &schemas.ChatMessageContent{ContentStr: bifrost.Ptr(pluginConfig.SystemMessageText)},
 		}
ui/app/workspace/mcp-logs/views/mcpLogsTable.tsx (1)

44-57: Consider adding runtime validation for sort_by values.

Per past review discussion, the type cast at line 53 provides compile-time safety but doesn't prevent invalid values from URL manipulation. While this was marked as addressed, the current implementation still relies on the cast without runtime validation.

If users can directly modify URL parameters, consider adding a runtime guard:

♻️ Optional runtime validation
 const handleSortingChange = (updaterOrValue: SortingState | ((old: SortingState) => SortingState)) => {
 	const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue;
 	setSorting(newSorting);
 	if (newSorting.length > 0) {
 		const { id, desc } = newSorting[0];
+		const validSortBy = (id === "timestamp" || id === "latency") ? id : "timestamp";
 		onPaginationChange({
 			...pagination,
-			sort_by: id as "timestamp" | "latency",
+			sort_by: validSortBy,
 			order: desc ? "desc" : "asc",
 		});
 	}
 };
transports/bifrost-http/handlers/plugins.go (1)

74-91: Keep plugin listings deterministic in the configStore‑nil path too.

The nil‑configStore branch skips sorting, so responses can vary across runs even though the other path is sorted. Consider sorting here as well for consistent API behavior.

♻️ Suggested tweak
 		for name, pluginStatus := range pluginStatus {
 			finalPlugins = append(finalPlugins, PluginResponse{
 				Name:       pluginStatus.Name,
 				ActualName: name,
 				Enabled:    true,
 				Config:     map[string]any{},
 				IsCustom:   true,
 				Path:       nil,
 				Status:     pluginStatus,
 			})
 		}
+		sort.Slice(finalPlugins, func(i, j int) bool {
+			return finalPlugins[i].Name < finalPlugins[j].Name
+		})
 		SendJSON(ctx, map[string]any{
 			"plugins": finalPlugins,
 			"count":   len(finalPlugins),
 		})
plugins/mocker/main.go (2)

494-497: Remove redundant Enabled guard in PreLLMHook.
Disabled plugins never enter the chain, so this check is dead code and adds per-call overhead. Based on learnings, rely on the loader instead.

♻️ Suggested change
-	// Skip processing if plugin is disabled
-	if !p.config.Enabled {
-		return req, nil, nil
-	}

801-808: Prefer bifrost.Ptr for string pointers.
Line 807 and Line 1032 currently take addresses of local strings; the repo convention is to use bifrost.Ptr(...). Based on learnings, align with the pointer helper.

♻️ Suggested change
-	// Use a static string to avoid allocation
-	static := "stop"
-	finishReason = &static
+	finishReason = bifrost.Ptr("stop")
-		finishReason := "stop"
+		finishReason := bifrost.Ptr("stop")
 		return req, &schemas.LLMPluginShortCircuit{
 			Response: &schemas.BifrostResponse{
@@
-						FinishReason: &finishReason,
+						FinishReason: finishReason,

Also applies to: 1011-1033

framework/plugins/soplugin_test.go (1)

119-133: Use bifrost.Ptr instead of stringPtr.
Line 131 and Line 160 rely on a local helper that returns &s. The repo standard is bifrost.Ptr(...); consider replacing usages and removing the helper. Based on learnings, keep pointer creation consistent.

♻️ Suggested change
+	bifrost "github.com/maximhq/bifrost/core"
-								ContentStr: stringPtr("Hello"),
+								ContentStr: bifrost.Ptr("Hello"),
-									ContentStr: stringPtr("Hello! How can I help you?"),
+									ContentStr: bifrost.Ptr("Hello! How can I help you?"),
-// Helper function to create a pointer to a string
-func stringPtr(s string) *string {
-	return &s
-}

Also applies to: 160-161, 649-651

transports/bifrost-http/server/plugins.go (1)

146-162: Handle registerPluginWithStatus errors for optional plugins.
Line 151 and Line 161 ignore the returned error, which can hide failures and leave status stale. Consider capturing the error and either logging + marking error status or returning.

♻️ Suggested change
-		s.registerPluginWithStatus(ctx, logging.PluginName, nil, config, false)
+		if err := s.registerPluginWithStatus(ctx, logging.PluginName, nil, config, false); err != nil {
+			return err
+		}
@@
-		s.registerPluginWithStatus(ctx, governance.PluginName, nil, config, false)
+		if err := s.registerPluginWithStatus(ctx, governance.PluginName, nil, config, false); err != nil {
+			return err
+		}
#!/bin/bash
# Inspect registerPluginWithStatus behavior to confirm proper error handling expectations.
rg -n "registerPluginWithStatus" --type go -A5 -B5
core/bifrost.go (2)

139-153: Defensively copy plugin slices on Init to avoid external mutation.

config.LLMPlugins / config.MCPPlugins are stored by reference; if the caller mutates the slices, it can race with runtime reads. Consider copying in Init (mirrors ReloadConfig).

♻️ Suggested defensive copy
-	bifrost.llmPlugins.Store(&config.LLMPlugins)
-	bifrost.mcpPlugins.Store(&config.MCPPlugins)
+	llmPluginsCopy := append([]schemas.LLMPlugin(nil), config.LLMPlugins...)
+	mcpPluginsCopy := append([]schemas.MCPPlugin(nil), config.MCPPlugins...)
+	bifrost.llmPlugins.Store(&llmPluginsCopy)
+	bifrost.mcpPlugins.Store(&mcpPluginsCopy)

2221-2261: Handle nil MCP plugin list like LLM reload does.

reloadMCPPlugin returns early when oldPlugins == nil, which can silently drop the first plugin if the pointer was never initialized. Mirror the LLM path by treating nil as an empty slice.

♻️ Suggested fix
-		oldPlugins := bifrost.mcpPlugins.Load()
-		if oldPlugins == nil {
-			return nil
-		}
-		// Create new slice with replaced plugin
-		newPlugins := make([]schemas.MCPPlugin, len(*oldPlugins))
-		copy(newPlugins, *oldPlugins)
+		oldPlugins := bifrost.mcpPlugins.Load()
+		var newPlugins []schemas.MCPPlugin
+		if oldPlugins == nil {
+			newPlugins = make([]schemas.MCPPlugin, 0)
+		} else {
+			newPlugins = make([]schemas.MCPPlugin, len(*oldPlugins))
+			copy(newPlugins, *oldPlugins)
+		}

@akshaydeo akshaydeo force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 6c8cf87 to 5089d3f Compare January 26, 2026 15:49
@akshaydeo akshaydeo mentioned this pull request Jan 26, 2026
16 tasks
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 (5)
docs/plugins/writing-go-plugin.mdx (1)

547-553: Inconsistent hook names in streaming log output example.

The v1.4.x streaming tab shows "PreHook called" and "PostHook called" but should use the new naming convention.

📝 Suggested fix
   <Tab title="v1.4.x+ (streaming)">

HTTPTransportPreHook called
-PreHook called
-PostHook called
+PreLLMHook called
+PostLLMHook called
HTTPTransportStreamChunkHook called (per chunk)

  </Tab>
framework/plugins/soplugin.go (1)

67-70: Missing nil guard on HTTPTransportStreamChunkHook.

Unlike other optional hooks (HTTPTransportPreHook, HTTPTransportPostHook, PreLLMHook, etc.), this method directly calls the underlying function pointer without checking for nil. If a plugin doesn't export HTTPTransportStreamChunkHook, this will panic at runtime.

🐛 Proposed fix
 // HTTPTransportStreamChunkHook intercepts streaming chunks before they are written to the client
 func (dp *DynamicPlugin) HTTPTransportStreamChunkHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, stream *schemas.BifrostStreamChunk) (*schemas.BifrostStreamChunk, error) {
+	if dp.httpTransportStreamChunkHook == nil {
+		return stream, nil // No-op if not implemented
+	}
 	return dp.httpTransportStreamChunkHook(ctx, req, stream)
 }
plugins/mocker/main.go (2)

871-874: Avoid nil dereference when overriding model.

For ResponsesRequest, mockResponse.ChatResponse is nil, so this assignment will panic. Guard and apply the override to the correct response type.

🐛 Proposed diff
-	// Override model if specified
-	if content.Model != nil {
-		mockResponse.ChatResponse.Model = *content.Model
-	}
+	// Override model if specified
+	if content.Model != nil {
+		switch {
+		case mockResponse.ChatResponse != nil:
+			mockResponse.ChatResponse.Model = *content.Model
+		case mockResponse.ResponsesResponse != nil:
+			mockResponse.ResponsesResponse.ExtraFields.ModelRequested = *content.Model
+		}
+	}

1002-1047: Default success short-circuit should match the request type.

DefaultBehaviorSuccess always builds a chat response; for ResponsesRequest this produces a mismatched shape. Either branch by request type or reuse the existing success generator to build the correct response.

🛠️ One way to align response type
 	case DefaultBehaviorSuccess:
-		finishReason := "stop"
-		return req, &schemas.LLMPluginShortCircuit{
-			Response: &schemas.BifrostResponse{
-				ChatResponse: &schemas.BifrostChatResponse{
-					Model: model,
-					Usage: &schemas.BifrostLLMUsage{
-						PromptTokens:     5,
-						CompletionTokens: 10,
-						TotalTokens:      15,
-					},
-					Choices: []schemas.BifrostResponseChoice{
-						{
-							Index: 0,
-							ChatNonStreamResponseChoice: &schemas.ChatNonStreamResponseChoice{
-								Message: &schemas.ChatMessage{
-									Role: schemas.ChatMessageRoleAssistant,
-									Content: &schemas.ChatMessageContent{
-										ContentStr: bifrost.Ptr("Mock plugin default response"),
-									},
-								},
-							},
-							FinishReason: &finishReason,
-						},
-					},
-					ExtraFields: schemas.BifrostResponseExtraFields{
-						RequestType:    schemas.ChatCompletionRequest,
-						Provider:       provider,
-						ModelRequested: model,
-					},
-				},
-			},
-		}, nil
+		resp := &Response{
+			Type: ResponseTypeSuccess,
+			Content: &SuccessResponse{
+				Message:      "Mock plugin default response",
+				FinishReason: bifrost.Ptr("stop"),
+				Usage: &Usage{
+					PromptTokens:     5,
+					CompletionTokens: 10,
+					TotalTokens:      15,
+				},
+			},
+		}
+		return p.generateSuccessShortCircuit(req, resp, time.Now())
transports/bifrost-http/lib/config.go (1)

1553-1594: MCP pricing config from store is not used in catalog initialization.

mcpPricingConfig is populated from the store at line 1566 when ConfigStore is available, but the mcpcatalog.Init call at line 1588 always uses buildMCPPricingDataFromFile regardless of whether the store was used. This means store-based pricing data is ignored when both file and store exist.

🐛 Proposed fix - use the correct pricing source
 	// Initialize MCP catalog
-	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
-		PricingData: buildMCPPricingDataFromFile(ctx, configData),
-	}, logger)
+	// Use store pricing if available, otherwise fall back to file
+	var mcpPricingData mcpcatalog.MCPPricingData
+	if config.ConfigStore != nil {
+		mcpPricingData = mcpPricingConfig.PricingData
+	} else {
+		mcpPricingData = buildMCPPricingDataFromFile(ctx, configData)
+	}
+	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
+		PricingData: mcpPricingData,
+	}, logger)
🤖 Fix all issues with AI agents
In `@docs/openapi/schemas/management/plugins.yaml`:
- Around line 17-22: The OpenAPI schema for plugin "types" is missing the
"observability" member; update the enum in the schema (the "types" -> "items" ->
"enum" list) to include "observability" so it matches implementations like
AsObservabilityPlugin and usages in examples/plugins/multi-interface (where
"observability" is emitted as a plugin type).

In `@plugins/logging/main.go`:
- Around line 748-753: The IsCodemodeTool check is running on fullToolName
(which includes a client prefix) so codemode tools like "codemode-listToolFiles"
slip through; update the logic to perform bifrost.IsCodemodeTool against the
unprefixed tool name instead: either move the
bifrost.IsCodemodeTool(fullToolName) check to after the serverLabel/toolName
extraction (use the extracted toolName) or strip the client prefix from
fullToolName before calling bifrost.IsCodemodeTool; modify the branch that
currently defines serverLabel and toolName (the code around serverLabel/toolName
parsing) and replace the early check to reference toolName (or the stripped
name) so codemode tools are correctly skipped.

In `@transports/bifrost-http/handlers/devpprof.go`:
- Around line 525-527: perRequestPatterns currently lists only LLM hook names so
goroutines for other per-request hooks aren't categorized; update the
perRequestPatterns slice in devpprof.go to also include the strings
"PreMCPHook", "PostMCPHook", "HTTPTransportPreHook", and "HTTPTransportPostHook"
so those hook goroutines are included in the goroutine summary (modify the
perRequestPatterns variable initialization accordingly).
♻️ Duplicate comments (49)
examples/plugins/llm-only/Makefile (1)

1-1: Add standard all and test phony targets.

checkmake flags missing required phony targets. This issue was already raised in a previous review with a suggested fix.

examples/plugins/http-transport-only/Makefile (1)

1-1: Add all and test phony targets.

The static analysis tool flags missing all and test phony targets, which are common Makefile conventions. This has already been noted in a previous review.

examples/plugins/http-transport-only/main.go (3)

95-99: Clamp non‑positive rate_window values.
Line 95-99 allows 0/negative windows, which effectively disables rate limiting.

🔧 Suggested fix
-		if rateWindow, ok := configMap["rate_window"].(float64); ok {
-			pluginConfig.RateWindow = int(rateWindow)
-			fmt.Printf("[HTTP-Transport-Only Plugin] Rate window: %d seconds\n", pluginConfig.RateWindow)
-		}
+		if rateWindow, ok := configMap["rate_window"].(float64); ok {
+			pluginConfig.RateWindow = int(rateWindow)
+			if pluginConfig.RateWindow <= 0 {
+				pluginConfig.RateWindow = 60
+				fmt.Println("[HTTP-Transport-Only Plugin] Invalid rate window; defaulting to 60 seconds")
+			} else {
+				fmt.Printf("[HTTP-Transport-Only Plugin] Rate window: %d seconds\n", pluginConfig.RateWindow)
+			}
+		}

176-178: Guard req.Headers before mutation.
Line 176-178 writes into req.Headers; a nil map will panic.

🐛 Proposed fix
 	// Example 4: Add custom headers
+	if req.Headers == nil {
+		req.Headers = map[string]string{}
+	}
 	req.Headers["X-Plugin-Processed"] = "true"
 	req.Headers["X-Request-Time"] = time.Now().Format(time.RFC3339)

189-210: Guard resp / resp.Headers before mutation.
Line 198+ writes headers without ensuring resp and resp.Headers are non‑nil.

🐛 Proposed fix
 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	fmt.Println("[HTTP-Transport-Only Plugin] HTTPTransportPostHook called")
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = map[string]string{}
+	}
 
 	// Calculate request duration
 	startTime := ctx.Value(schemas.BifrostContextKey("http-plugin-start-time"))
examples/plugins/http-transport-only/README.md (2)

124-126: Tighten wording to avoid repeated “only.”

Minor readability polish in the Notes list.

✏️ Suggested tweak
- - This plugin operates at the HTTP transport layer only
- - Works only when using bifrost-http, not when using Bifrost as a Go SDK
+ - This plugin operates at the HTTP transport layer
+ - Works with bifrost-http, not when using Bifrost as a Go SDK

55-86: Align the example rate_limit with the documented default.

The example shows 100 while the table says default is 10, which can confuse readers.

✏️ Suggested fix (example uses default)
@@
-        "rate_limit": 100,
+        "rate_limit": 10,
core/schemas/plugin_wasm.go (1)

5-7: Doc comment still implies streaming short-circuit in WASM build.

The WASM type has no Stream field; the comment should reflect that.

✏️ Suggested comment tweak
-// LLMPluginShortCircuit represents a plugin's decision to short-circuit the normal flow.
-// It can contain either a response (success short-circuit), a stream (streaming short-circuit), or an error (error short-circuit).
-// Streams are not supported in WASM plugins.
+// LLMPluginShortCircuit represents a plugin's decision to short-circuit the normal flow.
+// In WASM plugins, it can contain either a response (success short-circuit) or an error (error short-circuit).
+// Streams are not supported in WASM plugins.
framework/configstore/migrations.go (1)

2390-2394: Persist deduped tool lists when only duplicates are removed.
At Line 2391 and Line 2434, duplicates are skipped without setting needsUpdate, so a pure dedupe won’t be saved. Mark the record dirty when a duplicate is dropped.

🛠️ Suggested fix
-							if seenTools[tool] {
-								log.Printf("Duplicate tool name '%s' found for client '%s'. Keeping first occurrence.", tool, clientName)
-								continue
-							}
+							if seenTools[tool] {
+								log.Printf("Duplicate tool name '%s' found for client '%s'. Keeping first occurrence.", tool, clientName)
+								needsUpdate = true
+								continue
+							}
-							if seenAutoTools[tool] {
-								log.Printf("Duplicate auto-execute tool name '%s' found for client '%s'. Keeping first occurrence.", tool, clientName)
-								continue
-							}
+							if seenAutoTools[tool] {
+								log.Printf("Duplicate auto-execute tool name '%s' found for client '%s'. Keeping first occurrence.", tool, clientName)
+								needsUpdate = true
+								continue
+							}

Also applies to: 2433-2437

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

125-141: Prevent negative pricing and clear pricing when a tool is disabled.

min="0" doesn’t block typed/pasted negatives, and disabling a tool leaves stale pricing in the payload. This was flagged earlier and still appears unresolved.

🧹 Suggested fix for validation + cleanup
@@
-																				onChange={(e) => {
-																					const value = e.target.value === "" ? undefined : parseFloat(e.target.value);
+																				onChange={(e) => {
+																					const parsed = parseFloat(e.target.value);
+																					const value =
+																						e.target.value === "" || Number.isNaN(parsed) || parsed < 0 ? undefined : parsed;
 																					const newPricing = { ...field.value };
-																					if (value === undefined || isNaN(value)) {
+																					if (value === undefined) {
 																						delete newPricing[tool.name];
 																					} else {
 																						newPricing[tool.name] = value;
 																					}
 																					field.onChange(newPricing);
 																				}}
@@
 		// If tool is being removed from tools_to_execute, also remove it from tools_to_auto_execute
 		if (!checked) {
+			// Clear pricing for disabled tool
+			const currentPricing = form.getValues("tool_pricing") || {};
+			if (toolName in currentPricing) {
+				const { [toolName]: _, ...newPricing } = currentPricing;
+				form.setValue("tool_pricing", newPricing, { shouldDirty: true });
+			}
+
 			const currentAutoExecute = form.getValues("tools_to_auto_execute") || [];

Also applies to: 435-458

examples/plugins/multi-interface/main.go (3)

37-39: Protect requestCount against concurrent access.

Hooks can run concurrently; the current increments/reads can race and corrupt counts.

🔒 Suggested fix using atomic.Int64
@@
 import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"sync/atomic"
 	"time"
@@
-	requestCount int64
+	requestCount atomic.Int64
 	startTime    time.Time
 )
@@
 	if pluginConfig.TrackRequests {
-		requestCount++
-		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
+		current := requestCount.Add(1)
+		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", current)
 	}
@@
-			content = fmt.Sprintf("Processing request #%d. Server uptime: %v", requestCount, time.Since(startTime))
+			content = fmt.Sprintf("Processing request #%d. Server uptime: %v", requestCount.Load(), time.Since(startTime))
@@
-		fmt.Printf("[Multi-Interface Plugin] Cleanup called - processed %d requests over %v\n",
-			requestCount, uptime)
+		fmt.Printf("[Multi-Interface Plugin] Cleanup called - processed %d requests over %v\n",
+			requestCount.Load(), uptime)

Also applies to: 108-109, 176-178, 308-309


106-110: Initialize req.Headers before writing.

req.Headers can be nil; writing into it will panic.

🧩 Guard against nil headers
 	// Add request tracking (configurable)
 	if pluginConfig.TrackRequests {
+		if req.Headers == nil {
+			req.Headers = map[string]string{}
+		}
 		requestCount++
 		req.Headers[fmt.Sprintf("%s-Request-Number", pluginConfig.CustomHeaderPrefix)] = fmt.Sprintf("%d", requestCount)
 	}

119-149: Guard resp and resp.Headers before mutation.

Post hooks may receive a nil response or headers map, which would panic on write.

🧩 Nil-safe response/header handling
 func HTTPTransportPostHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, resp *schemas.HTTPResponse) error {
 	if !pluginConfig.EnableHTTPHooks {
 		return nil
 	}
+	if resp == nil {
+		return nil
+	}
+	if resp.Headers == nil {
+		resp.Headers = map[string]string{}
+	}
examples/plugins/multi-interface/Makefile (1)

1-1: Add standard all/test phony targets to satisfy checkmake.

This was flagged before and still applies.

🛠️ Suggested Makefile update
-.PHONY: build clean
+.PHONY: build clean all test
+
+all: build
+
+test:
+	`@echo` "No tests configured for this example"
examples/plugins/mcp-only/Makefile (1)

1-1: Add standard all/test phony targets to satisfy checkmake.

Same issue as earlier review feedback.

🛠️ Suggested Makefile update
-.PHONY: build clean
+.PHONY: build clean all test
+
+all: build
+
+test:
+	`@echo` "No tests configured for this example"
ui/app/workspace/mcp-logs/views/filters.tsx (1)

13-23: Unused fetchLogs / fetchStats props remain.
These props are still declared but never invoked; either remove them or wire them into filter updates.

core/mcp/clientmanager.go (2)

196-201: Remove redundant IsCodeModeClient assignment.
This field is set twice in the same block.

♻️ Suggested cleanup
 	config.Name = updatedConfig.Name
 	config.IsCodeModeClient = updatedConfig.IsCodeModeClient
 	config.Headers = updatedConfig.Headers
 	config.ToolsToExecute = updatedConfig.ToolsToExecute
 	config.ToolsToAutoExecute = updatedConfig.ToolsToAutoExecute
-	config.IsCodeModeClient = updatedConfig.IsCodeModeClient

453-456: Use initCtx for tool retrieval to avoid indefinite hangs.
For SSE/STDIO, ctx is long-lived; tool retrieval can block forever if the server stalls.

🔧 Suggested fix
-	tools, err := retrieveExternalTools(ctx, externalClient, config.Name)
+	tools, err := retrieveExternalTools(initCtx, externalClient, config.Name)
docs/openapi/paths/management/logging.yaml (1)

335-340: Align status description with single-value enum.
The description says “comma-separated list” while the schema restricts to a single enum value. Pick one behavior and make both endpoints consistent.

📝 Suggested fix (make description match enum)
-        description: Comma-separated list of statuses to filter by (processing, success, error)
+        description: Status to filter by (processing, success, error)
         schema:
           type: string
           enum: [processing, success, error]
-        description: Comma-separated list of statuses to filter by
+        description: Status to filter by
         schema:
           type: string
           enum: [processing, success, error]

Also applies to: 459-464

ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx (1)

123-132: Show 0ms latency instead of “NA”.
A latency of 0 is valid; the truthy check hides it.

🛠️ Suggested fix
-								value={moment(log.timestamp)
-									.add(log.latency || 0, "ms")
+								value={moment(log.timestamp)
+									.add(log.latency ?? 0, "ms")
 									.format("YYYY-MM-DD HH:mm:ss A")}
 							/>
-							<LogEntryDetailsView className="w-full" label="Latency" value={log.latency ? `${log.latency.toFixed(2)}ms` : "NA"} />
+							<LogEntryDetailsView
+								className="w-full"
+								label="Latency"
+								value={typeof log.latency === "number" ? `${log.latency.toFixed(2)}ms` : "NA"}
+							/>
core/mcp/codemodeexecutecode_test.go (1)

248-259: Truncation test has weak assertion.

The test only logs when the result is truncated but doesn't fail if truncation doesn't work correctly. If len(result) <= 200, the test passes silently without verifying the expected behavior.

ui/app/workspace/mcp-logs/views/emptyState.tsx (1)

44-47: Add error handling for clipboard operation.

navigator.clipboard.writeText() returns a Promise that can reject (e.g., permission denied, document not focused). Currently, failures are silent with no user feedback.

🩹 Proposed fix
 const copyToClipboard = () => {
-	navigator.clipboard.writeText(code);
-	toast.success("Copied to clipboard");
+	navigator.clipboard.writeText(code)
+		.then(() => toast.success("Copied to clipboard"))
+		.catch(() => toast.error("Failed to copy to clipboard"));
 };
transports/bifrost-http/handlers/logging.go (1)

830-837: Allow MCP log sorting by cost to match pagination contract.
PaginationOptions.SortBy already supports cost elsewhere, and MCP logs expose cost. Rejecting sort_by=cost blocks a valid sort option.

🛠️ Proposed fix
-	if sortBy == "timestamp" || sortBy == "latency" {
+	if sortBy == "timestamp" || sortBy == "latency" || sortBy == "cost" {
 		pagination.SortBy = sortBy
 	} else {
-		return nil, nil, fmt.Errorf("invalid sort_by: must be 'timestamp' or 'latency'")
+		return nil, nil, fmt.Errorf("invalid sort_by: must be 'timestamp', 'latency', or 'cost'")
 	}
transports/bifrost-http/server/plugins.go (1)

146-162: Handle registerPluginWithStatus errors for logging/governance.
Errors are ignored here, unlike telemetry. This can hide plugin startup failures and leave status misleading.

🛠️ Proposed fix
 	// 2. Logging (if enabled)
 	if s.Config.ClientConfig.EnableLogging && s.Config.LogsStore != nil {
 		config := &logging.Config{
 			DisableContentLogging: &s.Config.ClientConfig.DisableContentLogging,
 		}
-		s.registerPluginWithStatus(ctx, logging.PluginName, nil, config, false)
+		if err := s.registerPluginWithStatus(ctx, logging.PluginName, nil, config, false); err != nil {
+			logger.Warn("failed to register logging plugin: %v", err)
+		}
 	} else {
 		s.markPluginDisabled(logging.PluginName)
 	}

 	// 3. Governance (if enabled and not enterprise)
 	if s.Config.ClientConfig.EnableGovernance && ctx.Value(schemas.BifrostContextKeyIsEnterprise) == nil {
 		config := &governance.Config{
 			IsVkMandatory: &s.Config.ClientConfig.EnforceGovernanceHeader,
 		}
-		s.registerPluginWithStatus(ctx, governance.PluginName, nil, config, false)
+		if err := s.registerPluginWithStatus(ctx, governance.PluginName, nil, config, false); err != nil {
+			logger.Warn("failed to register governance plugin: %v", err)
+		}
 	} else {
 		s.markPluginDisabled(governance.PluginName)
 	}
docs/openapi/schemas/management/logging.yaml (2)

112-157: Document virtual_key_ fields and BifrostError for MCP logs.*
The backend MCP log model includes virtual_key_id/virtual_key_name, and error_details is a serialized BifrostError. The schema should reflect this.

🛠️ Proposed fix
 MCPToolLogEntry:
   type: object
   description: MCP tool execution log entry
   properties:
@@
     server_label:
       type: string
       description: Label of the MCP server that provided the tool
+    virtual_key_id:
+      type: string
+      description: Virtual key ID used for the tool execution
+    virtual_key_name:
+      type: string
+      nullable: true
+      description: Virtual key name
@@
-    error_details:
-      type: object
-      additionalProperties: true
-      description: Error details if execution failed
+    error_details:
+      $ref: '../../schemas/inference/common.yaml#/BifrostError'

159-200: Add virtual_key_ids filter to MCP schema.
Backend filters include virtual_key_ids, so the OpenAPI schema should expose it.

🛠️ Proposed fix
 MCPToolLogSearchFilters:
   type: object
   description: MCP tool log search filters
   properties:
@@
     llm_request_ids:
       type: array
       items:
         type: string
       description: Filter by linked LLM request IDs
+    virtual_key_ids:
+      type: array
+      items:
+        type: string
+      description: Filter by virtual key IDs
core/mcp/utils.go (2)

192-198: Stale comment example persists.

The comment at line 194 still references "calculator/add" but tool names are stored with - separator (e.g., "calculator-add").

📝 Suggested fix
-		// but tool names in ToolMap are stored with prefix (e.g., "calculator/add")
+		// but tool names in ToolMap are stored with prefix (e.g., "calculator-add")

224-230: Stale comment example.

Similar to line 194, line 226 references "calculator/add" but should use "calculator-add".

ui/app/workspace/mcp-logs/page.tsx (2)

182-200: Refresh stats after log deletion.

Deleting a log updates logs and totalItems but leaves the stat cards stale. Consider invoking fetchStats() after a successful delete to keep dashboard metrics consistent.

🛠️ Suggested fix
 const handleDelete = useCallback(
 	async (log: MCPToolLogEntry) => {
 		if (!hasDeleteAccess) {
 			throw new Error("No delete access");
 		}
 		try {
 			await deleteLogs({ ids: [log.id] }).unwrap();
 			setLogs((prevLogs) => prevLogs.filter((l) => l.id !== log.id));
 			setTotalItems((prev) => prev - 1);
+			fetchStats();
 		} catch (err) {
 			const errorMessage = getErrorMessage(err);
 			setError(errorMessage);
 			throw new Error(errorMessage);
 		}
 	},
-	[deleteLogs, hasDeleteAccess],
+	[deleteLogs, hasDeleteAccess, fetchStats],
 );

304-329: Guard stats updates against double-counting on repeated terminal log updates.

The stats update logic (lines 305-328) increments counters whenever log.status is "success" or "error", but doesn't check if the log was already in a terminal state before the update. If a completed log receives multiple WebSocket updates (e.g., cost backfill), stats will be double-counted.

🛠️ Suggested approach

Track the previous log's status before updating:

// In the update branch, before updating stats:
const existingLog = logs.find((l) => l.id === log.id);
const wasAlreadyTerminal = existingLog && 
    (existingLog.status === "success" || existingLog.status === "error");

// Only update stats if transitioning TO terminal state
if ((log.status === "success" || log.status === "error") && !wasAlreadyTerminal) {
    setStats((prevStats) => {
        // ... existing stats update logic
    });
}

Alternatively, refetch stats instead of incrementing to ensure accuracy.

framework/configstore/rdb.go (1)

885-894: Serialization mismatch between UpdateMCPClientConfig and BeforeSave for headers.

This map-based Updates approach marshals map[string]schemas.EnvVar directly (outputting full struct with value, env_var, and from_env fields), while TableMCPClient.BeforeSave converts headers to map[string]string. Since BeforeSave isn't triggered for map-based updates, this creates inconsistent JSON shapes and may persist resolved secrets.

♻️ Suggested fix to align with BeforeSave
-	headersJSON, err := json.Marshal(clientConfigCopy.Headers)
+	headersToSerialize := make(map[string]string, len(clientConfigCopy.Headers))
+	for key, value := range clientConfigCopy.Headers {
+		if value.IsFromEnv() {
+			headersToSerialize[key] = value.EnvVar
+		} else {
+			headersToSerialize[key] = value.GetValue()
+		}
+	}
+	headersJSON, err := json.Marshal(headersToSerialize)
transports/bifrost-http/handlers/plugins.go (2)

224-231: Error message implies a DB write that hasn't happened yet.

Plugin loading occurs before persistence, so "Plugin created in database but failed to load" is misleading.

📝 Suggested fix
 	if request.Enabled {
 		if err := h.pluginsLoader.ReloadPlugin(ctx, request.Name, request.Path, request.Config); err != nil {
 			logger.Error("failed to load plugin: %v", err)
-			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin created in database but failed to load: %v", err))
+			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin failed to load; not created: %v", err))
 			return
 		}
 	}

348-354: Error message should reflect stop vs reload.

When disabling a plugin, the failure message should say it failed to stop rather than "reload."

📝 Suggested fix
 	} else {
 		ctx.SetUserValue("isDisabled", true)
 		if err := h.pluginsLoader.RemovePlugin(ctx, name); err != nil {
-			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin updated in database but failed to reload: %v", err))
+			SendError(ctx, fasthttp.StatusInternalServerError, fmt.Sprintf("Plugin updated in database but failed to stop: %v", err))
 			return
 		}
 	}
examples/plugins/mcp-only/main.go (2)

95-112: Short-circuit response should match chat tool-call format.

This branch handles ChatAssistantMessageToolCall but returns a ResponsesMessage, which won't be interpreted as a tool-result in Chat flows. Return a ChatMessage tool result so callers receive the intended error.

🐛 Proposed fix
-				return req, &schemas.MCPPluginShortCircuit{
-					Response: &schemas.BifrostMCPResponse{
-						ResponsesMessage: &schemas.ResponsesMessage{
-							ResponsesToolMessage: &schemas.ResponsesToolMessage{
-								Error: &errorMsg,
-							},
-						},
-					},
-				}, nil
+				return req, &schemas.MCPPluginShortCircuit{
+					Response: &schemas.BifrostMCPResponse{
+						ChatMessage: &schemas.ChatMessage{
+							Role: schemas.ChatMessageRoleTool,
+							Content: &schemas.ChatMessageContent{
+								ContentStr: &errorMsg,
+							},
+							ChatToolMessage: &schemas.ChatToolMessage{
+								ToolCallID: req.ChatAssistantMessageToolCall.ID,
+							},
+						},
+					},
+				}, nil

114-121: Use the canonical request-id context key.

The audit trail is reading request_id using a custom key, but the canonical key is schemas.BifrostContextKeyRequestID, so this will always resolve to nil.

🐛 Suggested fix
-		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKey("request_id")))
+		auditMsg := fmt.Sprintf("MCP request processed at %v", ctx.Value(schemas.BifrostContextKeyRequestID))
framework/plugins/main.go (1)

64-78: Missing httpTransportStreamChunkHook check in AsHTTPTransportPlugin.

The HTTPTransportPlugin interface defines three hooks, but this function only checks two (httpTransportPreHook and httpTransportPostHook). A plugin implementing only the stream chunk hook would incorrectly return nil.

🔧 Suggested fix
 func AsHTTPTransportPlugin(plugin schemas.BasePlugin) schemas.HTTPTransportPlugin {
 	// Check if it's a DynamicPlugin first
 	if dp, ok := plugin.(*DynamicPlugin); ok {
 		// Only return as HTTPTransportPlugin if it actually has HTTP transport hooks
-		if dp.httpTransportPreHook != nil || dp.httpTransportPostHook != nil {
+		if dp.httpTransportPreHook != nil || dp.httpTransportPostHook != nil || dp.httpTransportStreamChunkHook != nil {
 			return dp
 		}
 		return nil
 	}
transports/bifrost-http/lib/config_test.go (1)

472-517: Preserve ClientID/ID when updating mock MCP clients.

UpdateMCPClientConfig rebuilds entries from clientConfig and can drop the stable ID or leave ClientID empty when callers rely on the id argument. Normalize ClientID from id and preserve the existing internal ID when updating.

🐛 Proposed fix
-	// Update the in-memory state to ensure GetMCPConfig returns updated data
-	for i := range m.mcpConfig.ClientConfigs {
-		if m.mcpConfig.ClientConfigs[i].ClientID == id {
-			// Found the entry, update it with the new config
-			m.mcpConfig.ClientConfigs[i] = tables.TableMCPClient{
-				ClientID:           clientConfig.ClientID,
-				Name:               clientConfig.Name,
-				IsCodeModeClient:   clientConfig.IsCodeModeClient,
-				ConnectionType:     clientConfig.ConnectionType,
-				ConnectionString:   clientConfig.ConnectionString,
-				StdioConfig:        clientConfig.StdioConfig,
-				Headers:            clientConfig.Headers,
-				ToolsToExecute:     clientConfig.ToolsToExecute,
-				ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-			}
-			return nil
-		}
-	}
-	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
-	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, tables.TableMCPClient{
-		ClientID:           clientConfig.ClientID,
-		Name:               clientConfig.Name,
-		IsCodeModeClient:   clientConfig.IsCodeModeClient,
-		ConnectionType:     clientConfig.ConnectionType,
-		ConnectionString:   clientConfig.ConnectionString,
-		StdioConfig:        clientConfig.StdioConfig,
-		Headers:            clientConfig.Headers,
-		ToolsToExecute:     clientConfig.ToolsToExecute,
-		ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
-	})
+	updated := clientConfig
+	if updated.ClientID == "" {
+		updated.ClientID = id
+	}
+
+	// Update the in-memory state to ensure GetMCPConfig returns updated data
+	for i := range m.mcpConfig.ClientConfigs {
+		if m.mcpConfig.ClientConfigs[i].ClientID == id {
+			updated.ID = m.mcpConfig.ClientConfigs[i].ID // preserve stable internal ID
+			m.mcpConfig.ClientConfigs[i] = updated
+			return nil
+		}
+	}
+	// If not found, create a new entry (similar to CreateMCPClientConfig behavior)
+	m.mcpConfig.ClientConfigs = append(m.mcpConfig.ClientConfigs, updated)
plugins/logging/utils.go (1)

188-210: Remove redundant nil guards in MCP metadata helpers.

PluginLogManager is always constructed via GetPluginLogManager() with an initialized plugin/store, so these guards are unnecessary and inconsistent with Search/GetStats.

♻️ Proposed diff
 func (p *PluginLogManager) GetAvailableToolNames(ctx context.Context) ([]string, error) {
-	if p == nil || p.plugin == nil || p.plugin.store == nil {
-		return []string{}, nil
-	}
 	return p.plugin.store.GetAvailableToolNames(ctx)
 }
@@
 func (p *PluginLogManager) GetAvailableServerLabels(ctx context.Context) ([]string, error) {
-	if p == nil || p.plugin == nil || p.plugin.store == nil {
-		return []string{}, nil
-	}
 	return p.plugin.store.GetAvailableServerLabels(ctx)
 }
@@
 func (p *PluginLogManager) GetAvailableMCPVirtualKeys(ctx context.Context) []KeyPair {
-	if p == nil || p.plugin == nil {
-		return []KeyPair{}
-	}
 	return p.plugin.GetAvailableMCPVirtualKeys(ctx)
 }

Based on learnings, rely on the construction invariant rather than nil guards.

core/mcp/codemodeexecutecode.go (2)

851-879: Propagate post-hook errors in short-circuit paths.

RunMCPPostHooks errors are currently ignored for short-circuit responses/errors, which can mask plugin failures. Handle finalErr the same way as the main path.

🛠️ Proposed diff
 	if shortCircuit != nil {
 		if shortCircuit.Response != nil {
-			finalResp, _ := pipeline.RunMCPPostHooks(nestedCtx, shortCircuit.Response, nil, preCount)
+			finalResp, finalErr := pipeline.RunMCPPostHooks(nestedCtx, shortCircuit.Response, nil, preCount)
+			if finalErr != nil {
+				if finalErr.Error != nil {
+					return nil, fmt.Errorf("%s", finalErr.Error.Message)
+				}
+				return nil, fmt.Errorf("plugin post-hooks returned error")
+			}
 			if finalResp != nil {
 				// Try ChatMessage first
 				if finalResp.ChatMessage != nil {
 					return extractResultFromChatMessage(finalResp.ChatMessage), nil
 				}
@@
 		if shortCircuit.Error != nil {
-			pipeline.RunMCPPostHooks(nestedCtx, nil, shortCircuit.Error, preCount)
+			_, finalErr := pipeline.RunMCPPostHooks(nestedCtx, nil, shortCircuit.Error, preCount)
+			if finalErr != nil {
+				if finalErr.Error != nil {
+					return nil, fmt.Errorf("%s", finalErr.Error.Message)
+				}
+				return nil, fmt.Errorf("plugin post-hooks returned error")
+			}
 			if shortCircuit.Error.Error != nil {
 				return nil, fmt.Errorf("%s", shortCircuit.Error.Error.Message)
 			}
 			return nil, fmt.Errorf("plugin short-circuit error")
 		}
 	}

1082-1085: Fix doc comment mismatch.

The comment mentions a boolean return, but the function returns (interface{}, error).

📝 Proposed diff
-// Returns the extracted result/error, and a boolean indicating if it was an error.
+// Returns the extracted result (if any) or a non-nil error when a tool error is present.
docs/openapi/schemas/management/mcp.yaml (1)

63-73: Resolve leftover merge conflict markers; YAML is still invalid.

The conflict markers under is_code_mode_client break parsing and are causing the OpenAPI bundle step to fail. Remove the markers and keep the intended fields.

🛠️ Suggested resolution
     is_code_mode_client:
       type: boolean
-<<<<<<< HEAD
-    is_ping_available:
-      type: boolean
-      default: true
-      description: |
-        Whether the MCP server supports ping for health checks.
-        If true, uses lightweight ping method for health checks.
-        If false, uses listTools method for health checks instead.
-=======
-      description: Whether this client is available in code mode
->>>>>>> 4e846193 (feat: plugin schema extensions for mcp plugins)
+      description: Whether this client is available in code mode
+    is_ping_available:
+      type: boolean
+      default: true
+      description: |
+        Whether the MCP server supports ping for health checks.
+        If true, uses lightweight ping method for health checks.
+        If false, uses listTools method for health checks instead.
     connection_type:
       $ref: '#/MCPConnectionType'
transports/bifrost-http/server/server.go (2)

182-186: Add a nil guard for ExecuteChatMCPTool inputs.

A nil toolCall will panic downstream; return a clear BifrostError instead.


653-735: Call Cleanup() on replaced/removed plugins to prevent leaks.

Reload/remove replaces plugin instances without invoking Cleanup(), which can leak goroutines or external resources.

🛠️ Suggested fix
 // 1. Instantiate new version
-	plugin, err := InstantiatePlugin(ctx, name, path, pluginConfig, s.Config)
+	oldPlugin, _ := s.Config.FindPluginByName(name)
+	plugin, err := InstantiatePlugin(ctx, name, path, pluginConfig, s.Config)
 	if err != nil {
 		return updateError("loading", err)
 	}

 // 2. Register (replaces old version atomically)
 	if err := s.Config.RegisterPlugin(plugin); err != nil {
 		return updateError("registering", err)
 	}
+	if oldPlugin != nil {
+		if err := oldPlugin.Cleanup(); err != nil {
+			logger.Warn("failed to cleanup old plugin %s: %v", name, err)
+		}
+	}
@@
-	if plugin, err := s.Config.FindPluginByName(name); err == nil {
-		_, isObservability = plugin.(schemas.ObservabilityPlugin)
-	}
+	var oldPlugin schemas.BasePlugin
+	if plugin, err := s.Config.FindPluginByName(name); err == nil {
+		oldPlugin = plugin
+		_, isObservability = plugin.(schemas.ObservabilityPlugin)
+	}
@@
 	if err := s.Config.UnregisterPlugin(name); err != nil {
 		return err
 	}
+	if oldPlugin != nil {
+		if err := oldPlugin.Cleanup(); err != nil {
+			logger.Warn("failed to cleanup plugin %s: %v", name, err)
+		}
+	}
core/bifrost.go (5)

2726-2743: Lazy MCP init is missing FetchNewRequestIDFunc.

When MCP is initialized via AddMCPClient, request IDs fall back to the default exec_* pattern instead of UUIDs, which is inconsistent with other init paths.
Line 2730.

🧩 Proposed fix
 			mcpConfig := schemas.MCPConfig{
 				ClientConfigs: []schemas.MCPClientConfig{},
 			}
+			mcpConfig.FetchNewRequestIDFunc = func() string {
+				return uuid.New().String()
+			}

69-72: Add missing mcpRequestPool field to avoid build failure.

Init(), getMCPRequest, and releaseMCPRequest reference bifrost.mcpRequestPool, but the struct doesn’t define it here, which will not compile.
Line 70–71.

🐛 Proposed fix
 	pluginPipelinePool  sync.Pool                           // Pool for PluginPipeline objects
 	bifrostRequestPool  sync.Pool                           // Pool for BifrostRequest objects
+	mcpRequestPool      sync.Pool                           // Pool for BifrostMCPRequest objects
 	logger              schemas.Logger                      // logger instance, default logger is used if not provided

322-324: ReloadConfig comment is still inaccurate.

The comment says account is updated, but the function only updates dropExcessRequests and plugin lists.
Line 323.

📝 Suggested fix
-// Currently we update account, drop excess requests, and plugin lists
+// Currently we update drop excess requests and plugin lists

3549-3549: Post-hook counts can drift during hot reloads.

Using len(*bifrost.llmPlugins.Load()) after PreHooks can skip or mis-order PostHooks if the plugin list changes mid-flight. Prefer the prehook snapshot (or the pipeline snapshot) for consistent execution.
Lines 3549, 3812, 4068.

🐛 Proposed fix
-	pluginCount := len(*bifrost.llmPlugins.Load())
+	pluginCount := preCount
-	recoveredResp, recoveredErr := pipeline.RunPostHooks(ctx, nil, &bifrostErrVal, len(*bifrost.llmPlugins.Load()))
+	recoveredResp, recoveredErr := pipeline.RunPostHooks(ctx, nil, &bifrostErrVal, preCount)
-				resp, bifrostErr := pipeline.RunPostHooks(ctx, result, err, len(*bifrost.llmPlugins.Load()))
+				resp, bifrostErr := pipeline.RunPostHooks(ctx, result, err, len(pipeline.llmPlugins))

Also applies to: 3812-3812, 4068-4069


2255-2266: reloadMCPPlugin can’t add the first MCP plugin.

When oldPlugins is nil, the method returns without adding the plugin, so the first MCP plugin can never be inserted via reload.
Line 2260.

🐛 Proposed fix
 	oldPlugins := bifrost.mcpPlugins.Load()
-	if oldPlugins == nil {
-		return nil
-	}
-	// Create new slice with replaced plugin
-	newPlugins := make([]schemas.MCPPlugin, len(*oldPlugins))
-	copy(newPlugins, *oldPlugins)
+	// Create new slice with replaced plugin or initialize empty slice
+	var newPlugins []schemas.MCPPlugin
+	if oldPlugins == nil {
+		newPlugins = make([]schemas.MCPPlugin, 0)
+	} else {
+		newPlugins = make([]schemas.MCPPlugin, len(*oldPlugins))
+		copy(newPlugins, *oldPlugins)
+	}
transports/bifrost-http/lib/config.go (1)

1484-1497: Propagate IsPingAvailable into TableMCPClient conversion.

The IsPingAvailable field from schemas.MCPClientConfig is not being propagated to configstoreTables.TableMCPClient. This can cause ping availability to incorrectly default to false for newly added MCP clients.

🔧 Proposed fix
 func convertSchemasMCPClientConfigToTable(clientConfig schemas.MCPClientConfig) configstoreTables.TableMCPClient {
 	return configstoreTables.TableMCPClient{
 		ClientID:           clientConfig.ID,
 		Name:               clientConfig.Name,
 		IsCodeModeClient:   clientConfig.IsCodeModeClient,
 		ConnectionType:     string(clientConfig.ConnectionType),
 		ConnectionString:   clientConfig.ConnectionString,
 		StdioConfig:        clientConfig.StdioConfig,
 		ToolsToExecute:     clientConfig.ToolsToExecute,
 		ToolsToAutoExecute: clientConfig.ToolsToAutoExecute,
 		Headers:            clientConfig.Headers,
+		IsPingAvailable:    clientConfig.IsPingAvailable,
 	}
 }
🧹 Nitpick comments (13)
examples/plugins/http-transport-only/main.go (1)

84-99: Avoid logging rate limit with a stale window value.
Line 90-91 logs pluginConfig.RateWindow before rate_window is parsed, so the log can be misleading when both are provided. Consider parsing rate_window first (or logging after both are parsed).

♻️ Suggested reorder
-		// Parse rate_limit
-		if rateLimit, ok := configMap["rate_limit"].(float64); ok {
-			pluginConfig.RateLimit = int(rateLimit)
-			if pluginConfig.RateLimit <= 0 {
-				fmt.Println("[HTTP-Transport-Only Plugin] Rate limiting disabled")
-			} else {
-				fmt.Printf("[HTTP-Transport-Only Plugin] Rate limit: %d requests per %d seconds\n",
-					pluginConfig.RateLimit, pluginConfig.RateWindow)
-			}
-		}
-
-		// Parse rate_window
-		if rateWindow, ok := configMap["rate_window"].(float64); ok {
-			pluginConfig.RateWindow = int(rateWindow)
-			fmt.Printf("[HTTP-Transport-Only Plugin] Rate window: %d seconds\n", pluginConfig.RateWindow)
-		}
+		// Parse rate_window
+		if rateWindow, ok := configMap["rate_window"].(float64); ok {
+			pluginConfig.RateWindow = int(rateWindow)
+			fmt.Printf("[HTTP-Transport-Only Plugin] Rate window: %d seconds\n", pluginConfig.RateWindow)
+		}
+
+		// Parse rate_limit
+		if rateLimit, ok := configMap["rate_limit"].(float64); ok {
+			pluginConfig.RateLimit = int(rateLimit)
+			if pluginConfig.RateLimit <= 0 {
+				fmt.Println("[HTTP-Transport-Only Plugin] Rate limiting disabled")
+			} else {
+				fmt.Printf("[HTTP-Transport-Only Plugin] Rate limit: %d requests per %d seconds\n",
+					pluginConfig.RateLimit, pluginConfig.RateWindow)
+			}
+		}
plugins/jsonparser/plugin_test.go (1)

74-77: Use bifrost.Ptr(...) for account pointers to match repo convention.

This keeps pointer creation consistent across the codebase.

♻️ Proposed change
@@
-		Account:    &account,
+		Account:    bifrost.Ptr(account),
@@
-		Account:    &account,
+		Account:    bifrost.Ptr(account),

Based on learnings, prefer bifrost.Ptr(...) for pointer creation.

Also applies to: 174-176

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

15-18: Unused store field in handler struct.

The store field is set in the constructor but never accessed in any handler method (executeTool, executeChatMCPTool, executeResponsesMCPTool). If it's intended for future use, consider documenting that intent; otherwise, remove it to avoid confusion.

ui/app/workspace/mcp-logs/views/emptyState.tsx (1)

85-247: Add baseUrl to the useMemo dependency array.

The useMemo computes baseUrl from getExampleBaseUrl() inside the callback but has an empty dependency array. While getExampleBaseUrl() likely returns a stable value during component lifecycle, the memoization intent is better expressed by including the computed value in dependencies.

♻️ Proposed fix
+	const baseUrl = getExampleBaseUrl();
+
 	// Generate examples dynamically using the port utility
 	const examples: Examples = useMemo(() => {
-		const baseUrl = getExampleBaseUrl();
-
 		return {
 			manual: {
 				python: `import openai
 // ... rest of examples using baseUrl
 		};
-	}, []);
+	}, [baseUrl]);
core/mcp/utils.go (1)

616-682: Consider guarding against circular schema references.

FixArraySchemas recurses into nested structures without tracking visited nodes. If a malicious or malformed MCP tool schema contains circular $ref references (after resolution), this could cause a stack overflow.

Since tool schemas come from external MCP servers, consider adding a depth limit or visited set to prevent unbounded recursion.

💡 Example depth-limited approach
func FixArraySchemas(properties map[string]interface{}) {
    fixArraySchemasWithDepth(properties, 0, 50) // max 50 levels
}

func fixArraySchemasWithDepth(properties map[string]interface{}, depth, maxDepth int) {
    if depth > maxDepth {
        logger.Warn(fmt.Sprintf("%s Schema recursion depth exceeded, stopping fix", MCPLogPrefix))
        return
    }
    // ... existing logic, but call fixArraySchemasWithDepth with depth+1
}
transports/bifrost-http/handlers/plugins.go (1)

120-137: Use PluginResponse type instead of anonymous struct for consistency.

The PluginResponse type was introduced but this code path still uses an anonymous struct with identical fields. This creates maintenance burden and inconsistency.

♻️ Suggested fix
-		finalPlugins = append(finalPlugins, struct {
-			Name       string               `json:"name"`
-			ActualName string               `json:"actualName"`
-			Enabled    bool                 `json:"enabled"`
-			Config     any                  `json:"config"`
-			IsCustom   bool                 `json:"isCustom"`
-			Path       *string              `json:"path"`
-			Status     schemas.PluginStatus `json:"status"`
-		}{
+		finalPlugins = append(finalPlugins, PluginResponse{
 			Name:       plugin.Name,
 			ActualName: pluginStatus.Name,
 			Enabled:    plugin.Enabled,
 			Config:     plugin.Config,
 			IsCustom:   plugin.IsCustom,
 			Path:       plugin.Path,
 			Status:     pluginStatus,
 		})
core/mcp/mcp.go (1)

82-100: Consider logging when plugin pipeline type assertion fails.

The adapter silently returns nil if the type assertion fails. This could make debugging difficult when plugin pipelines are misconfigured.

💡 Suggested improvement
 	if config.PluginPipelineProvider != nil && config.ReleasePluginPipeline != nil {
 		pluginPipelineProvider = func() PluginPipeline {
 			if pipeline := config.PluginPipelineProvider(); pipeline != nil {
 				if pp, ok := pipeline.(PluginPipeline); ok {
 					return pp
+				} else {
+					logger.Warn("%s Plugin pipeline does not implement expected interface", MCPLogPrefix)
 				}
 			}
 			return nil
 		}
framework/configstore/tables/mcp.go (1)

139-144: Inconsistent JSON library usage: json.Unmarshal vs sonic.Unmarshal.

The ToolPricing field uses json.Unmarshal while all other fields in AfterFind use sonic.Unmarshal. For consistency and performance, consider using sonic.Unmarshal here as well.

♻️ Suggested fix
 	if c.ToolPricingJSON != "" {
-		if err := json.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
+		if err := sonic.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
 			return err
 		}
 	}
plugins/semanticcache/search.go (1)

85-88: Update comment to reference PostLLMHook for consistency.

The comment mentions "PostHook storage" but should reference "PostLLMHook" to align with the renamed hook (as updated on line 124).

♻️ Suggested fix
-// generateEmbeddingsForStorage generates embeddings and stores them in context for PostHook storage.
+// generateEmbeddingsForStorage generates embeddings and stores them in context for PostLLMHook storage.
 // This is used when the vector store requires vectors but we're in direct-only cache mode.
framework/plugins/soplugin_test.go (1)

649-652: Consider using bifrost.Ptr() for consistency.

Based on learnings, the codebase prefers bifrost.Ptr() over local helper functions like stringPtr. This improves consistency across test utilities.

♻️ Suggested refactor
-// Helper function to create a pointer to a string
-func stringPtr(s string) *string {
-	return &s
-}
+// Use bifrost.Ptr() instead of this helper

Then replace usages like stringPtr("Hello") with bifrost.Ptr("Hello").

plugins/mocker/main.go (2)

499-502: Remove redundant Enabled guard in PreLLMHook.

The loader excludes disabled plugins, so this check is dead code and adds unnecessary branching.

♻️ Proposed diff
-	// Skip processing if plugin is disabled
-	if !p.config.Enabled {
-		return req, nil, nil
-	}

Based on learnings, rely on the loader to skip disabled plugins.


805-853: Use bifrost.Ptr for string pointers in mock responses.

Prefer bifrost.Ptr(...) over &localVar for pointer fields to match repo conventions.

♻️ Proposed diff
-	// Get finish reason with minimal allocation
-	var finishReason *string
-	if content.FinishReason != nil {
-		finishReason = content.FinishReason
-	} else {
-		// Use a static string to avoid allocation
-		static := "stop"
-		finishReason = &static
-	}
+	// Get finish reason with minimal allocation
+	finishReason := content.FinishReason
+	if finishReason == nil {
+		finishReason = bifrost.Ptr("stop")
+	}

 ...
-								ContentStr: &message,
+								ContentStr: bifrost.Ptr(message),
 ...
-						ContentStr: &message,
+						ContentStr: bifrost.Ptr(message),

Based on learnings, prefer bifrost.Ptr consistently for pointer fields.

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

2380-2420: Consider simplifying - CAS loop is redundant with mutex.

The pluginsMu mutex already guarantees exclusive access, so the CompareAndSwap loop will never fail and retry. The implementation is correct but could be simplified by directly storing the new slice without the CAS loop.

♻️ Optional simplification
 func (c *Config) RegisterPlugin(plugin schemas.BasePlugin) error {
 	c.pluginsMu.Lock()
 	defer c.pluginsMu.Unlock()
 
 	name := plugin.GetName()
+	oldPlugins := c.BasePlugins.Load()
+	var newPlugins []schemas.BasePlugin
 
-	for {
-		oldPlugins := c.BasePlugins.Load()
-		var newPlugins []schemas.BasePlugin
-
-		if oldPlugins == nil {
-			newPlugins = []schemas.BasePlugin{plugin}
-		} else {
-			newPlugins = make([]schemas.BasePlugin, 0, len(*oldPlugins)+1)
-
-			replaced := false
-			for _, p := range *oldPlugins {
-				if p.GetName() == name {
-					newPlugins = append(newPlugins, plugin)
-					replaced = true
-				} else {
-					newPlugins = append(newPlugins, p)
-				}
-			}
-
-			if !replaced {
-				newPlugins = append(newPlugins, plugin)
+	if oldPlugins == nil {
+		newPlugins = []schemas.BasePlugin{plugin}
+	} else {
+		newPlugins = make([]schemas.BasePlugin, 0, len(*oldPlugins)+1)
+		replaced := false
+		for _, p := range *oldPlugins {
+			if p.GetName() == name {
+				newPlugins = append(newPlugins, plugin)
+				replaced = true
+			} else {
+				newPlugins = append(newPlugins, p)
 			}
 		}
-
-		if c.BasePlugins.CompareAndSwap(oldPlugins, &newPlugins) {
-			c.rebuildInterfaceCaches()
-			return nil
+		if !replaced {
+			newPlugins = append(newPlugins, plugin)
 		}
-		// CAS failed, retry with new snapshot
 	}
+
+	c.BasePlugins.Store(&newPlugins)
+	c.rebuildInterfaceCaches()
+	return nil
 }

@akshaydeo akshaydeo force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 5089d3f to 0c41966 Compare January 26, 2026 19:19
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: 7

Caution

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

⚠️ Outside diff range comments (6)
docs/features/governance/virtual-keys.mdx (1)

493-544: Clarify that enforcement requires any virtual key header, not x-bf-vk specifically.

The enforcement check requires a virtual key to be present via any of the supported headers (x-bf-vk, Authorization Bearer, x-api-key, x-goog-api-key), but the documentation text "the request will be rejected if the x-bf-vk header is not present" only mentions one header. Update to reflect that enforcement accepts any supported virtual key header.

✏️ Suggested wording tweak
-When the governance header is enforced, the request will be rejected if the `x-bf-vk` header is not present.
+When enforcement is enabled, the request will be rejected if a virtual key is not provided via any of the supported headers (`x-bf-vk`, `Authorization` with Bearer token, `x-api-key`, or `x-goog-api-key`).
framework/plugins/soplugin.go (1)

67-70: Missing nil guard on HTTPTransportStreamChunkHook creates potential nil-pointer dereference.

All other optional hooks (HTTPTransportPreHook, HTTPTransportPostHook, PreLLMHook, PostLLMHook, PreMCPHook, PostMCPHook, Inject) have nil guards that return no-ops when unimplemented. However, HTTPTransportStreamChunkHook directly calls dp.httpTransportStreamChunkHook without checking for nil, which will panic if the plugin doesn't export this symbol.

🛡️ Proposed fix to add nil guard
 // HTTPTransportStreamChunkHook intercepts streaming chunks before they are written to the client
 func (dp *DynamicPlugin) HTTPTransportStreamChunkHook(ctx *schemas.BifrostContext, req *schemas.HTTPRequest, stream *schemas.BifrostStreamChunk) (*schemas.BifrostStreamChunk, error) {
+	if dp.httpTransportStreamChunkHook == nil {
+		return stream, nil // No-op if not implemented
+	}
 	return dp.httpTransportStreamChunkHook(ctx, req, stream)
 }
docs/plugins/writing-go-plugin.mdx (1)

547-562: Documentation inconsistency: v1.4.x streaming tab still shows old hook names.

Lines 550-552 show PreHook called and PostHook called but should show PreLLMHook called and PostLLMHook called for consistency with v1.4.x.

📝 Suggested fix
   <Tab title="v1.4.x+ (streaming)">

HTTPTransportPreHook called
-PreHook called
-PostHook called
+PreLLMHook called
+PostLLMHook called
HTTPTransportStreamChunkHook called (per chunk)

  </Tab>
plugins/governance/main.go (1)

690-727: Don’t skip provider/model usage tracking when VK is missing.
At Line 694 the early return prevents usage updates for non‑VK traffic, which will undercount provider/model budgets and rate limits even though the tracker supports empty virtual keys.

🛠️ Proposed fix
-	// Skip if no virtual key
-	if virtualKey == "" {
-		return result, err, nil
-	}
+	// Continue even without a virtual key so provider/model usage is tracked
transports/bifrost-http/lib/config.go (2)

1499-1594: MCPCatalog init ignores store pricing when ConfigStore is enabled.
mcpPricingConfig is built from the store but then not used; mcpcatalog.Init always uses file pricing. That drops DB pricing on startup and after restarts.

🔧 Proposed fix
-	mcpPricingConfig := &mcpcatalog.Config{}
+	mcpPricingConfig := &mcpcatalog.Config{
+		PricingData: buildMCPPricingDataFromFile(ctx, configData),
+	}
 	if config.ConfigStore != nil {
 		frameworkConfig, err := config.ConfigStore.GetFrameworkConfig(ctx)
 		if err != nil {
 			logger.Warn("failed to get framework config from store: %v", err)
 		}
@@
-		mcpPricingConfig.PricingData = buildMCPPricingDataFromStore(ctx, config.ConfigStore)
+		mcpPricingConfig.PricingData = buildMCPPricingDataFromStore(ctx, config.ConfigStore)
 	} else if configData.FrameworkConfig != nil && configData.FrameworkConfig.Pricing != nil {
 		pricingConfig.PricingURL = configData.FrameworkConfig.Pricing.PricingURL
 		syncDuration := time.Duration(*configData.FrameworkConfig.Pricing.PricingSyncInterval) * time.Second
 		pricingConfig.PricingSyncInterval = &syncDuration
 	}
@@
-	mcpCatalog, err := mcpcatalog.Init(ctx, &mcpcatalog.Config{
-		PricingData: buildMCPPricingDataFromFile(ctx, configData),
-	}, logger)
+	mcpCatalog, err := mcpcatalog.Init(ctx, mcpPricingConfig, logger)

83-117: Update config schema to include all MCP client fields exposed by the table types.

The mcp_client_config schema definition (line 1763 in transports/config.schema.json) is incomplete. It defines only name and connection_type as properties with "additionalProperties": false, but TableMCPClient exposes additional fields via JSON tags that are missing from the schema: client_id, is_code_mode_client, connection_string, headers, tools_to_execute, tools_to_auto_execute, is_ping_available, and config_hash.

While schema validation is not enforced at runtime during config loading, the schema doc should match the actual JSON structure. Additionally, verify that client_id generation (currently handled by migration for DB records) is also applied when configs are loaded from JSON files to ensure consistency across both initialization paths.

🤖 Fix all issues with AI agents
In `@docs/plugins/writing-go-plugin.mdx`:
- Around line 209-223: The v1.3.x docs incorrectly show v1.4.x hook names
(PreLLMHook/PostLLMHook); update the v1.3.x tab to use the legacy hook names and
signatures (PreHook and PostHook) instead of PreLLMHook/PostLLMHook, and adjust
any parameter names/types in that tab to match the v1.3.x API so readers of the
v1.3.x section see PreHook/PreHook signature and PostHook/PostHook signature
rather than the v1.4 names.

In `@framework/configstore/rdb.go`:
- Around line 888-915: The update path is unintentionally overwriting existing
is_ping_available because clientConfigCopy.IsPingAvailable is a bool zero-value
and the no-op if block does nothing; change the request type to use *bool for
IsPingAvailable (or add explicit presence detection) so you can tell "not set"
vs "false", then in the update logic only set "is_ping_available" in the updates
map when the pointer is non-nil (preserving the DB value otherwise); also update
mergeMCPRedactedValues or the HTTP handler that prepares clientConfigCopy to
preserve the existing IsPingAvailable when the incoming request omits it.

In `@transports/bifrost-http/handlers/plugins.go`:
- Around line 147-160: The loop over plugins from
h.pluginsLoader.GetPluginStatus(ctx) is comparing ctx.UserValue("name") to
pluginStatus.Name which misses matches when the map key is the actual plugin
identifier; change the comparison to use the map key (the loop variable `name`)
against ctx.UserValue("name") — e.g., retrieve the request name into a string
and replace `if pluginStatus.Name == ctx.UserValue("name")` with `if name ==
requestName` — then populate the PluginResponse (Name, ActualName, Enabled,
Config, IsCustom, Path, Status) as before so the handler correctly matches
plugins when h.configStore == nil.

In `@transports/bifrost-http/lib/config_test.go`:
- Around line 455-466: The mock mapping drops IsPingAvailable and ConfigHash
causing divergence; update the conversion that builds tables.TableMCPClient from
clientConfig (the tableClient construction) to set IsPingAvailable:
clientConfig.IsPingAvailable and ConfigHash: clientConfig.ConfigHash (or the
appropriately typed field) so the TableMCPClient includes those two fields and
mirrors the source MCPClientConfig.

In `@transports/bifrost-http/lib/config.go`:
- Around line 2257-2378: AppendPluginStateLogs currently appends directly to
oldEntry.Logs which can reuse the backing array shared with readers (e.g.,
GetPluginStatus) and cause races; instead, allocate a new slice copy of
oldEntry.Logs (with capacity len(oldEntry.Logs)+len(logs)), copy the existing
logs into it, then append the new logs and assign that new slice to
newEntry.Logs before storing back into c.pluginStatus[name]; update
AppendPluginStateLogs (and ensure it still holds pluginStatusMu) to use this
defensive copy.

In `@ui/app/workspace/mcp-logs/views/emptyState.tsx`:
- Around line 76-78: MCPEmptyStateProps and the MCPEmptyState component
currently accept and use isSocketConnected (and similar streaming-aware logic
used elsewhere); remove the isSocketConnected prop and any
socket/streaming-specific rendering from MCPEmptyState (and the code paths
referenced around the other block), and instead accept a simple display
string/ReactNode prop (e.g., message or details) that the parent can supply, or
move the live connection indicator rendering into the parent component so
MCPEmptyState only renders the provided static data (update prop types and all
usages of MCPEmptyState to stop passing isSocketConnected).

In `@ui/app/workspace/mcp-logs/views/mcpLogDetailsSheet.tsx`:
- Around line 75-113: The AlertDialog currently closes automatically because
AlertDialogAction triggers closure; change the dialog to be controlled by
passing open={open} and onOpenChange={onOpenChange} to AlertDialog (or the sheet
component) and update the AlertDialogAction click handler to accept the event
and call e.preventDefault() before running the async delete; call await
handleDelete(log) and only call onOpenChange(false) after success, and show
toast.error on failure without closing the dialog so the user can retry—update
references in this file to AlertDialog, AlertDialogAction, onOpenChange,
handleDelete, and log accordingly.
🧹 Nitpick comments (10)
plugins/governance/model_provider_governance_test.go (1)

1663-1667: Inconsistent naming: PostHook test names should be updated to PostLLMHook.

The section header and test function names still use PostHook terminology, but the actual method calls have been updated to PostLLMHook. For consistency with the PreLLMHook tests (which were properly renamed), these should also be updated.

♻️ Suggested fix

Update the section header:

 // ============================================================================
-// End-to-End Tests - PostHook Integration (Usage Tracking)
+// End-to-End Tests - PostLLMHook Integration (Usage Tracking)
 // ============================================================================

And rename the test functions (at lines 1667, 1735, 1803, 1871):

-func TestPostHook_UpdatesProviderBudgetUsage_NoVirtualKey(t *testing.T) {
+func TestPostLLMHook_UpdatesProviderBudgetUsage_NoVirtualKey(t *testing.T) {

-func TestPostHook_UpdatesProviderRateLimitUsage_NoVirtualKey(t *testing.T) {
+func TestPostLLMHook_UpdatesProviderRateLimitUsage_NoVirtualKey(t *testing.T) {

-func TestPostHook_UpdatesModelBudgetUsage_NoVirtualKey(t *testing.T) {
+func TestPostLLMHook_UpdatesModelBudgetUsage_NoVirtualKey(t *testing.T) {

-func TestPostHook_UpdatesModelRateLimitUsage_NoVirtualKey(t *testing.T) {
+func TestPostLLMHook_UpdatesModelRateLimitUsage_NoVirtualKey(t *testing.T) {
transports/bifrost-http/handlers/plugins.go (1)

120-136: Use PluginResponse instead of an anonymous struct.

This branch can return PluginResponse directly to match the other path and avoid duplicate struct definitions.

♻️ Suggested refactor
-		finalPlugins = append(finalPlugins, struct {
-			Name       string               `json:"name"`
-			ActualName string               `json:"actualName"`
-			Enabled    bool                 `json:"enabled"`
-			Config     any                  `json:"config"`
-			IsCustom   bool                 `json:"isCustom"`
-			Path       *string              `json:"path"`
-			Status     schemas.PluginStatus `json:"status"`
-		}{
+		finalPlugins = append(finalPlugins, PluginResponse{
 			Name:       plugin.Name,
 			ActualName: pluginStatus.Name,
 			Enabled:    plugin.Enabled,
 			Config:     plugin.Config,
 			IsCustom:   plugin.IsCustom,
 			Path:       plugin.Path,
 			Status:     pluginStatus,
 		})
examples/plugins/mcp-only/main.go (1)

100-106: Prefer bifrost.Ptr(...) for string pointers.

Use the project’s pointer helper instead of &errorMsg to match repo conventions.

♻️ Suggested refactor
-							Content: &schemas.ChatMessageContent{
-								ContentStr: &errorMsg,
-							},
+							Content: &schemas.ChatMessageContent{
+								ContentStr: bifrost.Ptr(errorMsg),
+							},

Based on learnings, prefer bifrost.Ptr(...) for pointer creation.

framework/logstore/tables.go (1)

507-557: Consider adding error logging for deserialization failures for debugging visibility.

The DeserializeFields method silently swallows JSON unmarshal errors by setting fields to nil. While this prevents operation failures (matching the pattern in Log.DeserializeFields), it may hide data corruption issues. Consider adding debug-level logging for these failures to aid troubleshooting, similar to the comment pattern "Log error but don't fail the operation" used in the Log struct.

core/schemas/mcp.go (1)

27-34: Consider documenting the expected concrete type for interface{}.

The PluginPipelineProvider and ReleasePluginPipeline fields use interface{} (likely to avoid import cycles). Consider adding a doc note about the expected concrete type that callers should provide/receive, to help consumers understand what to pass and assert.

Suggested doc enhancement
 // PluginPipelineProvider returns a plugin pipeline for running MCP plugin hooks.
 // Used when executeCode tool calls nested MCP tools to ensure plugins run for them.
 // The plugin pipeline should be released back to the pool using ReleasePluginPipeline.
+// The returned interface{} is expected to be a *PluginPipeline from the bifrost core package.
 PluginPipelineProvider func() interface{} `json:"-"`

 // ReleasePluginPipeline releases a plugin pipeline back to the pool.
 // This should be called after the plugin pipeline is no longer needed.
+// The pipeline parameter is expected to be a *PluginPipeline from the bifrost core package.
 ReleasePluginPipeline func(pipeline interface{}) `json:"-"`
framework/logstore/migrations.go (1)

693-753: Consider using transactional options for the MCP table creation migration.

Most other migrations explicitly enable transactions. Aligning this one keeps table + index creation atomic and consistent with the rest of the file.

♻️ Suggested adjustment
 func migrationCreateMCPToolLogsTable(ctx context.Context, db *gorm.DB) error {
-	m := migrator.New(db, migrator.DefaultOptions, []*migrator.Migration{{
+	opts := *migrator.DefaultOptions
+	opts.UseTransaction = true
+	m := migrator.New(db, &opts, []*migrator.Migration{{
 		ID: "mcp_tool_logs_init",
 		Migrate: func(tx *gorm.DB) error {
 			tx = tx.WithContext(ctx)
transports/bifrost-http/handlers/mcp.go (1)

410-418: Prefer bifrost.Ptr for EnvVar pointer creation.

Use the repo’s pointer helper for consistency.

♻️ Proposed tweak
-					if incomingValue.IsRedacted() && incomingValue.Equals(&oldRedactedValue) {
+					if incomingValue.IsRedacted() && incomingValue.Equals(bifrost.Ptr(oldRedactedValue)) {
 						merged.Headers[key] = oldRawValue
 					} else {
 						merged.Headers[key] = incomingValue
 					}
Based on learnings, prefer bifrost.Ptr over address-of for pointers.
framework/configstore/tables/mcp.go (1)

139-144: Inconsistent JSON library usage: json.Unmarshal vs sonic.Unmarshal.

ToolPricingJSON deserialization uses encoding/json.Unmarshal (line 141), while all other fields in AfterFind use sonic.Unmarshal (lines 119, 125, 130, 135). Similarly, BeforeSave uses encoding/json.Marshal for all fields. This inconsistency is minor but could cause subtle differences in edge cases.

Consider using sonic.Unmarshal for consistency:

♻️ Suggested consistency fix
 	if c.ToolPricingJSON != "" {
-		if err := json.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
+		if err := sonic.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
 			return err
 		}
 	}
plugins/logging/main.go (1)

786-795: Prefer bifrost.Ptr for pointer fields.

Use the repo’s pointer helper for consistency.

♻️ Suggested change
 if parentRequestID != "" {
-	entry.LLMRequestID = &parentRequestID
+	entry.LLMRequestID = bifrost.Ptr(parentRequestID)
 }
 
 if virtualKeyID != "" {
-	entry.VirtualKeyID = &virtualKeyID
+	entry.VirtualKeyID = bifrost.Ptr(virtualKeyID)
 }
 if virtualKeyName != "" {
-	entry.VirtualKeyName = &virtualKeyName
+	entry.VirtualKeyName = bifrost.Ptr(virtualKeyName)
 }
Based on learnings, prefer `bifrost.Ptr` over `&value` for pointer creation.
transports/bifrost-http/lib/config_test.go (1)

450-486: Prefer bifrost.Ptr(...) for pointer creation in tests.
This keeps pointer creation consistent with repo conventions (add the import if needed).

♻️ Proposed refactor
-		m.mcpConfig = &tables.MCPConfig{
-			ClientConfigs: []tables.TableMCPClient{},
-		}
+		m.mcpConfig = bifrost.Ptr(tables.MCPConfig{
+			ClientConfigs: []tables.TableMCPClient{},
+		})
-		m.mcpConfig = &tables.MCPConfig{
-			ClientConfigs: []tables.TableMCPClient{},
-		}
+		m.mcpConfig = bifrost.Ptr(tables.MCPConfig{
+			ClientConfigs: []tables.TableMCPClient{},
+		})
Based on learnings, prefer `bifrost.Ptr` for pointer creation.

Also applies to: 481-486

@akshaydeo akshaydeo mentioned this pull request Jan 27, 2026
16 tasks
@akshaydeo akshaydeo force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 0c41966 to 918feb3 Compare January 27, 2026 18:09
@akshaydeo akshaydeo mentioned this pull request Jan 27, 2026
16 tasks
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: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (1)
core/internal/llmtests/list_models.go (1)

83-86: Unreachable continue statement after t.Fatalf.

t.Fatalf terminates the test immediately via runtime.Goexit(), so the continue on line 86 will never execute.

Proposed fix
 		if model.ID == "" {
-			t.Fatalf("❌ Model at index %d has empty ID", i)
-			continue
+			t.Fatalf("❌ Model at index %d has empty ID", i)
 		}
🤖 Fix all issues with AI agents
In `@core/internal/mcptests/agent_test_helpers.go`:
- Around line 459-465: The test contains a debug fmt.Println("bifrostErr",
bifrostErr.Error.Message) which bypasses the testing framework; remove this
fmt.Println call and either drop it entirely or replace it with t.Logf (e.g.,
t.Logf("bifrostErr: %v", bifrostErr.Error.Message)) so test output is managed by
the testing framework—update the code around bifrostErr in the test helper (look
for the bifrostErr variable and the surrounding assertions
assert.Nil/require.NotNil) accordingly.
- Around line 493-504: The helper CreateToolCall currently calls
MustMarshalJSON(&testing.T{}, args) which creates a disconnected testing.T so
marshal failures won't be reported; replace that by marshaling directly: call
json.Marshal(args) inside CreateToolCall (or use jsoniter if project prefers),
check the error and panic (or use a helper that panics) on non-nil error, then
pass the resulting []byte into
schemas.ChatAssistantMessageToolCall.Function.Arguments; update imports to
include "encoding/json" and remove the fake &testing.T usage and any reference
to MustMarshalJSON in this function.

In `@core/internal/mcptests/codemode_basic_test.go`:
- Around line 598-604: The test block uses hasError without declaring it; when
checking gotestObj["error"] assign and declare the boolean in scope (e.g., use
hasError := ...) before asserting; update the gotest section where
gotestVal/gotestObj are extracted to declare hasError with short variable
declaration when retrieving the map key.
- Around line 485-512: The compile error comes from using hasError outside the
inner if-scope where it was declared; declare hasError in the outer scope before
the allowed_temp check (e.g., var hasError bool) and then assign to it inside
the tempObj check (use = not :=), so subsequent uses when inspecting
blockedEcho["error"] and blockedInprocess["error"] refer to the same
outer-scoped hasError; update the occurrences around allowedTemp/tempObj,
blockedEcho and blockedInprocess to use the outer hasError variable.

In `@core/internal/mcptests/codemode_security_test.go`:
- Around line 157-238: The test TestExecuteToolCode_CodeInjectionAttempts
currently only logs when a supposed-failing injection (tc.shouldFail==true)
succeeds; update the test to assert failure instead of just logging: after
calling bifrost.ExecuteChatMCPTool and parsing the response with
ParseCodeModeResponse, if tc.shouldFail and you do not observe bifrostErr,
hasError, or an "error" field in the returned object, call t.Fatalf (or
t.Errorf+return) to fail the test with a clear message referencing tc.name; also
ensure tests that expect success (tc.shouldFail==false) assert that no error was
returned by bifrost.ExecuteChatMCPTool / ParseCodeModeResponse so regressions
are caught.

In `@core/internal/mcptests/codemode_stdio_test.go`:
- Around line 357-461: In TestCodeMode_STDIO_ServerFiltering update the
negative-case branch (where tc.shouldSucceed is false) to fail the test if no
error is observed: after checking bifrostErr and after inspecting result via
ParseCodeModeResponse (hasError or "error" field), add a hard failure (e.g.,
require.FailNow or t.Fatalf) when neither a bifrostErr nor an observed error
string/field is present; modify the logic around bifrostErr, result,
ParseCodeModeResponse and the returnValue/errorField checks to ensure the test
explicitly fails when filtering does not produce any error.
- Around line 34-110: The InitMCPServerPaths function currently performs a
non-atomic check-then-set on the global mcpServerPaths causing races when tests
run t.Parallel(); change its initialization to use a sync.Once (declare a
package-level once variable, e.g., initMCPServerPathsOnce) and wrap the existing
initialization logic inside once.Do(...) so mcpServerPaths is set exactly once;
follow the same pattern used for the config variable in this file and ensure
InitMCPServerPaths simply calls initMCPServerPathsOnce.Do with the original
setup logic to eliminate the race.

In `@core/internal/mcptests/codemode_vs_noncodemode_test.go`:
- Around line 587-608: The current extractReturnValue function uses a regex (re)
that fails on nested JSON; replace the regex approach by locating the "Return
value:" marker in formattedResponse, take the substring starting at the first
non-space char after the marker, create a json.Decoder on
strings.NewReader(substring) and call decoder.Decode(&result) to parse the first
complete JSON value (this handles nested braces/brackets and ignores trailing
text); update imports to include "strings" and handle returned decode errors,
and remove the regexp usage and re variable.

In `@Makefile`:
- Around line 775-788: The REPORT_FILE path currently uses raw TESTCASE and
PATTERN values which may contain '/' and create nested paths; sanitize these by
normalizing TESTCASE and PATTERN (e.g., replace '/' with '_' or otherwise strip
path separators) before assigning REPORT_FILE so gotestsum writes a flat
filename; update the branches that set REPORT_FILE (the blocks referencing
TESTCASE, PATTERN, REPORT_FILE, TEST_REPORTS_DIR and the gotestsum invocations)
and apply the same normalization to the duplicate block later (the other
REPORT_FILE assignment around the 815-828 area) so both uses produce safe
filenames.
- Around line 736-742: The Makefile loop that builds examples/mcps/* currently
masks failures because it continues and can exit zero if the last build
succeeds; modify the loop to track failures by initializing a counter (e.g.,
build_failures=0) before the for mcp_dir loop, increment that counter whenever
the go build for an mcp (identified by mcp_dir / mcp_name) fails, and after the
loop check the counter and exit with a non‑zero status (exit 1) if any failures
occurred, otherwise print the existing "All MCP test servers built." success
message.
🟡 Minor comments (12)
core/internal/mcptests/codemode_vs_noncodemode_test.go-115-122 (1)

115-122: Unsafe type assertion may cause test panic.

The type assertion errorMsg.(string) at line 121 will panic if errorMsg is nil or not a string type. Use the two-value form for defensive test code.

🔧 Suggested fix
 	noncodemodeFailMap, ok := noncodemodeFailRaw.(map[string]interface{})
 	require.True(t, ok, "noncodemode_fail should be object")
 	errorMsg, hasError := noncodemodeFailMap["error"]
 	assert.True(t, hasError, "Non-CodeMode client should have error")
-	assert.Contains(t, errorMsg.(string), "not", "error should indicate tool not available")
+	if errorStr, ok := errorMsg.(string); ok {
+		assert.Contains(t, errorStr, "not", "error should indicate tool not available")
+	} else {
+		t.Errorf("expected error to be string, got %T", errorMsg)
+	}
core/internal/mcptests/codemode_vs_noncodemode_test.go-229-241 (1)

229-241: Multiple unsafe type assertions in loop may cause test panic.

Lines 231-233 contain chained type assertions without safety checks. If the response structure differs, the test panics instead of failing with a clear message.

🔧 Suggested fix
 	// Check each result
 	for _, resultRaw := range resultsArray {
-		resultMap := resultRaw.(map[string]interface{})
-		serverName := resultMap["server"].(string)
-		success := resultMap["success"].(bool)
+		resultMap, ok := resultRaw.(map[string]interface{})
+		require.True(t, ok, "result item should be object, got %T", resultRaw)
+		
+		serverNameRaw, ok := resultMap["server"]
+		require.True(t, ok, "result should have 'server' field")
+		serverName, ok := serverNameRaw.(string)
+		require.True(t, ok, "server should be string, got %T", serverNameRaw)
+		
+		successRaw, ok := resultMap["success"]
+		require.True(t, ok, "result should have 'success' field")
+		success, ok := successRaw.(bool)
+		require.True(t, ok, "success should be bool, got %T", successRaw)
 
 		switch serverName {
 		case "temperature", "edge":
core/internal/mcptests/codemode_tools_test.go-238-291 (1)

238-291: Add assertions for code-mode client isolation and timeout behavior.

TestCodeMode_CallingCodeModeClient and TestCodeMode_ToolExecutionTimeout only log results, so regressions won’t fail. Consider asserting the expected error/timeout (or make the timeout deterministic with an explicit deadline or long-running tool).

🔧 Example assertion for client isolation
-   returnValue, hasError, errorMsg := ParseCodeModeResponse(t, *result.Content.ContentStr)
-   if hasError {
-       t.Logf("Error: %s", errorMsg)
-   } else {
-       t.Logf("Result: %+v", returnValue)
-   }
+   returnValue, hasError, errorMsg := ParseCodeModeResponse(t, *result.Content.ContentStr)
+   if hasError {
+       assert.NotEmpty(t, errorMsg)
+       return
+   }
+   resultObj, ok := returnValue.(map[string]interface{})
+   require.True(t, ok)
+   assert.NotEmpty(t, resultObj["error"])
+   assert.Equal(t, "Code mode tools not accessible", resultObj["expected"])

Also applies to: 482-541

core/internal/mcptests/agent_filtering_test.go-77-132 (1)

77-132: Strengthen auto-execute assertions on follow-up LLM calls.

Setting mockLLM.chatCallCount = 1 initializes the counter to point to the second response. The assertion >= 1 doesn't verify a follow-up call occurred—the counter remains 1 if no call happens. For auto-execute paths, assert >= 2 to confirm a follow-up LLM call. For approval paths, keep == 1 to verify the agent stopped without additional calls.

🔧 Example adjustment
- assert.GreaterOrEqual(t, mockLLM.chatCallCount, 1, "should have made follow-up LLM call")
+ assert.GreaterOrEqual(t, mockLLM.chatCallCount, 2, "should include a follow-up LLM call after auto-execute")

Applies to lines 130, 318, 383, 506, 593, and 695 where auto-execute follow-up calls are expected. Keep == 1 assertions (e.g., line 691) unchanged for approval-stopping paths.

core/internal/mcptests/client_management_test.go-141-155 (1)

141-155: Guard against indefinite wait in concurrent removal test

If ExecuteChatMCPTool blocks (server unavailable), <-done can hang the test. Consider a timeout tied to the test context.

🐛 Suggested fix
-	// Wait for execution to complete
-	<-done
+	// Wait for execution to complete (with timeout)
+	select {
+	case <-done:
+	case <-time.After(5 * time.Second):
+		t.Fatal("tool execution did not заверш")
+	}
core/internal/mcptests/codemode_files_test.go-69-105 (1)

69-105: Test name implies tool binding but config stays default

Line 69 references tool-level binding, yet no binding-level configuration is applied, and the assertions still expect the default server layout (lines 101–105). Either configure CodeModeBindingLevel via the tool manager or rename the test to avoid misleading coverage.

core/internal/mcptests/codemode_files_test.go-420-435 (1)

420-435: Use json.Marshal for tool code arguments to avoid malformed JSON

These arguments are built via string concatenation/ReplaceAll, which leaves raw newlines and quotes in the JSON string. When tool-call parsing uses strict JSON unmarshaling (core/mcp/toolmanager.go:509), malformed Arguments will fail. Use json.Marshal instead, following the pattern in codemode_security_test.go (lines 215-218).

♻️ Suggested fix (example for one site)
+	codeJSON, err := json.Marshal(code)
+	require.NoError(t, err)
 	toolCall := schemas.ChatAssistantMessageToolCall{
 		ID:   schemas.Ptr("call-list-in-code"),
 		Type: schemas.Ptr("function"),
 		Function: schemas.ChatAssistantMessageToolCallFunction{
 			Name:      schemas.Ptr("executeToolCode"),
-			Arguments: `{"code": "` + code + `"}`,
+			Arguments: `{"code": ` + string(codeJSON) + `}`,
 		},
 	}

Add import (outside the shown range):

import "encoding/json"

Also applies to: 483-490, 663-671

core/internal/mcptests/codemode_agent_test.go-439-439 (1)

439-439: Weak assertion provides minimal verification.

The assertion assert.GreaterOrEqual(t, mockLLM.chatCallCount, 0, ...) will always pass since chatCallCount cannot be negative. This doesn't meaningfully verify the filtering behavior.

Consider a more specific assertion based on expected behavior:

💡 Suggested improvement
-	assert.GreaterOrEqual(t, mockLLM.chatCallCount, 0, "agent should handle tool filtering")
+	// After code execution fails due to blocked tool, agent should make a follow-up call
+	assert.GreaterOrEqual(t, mockLLM.chatCallCount, 1, "agent should make follow-up call after tool filtering error")
core/internal/mcptests/agent_request_id_test.go-86-101 (1)

86-101: Potential race condition in request ID generator.

The iteration variable is captured by the closure but incremented without synchronization. While the mutex protects requestIDsSeen, the iteration counter itself could have race conditions if the generator is called concurrently.

🔒 Thread-safe iteration counter
-	// Track request IDs seen during execution
-	requestIDsMutex := sync.Mutex{}
-	requestIDsSeen := []string{}
-
-	// Create request ID generator
-	iteration := 0
-	fetchNewRequestIDFunc := func(ctx *schemas.BifrostContext) string {
-		iteration++
-		newID := fmt.Sprintf("req-1-iter-%d", iteration)
-
-		// Track request IDs
-		requestIDsMutex.Lock()
-		requestIDsSeen = append(requestIDsSeen, newID)
-		requestIDsMutex.Unlock()
-
-		return newID
-	}
+	// Track request IDs seen during execution
+	requestIDsMutex := sync.Mutex{}
+	requestIDsSeen := []string{}
+
+	// Create request ID generator
+	fetchNewRequestIDFunc := func(ctx *schemas.BifrostContext) string {
+		requestIDsMutex.Lock()
+		defer requestIDsMutex.Unlock()
+		
+		newID := fmt.Sprintf("req-1-iter-%d", len(requestIDsSeen)+1)
+		requestIDsSeen = append(requestIDsSeen, newID)
+		return newID
+	}
core/internal/mcptests/agent_basic_test.go-663-718 (1)

663-718: Real LLM test has unused variable.

The variable responseText is assigned but only used in a log statement that doesn't reference it, making the assignment pointless.

🐛 Remove unused variable
 	// Check if response mentions the result (42)
-	responseText := *result.Choices[0].ChatNonStreamResponseChoice.Message.Content.ContentStr
 	// Don't assert exact match due to LLM variability, just log
 	t.Logf("Response contains calculation result")
-	_ = responseText
core/internal/mcptests/agent_error_handling_test.go-589-662 (1)

589-662: Test doesn't verify error message is actually preserved.

The test claims to verify "error message preservation" but doesn't actually check that the specific error message (expectedErrorMsg) appears in the LLM's conversation history or the tool result. The test just verifies the agent completes successfully.

Consider adding verification that the error message is preserved:

💡 Verify error message preservation
+	// To truly verify preservation, we would need to inspect the messages
+	// passed to the mock LLM on the second call (follow-up after error)
+	// This could be done by capturing the history in the mock
+	
 	t.Logf("✅ Error message preservation verified")
 	t.Logf("Original error: %s", expectedErrorMsg)

Alternatively, modify the mock to capture and expose the conversation history for inspection.

core/internal/mcptests/agent_parallel_execution_test.go-594-613 (1)

594-613: Remove unused sortToolResultsByID helper function.

The function at lines 594-613 is not called anywhere in the codebase and should be deleted.

🧹 Nitpick comments (19)
.gitignore (1)

24-25: Optional: Consider removing redundant root-level pattern.

Line 24's /dist pattern is now redundant since line 25's **/dist matches all dist directories recursively, including the root level. You can safely remove line 24 for cleaner configuration.

♻️ Cleanup suggestion
-/dist
 **/dist
core/internal/llmtests/web_search_tool.go (1)

819-853: Consider using standard library string functions.

These custom implementations reinvent strings.ToLower, strings.Index, and strings.Contains. Additionally, toLower has a UTF-8 bug: make([]rune, len(s)) allocates by byte length, but range s iterates with byte indices, causing misalignment for multi-byte characters (e.g., "Über").

♻️ Suggested refactor using standard library
+import "strings"
+
 // containsSubstring checks if s contains substr (case-insensitive)
 func containsSubstring(s, substr string) bool {
-	s = toLower(s)
-	substr = toLower(substr)
-	return len(s) >= len(substr) && indexOfSubstring(s, substr) >= 0
+	return strings.Contains(strings.ToLower(s), strings.ToLower(substr))
 }
-
-// toLower converts string to lowercase
-func toLower(s string) string {
-	result := make([]rune, len(s))
-	for i, r := range s {
-		if r >= 'A' && r <= 'Z' {
-			result[i] = r + 32
-		} else {
-			result[i] = r
-		}
-	}
-	return string(result)
-}
-
-// indexOfSubstring finds index of substr in s, or -1 if not found
-func indexOfSubstring(s, substr string) int {
-	if len(substr) == 0 {
-		return 0
-	}
-	if len(substr) > len(s) {
-		return -1
-	}
-
-	for i := 0; i <= len(s)-len(substr); i++ {
-		if s[i:i+len(substr)] == substr {
-			return i
-		}
-	}
-	return -1
-}
core/internal/mcptests/codemode_vs_noncodemode_test.go (1)

344-356: Consider guarding against potential nil dereferences.

Lines 346-350 access nested fields assuming non-nil values. While the manager likely guarantees structure, adding guards would prevent cryptic panics if the response is malformed.

💡 Optional defensive check
 	// Verify agent completed (may auto-execute or return tool calls)
 	assert.GreaterOrEqual(t, mocker.GetChatCallCount(), 1, "should make at least 1 follow-up call")
+	require.NotEmpty(t, result.Choices, "should have at least one choice")
+	require.NotNil(t, result.Choices[0].FinishReason, "should have finish reason pointer")
 	assert.NotEmpty(t, *result.Choices[0].FinishReason, "should have finish reason")
 
 	// Check that we processed the temperature tool (either in content or executed)
+	require.NotNil(t, result.Choices[0].ChatNonStreamResponseChoice, "should have non-stream response")
+	require.NotNil(t, result.Choices[0].ChatNonStreamResponseChoice.Message, "should have message")
 	content := result.Choices[0].ChatNonStreamResponseChoice.Message.Content
-	toolCalls := result.Choices[0].ChatNonStreamResponseChoice.Message.ChatAssistantMessage.ToolCalls
+	var toolCalls []schemas.ChatAssistantMessageToolCall
+	if result.Choices[0].ChatNonStreamResponseChoice.Message.ChatAssistantMessage != nil {
+		toolCalls = result.Choices[0].ChatNonStreamResponseChoice.Message.ChatAssistantMessage.ToolCalls
+	}
core/internal/mcptests/codemode_stdio_test.go (2)

116-165: Prefer bifrost.Ptr for tool-call pointers (avoid alias shadowing).
To align with repo conventions, consider renaming the local bifrost instance (e.g., bf) so you can pass bifrost.Ptr(toolCall) instead of &toolCall. Apply consistently across the file.

♻️ Proposed change (example)
-	_, bifrost := setupCodeModeWithSTDIOServers(t, "test-tools-server")
+	_, bf := setupCodeModeWithSTDIOServers(t, "test-tools-server")
 	ctx := createTestContext()
@@
-			result, bifrostErr := bifrost.ExecuteChatMCPTool(ctx, &toolCall)
+			result, bifrostErr := bf.ExecuteChatMCPTool(ctx, bifrost.Ptr(toolCall))

Based on learnings: In the maximhq/bifrost repository, prefer using bifrost.Ptr() to create pointers instead of the address operator (&) even when & would be valid syntactically. Apply this consistently across all code paths, including test utilities, to improve consistency and readability.


1579-1670: Relax tight wall‑clock thresholds to avoid flakes.
Using Date.now() with <100ms and narrow windows can be brittle under CI load, especially with many parallel tests. Consider loosening the bounds or making them relative to expected server delays.

♻️ Example adjustment (tune as needed)
-				assert.Less(t, elapsed, float64(100), "parallel execution should be fast")
+				assert.Less(t, elapsed, float64(500), "parallel execution should be fast even under CI load")
@@
-				assert.Greater(t, elapsed, float64(700), "should include slow operation time")
-				assert.Less(t, elapsed, float64(1500), "should not be much more than slow operation")
+				assert.Greater(t, elapsed, float64(600), "should include slow operation time")
+				assert.Less(t, elapsed, float64(2500), "should not be much more than slow operation")
core/internal/llmtests/test_retry_conditions.go (1)

44-63: Minor: Misleading error message when tool calls exist but lack valid names.

If tool calls are found (line 46) but none have a valid function name (lines 48-53), hasContent remains false and the code falls through to line 61, returning "all choices have empty content and no tool calls". However, tool calls were present—they just lacked valid names.

Suggested improvement for clearer error messaging
 		if len(toolCalls) == 0 {
 			return true, "no tool calls found in response"
 		}
 	}
 
 	if !hasContent {
-		return true, "all choices have empty content and no tool calls"
+		return true, "all choices have empty content and no valid tool calls"
 	}
core/internal/llmtests/multi_turn_conversation.go (1)

136-150: Indentation inconsistency affects readability.

Lines 136-149 appear to use a different indentation level compared to the surrounding code (lines 130-135). While this doesn't affect functionality, it impacts code readability and may indicate a copy-paste or editor formatting issue.

Suggested fix to align indentation
-	response2, bifrostErr := WithChatTestRetry(t, chatRetryConfig2, retryContext2, expectations2, "MultiTurnConversation_Step2", func() (*schemas.BifrostChatResponse, *schemas.BifrostError) {
-		bfCtx := schemas.NewBifrostContext(ctx, schemas.NoDeadline)
-		return client.ChatCompletionRequest(bfCtx, secondRequest)
-	})
-
-	if bifrostErr != nil {
-		t.Fatalf("❌ MultiTurnConversation_Step2 request failed after retries: %v", GetErrorMessage(bifrostErr))
-	}
-
-	// Validation already happened inside WithChatTestRetry via expectations2
-	// If we reach here, the model successfully remembered "Alice"
-	content := GetChatContent(response2)
-	t.Logf("✅ Model successfully remembered the name: %s", content)
-	t.Logf("✅ Multi-turn conversation completed successfully")
+		response2, bifrostErr := WithChatTestRetry(t, chatRetryConfig2, retryContext2, expectations2, "MultiTurnConversation_Step2", func() (*schemas.BifrostChatResponse, *schemas.BifrostError) {
+			bfCtx := schemas.NewBifrostContext(ctx, schemas.NoDeadline)
+			return client.ChatCompletionRequest(bfCtx, secondRequest)
+		})
+
+		if bifrostErr != nil {
+			t.Fatalf("❌ MultiTurnConversation_Step2 request failed after retries: %v", GetErrorMessage(bifrostErr))
+		}
+
+		// Validation already happened inside WithChatTestRetry via expectations2
+		// If we reach here, the model successfully remembered "Alice"
+		content := GetChatContent(response2)
+		t.Logf("✅ Model successfully remembered the name: %s", content)
+		t.Logf("✅ Multi-turn conversation completed successfully")
core/internal/mcptests/agent_filtering_test.go (1)

540-542: Avoid fixed sleeps for MCP client readiness.

The fixed 500ms sleep can be flaky on slower CI runners. Consider polling for client readiness with a bounded timeout (or a manager-ready hook) instead of a fixed delay.

Also applies to: 629-631

core/internal/mcptests/client_management_test.go (1)

221-238: Make invalid-config expectations explicit

This test currently passes even if the invalid config is accepted. If invalid configs should be rejected, assert the error (or explicitly assert the unchanged/error state).

♻️ Suggested fix
-	err := manager.EditClient(clientID, &invalidConfig)
-	// Should return error or leave client unchanged
-	if err == nil {
-		clients = manager.GetClients()
-		if len(clients) > 0 {
-			// Client might be in error state
-			t.Log("Edit with invalid config did not error, checking client state")
-		}
-	} else {
-		assert.Error(t, err, "should error with invalid config")
-	}
+	err := manager.EditClient(clientID, &invalidConfig)
+	require.Error(t, err, "should error with invalid config")
core/internal/mcptests/agent_test_helpers_example_test.go (1)

196-200: Consider using fmt.Sprintf for call ID generation.

The current approach "call-"+string(rune(i+'0')) only works correctly for single-digit indices. While safe here (loop runs 5 times), using fmt.Sprintf("call-%d", i) would be more robust and readable.

♻️ Suggested improvement
 	for i := 0; i < 5; i++ { // Add more responses than max depth
 		mocker.AddChatResponse(CreateAgentTurnWithToolCalls(
-			GetSampleEchoToolCall("call-"+string(rune(i+'0')), "test"),
+			GetSampleEchoToolCall(fmt.Sprintf("call-%d", i), "test"),
 		))
 	}
core/internal/mcptests/agent_multiconnection_test.go (1)

156-160: Consider making the content assertion more flexible.

The assertion expects the exact text "Output from allowed tools" which may be an implementation detail that could change. Consider using a more flexible assertion or verifying that content exists without checking for specific implementation text.

♻️ Suggested improvement
 	require.NotNil(t, choice.ChatNonStreamResponseChoice.Message)
 	require.NotNil(t, choice.ChatNonStreamResponseChoice.Message.Content)
 	require.NotNil(t, choice.ChatNonStreamResponseChoice.Message.Content.ContentStr)
-	assert.Contains(t, *choice.ChatNonStreamResponseChoice.Message.Content.ContentStr, "Output from allowed tools")
+	// Verify content exists from auto-executed echo
+	assert.NotEmpty(t, *choice.ChatNonStreamResponseChoice.Message.Content.ContentStr, "Should have content from auto-executed tools")
core/internal/mcptests/agent_basic_test.go (1)

17-22: MockLLMCaller lacks thread-safety for concurrent use.

The MockLLMCaller struct has mutable fields (chatCallCount, responsesCallCount) that are read and written without synchronization. While each test creates its own instance, if the mock were ever shared or if the agent internally makes concurrent calls, this could lead to data races.

Consider adding a mutex if concurrent access is expected:

♻️ Thread-safe alternative
 type MockLLMCaller struct {
 	chatResponses      []*schemas.BifrostChatResponse
 	responsesResponses []*schemas.BifrostResponsesResponse
 	chatCallCount      int
 	responsesCallCount int
+	mu                 sync.Mutex
 }

And protect access in the methods:

 func (m *MockLLMCaller) MakeChatRequest(ctx *schemas.BifrostContext, req *schemas.BifrostChatRequest) (*schemas.BifrostChatResponse, *schemas.BifrostError) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
 	if m.chatCallCount >= len(m.chatResponses) {
core/internal/mcptests/codemode_agent_test.go (1)

740-802: Timeout test may be flaky.

The test creates an infinite loop while(true) {} and expects it to be handled within a 2-second timeout. However:

  1. The assertion assert.GreaterOrEqual(t, mockLLM.chatCallCount, 0, ...) always passes
  2. The test doesn't verify that a timeout actually occurred

Consider adding explicit verification that the timeout was triggered:

♻️ Strengthen timeout verification
 	// Agent should handle timeout gracefully
-	assert.GreaterOrEqual(t, mockLLM.chatCallCount, 0, "agent should handle timeout gracefully")
+	// Verify the result contains indication of timeout or error handling
+	require.NotNil(t, result)
+	// The agent should have completed (either with error response or by handling timeout)
+	assert.NotEmpty(t, result.Choices, "should have choices in result after timeout handling")
core/internal/mcptests/codemode_agent_multiturn_test.go (1)

277-286: Finish reason assertion may be overly permissive.

The assertion accepts both "stop" and "tool_calls" as valid finish reasons without distinguishing between successful completion and pending approval. This reduces the test's ability to catch regressions.

Consider documenting the expected behavior more precisely or splitting into separate test cases:

// If the test is specifically about stopping at non-auto tools:
assert.Equal(t, "tool_calls", finishReason, "should stop at non-auto tool requiring approval")
// Or if completion is expected:
assert.Equal(t, "stop", finishReason, "should complete successfully")
core/internal/mcptests/agent_mixed_permissions_test.go (1)

243-251: Direct mutation of client config may not persist.

The code modifies clients[i].ExecutionConfig.ToolsToAutoExecute directly and then calls EditClient. Depending on the implementation, the modification might not be necessary if EditClient replaces the config entirely, or the direct mutation might cause issues if the slice is shared.

Consider using a copy to be explicit:

♻️ Explicit config update
 	clients := manager.GetClients()
 	for i := range clients {
 		if clients[i].ExecutionConfig.ID == "go-test-server" {
-			clients[i].ExecutionConfig.ToolsToAutoExecute = []string{"*"}
-			require.NoError(t, manager.EditClient(clients[i].ExecutionConfig.ID, clients[i].ExecutionConfig))
+			updatedConfig := clients[i].ExecutionConfig
+			updatedConfig.ToolsToAutoExecute = []string{"*"}
+			require.NoError(t, manager.EditClient(updatedConfig.ID, updatedConfig))
 		}
 	}
core/internal/mcptests/agent_error_handling_test.go (1)

107-189: Timeout test may not reliably trigger timeout behavior.

The test creates a context with 500ms timeout but the tool sleeps for 5 seconds. The test passes regardless of whether a timeout error occurs or a result is returned (lines 182-186). This makes the test unable to detect if timeout handling is broken.

Consider making the assertion more specific:

♻️ Strengthen timeout verification
-	// Should handle timeout gracefully
-	if bifrostErr != nil {
-		t.Logf("Timeout resulted in error (expected): %v", bifrostErr.Error)
-	} else if result != nil {
-		t.Logf("Timeout handled in result")
-	}
+	// Should handle timeout - either by returning an error or by returning a result with error info
+	// At minimum, verify the function returned without hanging
+	assert.True(t, bifrostErr != nil || result != nil, "should return either error or result on timeout")
+	
+	if bifrostErr != nil {
+		t.Logf("Timeout resulted in error (expected): %v", bifrostErr.Error)
+		// Optionally verify it's a timeout-related error
+	} else {
+		t.Logf("Timeout handled in result")
+	}
core/internal/mcptests/agent_parallel_execution_test.go (1)

406-409: 5-second timeout tool may cause test flakiness.

The "timeout" case sleeps for 5 seconds, but the test context timeout is only 2 seconds (line 431). This is intentional to test timeout handling, but consider adding a comment clarifying that this tool is expected to be cancelled by context timeout.

core/internal/mcptests/agent_state_transitions_test.go (1)

488-497: Shared slice access without synchronization relies on sequential execution.

The executionOrder slice is modified without mutex protection. This works because the mock responses ensure tools execute sequentially (one tool per turn). Consider adding a brief comment to clarify this assumption for future maintainers.

📝 Suggested comment for clarity
 // Counter to track state
+// NOTE: No mutex needed - tools execute sequentially (one per mock LLM turn)
 executionOrder := []string{}
core/internal/mcptests/agent_test_helpers.go (1)

471-487: Placeholder implementations should be tracked.

The AssertRequestIDChanged and AssertRequestIDPropagated functions are documented as placeholders. Consider adding a TODO comment with tracking information or removing if not needed for this PR.

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

🤖 Fix all issues with AI agents
In `@core/internal/mcptests/agent_test_helpers.go`:
- Around line 155-175: The switch over config.InProcessTools in SetupAgentTest
helper is missing support for "get_temperature", causing t.Fatalf when that tool
is listed; add a case "get_temperature" that calls require.NoError(t,
RegisterGetTemperatureTool(manager)) (matching the pattern used for other tools
like RegisterGetTimeTool/RegisterReadFileTool) so the helper accepts the same
config values as SetupAgentTest.
- Around line 734-754: The helper AssertAgentFinalResponseResponses currently
ignores the expectedFinishReason parameter; update it to assert the response's
finish-reason by checking the FinishReason field on the result
(schemas.BifrostResponsesResponse.FinishReason) — e.g., if expectedFinishReason
!= "" assert.Equal(t, expectedFinishReason, result.FinishReason) — or if you
prefer to remove ambiguity, delete the expectedFinishReason parameter from
AssertAgentFinalResponseResponses and update all callers to no longer pass it;
locate the function by name (AssertAgentFinalResponseResponses) and the schema
type (schemas.BifrostResponsesResponse / FinishReason) to apply the change.

In `@core/internal/mcptests/codemode_agent_test.go`:
- Around line 437-439: The assertion using assert.GreaterOrEqual(t,
mockLLM.chatCallCount, 0, ...) is tautological and should be strengthened to
verify a real follow-up occurred; change these to assert.GreaterOrEqual(t,
mockLLM.chatCallCount, 1, ...) (or assert.Equal/Greater depending on expected
exact count), or alternatively assert on the agent's final response/finish
reason to ensure the follow-up call happened—update the occurrences referencing
mockLLM.chatCallCount (and similar assertions at the other noted spots) to use a
meaningful minimum (>=1) or validate the final response/finish reason instead.

In `@core/internal/mcptests/codemode_files_test.go`:
- Around line 429-436: The code embeds raw code into the JSON arguments for
ChatAssistantMessageToolCall.Function.Arguments (in the toolCall construction
for executeToolCode), which can produce invalid JSON; fix by JSON-escaping the
code with the package helper mustJSONString before concatenating into the
Arguments string so Arguments becomes valid JSON (use mustJSONString(code)
instead of raw code); apply the same change to the other two identical call
sites that build ChatAssistantMessageToolCall with executeToolCode.

In `@core/internal/mcptests/codemode_security_test.go`:
- Around line 157-242: The test TestExecuteToolCode_CodeInjectionAttempts
currently only logs expected failures for cases where shouldFail is true, so it
must assert failures instead; update the subtest handling (inside the t.Run loop
where bifrost.ExecuteChatMCPTool is called) to assert that for tc.shouldFail
either bifrostErr != nil OR the parsed response from ParseCodeModeResponse
indicates an error (hasError == true or the returned object contains an "error"
field), and call t.Fatalf/t.Errorf when none of those conditions hold; reference
the test function TestExecuteToolCode_CodeInjectionAttempts, the injectionTests
table entries, the bifrost.ExecuteChatMCPTool call, and ParseCodeModeResponse to
locate and implement the assertion logic.
- Around line 105-151: The test TestReadToolFile_InvalidToolNames currently
constructs the ChatAssistantMessageToolCallFunction.Arguments via string
concatenation which can produce invalid JSON for values with backslashes or
NULs; change the code to build the arguments using json.Marshal (e.g., marshal a
struct or map like map[string]string{"fileName": tc.fileName}) and assign the
resulting JSON string to Function.Arguments before calling
bifrost.ExecuteChatMCPTool so the inputs are valid JSON and the test exercises
tool-name validation correctly.

In `@core/internal/mcptests/codemode_tools_test.go`:
- Around line 185-187: The skip message is inconsistent with other tests: update
the t.Skip call that checks config.HTTPServerURL to use the same environment
variable name as elsewhere (e.g., "MCP_HTTP_URL not set") so developers aren't
confused; locate the check referencing config.HTTPServerURL in
codemode_tools_test.go and change the string passed to t.Skip to match the
canonical "MCP_HTTP_URL not set".
🧹 Nitpick comments (21)
core/internal/llmtests/tool_calls_streaming.go (1)

163-175: Optional: JSON completeness heuristic could be more robust.

The current logic assumes tool call arguments are always JSON objects ({...}). If a provider ever returns a JSON array or primitive value as arguments, this heuristic would incorrectly append instead of replace.

This is likely fine for current LLM tool call conventions, but consider a more robust check if the API surface evolves.

🔧 Suggested improvement
-		if len(argsStr) > 0 && argsStr[0] == '{' && argsStr[len(argsStr)-1] == '}' && existing.Arguments != "" {
+		// Check if this looks like complete JSON (object or array)
+		isCompleteJSON := len(argsStr) > 1 && 
+			((argsStr[0] == '{' && argsStr[len(argsStr)-1] == '}') ||
+			 (argsStr[0] == '[' && argsStr[len(argsStr)-1] == ']'))
+		if isCompleteJSON && existing.Arguments != "" {
core/internal/llmtests/file_base64.go (1)

255-267: Unreachable else branch (dead code).

The else block at lines 264-267 can never execute. The early return at lines 255-258 handles the case where both foundHelloWorld and foundDocument are false, so the subsequent if/else if fully covers all remaining paths.

🧹 Remove unreachable code
 	if !foundHelloWorld && !foundDocument {
 		t.Errorf("❌ %s model failed to process PDF document - response doesn't reference expected content or document-related terms. Response: %s", apiName, content)
 		return
 	}

 	if foundHelloWorld {
 		t.Logf("✅ %s model successfully extracted 'Hello World' content from PDF document", apiName)
 	} else if foundDocument {
 		t.Logf("✅ %s model processed PDF document but may not have clearly identified the exact text", apiName)
-	} else {
-		t.Errorf("❌ %s response doesn't reference document content or expected keywords: %s", apiName, content)
-		return
 	}

 	t.Logf("✅ %s PDF document processing completed: %s", apiName, content)
core/internal/llmtests/validation_presets.go (1)

463-465: Remove the unused stringPtr helper function.

This function is not called anywhere in the file or the broader codebase. Each test file that needs this helper defines its own local version.

core/internal/llmtests/simple_chat.go (1)

145-148: Redundant failure check after earlier t.Fatalf.

Lines 145–148 are unreachable because Lines 126–132 already t.Fatalf on error. Removing this reduces noise.

♻️ Proposed cleanup
-		// Fail test if either API failed
-		if chatError != nil || responsesError != nil {
-			t.Fatalf("❌ SimpleChat test failed - one or both APIs failed")
-		}
core/internal/llmtests/web_search_tool.go (1)

827-837: Prefer strings.ToLower over custom implementation.

The custom toLower implementation has a subtle bug with multi-byte UTF-8 characters. When iterating with for i, r := range s, the index i is the byte position, not the rune position. Combined with make([]rune, len(s)) which allocates based on byte count, this causes:

  1. For strings with multi-byte characters, gaps are left in the result slice (filled with null runes)
  2. The resulting string will contain embedded null characters

For example, with input "HÉLLO" (where É is 2 bytes), byte indices would be 0, 1, 3, 4, 5, leaving position 2 as a null rune.

♻️ Suggested fix using standard library
-// toLower converts string to lowercase
-func toLower(s string) string {
-	result := make([]rune, len(s))
-	for i, r := range s {
-		if r >= 'A' && r <= 'Z' {
-			result[i] = r + 32
-		} else {
-			result[i] = r
-		}
-	}
-	return string(result)
-}
+import "strings"
+
+// toLower converts string to lowercase
+func toLower(s string) string {
+	return strings.ToLower(s)
+}

Alternatively, if you want to keep a custom implementation:

func toLower(s string) string {
	result := make([]rune, 0, len(s))
	for _, r := range s {
		if r >= 'A' && r <= 'Z' {
			result = append(result, r+32)
		} else {
			result = append(result, r)
		}
	}
	return string(result)
}
Makefile (1)

747-855: Make test-mcp depend on built MCP servers.
If the MCP tests expect binaries under examples/mcps/*/bin, wiring setup-mcp-tests as a prerequisite makes the target self-contained and prevents avoidable failures.

♻️ Suggested change
-test-mcp: install-gotestsum ## Run MCP tests by file type (Usage: make test-mcp TYPE=connection [TESTCASE=TestName] [PATTERN=substring])
+test-mcp: install-gotestsum setup-mcp-tests ## Run MCP tests by file type (Usage: make test-mcp TYPE=connection [TESTCASE=TestName] [PATTERN=substring])
core/internal/mcptests/codemode_basic_test.go (1)

155-199: Consider loosening the parallel execution timing threshold.
The <1500ms assertion can be flaky on slower CI runners or under load. A slightly higher threshold or a relative comparison (e.g., < 2s) would improve stability.

core/internal/mcptests/codemode_stdio_test.go (4)

106-106: Variable bifrost shadows the imported package name.

On line 106, the variable bifrost shadows the imported package bifrost from line 12. While this works due to Go's scoping rules, it can cause confusion and potential bugs if someone tries to use the package-level bifrost later in this function.

♻️ Suggested rename to avoid shadowing
-	bifrost := setupBifrost(t)
-	bifrost.SetMCPManager(manager)
+	bf := setupBifrost(t)
+	bf.SetMCPManager(manager)

-	return manager, bifrost
+	return manager, bf

77-84: Fragile path construction using .. parent directory navigation.

The path filepath.Join(bifrostRoot, "..", "examples") assumes the examples directory is always a sibling of the bifrost root. This could break if the repository structure changes or in CI environments with different checkout configurations.

Consider making examplesRoot configurable via an environment variable with this as a fallback, or documenting this assumption clearly.


119-120: Variable shadowing of bifrost package repeated throughout test file.

The same variable shadowing issue (bifrost variable vs imported package) occurs in multiple test functions. Consider a consistent rename across the file.

Also applies to: 170-171, 266-267, 360-361, 470-471, 584-585, 671-672, 784-785, 842-843, 920-921, 1000-1001, 1093-1094, 1174-1175, 1255-1256, 1348-1349, 1417-1418, 1505-1506, 1582-1583, 1679-1680, 1763-1764


1815-1822: Unused JSON unmarshal result check.

The error check on line 1818 swallows the parse error silently. If parsing fails, the test may not properly assert the failure case. Consider logging or handling the parse error.

♻️ Add logging for parse failures
 var execResult map[string]interface{}
 err := json.Unmarshal([]byte(*result.Content.ContentStr), &execResult)
 if err == nil {
     _, hasError := execResult["error"]
     assert.True(t, hasError, "Should have error in result")
+} else {
+    t.Logf("Response content was not JSON: %s", *result.Content.ContentStr)
 }
core/internal/mcptests/agent_parallel_execution_test.go (2)

594-612: Unused helper function sortToolResultsByID.

The function sortToolResultsByID is defined but never called in any test. If this was intended for future use, consider adding a TODO comment. Otherwise, remove it to avoid dead code.

♻️ Options

Either remove the function if unused:

-func sortToolResultsByID(results []*schemas.ChatMessage) []*schemas.ChatMessage {
-	sorted := make([]*schemas.ChatMessage, len(results))
-	copy(sorted, results)
-
-	sort.Slice(sorted, func(i, j int) bool {
-		idI := ""
-		idJ := ""
-
-		if sorted[i].ChatToolMessage != nil && sorted[i].ChatToolMessage.ToolCallID != nil {
-			idI = *sorted[i].ChatToolMessage.ToolCallID
-		}
-		if sorted[j].ChatToolMessage != nil && sorted[j].ChatToolMessage.ToolCallID != nil {
-			idJ = *sorted[j].ChatToolMessage.ToolCallID
-		}
-
-		return strings.Compare(idI, idJ) < 0
-	})
-
-	return sorted
-}

Or add a comment if planned for future use:

+// sortToolResultsByID sorts tool results by their call ID for deterministic assertions.
+// TODO: Use this in TestAgent_ParallelExecution_ResultCollectionOrder once result ordering is implemented.
 func sortToolResultsByID(results []*schemas.ChatMessage) []*schemas.ChatMessage {

5-5: Unused import: sort package.

The sort package is imported but only used by the unused sortToolResultsByID function. If that function is removed, this import should also be removed.

core/internal/mcptests/agent_context_filtering_test.go (1)

244-246: Clarify expected finish reason in comment vs assertion.

The comment on line 239 says "agent should stop at turn 1 for approval" but the assertion on line 245 expects finish_reason to be "tool_calls". This is actually correct behavior (stopping for approval returns tool_calls reason), but the test comment could be clearer.

📝 Clarify the comment
-	// Echo is allowed by context but NOT auto-executed (ToolsToAutoExecute is empty)
-	// So agent should stop at turn 1 for approval
+	// Echo is allowed by context but NOT auto-executed (ToolsToAutoExecute is empty)
+	// So agent should stop at turn 1 with tool_calls reason (waiting for approval)
 	AssertAgentStoppedAtTurn(t, mocker, 1)
core/internal/mcptests/codemode_agent_singleturn_test.go (1)

66-96: Consider simplifying the dynamic response validation logic.

The validation logic in Turn 2 (lines 66-96) has multiple nested conditions checking various content formats. While thorough, this could be simplified by extracting a helper function for tool result validation.

♻️ Extract helper for tool result validation

Consider creating a helper like:

// validateToolResultContains checks if a tool message contains expected data
func validateToolResultContains(history []schemas.ChatMessage, callID string, patterns ...string) bool {
    for _, msg := range history {
        if msg.Role == schemas.ChatMessageRoleTool &&
            msg.ToolCallID != nil &&
            *msg.ToolCallID == callID &&
            msg.Content != nil &&
            msg.Content.ContentStr != nil {
            content := *msg.Content.ContentStr
            for _, pattern := range patterns {
                if strings.Contains(content, pattern) {
                    return true
                }
            }
        }
    }
    return false
}

This would simplify the dynamic response to:

if validateToolResultContains(history, "call-1", "temperature", "°C", "return value") {
    return CreateChatResponseWithText("The temperature in London is 15°C")
}
core/internal/mcptests/agent_test_helpers_example_test.go (2)

196-200: Fragile call ID generation pattern.

string(rune(i+'0')) only produces correct single-character digits for i in 0–9. For i >= 10, this yields unexpected characters (e.g., ':' for 10). While the current loop only iterates 5 times, this pattern is error-prone if the loop bound changes.

♻️ Suggested fix
 	for i := 0; i < 5; i++ { // Add more responses than max depth
 		mocker.AddChatResponse(CreateAgentTurnWithToolCalls(
-			GetSampleEchoToolCall("call-"+string(rune(i+'0')), "test"),
+			GetSampleEchoToolCall(fmt.Sprintf("call-%d", i), "test"),
 		))
 	}

Note: This requires adding "fmt" to the imports.


218-227: Verify the expected behavior of MaxDepth.

The comment on lines 218-221 indicates uncertainty about whether MaxDepth=3 should result in 3 or 4 total LLM calls. The assertions allow a range (3-4 calls), which suggests the exact behavior isn't well-defined or documented.

Consider clarifying the expected behavior in the test or the underlying implementation to avoid ambiguity.

core/internal/mcptests/codemode_agent_multiturn_test.go (1)

486-494: Trivial assertion provides no value.

assert.GreaterOrEqual(t, mocker.GetChatCallCount(), 0, ...) is always true since call counts cannot be negative. This should either verify the actual expected count or be removed.

♻️ Suggested fix
-	// Verify result - either a message response or tool calls waiting for approval
-	assert.GreaterOrEqual(t, mocker.GetChatCallCount(), 0, "should make LLM calls during agent execution")
+	// Verify result - either a message response or tool calls waiting for approval
+	assert.GreaterOrEqual(t, mocker.GetChatCallCount(), 1, "should make at least one LLM call during agent execution")
core/internal/mcptests/agent_filtering_test.go (2)

392-452: Add an explicit assertion for the unavailable weather tool.

Right now this scenario only logs. Adding a direct assertion (e.g., tool returned for approval or an unavailable-tool marker in the response) will prevent silent regressions.


529-542: Replace fixed sleeps with readiness polling to avoid flaky STDIO tests.

A fixed 500ms delay can be brittle under CI load. Prefer readiness polling (e.g., require.Eventually with a client-connected predicate) and consider serializing STDIO tests that share external resources.

Also applies to: 629-631

core/internal/mcptests/agent_test_helpers.go (1)

493-520: Avoid constructing a zero-value testing.T.

Passing &testing.T{} into MustMarshalJSON can panic when it tries to record failures. Prefer accepting *testing.T and threading it through the helper chain. Update call sites accordingly.

🔧 Proposed fix
-func CreateToolCall(id, toolName string, args map[string]interface{}) schemas.ChatAssistantMessageToolCall {
-	argsJSON := MustMarshalJSON(&testing.T{}, args)
+func CreateToolCall(t *testing.T, id, toolName string, args map[string]interface{}) schemas.ChatAssistantMessageToolCall {
+	t.Helper()
+	argsJSON := MustMarshalJSON(t, args)
 	return schemas.ChatAssistantMessageToolCall{
 		ID:   schemas.Ptr(id),
 		Type: schemas.Ptr("function"),
 		Function: schemas.ChatAssistantMessageToolCallFunction{
 			Name:      schemas.Ptr(toolName),
 			Arguments: argsJSON,
 		},
 	}
 }
@@
-func CreateSTDIOToolCall(id, serverName, toolName string, args map[string]interface{}) schemas.ChatAssistantMessageToolCall {
+func CreateSTDIOToolCall(t *testing.T, id, serverName, toolName string, args map[string]interface{}) schemas.ChatAssistantMessageToolCall {
 	fullToolName := fmt.Sprintf("%s-%s", serverName, toolName)
-	return CreateToolCall(id, fullToolName, args)
+	return CreateToolCall(t, id, fullToolName, args)
 }
@@
-func CreateInProcessToolCall(id, toolName string, args map[string]interface{}) schemas.ChatAssistantMessageToolCall {
+func CreateInProcessToolCall(t *testing.T, id, toolName string, args map[string]interface{}) schemas.ChatAssistantMessageToolCall {
 	fullToolName := fmt.Sprintf("bifrostInternal-%s", toolName)
-	return CreateToolCall(id, fullToolName, args)
+	return CreateToolCall(t, id, fullToolName, args)
 }

@akshaydeo akshaydeo force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from aa146aa to fe9203e Compare January 28, 2026 21:08
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@core/bifrost.go`:
- Around line 4414-4432: Before calling bifrost.getPluginPipeline() and
pipeline.RunMCPPreHooks(ctx, mcpRequest) ensure a request ID is present for MCP
flows: check the request ID field on mcpRequest (e.g.,
mcpRequest.RequestContext.RequestID or equivalent) or the context, and if
missing generate a new ID and set it on mcpRequest and the context so downstream
hooks/logging/tracing see it; do this prior to acquiring the plugin pipeline and
before invoking pipeline.RunMCPPreHooks.

@akshaydeo akshaydeo force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch 2 times, most recently from c1d7c5b to 10fddc2 Compare January 29, 2026 11:56
@akshaydeo akshaydeo force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 10fddc2 to 3be2a0e Compare January 29, 2026 12:03
@akshaydeo akshaydeo force-pushed the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch from 3be2a0e to 95e2529 Compare January 29, 2026 13:21
@akshaydeo akshaydeo merged commit b350c72 into main Jan 29, 2026
10 checks passed
@akshaydeo akshaydeo deleted the 01-19-feat_plugin_schemas_refactor_and_added_mcp_plugins branch January 29, 2026 13:32
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.

Files API Support

2 participants