From c2e13b79684876481a252b56b19a707122441f72 Mon Sep 17 00:00:00 2001 From: Bryan Frimin Date: Fri, 20 Mar 2026 15:58:27 +0100 Subject: [PATCH] Add MCP audit report metadata and getAuditReportUrl tool Adds has_report, report_filename, and report_mime_type fields to Audit type to expose report attachment status. Introduces new getAuditReportUrl tool to generate presigned download URLs for audit reports. Updates NewAudit type converter to accept optional report object for enriching metadata. Signed-off-by: Bryan Frimin --- pkg/server/api/mcp/v1/schema.resolvers.go | 47 +++++++++++++++++++---- pkg/server/api/mcp/v1/specification.yaml | 41 ++++++++++++++++++++ pkg/server/api/mcp/v1/types/audit.go | 18 ++++++--- 3 files changed, 93 insertions(+), 13 deletions(-) diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index 6a67fa361..c9a1f9f32 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -1346,11 +1346,19 @@ func (r *Resolver) GetAuditTool(ctx context.Context, req *mcp.CallToolRequest, i audit, err := prb.Audits.Get(ctx, input.ID) if err != nil { - return nil, types.GetAuditOutput{}, fmt.Errorf("failed to get audit: %w", err) + return nil, types.GetAuditOutput{}, fmt.Errorf("cannot get audit: %w", err) + } + + var report *coredata.Report + if audit.ReportID != nil { + report, err = prb.Reports.Get(ctx, *audit.ReportID) + if err != nil { + return nil, types.GetAuditOutput{}, fmt.Errorf("cannot get audit report: %w", err) + } } return nil, types.GetAuditOutput{ - Audit: types.NewAudit(audit), + Audit: types.NewAudit(audit, report), }, nil } @@ -1375,16 +1383,16 @@ func (r *Resolver) AddAuditTool(ctx context.Context, req *mcp.CallToolRequest, i } return nil, types.AddAuditOutput{ - Audit: types.NewAudit(audit), + Audit: types.NewAudit(audit, nil), }, nil } func (r *Resolver) UpdateAuditTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateAuditInput) (*mcp.CallToolResult, types.UpdateAuditOutput, error) { r.MustAuthorize(ctx, input.ID, probo.ActionAuditUpdate) - svc := r.ProboService(ctx, input.ID) + prb := r.ProboService(ctx, input.ID) - audit, err := svc.Audits.Update( + audit, err := prb.Audits.Update( ctx, &probo.UpdateAuditRequest{ ID: input.ID, @@ -1396,11 +1404,19 @@ func (r *Resolver) UpdateAuditTool(ctx context.Context, req *mcp.CallToolRequest }, ) if err != nil { - return nil, types.UpdateAuditOutput{}, fmt.Errorf("failed to update audit: %w", err) + return nil, types.UpdateAuditOutput{}, fmt.Errorf("cannot update audit: %w", err) + } + + var report *coredata.Report + if audit.ReportID != nil { + report, err = prb.Reports.Get(ctx, *audit.ReportID) + if err != nil { + return nil, types.UpdateAuditOutput{}, fmt.Errorf("cannot get audit report: %w", err) + } } return nil, types.UpdateAuditOutput{ - Audit: types.NewAudit(audit), + Audit: types.NewAudit(audit, report), }, nil } @@ -3156,7 +3172,7 @@ func (r *Resolver) LinkFindingAuditTool(ctx context.Context, req *mcp.CallToolRe return nil, types.LinkFindingAuditOutput{ Finding: types.NewFinding(finding), - Audit: types.NewAudit(audit), + Audit: types.NewAudit(audit, nil), }, nil } @@ -3281,3 +3297,18 @@ func (r *Resolver) UpdateOrganizationContextTool(ctx context.Context, req *mcp.C OrganizationContext: types.NewOrganizationContext(orgContext), }, nil } + +func (r *Resolver) GetAuditReportUrlTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetAuditReportUrlInput) (*mcp.CallToolResult, types.GetAuditReportUrlOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionReportGetReportUrl) + + prb := r.ProboService(ctx, input.ID) + + url, err := prb.Audits.GenerateReportURL(ctx, input.ID, 15*time.Minute) + if err != nil { + return nil, types.GetAuditReportUrlOutput{}, fmt.Errorf("cannot generate audit report URL: %w", err) + } + + return nil, types.GetAuditReportUrlOutput{ + URL: *url, + }, nil +} diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 8ca4b0aa5..8d38fef4b 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -3935,6 +3935,7 @@ components: - framework_id - state - trust_center_visibility + - has_report - created_at - updated_at properties: @@ -3970,6 +3971,19 @@ components: trust_center_visibility: $ref: "#/components/schemas/TrustCenterVisibility" description: Trust center visibility + has_report: + type: boolean + description: Whether the audit has an attached report + report_filename: + type: + - string + - "null" + description: Report filename, null if no report attached + report_mime_type: + type: + - string + - "null" + description: Report MIME type, null if no report attached created_at: type: string format: date-time @@ -3979,6 +3993,24 @@ components: format: date-time description: Update timestamp + GetAuditReportUrlInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Audit ID + + GetAuditReportUrlOutput: + type: object + required: + - url + properties: + url: + type: string + description: Presigned download URL for the report (valid for 15 minutes) + ListAuditsInput: type: object required: @@ -7018,6 +7050,15 @@ tools: $ref: "#/components/schemas/UpdateAuditInput" outputSchema: $ref: "#/components/schemas/UpdateAuditOutput" + - name: getAuditReportUrl + description: Get a presigned download URL for an audit's attached report. Returns a time-limited URL valid for 15 minutes. The audit must have an attached report. + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetAuditReportUrlInput" + outputSchema: + $ref: "#/components/schemas/GetAuditReportUrlOutput" - name: listControls description: List all controls for the organization or framework hints: diff --git a/pkg/server/api/mcp/v1/types/audit.go b/pkg/server/api/mcp/v1/types/audit.go index c38cbf986..3db183fb5 100644 --- a/pkg/server/api/mcp/v1/types/audit.go +++ b/pkg/server/api/mcp/v1/types/audit.go @@ -19,25 +19,33 @@ import ( "go.probo.inc/probo/pkg/page" ) -func NewAudit(a *coredata.Audit) *Audit { - return &Audit{ +func NewAudit(a *coredata.Audit, report *coredata.Report) *Audit { + audit := &Audit{ ID: a.ID, Name: a.Name, OrganizationID: a.OrganizationID, FrameworkID: a.FrameworkID, State: a.State, TrustCenterVisibility: a.TrustCenterVisibility, + HasReport: a.ReportID != nil, ValidFrom: a.ValidFrom, ValidUntil: a.ValidUntil, CreatedAt: a.CreatedAt, UpdatedAt: a.UpdatedAt, } + + if report != nil { + audit.ReportFilename = &report.Filename + audit.ReportMimeType = &report.MimeType + } + + return audit } func NewListControlAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrderField]) ListControlAuditsOutput { audits := make([]*Audit, 0, len(auditPage.Data)) for _, v := range auditPage.Data { - audits = append(audits, NewAudit(v)) + audits = append(audits, NewAudit(v, nil)) } var nextCursor *page.CursorKey @@ -55,7 +63,7 @@ func NewListControlAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.A func NewListAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrderField]) ListAuditsOutput { audits := make([]*Audit, 0, len(auditPage.Data)) for _, v := range auditPage.Data { - audits = append(audits, NewAudit(v)) + audits = append(audits, NewAudit(v, nil)) } var nextCursor *page.CursorKey @@ -73,7 +81,7 @@ func NewListAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrd func NewListFindingAuditsOutput(auditPage *page.Page[*coredata.Audit, coredata.AuditOrderField]) ListFindingAuditsOutput { audits := make([]*Audit, 0, len(auditPage.Data)) for _, v := range auditPage.Data { - audits = append(audits, NewAudit(v)) + audits = append(audits, NewAudit(v, nil)) } var nextCursor *page.CursorKey