From 084726dbadf1d8f9daef752da751f65f75bbf792 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Thu, 30 Jul 2026 17:42:57 +0200 Subject: [PATCH] Expose missing console capabilities on MCP Add MCP tools and filters for mailing lists, detected trackers, compliance portal frameworks, and document/control/framework mutations that were available in the console but missing from MCP. Signed-off-by: Sacha Al Himdani --- pkg/server/api/api.go | 14 +- pkg/server/api/mcp/v1/resolver.go | 2 + pkg/server/api/mcp/v1/schema.resolvers.go | 477 +++++++++ pkg/server/api/mcp/v1/signer_metadata.go | 46 + pkg/server/api/mcp/v1/specification.yaml | 923 ++++++++++++++++++ .../v1/types/compliance_portal_framework.go | 60 ++ .../api/mcp/v1/types/detected_tracker.go | 66 ++ pkg/server/api/mcp/v1/types/mailing_list.go | 108 ++ pkg/server/api/mcp/v1/v1_handler.go | 3 + 9 files changed, 1695 insertions(+), 4 deletions(-) create mode 100644 pkg/server/api/mcp/v1/signer_metadata.go create mode 100644 pkg/server/api/mcp/v1/types/compliance_portal_framework.go create mode 100644 pkg/server/api/mcp/v1/types/detected_tracker.go create mode 100644 pkg/server/api/mcp/v1/types/mailing_list.go diff --git a/pkg/server/api/api.go b/pkg/server/api/api.go index f1dcb66a9..12c7406ce 100644 --- a/pkg/server/api/api.go +++ b/pkg/server/api/api.go @@ -112,10 +112,11 @@ type ( ) var ( - ErrMissingProboService = errors.New("server configuration requires a valid probo.Service instance") - ErrMissingIAMService = errors.New("server configuration requires a valid iam.Service instance") - ErrMissingSlackService = errors.New("server configuration requires a valid slack.Service instance") - ErrMissingITAMService = errors.New("server configuration requires a valid itam.Service instance") + ErrMissingProboService = errors.New("server configuration requires a valid probo.Service instance") + ErrMissingIAMService = errors.New("server configuration requires a valid iam.Service instance") + ErrMissingSlackService = errors.New("server configuration requires a valid slack.Service instance") + ErrMissingITAMService = errors.New("server configuration requires a valid itam.Service instance") + ErrMissingMailmanService = errors.New("server configuration requires a valid mailman.Service instance") ) func methodNotAllowed(w http.ResponseWriter, r *http.Request) { @@ -159,6 +160,10 @@ func NewServer(cfg Config) (*Server, error) { return nil, ErrMissingITAMService } + if cfg.Mailman == nil { + return nil, ErrMissingMailmanService + } + csrf := http.NewCrossOriginProtection() for _, origin := range cfg.AllowedOrigins { if err := csrf.AddTrustedOrigin(origin); err != nil { @@ -246,6 +251,7 @@ func NewServer(cfg Config) (*Server, error) { cfg.CookieBanner, cfg.RiskManagement, cfg.ITAM, + cfg.Mailman, cfg.TokenSecret, cfg.File, cfg.BaseURL, diff --git a/pkg/server/api/mcp/v1/resolver.go b/pkg/server/api/mcp/v1/resolver.go index 76343b0a0..b472cccb6 100644 --- a/pkg/server/api/mcp/v1/resolver.go +++ b/pkg/server/api/mcp/v1/resolver.go @@ -39,6 +39,7 @@ import ( "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/itam" + "go.probo.inc/probo/pkg/mailman" "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/prosemirror" "go.probo.inc/probo/pkg/resourcealias" @@ -63,6 +64,7 @@ type Resolver struct { cookieBanner *cookiebanner.Service riskManagement *riskmanagement.Service itamSvc *itam.Service + mailman *mailman.Service logger *log.Logger fileManager *filemanager.Service baseURL *baseurl.BaseURL diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index 9b9aef5e7..cd7d34ba2 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -21,6 +21,7 @@ import ( "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/itam" "go.probo.inc/probo/pkg/mail" + "go.probo.inc/probo/pkg/mailman" "go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/resourcealias" @@ -7645,3 +7646,479 @@ func (r *Resolver) CreateDeviceTool(ctx context.Context, req *mcp.CallToolReques EnrollmentURL: urls.EnrollmentURL, }, nil } + +func (r *Resolver) GetThirdPartyTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetThirdPartyInput) (*mcp.CallToolResult, types.GetThirdPartyOutput, error) { + scope, err := r.Authorize(ctx, input.ID, probo.ActionThirdPartyGet) + if err != nil { + return nil, types.GetThirdPartyOutput{}, err + } + + thirdParty, err := r.proboSvc.ThirdParties.Get(ctx, scope, input.ID) + if err != nil { + return nil, types.GetThirdPartyOutput{}, fmt.Errorf("cannot get third party: %w", err) + } + + return nil, types.GetThirdPartyOutput{ + ThirdParty: types.NewThirdParty(thirdParty), + }, nil +} + +func (r *Resolver) DeleteEvidenceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteEvidenceInput) (*mcp.CallToolResult, types.DeleteEvidenceOutput, error) { + scope, err := r.Authorize(ctx, input.ID, probo.ActionEvidenceDelete) + if err != nil { + return nil, types.DeleteEvidenceOutput{}, err + } + + err = r.proboSvc.Evidences.Delete(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteEvidenceOutput{}, fmt.Errorf("cannot delete evidence: %w", err) + } + + return nil, types.DeleteEvidenceOutput{ + DeletedEvidenceID: input.ID, + }, nil +} + +func (r *Resolver) DeleteFrameworkTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteFrameworkInput) (*mcp.CallToolResult, types.DeleteFrameworkOutput, error) { + scope, err := r.Authorize(ctx, input.ID, probo.ActionFrameworkDelete) + if err != nil { + return nil, types.DeleteFrameworkOutput{}, err + } + + err = r.proboSvc.Frameworks.Delete(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteFrameworkOutput{}, fmt.Errorf("cannot delete framework: %w", err) + } + + return nil, types.DeleteFrameworkOutput{ + DeletedFrameworkID: input.ID, + }, nil +} + +func (r *Resolver) DeleteAuditReportTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteAuditReportInput) (*mcp.CallToolResult, types.DeleteAuditReportOutput, error) { + scope, err := r.Authorize(ctx, input.ID, probo.ActionAuditReportDelete) + if err != nil { + return nil, types.DeleteAuditReportOutput{}, err + } + + audit, err := r.proboSvc.Audits.DeleteReport(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteAuditReportOutput{}, fmt.Errorf("cannot delete audit report: %w", err) + } + + return nil, types.DeleteAuditReportOutput{ + Audit: types.NewAudit(audit, nil), + }, nil +} + +func (r *Resolver) DeleteControlTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteControlInput) (*mcp.CallToolResult, types.DeleteControlOutput, error) { + scope, err := r.Authorize(ctx, input.ID, probo.ActionControlDelete) + if err != nil { + return nil, types.DeleteControlOutput{}, err + } + + err = r.proboSvc.Controls.Delete(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteControlOutput{}, fmt.Errorf("cannot delete control: %w", err) + } + + return nil, types.DeleteControlOutput{ + DeletedControlID: input.ID, + }, nil +} + +func (r *Resolver) ApproveDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ApproveDocumentVersionInput) (*mcp.CallToolResult, types.ApproveDocumentVersionOutput, error) { + scope, err := r.Authorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionApprove) + if err != nil { + return nil, types.ApproveDocumentVersionOutput{}, err + } + + identity := authn.IdentityFromContext(ctx) + signer := signerMetadataFromToolRequest(req) + + decision, err := r.proboSvc.DocumentApprovals.Approve( + ctx, + scope, + probo.ApproveDocumentVersionRequest{ + DocumentVersionID: input.DocumentVersionID, + IdentityID: identity.ID, + Comment: input.Comment, + SignerFullName: identity.FullName, + SignerEmail: identity.EmailAddress, + SignerIPAddr: signer.IPAddr, + SignerUA: signer.UA, + }, + ) + if err != nil { + return nil, types.ApproveDocumentVersionOutput{}, fmt.Errorf("cannot approve document version: %w", err) + } + + return nil, types.ApproveDocumentVersionOutput{ + DocumentVersionApprovalDecision: types.NewDocumentVersionApprovalDecision(decision), + }, nil +} + +func (r *Resolver) RejectDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.RejectDocumentVersionInput) (*mcp.CallToolResult, types.RejectDocumentVersionOutput, error) { + scope, err := r.Authorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionReject) + if err != nil { + return nil, types.RejectDocumentVersionOutput{}, err + } + + identity := authn.IdentityFromContext(ctx) + + decision, err := r.proboSvc.DocumentApprovals.Reject( + ctx, + scope, + probo.RejectDocumentVersionRequest{ + DocumentVersionID: input.DocumentVersionID, + IdentityID: identity.ID, + Comment: input.Comment, + }, + ) + if err != nil { + return nil, types.RejectDocumentVersionOutput{}, fmt.Errorf("cannot reject document version: %w", err) + } + + return nil, types.RejectDocumentVersionOutput{ + DocumentVersionApprovalDecision: types.NewDocumentVersionApprovalDecision(decision), + }, nil +} + +func (r *Resolver) SignDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.SignDocumentInput) (*mcp.CallToolResult, types.SignDocumentOutput, error) { + scope, err := r.Authorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionSign) + if err != nil { + return nil, types.SignDocumentOutput{}, err + } + + identity := authn.IdentityFromContext(ctx) + signer := signerMetadataFromToolRequest(req) + + signature, err := r.proboSvc.Documents.SignDocumentVersionByIdentity( + ctx, + scope, + probo.SignDocumentVersionRequest{ + DocumentVersionID: input.DocumentVersionID, + IdentityID: identity.ID, + SignerFullName: identity.FullName, + SignerEmail: identity.EmailAddress, + SignerIPAddr: signer.IPAddr, + SignerUA: signer.UA, + }, + ) + if err != nil { + return nil, types.SignDocumentOutput{}, fmt.Errorf("cannot sign document: %w", err) + } + + return nil, types.SignDocumentOutput{ + DocumentVersionSignature: types.NewDocumentVersionSignature(signature), + }, nil +} + +func (r *Resolver) GetMailingListTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetMailingListInput) (*mcp.CallToolResult, types.GetMailingListOutput, error) { + scope, err := r.Authorize(ctx, input.CompliancePortalID, management.ActionMailingListSubscriberList) + if err != nil { + return nil, types.GetMailingListOutput{}, err + } + + mailingList, err := r.management.GetMailingList(ctx, scope, input.CompliancePortalID) + if err != nil { + return nil, types.GetMailingListOutput{}, fmt.Errorf("cannot get mailing list: %w", err) + } + + return nil, types.GetMailingListOutput{MailingList: types.NewMailingList(mailingList)}, nil +} + +func (r *Resolver) UpdateMailingListTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateMailingListInput) (*mcp.CallToolResult, types.UpdateMailingListOutput, error) { + if _, err := r.Authorize(ctx, input.ID, management.ActionMailingListUpdate); err != nil { + return nil, types.UpdateMailingListOutput{}, err + } + + mailingList, err := r.mailman.UpdateMailingList(ctx, input.ID, input.ReplyTo) + if err != nil { + return nil, types.UpdateMailingListOutput{}, fmt.Errorf("cannot update mailing list: %w", err) + } + + return nil, types.UpdateMailingListOutput{MailingList: types.NewMailingList(mailingList)}, nil +} + +func (r *Resolver) ListMailingListSubscribersTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListMailingListSubscribersInput) (*mcp.CallToolResult, types.ListMailingListSubscribersOutput, error) { + if _, err := r.Authorize(ctx, input.MailingListID, management.ActionMailingListSubscriberList); err != nil { + return nil, types.ListMailingListSubscribersOutput{}, err + } + + pageOrderBy := page.OrderBy[coredata.MailingListSubscriberOrderField]{ + Field: coredata.MailingListSubscriberOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.MailingListSubscriberOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := r.mailman.ListSubscribers(ctx, input.MailingListID, cursor) + if err != nil { + return nil, types.ListMailingListSubscribersOutput{}, fmt.Errorf("cannot list mailing list subscribers: %w", err) + } + + return nil, types.NewListMailingListSubscribersOutput(p), nil +} + +func (r *Resolver) AddMailingListSubscriberTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddMailingListSubscriberInput) (*mcp.CallToolResult, types.AddMailingListSubscriberOutput, error) { + if _, err := r.Authorize(ctx, input.MailingListID, management.ActionMailingListSubscriberCreate); err != nil { + return nil, types.AddMailingListSubscriberOutput{}, err + } + + subscriber, err := r.mailman.CreateSubscriber( + ctx, + &mailman.CreateSubscriberRequest{ + MailingListID: input.MailingListID, + Email: input.Email, + FullName: input.FullName, + Confirmed: input.Confirmed != nil && *input.Confirmed, + }, + ) + if err != nil { + return nil, types.AddMailingListSubscriberOutput{}, fmt.Errorf("cannot add mailing list subscriber: %w", err) + } + + return nil, types.AddMailingListSubscriberOutput{ + MailingListSubscriber: types.NewMailingListSubscriber(subscriber), + }, nil +} + +func (r *Resolver) DeleteMailingListSubscriberTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteMailingListSubscriberInput) (*mcp.CallToolResult, types.DeleteMailingListSubscriberOutput, error) { + if _, err := r.Authorize(ctx, input.ID, management.ActionMailingListSubscriberDelete); err != nil { + return nil, types.DeleteMailingListSubscriberOutput{}, err + } + + if err := r.mailman.DeleteSubscriber(ctx, input.ID); err != nil { + return nil, types.DeleteMailingListSubscriberOutput{}, fmt.Errorf("cannot delete mailing list subscriber: %w", err) + } + + return nil, types.DeleteMailingListSubscriberOutput{ + DeletedMailingListSubscriberID: input.ID, + }, nil +} + +func (r *Resolver) ListMailingListUpdatesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListMailingListUpdatesInput) (*mcp.CallToolResult, types.ListMailingListUpdatesOutput, error) { + if _, err := r.Authorize(ctx, input.MailingListID, management.ActionMailingListUpdateList); err != nil { + return nil, types.ListMailingListUpdatesOutput{}, err + } + + pageOrderBy := page.OrderBy[coredata.MailingListUpdateOrderField]{ + Field: coredata.MailingListUpdateOrderFieldUpdatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.MailingListUpdateOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := r.mailman.ListMailingListUpdates(ctx, input.MailingListID, cursor) + if err != nil { + return nil, types.ListMailingListUpdatesOutput{}, fmt.Errorf("cannot list mailing list updates: %w", err) + } + + return nil, types.NewListMailingListUpdatesOutput(p), nil +} + +func (r *Resolver) AddMailingListUpdateTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddMailingListUpdateInput) (*mcp.CallToolResult, types.AddMailingListUpdateOutput, error) { + if _, err := r.Authorize(ctx, input.MailingListID, management.ActionMailingListUpdateCreate); err != nil { + return nil, types.AddMailingListUpdateOutput{}, err + } + + update, err := r.mailman.CreateMailingListUpdate( + ctx, + &mailman.CreateMailingListUpdateRequest{ + MailingListID: input.MailingListID, + Title: input.Title, + Body: input.Body, + }, + ) + if err != nil { + return nil, types.AddMailingListUpdateOutput{}, fmt.Errorf("cannot add mailing list update: %w", err) + } + + return nil, types.AddMailingListUpdateOutput{ + MailingListUpdate: types.NewMailingListUpdate(update), + }, nil +} + +func (r *Resolver) UpdateMailingListUpdateTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateMailingListUpdateInput) (*mcp.CallToolResult, types.UpdateMailingListUpdateOutput, error) { + if _, err := r.Authorize(ctx, input.ID, management.ActionMailingListUpdateUpdate); err != nil { + return nil, types.UpdateMailingListUpdateOutput{}, err + } + + update, err := r.mailman.UpdateMailingListUpdate( + ctx, + &mailman.UpdateMailingListUpdateRequest{ + ID: input.ID, + Title: input.Title, + Body: input.Body, + }, + ) + if err != nil { + return nil, types.UpdateMailingListUpdateOutput{}, fmt.Errorf("cannot update mailing list update: %w", err) + } + + return nil, types.UpdateMailingListUpdateOutput{ + MailingListUpdate: types.NewMailingListUpdate(update), + }, nil +} + +func (r *Resolver) SendMailingListUpdateTool(ctx context.Context, req *mcp.CallToolRequest, input *types.SendMailingListUpdateInput) (*mcp.CallToolResult, types.SendMailingListUpdateOutput, error) { + if _, err := r.Authorize(ctx, input.ID, management.ActionMailingListUpdateUpdate); err != nil { + return nil, types.SendMailingListUpdateOutput{}, err + } + + update, err := r.mailman.SendMailingListUpdate(ctx, input.ID) + if err != nil { + return nil, types.SendMailingListUpdateOutput{}, fmt.Errorf("cannot send mailing list update: %w", err) + } + + return nil, types.SendMailingListUpdateOutput{ + MailingListUpdate: types.NewMailingListUpdate(update), + }, nil +} + +func (r *Resolver) DeleteMailingListUpdateTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteMailingListUpdateInput) (*mcp.CallToolResult, types.DeleteMailingListUpdateOutput, error) { + if _, err := r.Authorize(ctx, input.ID, management.ActionMailingListUpdateDelete); err != nil { + return nil, types.DeleteMailingListUpdateOutput{}, err + } + + if err := r.mailman.DeleteMailingListUpdate(ctx, input.ID); err != nil { + return nil, types.DeleteMailingListUpdateOutput{}, fmt.Errorf("cannot delete mailing list update: %w", err) + } + + return nil, types.DeleteMailingListUpdateOutput{ + DeletedMailingListUpdateID: input.ID, + }, nil +} + +func (r *Resolver) ListDetectedTrackersTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDetectedTrackersInput) (*mcp.CallToolResult, types.ListDetectedTrackersOutput, error) { + scope, err := r.Authorize(ctx, input.TrackerPatternID, probo.ActionTrackerPatternGet) + if err != nil { + return nil, types.ListDetectedTrackersOutput{}, err + } + + pageOrderBy := page.OrderBy[coredata.DetectedTrackerOrderField]{ + Field: coredata.DetectedTrackerOrderFieldLastDetectedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.DetectedTrackerOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + trackers, err := r.cookieBanner.ListDetectedTrackersForPattern(ctx, scope, input.TrackerPatternID, cursor) + if err != nil { + return nil, types.ListDetectedTrackersOutput{}, fmt.Errorf("cannot list detected trackers: %w", err) + } + + return nil, types.NewListDetectedTrackersOutput(page.NewPage(trackers, cursor)), nil +} + +func (r *Resolver) ListCompliancePortalFrameworksTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListCompliancePortalFrameworksInput) (*mcp.CallToolResult, types.ListCompliancePortalFrameworksOutput, error) { + scope, err := r.Authorize(ctx, input.CompliancePortalID, management.ActionComplianceFrameworkList) + if err != nil { + return nil, types.ListCompliancePortalFrameworksOutput{}, err + } + + pageOrderBy := page.OrderBy[coredata.ComplianceFrameworkOrderField]{ + Field: coredata.ComplianceFrameworkOrderFieldRank, + Direction: page.OrderDirectionAsc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.ComplianceFrameworkOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := r.management.ListFrameworksWithHidden(ctx, scope, input.CompliancePortalID, cursor) + if err != nil { + return nil, types.ListCompliancePortalFrameworksOutput{}, fmt.Errorf("cannot list compliance portal frameworks: %w", err) + } + + return nil, types.NewListCompliancePortalFrameworksOutput(p), nil +} + +func (r *Resolver) AddCompliancePortalFrameworkTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddCompliancePortalFrameworkInput) (*mcp.CallToolResult, types.AddCompliancePortalFrameworkOutput, error) { + scope, err := r.Authorize(ctx, input.CompliancePortalID, management.ActionComplianceFrameworkCreate) + if err != nil { + return nil, types.AddCompliancePortalFrameworkOutput{}, err + } + + framework, err := r.management.CreateFramework( + ctx, + scope, + &management.CreateFrameworkRequest{ + CompliancePortalID: input.CompliancePortalID, + FrameworkID: input.FrameworkID, + }, + ) + if err != nil { + return nil, types.AddCompliancePortalFrameworkOutput{}, fmt.Errorf("cannot add compliance portal framework: %w", err) + } + + return nil, types.AddCompliancePortalFrameworkOutput{ + CompliancePortalFramework: types.NewCompliancePortalFramework(framework), + }, nil +} + +func (r *Resolver) UpdateCompliancePortalFrameworkTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateCompliancePortalFrameworkInput) (*mcp.CallToolResult, types.UpdateCompliancePortalFrameworkOutput, error) { + scope, err := r.Authorize(ctx, input.ID, management.ActionComplianceFrameworkUpdateRank) + if err != nil { + return nil, types.UpdateCompliancePortalFrameworkOutput{}, err + } + + framework, err := r.management.UpdateFramework( + ctx, + scope, + &management.UpdateFrameworkRequest{ + ID: input.ID, + Rank: input.Rank, + }, + ) + if err != nil { + return nil, types.UpdateCompliancePortalFrameworkOutput{}, fmt.Errorf("cannot update compliance portal framework: %w", err) + } + + return nil, types.UpdateCompliancePortalFrameworkOutput{ + CompliancePortalFramework: types.NewCompliancePortalFramework(framework), + }, nil +} + +func (r *Resolver) DeleteCompliancePortalFrameworkTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteCompliancePortalFrameworkInput) (*mcp.CallToolResult, types.DeleteCompliancePortalFrameworkOutput, error) { + scope, err := r.Authorize(ctx, input.ID, management.ActionComplianceFrameworkDelete) + if err != nil { + return nil, types.DeleteCompliancePortalFrameworkOutput{}, err + } + + err = r.management.DeleteFramework( + ctx, + scope, + &management.DeleteFrameworkRequest{ID: input.ID}, + ) + if err != nil { + return nil, types.DeleteCompliancePortalFrameworkOutput{}, fmt.Errorf("cannot delete compliance portal framework: %w", err) + } + + return nil, types.DeleteCompliancePortalFrameworkOutput{ + DeletedCompliancePortalFrameworkID: input.ID, + }, nil +} diff --git a/pkg/server/api/mcp/v1/signer_metadata.go b/pkg/server/api/mcp/v1/signer_metadata.go new file mode 100644 index 000000000..0661db499 --- /dev/null +++ b/pkg/server/api/mcp/v1/signer_metadata.go @@ -0,0 +1,46 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package mcp_v1 + +import ( + "net/http" + + "github.com/modelcontextprotocol/go-sdk/mcp" + "go.probo.inc/probo/pkg/server/api/clientip" +) + +type signerMetadata struct { + IPAddr string + UA string +} + +func signerMetadataFromToolRequest(req *mcp.CallToolRequest) signerMetadata { + if req == nil || req.Extra == nil || req.Extra.Header == nil { + return signerMetadata{} + } + + httpReq := &http.Request{Header: req.Extra.Header} + + return signerMetadata{ + IPAddr: clientip.Extract(httpReq), + UA: req.Extra.Header.Get("User-Agent"), + } +} diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index d8222ed4a..b7fc321a0 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -673,6 +673,12 @@ components: cursor: $ref: "#/components/schemas/CursorKey" description: Page cursor + filter: + type: object + properties: + query: + type: string + description: Search query ListThirdPartiesOutput: type: object @@ -1173,6 +1179,23 @@ components: thirdParty: $ref: "#/components/schemas/ThirdParty" + GetThirdPartyInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Third party ID + + GetThirdPartyOutput: + type: object + required: + - thirdParty + properties: + thirdParty: + $ref: "#/components/schemas/ThirdParty" + DeleteThirdPartyInput: type: object required: @@ -2382,6 +2405,24 @@ components: format: date-time description: Update timestamp + DeleteEvidenceInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Evidence ID + + DeleteEvidenceOutput: + type: object + required: + - deleted_evidence_id + properties: + deleted_evidence_id: + $ref: "#/components/schemas/GID" + description: Deleted evidence ID + LinkMeasureInput: type: object required: @@ -2654,6 +2695,24 @@ components: framework: $ref: "#/components/schemas/Framework" + DeleteFrameworkInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Framework ID + + DeleteFrameworkOutput: + type: object + required: + - deleted_framework_id + properties: + deleted_framework_id: + $ref: "#/components/schemas/GID" + description: Deleted framework ID + AssetType: type: string enum: @@ -5082,6 +5141,23 @@ components: type: string description: Presigned download URL for the report (valid for 15 minutes) + DeleteAuditReportInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Audit ID + + DeleteAuditReportOutput: + type: object + required: + - audit + properties: + audit: + $ref: "#/components/schemas/Audit" + ListAuditsInput: type: object required: @@ -5468,6 +5544,24 @@ components: control: $ref: "#/components/schemas/Control" + DeleteControlInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Control ID + + DeleteControlOutput: + type: object + required: + - deleted_control_id + properties: + deleted_control_id: + $ref: "#/components/schemas/GID" + description: Deleted control ID + LinkControlInput: type: object required: @@ -6855,6 +6949,63 @@ components: document_version: $ref: "#/components/schemas/DocumentVersion" + ApproveDocumentVersionInput: + type: object + required: + - document_version_id + properties: + document_version_id: + $ref: "#/components/schemas/GID" + description: Document version ID + comment: + type: string + description: Optional approval comment + + ApproveDocumentVersionOutput: + type: object + required: + - document_version_approval_decision + properties: + document_version_approval_decision: + $ref: "#/components/schemas/DocumentVersionApprovalDecision" + + RejectDocumentVersionInput: + type: object + required: + - document_version_id + properties: + document_version_id: + $ref: "#/components/schemas/GID" + description: Document version ID + comment: + type: string + description: Optional rejection comment + + RejectDocumentVersionOutput: + type: object + required: + - document_version_approval_decision + properties: + document_version_approval_decision: + $ref: "#/components/schemas/DocumentVersionApprovalDecision" + + SignDocumentInput: + type: object + required: + - document_version_id + properties: + document_version_id: + $ref: "#/components/schemas/GID" + description: Document version ID + + SignDocumentOutput: + type: object + required: + - document_version_signature + properties: + document_version_signature: + $ref: "#/components/schemas/DocumentVersionSignature" + ListDocumentVersionApprovalQuorumsInput: type: object required: @@ -8794,6 +8945,23 @@ components: audit_log_entry: $ref: "#/components/schemas/AuditLogEntry" + AuditLogEntryOrderField: + type: string + enum: + - CREATED_AT + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AuditLogEntryOrderField + + AuditLogEntryOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/AuditLogEntryOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + ListAuditLogEntriesInput: type: object required: @@ -8802,6 +8970,9 @@ components: organization_id: $ref: "#/components/schemas/GID" description: Organization ID + order_by: + $ref: "#/components/schemas/AuditLogEntryOrderBy" + description: Audit log entry order by size: type: integer description: Page size @@ -10829,6 +11000,14 @@ components: cookie_banner: $ref: "#/components/schemas/CookieBanner" + CookieCategoryKind: + type: string + enum: + - NORMAL + - NECESSARY + - UNCATEGORISED + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.CookieCategoryKind + ListCookieCategoriesInput: type: object required: @@ -10841,6 +11020,12 @@ components: type: integer cursor: $ref: "#/components/schemas/CursorKey" + filter: + type: object + properties: + exclude_kind: + $ref: "#/components/schemas/CookieCategoryKind" + description: Exclude categories of this kind. Defaults to UNCATEGORISED when omitted. ListCookieCategoriesOutput: type: object @@ -11375,6 +11560,7 @@ components: type: string enum: - GOOGLE_WORKSPACE + - MICROSOFT_365 go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.SCIMBridgeType SCIMBridgeState: @@ -12936,6 +13122,467 @@ components: UnlinkRiskAssessmentScenarioRiskOutput: type: object + # --- Mailing lists --- + MailingList: + type: object + required: [id, organization_id, created_at, updated_at] + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + reply_to: + $ref: "#/components/schemas/EmailAddress" + nullable: true + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + MailingListSubscriberStatus: + type: string + enum: [PENDING, CONFIRMED] + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.MailingListSubscriberStatus + + MailingListSubscriberOrderField: + type: string + enum: [CREATED_AT] + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.MailingListSubscriberOrderField + + MailingListSubscriberOrderBy: + type: object + required: [field, direction] + properties: + field: + $ref: "#/components/schemas/MailingListSubscriberOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + MailingListSubscriber: + type: object + required: [id, organization_id, mailing_list_id, full_name, email, status, created_at, updated_at] + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + mailing_list_id: + $ref: "#/components/schemas/GID" + full_name: + type: string + email: + $ref: "#/components/schemas/EmailAddress" + status: + $ref: "#/components/schemas/MailingListSubscriberStatus" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + MailingListUpdateStatus: + type: string + enum: [DRAFT, ENQUEUED, PROCESSING, SENT] + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.MailingListUpdateStatus + + MailingListUpdateOrderField: + type: string + enum: [CREATED_AT, UPDATED_AT] + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.MailingListUpdateOrderField + + MailingListUpdateOrderBy: + type: object + required: [field, direction] + properties: + field: + $ref: "#/components/schemas/MailingListUpdateOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + MailingListUpdate: + type: object + required: [id, organization_id, mailing_list_id, title, body, status, created_at, updated_at] + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + mailing_list_id: + $ref: "#/components/schemas/GID" + title: + type: string + body: + type: string + status: + $ref: "#/components/schemas/MailingListUpdateStatus" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + GetMailingListInput: + type: object + required: [compliance_portal_id] + properties: + compliance_portal_id: + $ref: "#/components/schemas/GID" + + GetMailingListOutput: + type: object + properties: + mailing_list: + $ref: "#/components/schemas/MailingList" + nullable: true + description: Null when the portal has no mailing list + + UpdateMailingListInput: + type: object + required: [id] + properties: + id: + $ref: "#/components/schemas/GID" + reply_to: + $ref: "#/components/schemas/EmailAddress" + nullable: true + description: Reply-to email address; null clears it + + UpdateMailingListOutput: + type: object + required: [mailing_list] + properties: + mailing_list: + $ref: "#/components/schemas/MailingList" + + ListMailingListSubscribersInput: + type: object + required: [mailing_list_id] + properties: + mailing_list_id: + $ref: "#/components/schemas/GID" + order_by: + $ref: "#/components/schemas/MailingListSubscriberOrderBy" + size: + type: integer + cursor: + $ref: "#/components/schemas/CursorKey" + + ListMailingListSubscribersOutput: + type: object + required: [mailing_list_subscribers] + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + mailing_list_subscribers: + type: array + items: + $ref: "#/components/schemas/MailingListSubscriber" + + AddMailingListSubscriberInput: + type: object + required: [mailing_list_id, full_name, email] + properties: + mailing_list_id: + $ref: "#/components/schemas/GID" + full_name: + type: string + email: + $ref: "#/components/schemas/EmailAddress" + confirmed: + type: boolean + description: When false or omitted, a confirmation email is sent + + AddMailingListSubscriberOutput: + type: object + required: [mailing_list_subscriber] + properties: + mailing_list_subscriber: + $ref: "#/components/schemas/MailingListSubscriber" + + DeleteMailingListSubscriberInput: + type: object + required: [id] + properties: + id: + $ref: "#/components/schemas/GID" + + DeleteMailingListSubscriberOutput: + type: object + required: [deleted_mailing_list_subscriber_id] + properties: + deleted_mailing_list_subscriber_id: + $ref: "#/components/schemas/GID" + + ListMailingListUpdatesInput: + type: object + required: [mailing_list_id] + properties: + mailing_list_id: + $ref: "#/components/schemas/GID" + order_by: + $ref: "#/components/schemas/MailingListUpdateOrderBy" + size: + type: integer + cursor: + $ref: "#/components/schemas/CursorKey" + + ListMailingListUpdatesOutput: + type: object + required: [mailing_list_updates] + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + mailing_list_updates: + type: array + items: + $ref: "#/components/schemas/MailingListUpdate" + + AddMailingListUpdateInput: + type: object + required: [mailing_list_id, title, body] + properties: + mailing_list_id: + $ref: "#/components/schemas/GID" + title: + type: string + body: + type: string + + AddMailingListUpdateOutput: + type: object + required: [mailing_list_update] + properties: + mailing_list_update: + $ref: "#/components/schemas/MailingListUpdate" + + UpdateMailingListUpdateInput: + type: object + required: [id] + properties: + id: + $ref: "#/components/schemas/GID" + title: + type: string + body: + type: string + + UpdateMailingListUpdateOutput: + type: object + required: [mailing_list_update] + properties: + mailing_list_update: + $ref: "#/components/schemas/MailingListUpdate" + + SendMailingListUpdateInput: + type: object + required: [id] + properties: + id: + $ref: "#/components/schemas/GID" + + SendMailingListUpdateOutput: + type: object + required: [mailing_list_update] + properties: + mailing_list_update: + $ref: "#/components/schemas/MailingListUpdate" + + DeleteMailingListUpdateInput: + type: object + required: [id] + properties: + id: + $ref: "#/components/schemas/GID" + + DeleteMailingListUpdateOutput: + type: object + required: [deleted_mailing_list_update_id] + properties: + deleted_mailing_list_update_id: + $ref: "#/components/schemas/GID" + + # --- Detected trackers --- + DetectedTrackerOrderField: + type: string + enum: [INITIATOR_URL, LAST_DETECTED_AT] + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.DetectedTrackerOrderField + + DetectedTrackerOrderBy: + type: object + required: [field, direction] + properties: + field: + $ref: "#/components/schemas/DetectedTrackerOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + DetectedTracker: + type: object + required: [id, identifier, last_detected_at, created_at] + properties: + id: + $ref: "#/components/schemas/GID" + identifier: + type: string + initiator_url: + type: string + nullable: true + max_age_seconds: + type: integer + nullable: true + source: + type: string + enum: [SCRIPT, PRE_EXISTING, HTTP, EXTENSION] + nullable: true + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.CookieSource + last_detected_at: + type: string + format: date-time + created_at: + type: string + format: date-time + + ListDetectedTrackersInput: + type: object + required: [tracker_pattern_id] + properties: + tracker_pattern_id: + $ref: "#/components/schemas/GID" + order_by: + $ref: "#/components/schemas/DetectedTrackerOrderBy" + size: + type: integer + cursor: + $ref: "#/components/schemas/CursorKey" + + ListDetectedTrackersOutput: + type: object + required: [detected_trackers] + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + detected_trackers: + type: array + items: + $ref: "#/components/schemas/DetectedTracker" + + # --- Compliance portal frameworks --- + CompliancePortalFrameworkVisibility: + type: string + enum: [NONE, PUBLIC] + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ComplianceFrameworkVisibility + + CompliancePortalFrameworkOrderField: + type: string + enum: [CREATED_AT, RANK] + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ComplianceFrameworkOrderField + + CompliancePortalFrameworkOrderBy: + type: object + required: [field, direction] + properties: + field: + $ref: "#/components/schemas/CompliancePortalFrameworkOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + CompliancePortalFramework: + type: object + required: [id, organization_id, compliance_portal_id, framework_id, rank, visibility, created_at, updated_at] + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + compliance_portal_id: + $ref: "#/components/schemas/GID" + framework_id: + $ref: "#/components/schemas/GID" + rank: + type: integer + visibility: + $ref: "#/components/schemas/CompliancePortalFrameworkVisibility" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + ListCompliancePortalFrameworksInput: + type: object + required: [compliance_portal_id] + properties: + compliance_portal_id: + $ref: "#/components/schemas/GID" + order_by: + $ref: "#/components/schemas/CompliancePortalFrameworkOrderBy" + size: + type: integer + cursor: + $ref: "#/components/schemas/CursorKey" + + ListCompliancePortalFrameworksOutput: + type: object + required: [compliance_portal_frameworks] + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + compliance_portal_frameworks: + type: array + items: + $ref: "#/components/schemas/CompliancePortalFramework" + + AddCompliancePortalFrameworkInput: + type: object + required: [compliance_portal_id, framework_id] + properties: + compliance_portal_id: + $ref: "#/components/schemas/GID" + framework_id: + $ref: "#/components/schemas/GID" + + AddCompliancePortalFrameworkOutput: + type: object + required: [compliance_portal_framework] + properties: + compliance_portal_framework: + $ref: "#/components/schemas/CompliancePortalFramework" + + UpdateCompliancePortalFrameworkInput: + type: object + required: [id, rank] + properties: + id: + $ref: "#/components/schemas/GID" + rank: + type: integer + + UpdateCompliancePortalFrameworkOutput: + type: object + required: [compliance_portal_framework] + properties: + compliance_portal_framework: + $ref: "#/components/schemas/CompliancePortalFramework" + + DeleteCompliancePortalFrameworkInput: + type: object + required: [id] + properties: + id: + $ref: "#/components/schemas/GID" + + DeleteCompliancePortalFrameworkOutput: + type: object + required: [deleted_compliance_portal_framework_id] + properties: + deleted_compliance_portal_framework_id: + $ref: "#/components/schemas/GID" + tools: - name: listOrganizations title: List Organizations @@ -13093,6 +13740,18 @@ tools: $ref: "#/components/schemas/UpdateThirdPartyInput" outputSchema: $ref: "#/components/schemas/UpdateThirdPartyOutput" + - name: getThirdParty + title: Get Third Party + description: Get a third party by ID + hints: + readonly: true + destructive: false + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/GetThirdPartyInput" + outputSchema: + $ref: "#/components/schemas/GetThirdPartyOutput" - name: listThirdPartyRiskAssessments title: List Third Party Risk Assessments description: List all risk assessments for a thirdParty @@ -13405,6 +14064,18 @@ tools: $ref: "#/components/schemas/ListMeasureEvidencesInput" outputSchema: $ref: "#/components/schemas/ListMeasureEvidencesOutput" + - name: deleteEvidence + title: Delete Evidence + description: Delete an evidence + hints: + readonly: false + destructive: true + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/DeleteEvidenceInput" + outputSchema: + $ref: "#/components/schemas/DeleteEvidenceOutput" - name: linkMeasure title: Link Measure description: Link a measure to a resource (control, risk, document, or third party). The resource type is determined from the resource_id GID. @@ -13489,6 +14160,18 @@ tools: $ref: "#/components/schemas/UpdateFrameworkInput" outputSchema: $ref: "#/components/schemas/UpdateFrameworkOutput" + - name: deleteFramework + title: Delete Framework + description: Delete a framework + hints: + readonly: false + destructive: true + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/DeleteFrameworkInput" + outputSchema: + $ref: "#/components/schemas/DeleteFrameworkOutput" - name: listAssets title: List Assets description: List all assets for the organization @@ -14089,6 +14772,18 @@ tools: $ref: "#/components/schemas/GetAuditReportUrlInput" outputSchema: $ref: "#/components/schemas/GetAuditReportUrlOutput" + - name: deleteAuditReport + title: Delete Audit Report + description: Delete the report file attached to an audit + hints: + readonly: false + destructive: true + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/DeleteAuditReportInput" + outputSchema: + $ref: "#/components/schemas/DeleteAuditReportOutput" - name: listControls title: List Controls description: List all controls for the organization or framework @@ -14137,6 +14832,18 @@ tools: $ref: "#/components/schemas/UpdateControlInput" outputSchema: $ref: "#/components/schemas/UpdateControlOutput" + - name: deleteControl + title: Delete Control + description: Delete a control + hints: + readonly: false + destructive: true + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/DeleteControlInput" + outputSchema: + $ref: "#/components/schemas/DeleteControlOutput" - name: linkControl title: Link Control description: Link a resource to a control (measure, document, audit, or obligation). The resource type is determined from the resource_id GID. @@ -14521,6 +15228,42 @@ tools: $ref: "#/components/schemas/VoidDocumentVersionApprovalInput" outputSchema: $ref: "#/components/schemas/VoidDocumentVersionApprovalOutput" + - name: approveDocumentVersion + title: Approve Document Version + description: Approve a document version pending approval as the authenticated user + hints: + readonly: false + destructive: false + idempotent: false + openWorld: false + inputSchema: + $ref: "#/components/schemas/ApproveDocumentVersionInput" + outputSchema: + $ref: "#/components/schemas/ApproveDocumentVersionOutput" + - name: rejectDocumentVersion + title: Reject Document Version + description: Reject a document version pending approval as the authenticated user + hints: + readonly: false + destructive: false + idempotent: false + openWorld: false + inputSchema: + $ref: "#/components/schemas/RejectDocumentVersionInput" + outputSchema: + $ref: "#/components/schemas/RejectDocumentVersionOutput" + - name: signDocument + title: Sign Document + description: Sign a document version as the authenticated user + hints: + readonly: false + destructive: false + idempotent: false + openWorld: false + inputSchema: + $ref: "#/components/schemas/SignDocumentInput" + outputSchema: + $ref: "#/components/schemas/SignDocumentOutput" - name: listStatementsOfApplicability title: List Statements of Applicability description: List all statements of applicability for the organization @@ -16453,3 +17196,183 @@ tools: $ref: "#/components/schemas/UnlinkRiskAssessmentScenarioRiskInput" outputSchema: $ref: "#/components/schemas/UnlinkRiskAssessmentScenarioRiskOutput" + - name: getMailingList + title: Get Mailing List + description: Get the mailing list for a compliance portal + hints: + readonly: true + destructive: false + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/GetMailingListInput" + outputSchema: + $ref: "#/components/schemas/GetMailingListOutput" + - name: updateMailingList + title: Update Mailing List + description: Update mailing list settings + hints: + readonly: false + destructive: false + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/UpdateMailingListInput" + outputSchema: + $ref: "#/components/schemas/UpdateMailingListOutput" + - name: listMailingListSubscribers + title: List Mailing List Subscribers + description: List subscribers for a mailing list + hints: + readonly: true + destructive: false + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/ListMailingListSubscribersInput" + outputSchema: + $ref: "#/components/schemas/ListMailingListSubscribersOutput" + - name: addMailingListSubscriber + title: Add Mailing List Subscriber + description: Add a subscriber to a mailing list + hints: + readonly: false + destructive: false + idempotent: false + openWorld: false + inputSchema: + $ref: "#/components/schemas/AddMailingListSubscriberInput" + outputSchema: + $ref: "#/components/schemas/AddMailingListSubscriberOutput" + - name: deleteMailingListSubscriber + title: Delete Mailing List Subscriber + description: Delete a mailing list subscriber + hints: + readonly: false + destructive: true + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/DeleteMailingListSubscriberInput" + outputSchema: + $ref: "#/components/schemas/DeleteMailingListSubscriberOutput" + - name: listMailingListUpdates + title: List Mailing List Updates + description: List updates for a mailing list + hints: + readonly: true + destructive: false + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/ListMailingListUpdatesInput" + outputSchema: + $ref: "#/components/schemas/ListMailingListUpdatesOutput" + - name: addMailingListUpdate + title: Add Mailing List Update + description: Create a draft mailing list update + hints: + readonly: false + destructive: false + idempotent: false + openWorld: false + inputSchema: + $ref: "#/components/schemas/AddMailingListUpdateInput" + outputSchema: + $ref: "#/components/schemas/AddMailingListUpdateOutput" + - name: updateMailingListUpdate + title: Update Mailing List Update + description: Update a draft mailing list update + hints: + readonly: false + destructive: false + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/UpdateMailingListUpdateInput" + outputSchema: + $ref: "#/components/schemas/UpdateMailingListUpdateOutput" + - name: sendMailingListUpdate + title: Send Mailing List Update + description: Queue a draft mailing list update for sending + hints: + readonly: false + destructive: false + idempotent: false + openWorld: true + inputSchema: + $ref: "#/components/schemas/SendMailingListUpdateInput" + outputSchema: + $ref: "#/components/schemas/SendMailingListUpdateOutput" + - name: deleteMailingListUpdate + title: Delete Mailing List Update + description: Delete a mailing list update + hints: + readonly: false + destructive: true + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/DeleteMailingListUpdateInput" + outputSchema: + $ref: "#/components/schemas/DeleteMailingListUpdateOutput" + - name: listDetectedTrackers + title: List Detected Trackers + description: List detected trackers for a tracker pattern + hints: + readonly: true + destructive: false + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/ListDetectedTrackersInput" + outputSchema: + $ref: "#/components/schemas/ListDetectedTrackersOutput" + - name: listCompliancePortalFrameworks + title: List Compliance Portal Frameworks + description: List frameworks shown on a compliance portal + hints: + readonly: true + destructive: false + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/ListCompliancePortalFrameworksInput" + outputSchema: + $ref: "#/components/schemas/ListCompliancePortalFrameworksOutput" + - name: addCompliancePortalFramework + title: Add Compliance Portal Framework + description: Add an organization framework to a compliance portal + hints: + readonly: false + destructive: false + idempotent: false + openWorld: false + inputSchema: + $ref: "#/components/schemas/AddCompliancePortalFrameworkInput" + outputSchema: + $ref: "#/components/schemas/AddCompliancePortalFrameworkOutput" + - name: updateCompliancePortalFramework + title: Update Compliance Portal Framework + description: Update the rank of a compliance portal framework + hints: + readonly: false + destructive: false + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/UpdateCompliancePortalFrameworkInput" + outputSchema: + $ref: "#/components/schemas/UpdateCompliancePortalFrameworkOutput" + - name: deleteCompliancePortalFramework + title: Delete Compliance Portal Framework + description: Remove a framework from a compliance portal + hints: + readonly: false + destructive: true + idempotent: true + openWorld: false + inputSchema: + $ref: "#/components/schemas/DeleteCompliancePortalFrameworkInput" + outputSchema: + $ref: "#/components/schemas/DeleteCompliancePortalFrameworkOutput" diff --git a/pkg/server/api/mcp/v1/types/compliance_portal_framework.go b/pkg/server/api/mcp/v1/types/compliance_portal_framework.go new file mode 100644 index 000000000..d3e250004 --- /dev/null +++ b/pkg/server/api/mcp/v1/types/compliance_portal_framework.go @@ -0,0 +1,60 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewCompliancePortalFramework(cf *coredata.ComplianceFramework) *CompliancePortalFramework { + return &CompliancePortalFramework{ + ID: cf.ID, + OrganizationID: cf.OrganizationID, + CompliancePortalID: cf.CompliancePortalID, + FrameworkID: cf.FrameworkID, + Rank: cf.Rank, + Visibility: cf.Visibility, + CreatedAt: cf.CreatedAt, + UpdatedAt: cf.UpdatedAt, + } +} + +func NewListCompliancePortalFrameworksOutput( + p *page.Page[*coredata.ComplianceFramework, coredata.ComplianceFrameworkOrderField], +) ListCompliancePortalFrameworksOutput { + frameworks := make([]*CompliancePortalFramework, 0, len(p.Data)) + for _, cf := range p.Data { + frameworks = append(frameworks, NewCompliancePortalFramework(cf)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListCompliancePortalFrameworksOutput{ + NextCursor: nextCursor, + CompliancePortalFrameworks: frameworks, + } +} diff --git a/pkg/server/api/mcp/v1/types/detected_tracker.go b/pkg/server/api/mcp/v1/types/detected_tracker.go new file mode 100644 index 000000000..38ee13e71 --- /dev/null +++ b/pkg/server/api/mcp/v1/types/detected_tracker.go @@ -0,0 +1,66 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewDetectedTracker(dt *coredata.DetectedTracker) *DetectedTracker { + var source *DetectedTrackerSource + + if dt.Source != nil { + s := DetectedTrackerSource(string(*dt.Source)) + source = &s + } + + return &DetectedTracker{ + ID: dt.ID, + Identifier: dt.Identifier, + InitiatorURL: dt.InitiatorURL, + MaxAgeSeconds: dt.MaxAgeSeconds, + Source: source, + LastDetectedAt: dt.LastDetectedAt, + CreatedAt: dt.CreatedAt, + } +} + +func NewListDetectedTrackersOutput( + p *page.Page[*coredata.DetectedTracker, coredata.DetectedTrackerOrderField], +) ListDetectedTrackersOutput { + trackers := make([]*DetectedTracker, 0, len(p.Data)) + for _, dt := range p.Data { + trackers = append(trackers, NewDetectedTracker(dt)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListDetectedTrackersOutput{ + NextCursor: nextCursor, + DetectedTrackers: trackers, + } +} diff --git a/pkg/server/api/mcp/v1/types/mailing_list.go b/pkg/server/api/mcp/v1/types/mailing_list.go new file mode 100644 index 000000000..faafb14da --- /dev/null +++ b/pkg/server/api/mcp/v1/types/mailing_list.go @@ -0,0 +1,108 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewMailingList(ml *coredata.MailingList) *MailingList { + if ml == nil { + return nil + } + + return &MailingList{ + ID: ml.ID, + OrganizationID: ml.OrganizationID, + ReplyTo: ml.ReplyTo, + CreatedAt: ml.CreatedAt, + UpdatedAt: ml.UpdatedAt, + } +} + +func NewMailingListSubscriber(s *coredata.MailingListSubscriber) *MailingListSubscriber { + return &MailingListSubscriber{ + ID: s.ID, + OrganizationID: s.OrganizationID, + MailingListID: s.MailingListID, + FullName: s.FullName, + Email: s.Email, + Status: s.Status, + CreatedAt: s.CreatedAt, + UpdatedAt: s.UpdatedAt, + } +} + +func NewListMailingListSubscribersOutput( + p *page.Page[*coredata.MailingListSubscriber, coredata.MailingListSubscriberOrderField], +) ListMailingListSubscribersOutput { + subscribers := make([]*MailingListSubscriber, 0, len(p.Data)) + for _, s := range p.Data { + subscribers = append(subscribers, NewMailingListSubscriber(s)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListMailingListSubscribersOutput{ + NextCursor: nextCursor, + MailingListSubscribers: subscribers, + } +} + +func NewMailingListUpdate(u *coredata.MailingListUpdate) *MailingListUpdate { + return &MailingListUpdate{ + ID: u.ID, + OrganizationID: u.OrganizationID, + MailingListID: u.MailingListID, + Title: u.Title, + Body: u.Body, + Status: u.Status, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + } +} + +func NewListMailingListUpdatesOutput( + p *page.Page[*coredata.MailingListUpdate, coredata.MailingListUpdateOrderField], +) ListMailingListUpdatesOutput { + updates := make([]*MailingListUpdate, 0, len(p.Data)) + for _, u := range p.Data { + updates = append(updates, NewMailingListUpdate(u)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListMailingListUpdatesOutput{ + NextCursor: nextCursor, + MailingListUpdates: updates, + } +} diff --git a/pkg/server/api/mcp/v1/v1_handler.go b/pkg/server/api/mcp/v1/v1_handler.go index 530bed9fd..a255e3322 100644 --- a/pkg/server/api/mcp/v1/v1_handler.go +++ b/pkg/server/api/mcp/v1/v1_handler.go @@ -35,6 +35,7 @@ import ( "go.probo.inc/probo/pkg/filemanager" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/itam" + "go.probo.inc/probo/pkg/mailman" "go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/resourcealias" "go.probo.inc/probo/pkg/riskmanagement" @@ -56,6 +57,7 @@ func NewMux( cookieBannerSvc *cookiebanner.Service, riskManagementSvc *riskmanagement.Service, itamSvc *itam.Service, + mailmanSvc *mailman.Service, tokenSecret string, fileManagerSvc *filemanager.Service, baseURL *baseurl.BaseURL, @@ -75,6 +77,7 @@ func NewMux( cookieBanner: cookieBannerSvc, riskManagement: riskManagementSvc, itamSvc: itamSvc, + mailman: mailmanSvc, logger: logger, fileManager: fileManagerSvc, baseURL: baseURL,