Skip to content

Commit 67b073e

Browse files
feat[backend](mcp): added mcp module
1 parent 852e2bd commit 67b073e

43 files changed

Lines changed: 5594 additions & 84 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ type config struct {
5151

5252
// Features
5353
tfaEnabled bool
54+
55+
// MCP (Model Context Protocol) — exposes the backend usecases to MCP-aware
56+
// AI clients at /api/v1/mcp. Auth/permissions/audit reuse the REST stack.
57+
mcpEnabled bool
58+
mcpVersion string
5459
}
5560

5661
func loadConfig() *config {
@@ -92,5 +97,8 @@ func loadConfig() *config {
9297
uploadDir: env.String("UPLOAD_DIR", "./uploads", false),
9398

9499
tfaEnabled: env.Bool("APP_TFA_ENABLED", true),
100+
101+
mcpEnabled: env.Bool("MCP_ENABLED", true),
102+
mcpVersion: env.String("MCP_SERVER_VERSION", "v1", false),
95103
}
96104
}

backend/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/google/uuid v1.6.0
2222
github.com/jackc/pgx/v5 v5.5.5
2323
github.com/joho/godotenv v1.5.1
24+
github.com/modelcontextprotocol/go-sdk v1.4.0
2425
github.com/pquerna/otp v1.5.0
2526
github.com/swaggo/files v1.0.1
2627
github.com/swaggo/gin-swagger v1.6.0
@@ -96,6 +97,7 @@ require (
9697
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
9798
github.com/google/s2a-go v0.1.9 // indirect
9899
github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect
100+
github.com/google/jsonschema-go v0.4.2 // indirect
99101
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
100102
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
101103
github.com/jackc/pgpassfile v1.0.0 // indirect
@@ -122,12 +124,15 @@ require (
122124
github.com/quic-go/qpack v0.6.0 // indirect
123125
github.com/quic-go/quic-go v0.59.0 // indirect
124126
github.com/russellhaering/goxmldsig v1.4.0 // indirect
127+
github.com/segmentio/asm v1.2.1 // indirect
128+
github.com/segmentio/encoding v0.5.3 // indirect
125129
github.com/sirupsen/logrus v1.9.3 // indirect
126130
github.com/swaggo/swag v1.16.3 // indirect
127131
github.com/tidwall/match v1.2.0 // indirect
128132
github.com/tidwall/pretty v1.2.1 // indirect
129133
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
130134
github.com/ugorji/go/codec v1.3.1 // indirect
135+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
131136
go.mongodb.org/mongo-driver v1.14.0 // indirect
132137
go.opencensus.io v0.24.0 // indirect
133138
go.opentelemetry.io/auto/sdk v1.2.1 // indirect

backend/go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,3 +540,13 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
540540
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
541541
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
542542
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
543+
github.com/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8=
544+
github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E=
545+
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
546+
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
547+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
548+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
549+
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
550+
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
551+
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
552+
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=

backend/modules.go

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
incidents_connectors "github.com/utmstack/utmstack/backend/modules/incidents/connectors"
2727
"github.com/utmstack/utmstack/backend/modules/integrations"
2828
"github.com/utmstack/utmstack/backend/modules/loganalyzer"
29+
mcpmod "github.com/utmstack/utmstack/backend/modules/mcp"
2930
"github.com/utmstack/utmstack/backend/modules/notifications"
3031
notifications_domain "github.com/utmstack/utmstack/backend/modules/notifications/domain"
3132
opensearchgw "github.com/utmstack/utmstack/backend/modules/opensearch"
@@ -68,6 +69,7 @@ type modules struct {
6869
notifications *notifications.Module
6970
socAI *socai.Module
7071
adaudit *adaudit.Module
72+
mcp *mcpmod.Module
7173
signer *jwtpkg.Signer
7274
}
7375

@@ -167,8 +169,44 @@ func initModules(db *gorm.DB, cfg *config) *modules {
167169
)
168170
}
169171

172+
iamMod := iam.NewModule(authUsecase, userUsecase, roleUsecase, tfaUsecase, apiKeyUsecase, idpUsecase, samlUsecase, cfg.uploadDir)
173+
socAIMod := socai.NewModule(cfg.socAIBaseURL, cfg.internalKey)
174+
incidentsMod := incidents.NewModule(
175+
db,
176+
incidents_connectors.NewNoopMailer(),
177+
incidents.NewAlertsGatewayFromUsecase(alertsMod.GetAlertUsecase()),
178+
incidents.NewIAMGatewayFromRepo(userRepo),
179+
auditMod.Logger(),
180+
)
181+
adauditMod := adaudit.NewModule(db)
182+
183+
var mcpModule *mcpmod.Module
184+
if cfg.mcpEnabled {
185+
mcpModule = mcpmod.NewModule(&mcpmod.Deps{
186+
IAM: iamMod,
187+
Alerts: alertsMod,
188+
Incidents: incidentsMod,
189+
SOAR: soarMod,
190+
Compliance: complianceMod,
191+
Audit: auditMod,
192+
Dashboards: dashboardsMod,
193+
LogAnalyzer: loganalyzerMod,
194+
OpenSearch: opensearchMod,
195+
EventProcessing: eventProcessingMod,
196+
Datasources: datasourcesMod,
197+
Integrations: integrationsMod,
198+
Notifications: notificationsMod,
199+
ADAudit: adauditMod,
200+
SOCAI: socAIMod,
201+
Billing: billingMod,
202+
AppConfig: configMod,
203+
ServerName: cfg.serverName,
204+
ServerVersion: cfg.mcpVersion,
205+
})
206+
}
207+
170208
return &modules{
171-
iam: iam.NewModule(authUsecase, userUsecase, roleUsecase, tfaUsecase, apiKeyUsecase, idpUsecase, samlUsecase, cfg.uploadDir),
209+
iam: iamMod,
172210
audit: auditMod,
173211
appconfig: configMod,
174212
billing: billingMod,
@@ -182,16 +220,11 @@ func initModules(db *gorm.DB, cfg *config) *modules {
182220
eventProcessing: eventProcessingMod,
183221
opensearchGateway: opensearchMod,
184222
integrations: integrationsMod,
185-
socAI: socai.NewModule(cfg.socAIBaseURL, cfg.internalKey),
186-
incidents: incidents.NewModule(
187-
db,
188-
incidents_connectors.NewNoopMailer(),
189-
incidents.NewAlertsGatewayFromUsecase(alertsMod.GetAlertUsecase()),
190-
incidents.NewIAMGatewayFromRepo(userRepo),
191-
auditMod.Logger(),
192-
),
193-
notifications: notificationsMod,
194-
adaudit: adaudit.NewModule(db),
195-
signer: signer,
223+
socAI: socAIMod,
224+
incidents: incidentsMod,
225+
notifications: notificationsMod,
226+
adaudit: adauditMod,
227+
mcp: mcpModule,
228+
signer: signer,
196229
}
197230
}

backend/modules/adaudit/module.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package adaudit
22

33
import (
4+
"github.com/utmstack/utmstack/backend/modules/adaudit/connectors"
45
"github.com/utmstack/utmstack/backend/modules/adaudit/handler"
56
"github.com/utmstack/utmstack/backend/modules/adaudit/repository"
67
"github.com/utmstack/utmstack/backend/modules/adaudit/usecase"
@@ -9,12 +10,14 @@ import (
910

1011
type Module struct {
1112
handler *handler.ADUserHandler
13+
uc connectors.ADUserUsecase
1214
}
1315

1416
func NewModule(db *gorm.DB) *Module {
1517
repo := repository.NewADUserRepository(db)
1618
uc := usecase.NewADUserUsecase(repo)
17-
return &Module{handler: handler.NewADUserHandler(uc)}
19+
return &Module{handler: handler.NewADUserHandler(uc), uc: uc}
1820
}
1921

20-
func (m *Module) Handler() *handler.ADUserHandler { return m.handler }
22+
func (m *Module) Handler() *handler.ADUserHandler { return m.handler }
23+
func (m *Module) GetUsecase() connectors.ADUserUsecase { return m.uc }

backend/modules/alerts/module.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@ type Module struct {
1212
alertHandler *handler.AlertHandler
1313
alertUsecase connectors.AlertUsecase
1414
alertTagHandler *handler.AlertTagHandler
15+
alertTagUsecase connectors.AlertTagUsecase
1516
alertTagRuleHandler *handler.AlertTagRuleHandler
17+
alertTagRuleUsecase connectors.AlertTagRuleUsecase
1618
adversaryHandler *handler.AdversaryHandler
19+
adversaryUsecase connectors.AdversaryUsecase
1720
}
1821

1922
func NewModule(db *gorm.DB) *Module {
@@ -39,8 +42,11 @@ func NewModule(db *gorm.DB) *Module {
3942
alertHandler: alertH,
4043
alertUsecase: alertUC,
4144
alertTagHandler: alertTagH,
45+
alertTagUsecase: alertTagUC,
4246
alertTagRuleHandler: alertTagRuleH,
47+
alertTagRuleUsecase: alertTagRuleUC,
4348
adversaryHandler: adversaryH,
49+
adversaryUsecase: adversaryUC,
4450
}
4551
}
4652

@@ -50,8 +56,16 @@ func (m *Module) GetAlertUsecase() connectors.AlertUsecase { return m.alertUseca
5056

5157
func (m *Module) GetAlertTagHandler() *handler.AlertTagHandler { return m.alertTagHandler }
5258

59+
func (m *Module) GetAlertTagUsecase() connectors.AlertTagUsecase { return m.alertTagUsecase }
60+
5361
func (m *Module) GetAlertTagRuleHandler() *handler.AlertTagRuleHandler {
5462
return m.alertTagRuleHandler
5563
}
5664

65+
func (m *Module) GetAlertTagRuleUsecase() connectors.AlertTagRuleUsecase {
66+
return m.alertTagRuleUsecase
67+
}
68+
5769
func (m *Module) GetAdversaryHandler() *handler.AdversaryHandler { return m.adversaryHandler }
70+
71+
func (m *Module) GetAdversaryUsecase() connectors.AdversaryUsecase { return m.adversaryUsecase }

backend/modules/audit/domain/event_type.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ const (
165165
// Replacing the signed LICENSE envelope (admin-only).
166166
LICENSE_UPLOAD_ATTEMPT ApplicationEventType = "LICENSE_UPLOAD_ATTEMPT"
167167
LICENSE_UPLOAD_SUCCESS ApplicationEventType = "LICENSE_UPLOAD_SUCCESS"
168+
169+
// MCP tool calls — every invocation of an MCP-exposed tool is audited
170+
// through the same Logger sink used by REST handlers, so /audit covers
171+
// both transports uniformly. ResourceType holds the tool name.
172+
APP_EVENT_MCP_TOOL_CALL ApplicationEventType = "MCP_TOOL_CALL"
168173
// SERVER_MODULE_CREATE_ATTEMPT ApplicationEventType = "SERVER_MODULE_CREATE_ATTEMPT"
169174
// SERVER_MODULE_CREATE_SUCCESS ApplicationEventType = "SERVER_MODULE_CREATE_SUCCESS"
170175
// SERVER_MODULE_UPDATE_ATTEMPT ApplicationEventType = "SERVER_MODULE_UPDATE_ATTEMPT"

backend/modules/compliance/module.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,14 @@ type Module struct {
2626

2727
frameworkUC connectors.FrameworkUsecase
2828
evaluatorUC connectors.EvaluatorUsecase
29+
scheduleUC connectors.ScheduleUsecase
2930
evalInterval time.Duration
3031
}
3132

33+
func (m *Module) GetFrameworkUsecase() connectors.FrameworkUsecase { return m.frameworkUC }
34+
func (m *Module) GetEvaluatorUsecase() connectors.EvaluatorUsecase { return m.evaluatorUC }
35+
func (m *Module) GetScheduleUsecase() connectors.ScheduleUsecase { return m.scheduleUC }
36+
3237
func NewModule(db *gorm.DB, mailSvc mail_connectors.MailService) *Module {
3338
scheduleRepo := repository.NewScheduleRepository(db)
3439

@@ -77,6 +82,7 @@ func NewModule(db *gorm.DB, mailSvc mail_connectors.MailService) *Module {
7782
coverage: coverageIdx,
7883
frameworkUC: frameworkUC,
7984
evaluatorUC: evaluatorUC,
85+
scheduleUC: scheduleUC,
8086
evalInterval: time.Duration(evalHours) * time.Hour,
8187
}
8288
}

backend/modules/dashboards/module.go

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package dashboards
22

33
import (
4+
"github.com/utmstack/utmstack/backend/modules/dashboards/connectors"
45
"github.com/utmstack/utmstack/backend/modules/dashboards/handler"
56
"github.com/utmstack/utmstack/backend/modules/dashboards/repository"
67
"github.com/utmstack/utmstack/backend/modules/dashboards/usecase"
@@ -11,17 +12,27 @@ type Module struct {
1112
dashboardHandler *handler.DashboardHandler
1213
visualizationHandler *handler.VisualizationHandler
1314
layoutHandler *handler.LayoutHandler
15+
dashboardUC connectors.DashboardUsecase
16+
visualizationUC connectors.VisualizationUsecase
17+
layoutUC connectors.LayoutUsecase
1418
}
1519

1620
func NewModule(db *gorm.DB) *Module {
1721
dashRepo := repository.NewDashboardRepository(db)
1822
vizRepo := repository.NewVisualizationRepository(db)
1923
layoutRepo := repository.NewLayoutRepository(db)
2024

25+
dashUC := usecase.NewDashboardUsecase(dashRepo)
26+
vizUC := usecase.NewVisualizationUsecase(vizRepo)
27+
layoutUC := usecase.NewLayoutUsecase(layoutRepo)
28+
2129
return &Module{
22-
dashboardHandler: handler.NewDashboardHandler(usecase.NewDashboardUsecase(dashRepo)),
23-
visualizationHandler: handler.NewVisualizationHandler(usecase.NewVisualizationUsecase(vizRepo)),
24-
layoutHandler: handler.NewLayoutHandler(usecase.NewLayoutUsecase(layoutRepo)),
30+
dashboardHandler: handler.NewDashboardHandler(dashUC),
31+
visualizationHandler: handler.NewVisualizationHandler(vizUC),
32+
layoutHandler: handler.NewLayoutHandler(layoutUC),
33+
dashboardUC: dashUC,
34+
visualizationUC: vizUC,
35+
layoutUC: layoutUC,
2536
}
2637
}
2738

@@ -30,3 +41,9 @@ func (m *Module) GetVisualizationHandler() *handler.VisualizationHandler {
3041
return m.visualizationHandler
3142
}
3243
func (m *Module) GetLayoutHandler() *handler.LayoutHandler { return m.layoutHandler }
44+
45+
func (m *Module) GetDashboardUsecase() connectors.DashboardUsecase { return m.dashboardUC }
46+
func (m *Module) GetVisualizationUsecase() connectors.VisualizationUsecase {
47+
return m.visualizationUC
48+
}
49+
func (m *Module) GetLayoutUsecase() connectors.LayoutUsecase { return m.layoutUC }

backend/modules/datasources/module.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ type Module struct {
1414
assetGroupHandler *handler.AssetGroupHandler
1515
connectionKeyHandler *handler.ConnectionKeyHandler
1616
reconciler *usecase.StatsReconciler
17+
datasourceUC connectors.DatasourceUsecase
18+
assetGroupUC connectors.AssetGroupUsecase
19+
license connectors.LicenseCapProvider
1720
}
1821

1922
func NewModule(dsUC connectors.DatasourceUsecase, groupUC connectors.AssetGroupUsecase, reconciler *usecase.StatsReconciler, license connectors.LicenseCapProvider, agentClient *agentmanager.AgentManagerClient) *Module {
@@ -22,6 +25,9 @@ func NewModule(dsUC connectors.DatasourceUsecase, groupUC connectors.AssetGroupU
2225
assetGroupHandler: handler.NewAssetGroupHandler(groupUC),
2326
connectionKeyHandler: handler.NewConnectionKeyHandler(agentClient),
2427
reconciler: reconciler,
28+
datasourceUC: dsUC,
29+
assetGroupUC: groupUC,
30+
license: license,
2531
}
2632
}
2733

@@ -36,3 +42,7 @@ func (m *Module) GetAssetGroupHandler() *handler.AssetGroupHandler { return m.as
3642
func (m *Module) GetConnectionKeyHandler() *handler.ConnectionKeyHandler {
3743
return m.connectionKeyHandler
3844
}
45+
46+
func (m *Module) GetDatasourceUsecase() connectors.DatasourceUsecase { return m.datasourceUC }
47+
func (m *Module) GetAssetGroupUsecase() connectors.AssetGroupUsecase { return m.assetGroupUC }
48+
func (m *Module) LicenseCap() connectors.LicenseCapProvider { return m.license }

0 commit comments

Comments
 (0)