refactor: enhance MCP tool management with risk policies and confirmation handling

This commit is contained in:
mlogclub
2026-07-28 11:33:15 +08:00
parent ed109047d3
commit 07370fe9a2
17 changed files with 448 additions and 117 deletions
@@ -0,0 +1,39 @@
package services
import (
"testing"
"agent-desk/internal/pkg/dto/request"
)
func TestValidateMCPToolRiskPolicyRejectsTrustedToolOverride(t *testing.T) {
_, err := validateMCPToolRiskPolicy(request.AIAgentMCPToolRequest{
ToolCode: "system/server_time",
RiskLevel: "write",
RequireConfirmation: true,
})
if err == nil {
t.Fatal("expected trusted system tool policy override to be rejected")
}
item, err := validateMCPToolRiskPolicy(request.AIAgentMCPToolRequest{
ToolCode: "system/server_time",
RiskLevel: "read",
})
if err != nil {
t.Fatalf("validate trusted system tool policy: %v", err)
}
if item.Title != "获取当前时间" || item.RiskLevel != "read" || item.RequireConfirmation {
t.Fatalf("unexpected normalized trusted policy: %#v", item)
}
}
func TestValidateMCPToolRiskPolicyRequiresWriteConfirmation(t *testing.T) {
_, err := validateMCPToolRiskPolicy(request.AIAgentMCPToolRequest{
ToolCode: "crm/update_customer",
RiskLevel: "write",
})
if err == nil {
t.Fatal("expected write tool without confirmation to be rejected")
}
}
+21 -10
View File
@@ -216,11 +216,8 @@ func (s *aIAgentService) validatePublishableAgent(db *gorm.DB, agent *models.AIA
if err != nil || definition.InputSchema == nil {
return errorsx.InvalidParam("ai agent MCP tool definition is unavailable")
}
if item.RiskLevel != aitooling.RiskLevelRead && item.RiskLevel != aitooling.RiskLevelWrite {
return errorsx.InvalidParam("ai agent MCP tool risk level is invalid")
}
if item.RiskLevel == aitooling.RiskLevelWrite && !item.RequireConfirmation {
return errorsx.InvalidParam("write MCP tools must require confirmation")
if _, err := validateMCPToolRiskPolicy(item); err != nil {
return err
}
}
for _, binding := range s.ListEnabledWorkflowBindings(db, agent.ID) {
@@ -522,12 +519,10 @@ func (s *aIAgentService) normalizeMCPTools(input []request.AIAgentMCPToolRequest
return nil, err
}
normalized.RiskLevel = strings.ToLower(strings.TrimSpace(item.RiskLevel))
if normalized.RiskLevel != aitooling.RiskLevelRead && normalized.RiskLevel != aitooling.RiskLevelWrite {
return nil, errorsx.InvalidParam("MCP tool risk level must be read or write")
}
normalized.RequireConfirmation = item.RequireConfirmation
if normalized.RiskLevel == aitooling.RiskLevelWrite && !normalized.RequireConfirmation {
return nil, errorsx.InvalidParam("write MCP tools must require confirmation")
normalized, err = validateMCPToolRiskPolicy(normalized)
if err != nil {
return nil, err
}
key := strings.TrimSpace(normalized.ToolCode)
if _, exists := seen[key]; exists {
@@ -539,6 +534,22 @@ func (s *aIAgentService) normalizeMCPTools(input []request.AIAgentMCPToolRequest
return ret, nil
}
func validateMCPToolRiskPolicy(item request.AIAgentMCPToolRequest) (request.AIAgentMCPToolRequest, error) {
if policy, ok := toolx.GetTrustedMCPToolPolicy(item.ToolCode); ok {
if item.RiskLevel != policy.RiskLevel || item.RequireConfirmation != policy.RequireConfirmation {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("system MCP tool risk policy cannot be changed")
}
return toolx.ApplyTrustedMCPToolPolicy(item), nil
}
if item.RiskLevel != aitooling.RiskLevelRead && item.RiskLevel != aitooling.RiskLevelWrite {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("MCP tool risk level must be read or write")
}
if item.RiskLevel == aitooling.RiskLevelWrite && !item.RequireConfirmation {
return request.AIAgentMCPToolRequest{}, errorsx.InvalidParam("write MCP tools must require confirmation")
}
return item, nil
}
func (s *aIAgentService) UpdateSort(ids []int64) error {
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
for i, id := range ids {
+42 -18
View File
@@ -22,15 +22,18 @@ func newToolCatalogService() *toolCatalogService {
type toolCatalogService struct{}
type MCPToolCatalogItem struct {
ToolCode string
ServerCode string
ToolName string
SourceType enums.ToolSourceType
AutoInjected bool
Title string
Description string
InputSchema any
OutputSchema any
ToolCode string
ServerCode string
ToolName string
SourceType enums.ToolSourceType
AutoInjected bool
Title string
Description string
InputSchema any
OutputSchema any
RiskLevel string
RequireConfirmation bool
RiskEditable bool
}
func (s *toolCatalogService) ListMCPTools(ctx context.Context) ([]MCPToolCatalogItem, error) {
@@ -71,16 +74,37 @@ func (s *toolCatalogService) ListMCPToolsWithLocale(ctx context.Context, locale
return nil, err
}
for _, item := range tools {
toolCode := toolx.BuildMCPToolCode(serverCode, item.Name)
title := strings.TrimSpace(item.Title)
riskLevel := toolx.MCPRiskLevelWrite
requireConfirmation := true
riskEditable := true
if item.ReadOnlyHint {
riskLevel = toolx.MCPRiskLevelRead
requireConfirmation = false
}
if policy, ok := toolx.GetTrustedMCPToolPolicy(toolCode); ok {
title = policy.Title
riskLevel = policy.RiskLevel
requireConfirmation = policy.RequireConfirmation
riskEditable = false
}
if title == "" {
title = strings.TrimSpace(item.Name)
}
ret = append(ret, MCPToolCatalogItem{
ToolCode: toolx.BuildMCPToolCode(serverCode, item.Name),
ServerCode: serverCode,
ToolName: strings.TrimSpace(item.Name),
SourceType: enums.ToolSourceTypeMCP,
AutoInjected: false,
Title: strings.TrimSpace(item.Title),
Description: strings.TrimSpace(item.Description),
InputSchema: item.InputSchema,
OutputSchema: item.OutputSchema,
ToolCode: toolCode,
ServerCode: serverCode,
ToolName: strings.TrimSpace(item.Name),
SourceType: enums.ToolSourceTypeMCP,
AutoInjected: false,
Title: title,
Description: strings.TrimSpace(item.Description),
InputSchema: item.InputSchema,
OutputSchema: item.OutputSchema,
RiskLevel: riskLevel,
RequireConfirmation: requireConfirmation,
RiskEditable: riskEditable,
})
}
}