Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions framework/logstore/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ var logstoreMigrationSteps = []migrationStep{
// twice on a fresh DB is harmless.
{IDs: []string{"logs_recreate_matviews_with_app_column"}, run: migrationRecreateMatViewsWithUserAgentColumn},
{IDs: []string{"mcp_tool_logs_add_endpoint_columns"}, run: migrationAddEndpointColumnsToMCPToolLogs},
{IDs: []string{"mcp_tool_logs_add_plugin_logs_column"}, run: migrationAddMCPPluginLogsColumn},
}

// areThereAnyPendingMigrations returns true if there are any pending migrations to be applied.
Expand Down Expand Up @@ -2853,6 +2854,28 @@ func migrationAddPluginLogsColumn(ctx context.Context, db *gorm.DB, logger schem
return nil
}

// migrationAddMCPPluginLogsColumn adds the plugin_logs column to MCP tool logs.
func migrationAddMCPPluginLogsColumn(ctx context.Context, db *gorm.DB, logger schemas.Logger) error {
migrationName := "mcp_tool_logs_add_plugin_logs_column"
logger.Info("[logstore] starting migration %s", migrationName)
defer logger.Info("[logstore] finished migration %s", migrationName)
opts := *migrator.DefaultOptions
opts.UseTransaction = true
m := migrator.New(db, &opts, []*migrator.Migration{{
ID: migrationName,
Migrate: func(tx *gorm.DB) error {
return addColumnIfNotExists(tx.WithContext(ctx), logger, &MCPToolLog{}, "plugin_logs")
},
Rollback: func(tx *gorm.DB) error {
return dropColumnIfExists(tx.WithContext(ctx), logger, &MCPToolLog{}, "plugin_logs")
},
}})
if err := m.Migrate(); err != nil {
return fmt.Errorf("error while adding MCP plugin logs column: %s", err.Error())
}
Comment on lines +2873 to +2875

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n 'error while adding MCP plugin logs column' framework/logstore/migrations.go

Repository: maximhq/bifrost

Length of output: 240


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- migration context ---'
sed -n '2825,2890p' framework/logstore/migrations.go
printf '%s\n' '--- migration symbol and callers ---'
rg -n -C 3 'MCP plugin logs|MigrateMCP|mcp.*logs|MCP.*Logs' framework/logstore --glob '*.go'
printf '%s\n' '--- Go module files ---'
find . -name go.mod -print

Repository: maximhq/bifrost

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail
printf '%s\n' '--- migration registration and related error handling ---'
sed -n '205,305p' framework/logstore/migrations.go
sed -n '1068,1088p' framework/logstore/migrations.go
printf '%s\n' '--- exact string-formatting pattern in migration errors ---'
rg -n 'fmt\.Errorf\(.*err\.Error\(\)' framework/logstore/migrations.go
printf '%s\n' '--- repository stack state ---'
git status --short --branch
git log --oneline -5
printf '%s\n' '--- module declarations ---'
find . -name go.mod -print -exec sed -n '1,12p' {} \;

Repository: maximhq/bifrost

Length of output: 29352


Preserve the migration error chain.

Use %w instead of %s with err.Error() so callers can inspect the underlying migration, database, or context error with errors.Is and errors.As.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@framework/logstore/migrations.go` around lines 2873 - 2875, Update the error
wrapping in the Migrate call to use the %w verb with the original err value
instead of formatting err.Error(), preserving the underlying error chain for
errors.Is and errors.As while retaining the existing context message.

Source: Coding guidelines

return nil
}

// migrationAddAliasColumn adds the alias column to the logs table.
// The alias field stores the original model name the caller used when routing resolved it to a different model via alias mapping.
// Index creation is deferred to ensurePerformanceIndexes (called post-startup in a background goroutine)
Expand Down
17 changes: 17 additions & 0 deletions framework/logstore/migrations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@ func TestMigrationAddMCPRedactionMappingColumn(t *testing.T) {
assert.Equal(t, int64(1), count)
}

// TestMigrationAddMCPPluginLogsColumn verifies the MCP plugin-log column is additive, idempotent, and preserves existing rows.
func TestMigrationAddMCPPluginLogsColumn(t *testing.T) {
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "migrations.db")), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
require.NoError(t, err)
require.NoError(t, db.Exec("CREATE TABLE mcp_tool_logs (id TEXT PRIMARY KEY)").Error)
require.NoError(t, db.Exec("INSERT INTO mcp_tool_logs (id) VALUES (?)", "mcp-existing").Error)

ctx := context.Background()
require.NoError(t, migrationAddMCPPluginLogsColumn(ctx, db, testLogger{}))
require.True(t, db.Migrator().HasColumn(&MCPToolLog{}, "PluginLogs"))
require.NoError(t, migrationAddMCPPluginLogsColumn(ctx, db, testLogger{}))

var count int64
require.NoError(t, db.Table("mcp_tool_logs").Where("id = ?", "mcp-existing").Count(&count).Error)
assert.Equal(t, int64(1), count)
}

// pgTestSchema is this package's dedicated Postgres schema. Test packages
// (configstore, configstore/tables, logstore) run in parallel against the same
// database, so each one works in its own schema to avoid clobbering the
Expand Down
4 changes: 4 additions & 0 deletions framework/logstore/payload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ func TestMCPToolLogPayload_RoundTripFullLog(t *testing.T) {
MetadataParsed: map[string]interface{}{
"trace": "abc",
},
PluginLogs: `{"guardrails":[{"plugin_name":"guardrails","level":"info","message":"arguments redacted","timestamp":1}]}`,
RedactionData: &schemas.RedactionData{
ReversibleMappings: schemas.RedactionMapsByPhase{Input: map[string]string{"EMAIL-1": "private@example.com"}},
},
Expand All @@ -230,6 +231,7 @@ func TestMCPToolLogPayload_RoundTripFullLog(t *testing.T) {
assert.Equal(t, true, dbEntry.ResultParsed.(map[string]interface{})["ok"])
assert.Equal(t, "stored for round trip", dbEntry.ErrorDetailsParsed.Error.Message)
assert.Equal(t, "abc", dbEntry.MetadataParsed["trace"])
assert.Equal(t, entry.PluginLogs, dbEntry.PluginLogs)
assert.Equal(t, entry.RedactionMapping, dbEntry.RedactionMapping)
assert.Nil(t, dbEntry.RedactionData)
}
Expand Down Expand Up @@ -276,6 +278,7 @@ func TestPrepareMCPToolDBEntry_KeepsOnlyInputPreview(t *testing.T) {
MetadataParsed: map[string]interface{}{
"trace": "abc",
},
PluginLogs: `{"guardrails":[{"message":"arguments redacted"}]}`,
}

PrepareMCPToolDBEntry(entry)
Expand All @@ -290,6 +293,7 @@ func TestPrepareMCPToolDBEntry_KeepsOnlyInputPreview(t *testing.T) {
assert.Nil(t, entry.ErrorDetailsParsed)
assert.NotEmpty(t, entry.Arguments)
assert.NotEmpty(t, entry.Metadata)
assert.Equal(t, `{"guardrails":[{"message":"arguments redacted"}]}`, entry.PluginLogs)

var preview string
require.NoError(t, sonic.Unmarshal([]byte(entry.Arguments), &preview))
Expand Down
1 change: 1 addition & 0 deletions framework/logstore/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,7 @@ type MCPToolLog struct {
Cost *float64 `gorm:"index:idx_mcp_logs_cost" json:"cost,omitempty"` // Cost in dollars (per execution cost)
Status string `gorm:"type:varchar(50);index:idx_mcp_logs_status;not null" json:"status"` // "processing", "success", or "error"
Metadata string `gorm:"type:text" json:"-"` // JSON serialized map[string]interface{}
PluginLogs string `gorm:"type:text" json:"plugin_logs,omitempty"` // JSON serialized plugin logs grouped by plugin name
HasObject bool `gorm:"default:false" json:"-"` // True when payload is stored in object storage
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`

Expand Down
21 changes: 14 additions & 7 deletions plugins/logging/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -1629,13 +1629,7 @@ func (p *LoggerPlugin) Inject(_ context.Context, trace *schemas.Trace) error {
return nil
}
// Serialize plugin logs once for all entries
var pluginLogsJSON string
if len(trace.PluginLogs) > 0 {
grouped := schemas.GroupPluginLogsByName(trace.PluginLogs)
if data, err := sonic.Marshal(grouped); err == nil {
pluginLogsJSON = string(data)
}
}
pluginLogsJSON := serializePluginLogs(trace.PluginLogs)
p.logger.Debug("Inject: enqueuing %d log entries", len(pending.entries))
// Enqueue each log entry (supports multiple attempts per trace)
for _, entry := range pending.entries {
Expand All @@ -1646,6 +1640,18 @@ func (p *LoggerPlugin) Inject(_ context.Context, trace *schemas.Trace) error {
return nil
}

// serializePluginLogs groups plugin logs by plugin name for persistence and UI rendering.
func serializePluginLogs(logs []schemas.PluginLogEntry) string {
if len(logs) == 0 {
return ""
}
data, err := sonic.Marshal(schemas.GroupPluginLogsByName(logs))
if err != nil {
return ""
}
return string(data)
}

// MCP Plugin Interface Implementation

// SetMCPToolLogCallback sets a callback function that will be called for each MCP tool log entry
Expand Down Expand Up @@ -1910,6 +1916,7 @@ func (p *LoggerPlugin) PostMCPHook(ctx *schemas.BifrostContext, resp *schemas.Bi
callback := p.mcpToolLogCallback
p.mu.Unlock()
attachMCPLogRedactionData(ctx, entry, p.contentLoggingEnabled(ctx))
entry.PluginLogs = serializePluginLogs(ctx.GetPluginLogs())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n -C 4 'GetPluginLogs|PluginLogEntry|contentLoggingEnabled|DisableContentLogging|\.Log\(' plugins core framework

Repository: maximhq/bifrost

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- logging implementation ---'
rg -n -C 12 'entry\.PluginLogs|serializePluginLogs|func \(.*\) contentLoggingEnabled|func \(.*\) resolveContentPolicy|PluginLogEntry|GetPluginLogs' plugins/logging --glob '*.go'

printf '%s\n' '--- context log implementations ---'
rg -n -C 10 'func .*Log\(|PluginLogs|WithPluginScope|ReleasePluginScope' --glob '*.go' core framework plugins | head -n 1200

printf '%s\n' '--- plugin_logs response/API exposure ---'
rg -n -C 8 'plugin_logs|PluginLogs' --glob '*.go' --glob '*.sql' --glob '*.json' .

Repository: maximhq/bifrost

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact update path ---'
sed -n '1820,1970p' plugins/logging/main.go

printf '%s\n' '--- policy helpers and nearby tests ---'
rg -n -C 20 'contentLoggingEnabled|resolveContentPolicy|ContentPolicy|DisableContentLogging' plugins/logging/main.go plugins/logging/*.go

printf '%s\n' '--- log entry schema and serializer ---'
rg -n -C 15 'type PluginLogEntry|func serializePluginLogs|serializePluginLogs\(' --glob '*.go' .

Repository: maximhq/bifrost

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib, re

roots = [pathlib.Path("plugins"), pathlib.Path("core"), pathlib.Path("framework")]
patterns = [
    re.compile(r'\.Log\s*\('),
    re.compile(r'PluginLogEntry'),
    re.compile(r'GetPluginLogs'),
]
for root in roots:
    for path in root.rglob("*.go"):
        try:
            text = path.read_text()
        except Exception:
            continue
        hits = []
        for i, line in enumerate(text.splitlines(), 1):
            if any(p.search(line) for p in patterns):
                hits.append((i, line.strip()))
        if hits:
            print(f"\n## {path}")
            for i, line in hits:
                print(f"{i}: {line}")
PY

Repository: maximhq/bifrost

Length of output: 24365


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- disabled-content tests and plugin-log tests ---'
rg -n -C 12 'PluginLogs|plugin logs|content.*log|log.*content|DisableContentLogging' plugins/logging/*_test.go

printf '%s\n' '--- all production plugin log writes ---'
rg -n -C 8 '\.Log\s*\(' plugins --glob '*.go' --glob '!**/*_test.go'

Repository: maximhq/bifrost

Length of output: 49224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- repository stack metadata ---'
git status --short
git branch --all --no-color | head -n 100
find . -maxdepth 3 -type f \( -iname '*stack*' -o -name 'stack.yaml' -o -name 'stack.yml' \) -print

Repository: maximhq/bifrost

Length of output: 319


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PluginLogEntry and context behavior ---'
sed -n '440,475p' core/schemas/bifrost.go
sed -n '730,815p' core/schemas/context.go

printf '%s\n' '--- MCP log schema, serialization, and hidden-content handling ---'
rg -n -C 12 'type MCPToolLog|PluginLogs.*string|ContentHidden|content_hidden|SerializeFields|DeserializeFields' framework/logstore core --glob '*.go'

printf '%s\n' '--- MCP log retrieval/API paths ---'
rg -n -C 10 'GetMCPToolLog|SearchMCPToolLogs|MCPToolLog' --glob '*.go' --glob '!**/*_test.go' . | head -n 1600

Repository: maximhq/bifrost

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- PostMCPHook tests around content policy ---'
sed -n '1030,1155p' plugins/logging/operations_test.go
rg -n -C 15 'PostMCPHook|MCPToolLog|PluginLogs' plugins/logging/*_test.go | grep -E '^[^:]+:[0-9]+:|content|disabled|PluginLogs|PostMCPHook|MCPToolLog' | head -n 1200

printf '%s\n' '--- all production ctx.Log producers, excluding test files ---'
rg -n -C 5 '\.Log\s*\(' plugins --glob '*.go' --glob '!**/*_test.go'

Repository: maximhq/bifrost

Length of output: 15064


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

path = Path("plugins/logging/main.go")
text = path.read_text()
start = text.index("func (p *LoggerPlugin) PostMCPHook")
end = text.index("\n}", start) + 2
body = text[start:end]

assignment = "entry.PluginLogs = serializePluginLogs(ctx.GetPluginLogs())"
print("post_mcp_hook_contains_unconditional_plugin_log_assignment:",
      assignment in body)
print("assignment_count_in_post_mcp_hook:", body.count(assignment))
print("assignment_context:")
for line_no, line in enumerate(body.splitlines(), 1):
    if "PluginLogs" in line or "contentLoggingEnabled" in line or "resolveContentPolicy" in line:
        print(f"{line_no}: {line}")

policy = text[text.index("type contentPolicy"):text.index("// applyMCPGovernanceFieldsToEntry")]
print("content_logging_enabled_returns_store_content:",
      "return p.resolveContentPolicy(ctx).storeContent" in text)
print("disabled_without_object_storage_returns_empty_policy:",
      "return contentPolicy{}" in policy)
PY

Repository: maximhq/bifrost

Length of output: 699


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact logstore definitions ---'
rg -l 'type MCPToolLog|ContentHidden|PluginLogs' framework/logstore core | sort
for f in $(rg -l 'type MCPToolLog|ContentHidden|PluginLogs' framework/logstore core | sort | head -n 20); do
  echo "## $f"
  rg -n -C 20 'type MCPToolLog|PluginLogs|ContentHidden|content_hidden|SerializeFields|DeserializeFields' "$f"
done

Repository: maximhq/bifrost

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- API route and response serialization references ---'
rg -n -C 12 'mcp.*log|MCP.*log|plugin_logs|PluginLogs' . \
  --glob '*.go' \
  --glob '!**/*_test.go' \
  --glob '!plugins/logging/main.go' \
  --glob '!framework/logstore/*.go' | head -n 1800

Repository: maximhq/bifrost

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- MCP payload preparation and serialization ---'
sed -n '1057,1135p' framework/logstore/tables.go
sed -n '385,435p' framework/logstore/payload.go

printf '%s\n' '--- MCP handler response paths ---'
sed -n '2555,2665p' transports/bifrost-http/handlers/logging.go
rg -n -A 45 -B 8 'func \(h \*LoggingHandler\) getMCPLogByID|func \(h \*LoggingHandler\) getMCPLogs' transports/bifrost-http/handlers/logging.go

Repository: maximhq/bifrost

Length of output: 18069


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

tables = Path("framework/logstore/tables.go").read_text()
payload = Path("framework/logstore/payload.go").read_text()
handler = Path("transports/bifrost-http/handlers/logging.go").read_text()

mcp_start = tables.index("type MCPToolLog struct")
mcp_end = tables.index("\n}", mcp_start) + 2
mcp = tables[mcp_start:mcp_end]

prep_start = payload.index("func PrepareMCPToolDBEntry")
prep_end = payload.index("\n}", prep_start) + 2
prep = payload[prep_start:prep_end]

print("mcp_plugin_logs_has_json_field:", '`json:"plugin_logs,omitempty"`' in mcp)
print("mcp_has_content_hidden_field:", "ContentHidden" in mcp)
print("prepare_mcp_clears_plugin_logs:", "PluginLogs =" in prep or "PluginLogs:" in prep)
print("handler_serializes_mcp_search_result:", "SendJSON(ctx, result)" in handler)
print("handler_serializes_mcp_detail_log:", "SendJSON(ctx, log)" in handler)
print("mcp_detail_route_present:", '"/api/mcp-logs/{id}"' in handler)
PY

Repository: maximhq/bifrost

Length of output: 379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact MCP handler methods ---'
rg -n 'func \(h \*LoggingHandler\) (getMCPLogs|getMCPLogByID)' transports/bifrost-http/handlers/logging.go
for n in $(rg -n 'func \(h \*LoggingHandler\) (getMCPLogs|getMCPLogByID)' transports/bifrost-http/handlers/logging.go | cut -d: -f1); do
  start=$((n-2)); end=$((n+100))
  sed -n "${start},${end}p" transports/bifrost-http/handlers/logging.go
done

Repository: maximhq/bifrost

Length of output: 15591


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- MCP content-hidden regression coverage ---'
sed -n '1,240p' framework/logstore/contenthidden_test.go
rg -n -C 12 'PluginLogs|plugin_logs|ContentHidden|content_hidden' framework/logstore/*_test.go plugins/logging/*_test.go transports/bifrost-http/handlers/*_test.go

Repository: maximhq/bifrost

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

tables = Path("framework/logstore/tables.go").read_text()
payload = Path("framework/logstore/payload.go").read_text()
handler = Path("transports/bifrost-http/handlers/logging.go").read_text()

mcp_start = tables.index("type MCPToolLog struct")
mcp_end = tables.index("\n}", mcp_start) + 2
mcp = tables[mcp_start:mcp_end]

prep_start = payload.index("func PrepareMCPToolDBEntry")
prep_end = payload.index("\n}", prep_start) + 2
prep = payload[prep_start:prep_end]

print("mcp_plugin_logs_json_visible:", 'json:"plugin_logs,omitempty"' in mcp)
print("mcp_has_content_hidden_field:", "ContentHidden" in mcp)
print("prepare_mcp_clears_plugin_logs:", "PluginLogs" in prep)
print("handler_serializes_mcp_search_result:", "SendJSON(ctx, result)" in handler)
print("handler_serializes_mcp_detail_log:", "SendJSON(ctx, log)" in handler)
print("mcp_detail_route_present:", '"/api/mcp-logs/{id}"' in handler)
PY

Repository: maximhq/bifrost

Length of output: 376


Apply the visible-content policy to MCP plugin logs.

When content is disabled or hidden, assign PluginLogs only if p.resolveContentPolicy(ctx).visible() is true. MCPToolLog.PluginLogs remains persisted and JSON-visible through both MCP log endpoints.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@plugins/logging/main.go` at line 1919, Update the assignment to
entry.PluginLogs using the content policy from p.resolveContentPolicy(ctx): only
assign serializePluginLogs(ctx.GetPluginLogs()) when the policy is visible.
Preserve MCPToolLog.PluginLogs persistence and JSON visibility through both MCP
log endpoints.

Sources: Coding guidelines, Path instructions

p.enqueueMCPToolLogEntry(entry, callback)

return resp, bifrostErr, nil
Expand Down
21 changes: 16 additions & 5 deletions plugins/logging/operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package logging

import (
"context"
"encoding/json"
"errors"
"path/filepath"
"strings"
Expand Down Expand Up @@ -1032,9 +1033,9 @@ func TestBuildLogEntriesOmitEmptyUserAgent(t *testing.T) {
}
}

// TestMCPHooksDeferDBWriteUntilPostHookBatch verifies MCP logs are kept in
// memory after PreMCPHook and persisted by the batch writer after PostMCPHook.
func TestMCPHooksDeferDBWriteUntilPostHookBatch(t *testing.T) {
// TestMCPHooksPersistPluginLogs verifies PostMCPHook stores the plugin-log
// snapshot accumulated before logging's post-hook runs.
func TestMCPHooksPersistPluginLogs(t *testing.T) {
store := newTestStore(t)
plugin, err := Init(context.Background(), &Config{}, testLogger{}, store, nil, nil)
if err != nil {
Expand Down Expand Up @@ -1085,6 +1086,11 @@ func TestMCPHooksDeferDBWriteUntilPostHookBatch(t *testing.T) {
}

result := `{"answer":"done"}`
guardrailsName := "guardrails"
guardrailsCtx := ctx.WithPluginScope(&guardrailsName)
guardrailsCtx.Log(schemas.LogLevelInfo, "MCP tool arguments redacted")
guardrailsCtx.ReleasePluginScope()

_, _, err = plugin.PostMCPHook(ctx, &schemas.BifrostMCPResponse{
ChatMessage: &schemas.ChatMessage{
Role: schemas.ChatMessageRoleTool,
Expand All @@ -1106,7 +1112,6 @@ func TestMCPHooksDeferDBWriteUntilPostHookBatch(t *testing.T) {
if got := pendingEntry.RedactionData.ReversibleMappings.Output["EMAIL-2"]; got != "result@example.com" {
t.Fatalf("output redaction mapping = %q, want %q", got, "result@example.com")
}

if err := plugin.Cleanup(); err != nil {
t.Fatalf("Cleanup() error = %v", err)
}
Expand All @@ -1128,6 +1133,13 @@ func TestMCPHooksDeferDBWriteUntilPostHookBatch(t *testing.T) {
if logEntry.Latency == nil || *logEntry.Latency != 42 {
t.Fatalf("expected latency 42, got %#v", logEntry.Latency)
}
var pluginLogs map[string][]schemas.PluginLogEntry
if err := json.Unmarshal([]byte(logEntry.PluginLogs), &pluginLogs); err != nil {
t.Fatalf("expected valid plugin logs JSON, got %q: %v", logEntry.PluginLogs, err)
}
if got := pluginLogs[guardrailsName]; len(got) != 1 || got[0].Message != "MCP tool arguments redacted" {
t.Fatalf("expected guardrails plugin log to be persisted, got %#v", pluginLogs)
}
assertMCPLogGovernanceFields(t, logEntry, "user-1", "team-1", "customer-1", "bu-1")
}

Expand Down Expand Up @@ -1164,7 +1176,6 @@ func TestPostMCPHookFallbackStampsGovernanceFields(t *testing.T) {
if err != nil {
t.Fatalf("PostMCPHook() error = %v", err)
}

if err := plugin.Cleanup(); err != nil {
t.Fatalf("Cleanup() error = %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion plugins/logging/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ func estimateMCPToolLogEntrySize(log *logstore.MCPToolLog) int {
if log == nil {
return 0
}
return len(log.Arguments) + len(log.Result) + len(log.ErrorDetails) + len(log.Metadata) + 512
return len(log.Arguments) + len(log.Result) + len(log.ErrorDetails) + len(log.Metadata) + len(log.PluginLogs) + 512
}

// buildStaleMCPToolLogEntry converts a pending MCP processing row into a
Expand Down
11 changes: 9 additions & 2 deletions ui/app/workspace/logs/sheets/logDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -715,8 +715,15 @@ export function LogDetailView({
<div className="flex items-center gap-3">
{revealAvailable && (
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-[11px] font-medium">Show original values</span>
<Switch checked={revealEnabled} onCheckedChange={handleToggleReveal} data-testid="logdetails-reveal-toggle" />
<label htmlFor="logdetails-reveal-toggle" className="text-muted-foreground text-[11px] font-medium">
Show original values
</label>
<Switch
id="logdetails-reveal-toggle"
checked={revealEnabled}
onCheckedChange={handleToggleReveal}
data-testid="logdetails-reveal-toggle"
/>
</div>
)}
{onClose ? (
Expand Down
10 changes: 9 additions & 1 deletion ui/app/workspace/logs/views/pluginLogsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ interface PluginLogsViewProps {
pluginLogs: string;
}

function formatPluginName(name: string): string {
return name
.split(/[-_\s]+/)
.filter(Boolean)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
}

export default function PluginLogsView({ pluginLogs }: PluginLogsViewProps) {
let parsed: Record<string, PluginLogEntry[]>;
try {
Expand Down Expand Up @@ -58,7 +66,7 @@ function PluginSection({ name, entries }: { name: string; entries: PluginLogEntr
className="hover:bg-muted/50 flex w-full items-center gap-2 px-4 py-2 text-left text-sm"
>
{isOpen ? <ChevronDown className="h-3.5 w-3.5" /> : <ChevronRight className="h-3.5 w-3.5" />}
<span className="font-medium">{name}</span>
<span className="font-medium">{formatPluginName(name)}</span>
<span className="text-muted-foreground text-xs">({entries.length})</span>
</button>
{isOpen && (
Expand Down
Loading
Loading