Skip to content
Merged
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
10 changes: 10 additions & 0 deletions backend/docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3784,6 +3784,12 @@ const docTemplate = `{
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "是否用远端元数据覆盖管理员自定义的工具名称和说明",
"name": "overwrite_customized_metadata",
"in": "query"
}
],
"responses": {
Expand Down Expand Up @@ -19217,6 +19223,7 @@ const docTemplate = `{
"lastError",
"lastSyncedAt",
"name",
"requiresToolMetadataSyncConfirmation",
"sortOrder",
"status",
"toolCount",
Expand Down Expand Up @@ -19249,6 +19256,9 @@ const docTemplate = `{
"name": {
"type": "string"
},
"requiresToolMetadataSyncConfirmation": {
"type": "boolean"
},
"sortOrder": {
"type": "integer"
},
Expand Down
10 changes: 10 additions & 0 deletions backend/docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -3777,6 +3777,12 @@
"name": "id",
"in": "path",
"required": true
},
{
"type": "boolean",
"description": "是否用远端元数据覆盖管理员自定义的工具名称和说明",
"name": "overwrite_customized_metadata",
"in": "query"
}
],
"responses": {
Expand Down Expand Up @@ -19210,6 +19216,7 @@
"lastError",
"lastSyncedAt",
"name",
"requiresToolMetadataSyncConfirmation",
"sortOrder",
"status",
"toolCount",
Expand Down Expand Up @@ -19242,6 +19249,9 @@
"name": {
"type": "string"
},
"requiresToolMetadataSyncConfirmation": {
"type": "boolean"
},
"sortOrder": {
"type": "integer"
},
Expand Down
7 changes: 7 additions & 0 deletions backend/docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5613,6 +5613,8 @@ definitions:
x-omitempty: false
name:
type: string
requiresToolMetadataSyncConfirmation:
type: boolean
sortOrder:
type: integer
status:
Expand All @@ -5630,6 +5632,7 @@ definitions:
- lastError
- lastSyncedAt
- name
- requiresToolMetadataSyncConfirmation
- sortOrder
- status
- toolCount
Expand Down Expand Up @@ -10261,6 +10264,10 @@ paths:
name: id
required: true
type: integer
- description: 是否用远端元数据覆盖管理员自定义的工具名称和说明
in: query
name: overwrite_customized_metadata
type: boolean
produces:
- application/json
responses:
Expand Down
12 changes: 7 additions & 5 deletions backend/internal/application/mcp/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ type ToolInput struct {

// SyncServerToolsInput 描述一次 MCP 工具同步请求。
type SyncServerToolsInput struct {
ServerID uint
RequestID string
ServerID uint
RequestID string
OverwriteCustomizedMetadata bool
}

// NewServiceWithRuntime 创建 MCP 应用服务。
Expand Down Expand Up @@ -190,16 +191,17 @@ func (s *Service) SyncServerTools(ctx context.Context, input SyncServerToolsInpu
Status: "active",
})
}
if err = s.repo.ReplaceServerTools(ctx, serverID, items); err != nil {
if err = s.repo.ReplaceServerTools(ctx, serverID, items, input.OverwriteCustomizedMetadata); err != nil {
return fail(err)
}
result, err := s.repo.ListTools(ctx, serverID, false)
if err != nil {
return fail(err)
}
s.writeToolSyncEvent(ctx, input.RequestID, "info", "mcp.tools_synced", serverID, "MCP 工具已同步", map[string]interface{}{
"server_id": serverID,
"tool_count": len(result),
"server_id": serverID,
"tool_count": len(result),
"overwrite_customized_metadata": input.OverwriteCustomizedMetadata,
})
return result, nil
}
Expand Down
27 changes: 14 additions & 13 deletions backend/internal/domain/mcp/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import "time"

// Server 表示管理员维护的 MCP 服务。
type Server struct {
ID uint
Name string
BaseURL string
AuthTokenEnc string
HeadersJSON string
Status string
SortOrder int
ToolCount int
ActiveToolCount int
LastSyncedAt *time.Time
LastError string
CreatedAt time.Time
UpdatedAt time.Time
ID uint
Name string
BaseURL string
AuthTokenEnc string
HeadersJSON string
Status string
SortOrder int
ToolCount int
ActiveToolCount int
RequiresToolMetadataSyncConfirmation bool
LastSyncedAt *time.Time
LastError string
CreatedAt time.Time
UpdatedAt time.Time
}

type ServerWithTools struct {
Expand Down
15 changes: 8 additions & 7 deletions backend/internal/infra/persistence/models/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ func (MCPServer) TableName() string {
// MCPTool 存储 MCP 服务发现的工具。
type MCPTool struct {
ControlPlaneModel
ServerID uint `gorm:"not null;default:0;uniqueIndex:idx_mcp_tools_server_name,priority:1;index:idx_mcp_tools_server_id;comment:MCP服务ID"`
Name string `gorm:"size:160;not null;default:'';uniqueIndex:idx_mcp_tools_server_name,priority:2;comment:工具名称"`
DisplayName string `gorm:"size:160;not null;default:'';comment:展示名称"`
Description string `gorm:"type:text;not null;default:'';comment:工具说明"`
InputSchemaJSON string `gorm:"type:text;not null;default:'{}';comment:输入JSON Schema"`
Status string `gorm:"size:32;not null;default:'inactive';index:idx_mcp_tools_status;comment:工具状态(active/inactive)"`
SortOrder int `gorm:"not null;default:0;index:idx_mcp_tools_sort_order;comment:展示顺序"`
ServerID uint `gorm:"not null;default:0;uniqueIndex:idx_mcp_tools_server_name,priority:1;index:idx_mcp_tools_server_id;comment:MCP服务ID"`
Name string `gorm:"size:160;not null;default:'';uniqueIndex:idx_mcp_tools_server_name,priority:2;comment:工具名称"`
DisplayName string `gorm:"size:160;not null;default:'';comment:展示名称"`
Description string `gorm:"type:text;not null;default:'';comment:工具说明"`
MetadataCustomized *bool `gorm:"comment:名称或说明是否由管理员修改(NULL表示升级前状态待确认)"`
InputSchemaJSON string `gorm:"type:text;not null;default:'{}';comment:输入JSON Schema"`
Status string `gorm:"size:32;not null;default:'inactive';index:idx_mcp_tools_status;comment:工具状态(active/inactive)"`
SortOrder int `gorm:"not null;default:0;index:idx_mcp_tools_sort_order;comment:展示顺序"`
}

func (MCPTool) TableName() string {
Expand Down
130 changes: 99 additions & 31 deletions backend/internal/infra/persistence/postgres/mcp/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func listServers(ctx context.Context, db *gorm.DB) ([]domainmcp.Server, error) {
return nil, err
}
activeCounts := map[uint]int{}
metadataConfirmationServers := map[uint]bool{}
if len(rows) > 0 {
serverIDs := make([]uint, 0, len(rows))
for _, row := range rows {
Expand All @@ -105,11 +106,23 @@ func listServers(ctx context.Context, db *gorm.DB) ([]domainmcp.Server, error) {
for _, item := range counts {
activeCounts[item.ServerID] = item.Count
}
var confirmationServerIDs []uint
if err := db.WithContext(ctx).
Model(&model.MCPTool{}).
Distinct("server_id").
Where("server_id IN ? AND (metadata_customized = ? OR metadata_customized IS NULL)", serverIDs, true).
Pluck("server_id", &confirmationServerIDs).Error; err != nil {
return nil, err
}
for _, serverID := range confirmationServerIDs {
metadataConfirmationServers[serverID] = true
}
}
items := make([]domainmcp.Server, 0, len(rows))
for _, row := range rows {
item := toDomainServer(row)
item.ActiveToolCount = activeCounts[row.ID]
item.RequiresToolMetadataSyncConfirmation = metadataConfirmationServers[row.ID]
items = append(items, item)
}
return items, nil
Expand All @@ -121,6 +134,22 @@ func (r *Repo) GetServer(ctx context.Context, serverID uint) (*domainmcp.Server,
return nil, err
}
item := toDomainServer(row)
var activeToolCount int64
if err := r.db.WithContext(ctx).
Model(&model.MCPTool{}).
Where("server_id = ? AND status = ?", serverID, "active").
Count(&activeToolCount).Error; err != nil {
return nil, err
}
var metadataConfirmationCount int64
if err := r.db.WithContext(ctx).
Model(&model.MCPTool{}).
Where("server_id = ? AND (metadata_customized = ? OR metadata_customized IS NULL)", serverID, true).
Count(&metadataConfirmationCount).Error; err != nil {
return nil, err
}
item.ActiveToolCount = int(activeToolCount)
item.RequiresToolMetadataSyncConfirmation = metadataConfirmationCount > 0
return &item, nil
}

Expand All @@ -140,7 +169,7 @@ func (r *Repo) DeleteServer(ctx context.Context, serverID uint) error {
})
}

func (r *Repo) ReplaceServerTools(ctx context.Context, serverID uint, tools []domainmcp.Tool) error {
func (r *Repo) ReplaceServerTools(ctx context.Context, serverID uint, tools []domainmcp.Tool, overwriteCustomizedMetadata bool) error {
now := time.Now()
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var maxSortOrder int
Expand All @@ -153,24 +182,47 @@ func (r *Repo) ReplaceServerTools(ctx context.Context, serverID uint, tools []do
rows := make([]model.MCPTool, 0, len(tools))
names := make([]string, 0, len(tools))
for index, tool := range tools {
metadataCustomized := false
names = append(names, tool.Name)
rows = append(rows, model.MCPTool{
ServerID: serverID,
Name: tool.Name,
DisplayName: tool.DisplayName,
Description: tool.Description,
InputSchemaJSON: tool.InputSchemaJSON,
Status: tool.Status,
SortOrder: maxSortOrder + (index+1)*100,
ServerID: serverID,
Name: tool.Name,
DisplayName: tool.DisplayName,
Description: tool.Description,
MetadataCustomized: &metadataCustomized,
InputSchemaJSON: tool.InputSchemaJSON,
Status: tool.Status,
SortOrder: maxSortOrder + (index+1)*100,
})
}
if len(rows) > 0 {
targetColumn := func(name string) string {
if tx.Dialector.Name() == "postgres" {
return `"mcp_tools"."` + name + `"`
}
return `"` + name + `"`
}
metadataCustomizedColumn := targetColumn("metadata_customized")
displayNameColumn := targetColumn("display_name")
descriptionColumn := targetColumn("description")
legacyMetadataDiffers := "(" + displayNameColumn + ` <> excluded."display_name" OR ` + descriptionColumn + ` <> excluded."description")`
metadataAssignments := map[string]interface{}{
"display_name": gorm.Expr("CASE WHEN COALESCE(" + metadataCustomizedColumn + ", TRUE) THEN " + displayNameColumn + ` ELSE excluded."display_name" END`),
"description": gorm.Expr("CASE WHEN COALESCE(" + metadataCustomizedColumn + ", TRUE) THEN " + descriptionColumn + ` ELSE excluded."description" END`),
"metadata_customized": gorm.Expr("CASE WHEN " + metadataCustomizedColumn + " IS NULL THEN " + legacyMetadataDiffers + " ELSE " + metadataCustomizedColumn + " END"),
}
if overwriteCustomizedMetadata {
metadataAssignments = map[string]interface{}{
"display_name": gorm.Expr(`excluded."display_name"`),
"description": gorm.Expr(`excluded."description"`),
"metadata_customized": false,
}
}
metadataAssignments["input_schema_json"] = gorm.Expr(`excluded."input_schema_json"`)
metadataAssignments["updated_at"] = gorm.Expr(`excluded."updated_at"`)
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "server_id"}, {Name: "name"}},
DoUpdates: clause.AssignmentColumns([]string{
"input_schema_json",
"updated_at",
}),
Columns: []clause.Column{{Name: "server_id"}, {Name: "name"}},
DoUpdates: clause.Assignments(metadataAssignments),
}).Create(&rows).Error; err != nil {
return err
}
Expand Down Expand Up @@ -249,27 +301,43 @@ func (r *Repo) ListToolsByIDs(ctx context.Context, toolIDs []uint) ([]domainmcp.
}

func (r *Repo) UpdateTool(ctx context.Context, toolID uint, input repository.UpdateMCPToolInput) (*domainmcp.Tool, error) {
updates := map[string]interface{}{}
if input.DisplayName != nil {
updates["display_name"] = *input.DisplayName
}
if input.Description != nil {
updates["description"] = *input.Description
}
if input.Status != nil {
updates["status"] = *input.Status
}
if len(updates) > 0 {
if err := r.db.WithContext(ctx).Model(&model.MCPTool{}).Where("id = ?", toolID).Updates(updates).Error; err != nil {
return nil, err
var result domainmcp.Tool
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var row model.MCPTool
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, "id = ?", toolID).Error; err != nil {
return err
}
}
var row model.MCPTool
if err := r.db.WithContext(ctx).First(&row, "id = ?", toolID).Error; err != nil {
updates := map[string]interface{}{}
metadataChanged := false
if input.DisplayName != nil && *input.DisplayName != row.DisplayName {
updates["display_name"] = *input.DisplayName
metadataChanged = true
}
if input.Description != nil && *input.Description != row.Description {
updates["description"] = *input.Description
metadataChanged = true
}
if metadataChanged {
updates["metadata_customized"] = true
}
if input.Status != nil && *input.Status != row.Status {
updates["status"] = *input.Status
}
if len(updates) > 0 {
if err := tx.Model(&model.MCPTool{}).Where("id = ?", toolID).Updates(updates).Error; err != nil {
return err
}
if err := tx.First(&row, "id = ?", toolID).Error; err != nil {
return err
}
}
result = toDomainTool(row)
return nil
})
if err != nil {
return nil, err
}
item := toDomainTool(row)
return &item, nil
return &result, nil
}

func (r *Repo) UpdateServerToolsStatus(ctx context.Context, serverID uint, toolIDs []uint, status string) ([]domainmcp.Tool, error) {
Expand Down
Loading
Loading