From 2a286cc963dd70093efbfe67718889087853a471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Thu, 23 Jul 2026 13:46:24 -0600 Subject: [PATCH 1/2] fix[backend](socai/mcp): fixed create/update rule schemas --- backend/modules/mcp/tools_eventprocessing.go | 95 ++++++++++++++++++-- backend/modules/mcp/tools_small.go | 9 +- 2 files changed, 96 insertions(+), 8 deletions(-) diff --git a/backend/modules/mcp/tools_eventprocessing.go b/backend/modules/mcp/tools_eventprocessing.go index 76a5cbdba..106f5f8f4 100644 --- a/backend/modules/mcp/tools_eventprocessing.go +++ b/backend/modules/mcp/tools_eventprocessing.go @@ -2,12 +2,25 @@ package mcp import ( "context" + "encoding/json" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/utmstack/utmstack/backend/modules/eventprocessing/dto" "github.com/utmstack/utmstack/backend/pkg/authz" ) +// toRaw re-encodes an already-decoded JSON value as json.RawMessage. +// ponytail: MCP wrapper inputs use `any` for fields the DTO stores as +// json.RawMessage — reflection on RawMessage produces "array of uint8" in the +// generated tool schema, blocking LLM clients. See epRuleCreateInput. +func toRaw(v any) json.RawMessage { + if v == nil { + return nil + } + b, _ := json.Marshal(v) // v came from json.Unmarshal into any, cannot fail + return b +} + func registerEventProcessing(m *Module) { registerEPRegexPatterns(m) registerEPTenantConfigs(m) @@ -188,24 +201,96 @@ type epRulePropertyValuesInput struct { Value string `json:"value,omitempty"` } +// epRuleCreateInput mirrors dto.CreateCorrelationRuleRequest for MCP tool +// registration. The five DSL fields are typed as `any` so the generated JSON +// schema advertises "anything goes" instead of "array of uint8" (the default +// for json.RawMessage under reflection). Values are re-marshaled via toRaw +// before being handed to the usecase. +type epRuleCreateInput struct { + Name string `json:"name"` + Adversary string `json:"adversary,omitempty"` + Confidentiality int `json:"confidentiality"` + Integrity int `json:"integrity"` + Availability int `json:"availability"` + Category string `json:"category,omitempty"` + Technique string `json:"technique,omitempty"` + Description string `json:"description,omitempty"` + + References any `json:"references,omitempty"` + Definition any `json:"definition"` + GroupBy any `json:"groupBy,omitempty"` + DeduplicateBy any `json:"deduplicateBy,omitempty"` + Correlation any `json:"correlation,omitempty"` + + RuleActive bool `json:"ruleActive"` + DataTypes []dto.DataTypeRef `json:"dataTypes,omitempty"` +} + +type epRuleUpdateInput struct { + RelPath string `json:"relPath"` + epRuleCreateInput +} + +func (in epRuleCreateInput) toDTO() dto.CreateCorrelationRuleRequest { + return dto.CreateCorrelationRuleRequest{ + RuleName: in.Name, + RuleAdversary: in.Adversary, + RuleConfidentiality: in.Confidentiality, + RuleIntegrity: in.Integrity, + RuleAvailability: in.Availability, + RuleCategory: in.Category, + RuleTechnique: in.Technique, + RuleDescription: in.Description, + RuleReferencesDef: toRaw(in.References), + RuleDefinitionDef: toRaw(in.Definition), + RuleGroupByDef: toRaw(in.GroupBy), + DeduplicateByDef: toRaw(in.DeduplicateBy), + CorrelationDef: toRaw(in.Correlation), + RuleActive: in.RuleActive, + DataTypes: in.DataTypes, + } +} + +func (in epRuleUpdateInput) toDTO() dto.UpdateCorrelationRuleRequest { + c := in.epRuleCreateInput.toDTO() + return dto.UpdateCorrelationRuleRequest{ + RelPath: in.RelPath, + RuleName: c.RuleName, + RuleAdversary: c.RuleAdversary, + RuleConfidentiality: c.RuleConfidentiality, + RuleIntegrity: c.RuleIntegrity, + RuleAvailability: c.RuleAvailability, + RuleCategory: c.RuleCategory, + RuleTechnique: c.RuleTechnique, + RuleDescription: c.RuleDescription, + RuleReferencesDef: c.RuleReferencesDef, + RuleDefinitionDef: c.RuleDefinitionDef, + RuleGroupByDef: c.RuleGroupByDef, + DeduplicateByDef: c.DeduplicateByDef, + CorrelationDef: c.CorrelationDef, + RuleActive: c.RuleActive, + DataTypes: c.DataTypes, + } +} + func registerEPCorrelationRules(m *Module) { uc := m.deps.EventProcessing.GetCorrelationRuleUsecase() Add(m, &mcp.Tool{ Name: "correlation_rule.create", Title: "Create correlation rule", }, Gate{Permission: "eventprocessing.write"}, - func(ctx context.Context, _ *authz.Actor, in dto.CreateCorrelationRuleRequest) (any, error) { - if err := uc.Create(ctx, in); err != nil { + func(ctx context.Context, _ *authz.Actor, in epRuleCreateInput) (any, error) { + if err := uc.Create(ctx, in.toDTO()); err != nil { return nil, err } - return map[string]any{"name": in.RuleName, "created": true}, nil + return map[string]any{"name": in.Name, "created": true}, nil }) Add(m, &mcp.Tool{ Name: "correlation_rule.update", Title: "Update correlation rule", }, Gate{Permission: "eventprocessing.write"}, - func(ctx context.Context, _ *authz.Actor, in dto.UpdateCorrelationRuleRequest) (any, error) { - if err := uc.Update(ctx, in); err != nil { + func(ctx context.Context, _ *authz.Actor, in epRuleUpdateInput) (any, error) { + if err := uc.Update(ctx, in.toDTO()); err != nil { return nil, err } return map[string]any{"rel_path": in.RelPath, "updated": true}, nil diff --git a/backend/modules/mcp/tools_small.go b/backend/modules/mcp/tools_small.go index 15874a50c..e297d2ade 100644 --- a/backend/modules/mcp/tools_small.go +++ b/backend/modules/mcp/tools_small.go @@ -315,7 +315,9 @@ func registerBilling(m *Module) { // ---- socai.* --------------------------------------------------------------- type socaiAnalyzeInput struct { - Alert json.RawMessage `json:"alert" jsonschema:"Alert payload (any JSON object) forwarded to the external SOC AI service"` + // ponytail: `any` (not json.RawMessage) — RawMessage reflects as []uint8 + // and the SDK would advertise an "array of integer 0..255" schema. + Alert any `json:"alert" jsonschema:"Alert payload (any JSON object) forwarded to the external SOC AI service"` } func registerSOCAI(m *Module) { @@ -327,10 +329,11 @@ func registerSOCAI(m *Module) { Annotations: &mcp.ToolAnnotations{}, }, Gate{}, func(ctx context.Context, _ *authz.Actor, in socaiAnalyzeInput) (any, error) { - if len(in.Alert) == 0 { + if in.Alert == nil { return nil, fmt.Errorf("alert is required") } - status, body, err := client.Analyze(ctx, []byte(in.Alert)) + raw, _ := json.Marshal(in.Alert) + status, body, err := client.Analyze(ctx, raw) if err != nil { return nil, err } From 7d8096f015efe644590c5fe4361d87573e65947f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Thu, 23 Jul 2026 14:42:42 -0600 Subject: [PATCH 2/2] fix[backend](socai/mcp): added error handling for unmarshall --- backend/modules/mcp/tools_eventprocessing.go | 4 ---- backend/modules/mcp/tools_small.go | 7 ++++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/backend/modules/mcp/tools_eventprocessing.go b/backend/modules/mcp/tools_eventprocessing.go index 106f5f8f4..a33eccd51 100644 --- a/backend/modules/mcp/tools_eventprocessing.go +++ b/backend/modules/mcp/tools_eventprocessing.go @@ -9,10 +9,6 @@ import ( "github.com/utmstack/utmstack/backend/pkg/authz" ) -// toRaw re-encodes an already-decoded JSON value as json.RawMessage. -// ponytail: MCP wrapper inputs use `any` for fields the DTO stores as -// json.RawMessage — reflection on RawMessage produces "array of uint8" in the -// generated tool schema, blocking LLM clients. See epRuleCreateInput. func toRaw(v any) json.RawMessage { if v == nil { return nil diff --git a/backend/modules/mcp/tools_small.go b/backend/modules/mcp/tools_small.go index e297d2ade..94b000610 100644 --- a/backend/modules/mcp/tools_small.go +++ b/backend/modules/mcp/tools_small.go @@ -315,8 +315,6 @@ func registerBilling(m *Module) { // ---- socai.* --------------------------------------------------------------- type socaiAnalyzeInput struct { - // ponytail: `any` (not json.RawMessage) — RawMessage reflects as []uint8 - // and the SDK would advertise an "array of integer 0..255" schema. Alert any `json:"alert" jsonschema:"Alert payload (any JSON object) forwarded to the external SOC AI service"` } @@ -332,7 +330,10 @@ func registerSOCAI(m *Module) { if in.Alert == nil { return nil, fmt.Errorf("alert is required") } - raw, _ := json.Marshal(in.Alert) + raw, err := json.Marshal(in.Alert) + if err != nil { + return nil, fmt.Errorf("marshal alert: %w", err) + } status, body, err := client.Analyze(ctx, raw) if err != nil { return nil, err