Add access review MCP tools

Add MCP tool definitions and resolvers for access review
campaigns, sources, entries, decisions, and flags. Wire
accessreview.Service into the MCP Resolver.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-04-02 11:51:46 +02:00
parent ebfc0b7e31
commit b571b074db
5 changed files with 1691 additions and 45 deletions

View File

@@ -22,6 +22,7 @@ import (
"fmt"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/accessreview"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/probo"
@@ -30,9 +31,10 @@ import (
)
type Resolver struct {
proboSvc *probo.Service
iamSvc *iam.Service
logger *log.Logger
proboSvc *probo.Service
iamSvc *iam.Service
accessReview *accessreview.Service
logger *log.Logger
}
func markdownToProseMirrorJSON(markdown string) (string, error) {

View File

@@ -1,17 +1,3 @@
// Copyright (c) 2025-2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package mcp_v1
// This file will be automatically regenerated based on the schema, any resolver implementations
@@ -26,6 +12,7 @@ import (
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
"go.probo.inc/probo/pkg/accessreview"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
@@ -1404,9 +1391,9 @@ func (r *Resolver) AddAuditTool(ctx context.Context, req *mcp.CallToolRequest, i
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)
prb := r.ProboService(ctx, input.ID)
svc := r.ProboService(ctx, input.ID)
audit, err := prb.Audits.Update(
audit, err := svc.Audits.Update(
ctx,
&probo.UpdateAuditRequest{
ID: input.ID,
@@ -1423,7 +1410,7 @@ func (r *Resolver) UpdateAuditTool(ctx context.Context, req *mcp.CallToolRequest
var report *coredata.Report
if audit.ReportID != nil {
report, err = prb.Reports.Get(ctx, *audit.ReportID)
report, err = svc.Reports.Get(ctx, *audit.ReportID)
if err != nil {
return nil, types.UpdateAuditOutput{}, fmt.Errorf("cannot get audit report: %w", err)
}
@@ -3180,6 +3167,528 @@ func (r *Resolver) ListFindingAuditsTool(ctx context.Context, req *mcp.CallToolR
return nil, types.NewListFindingAuditsOutput(auditPage), nil
}
// ListAccessReviewCampaignsTool handles the listAccessReviewCampaigns tool
// List access review campaigns for an organization
func (r *Resolver) ListAccessReviewCampaignsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListAccessReviewCampaignsInput) (*mcp.CallToolResult, types.ListAccessReviewCampaignsOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionAccessReviewCampaignList)
scope := coredata.NewScopeFromObjectID(input.OrganizationID)
pageOrderBy := page.OrderBy[coredata.AccessReviewCampaignOrderField]{
Field: coredata.AccessReviewCampaignOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if input.OrderBy != nil {
pageOrderBy = page.OrderBy[coredata.AccessReviewCampaignOrderField]{
Field: input.OrderBy.Field,
Direction: input.OrderBy.Direction,
}
}
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
p, err := r.accessReview.Campaigns(scope).ListForOrganizationID(ctx, input.OrganizationID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list access review campaigns: %w", err))
}
return nil, types.NewListAccessReviewCampaignsOutput(p), nil
}
// ListAccessEntriesTool handles the listAccessEntries tool
// List access entries for a campaign with optional filters
func (r *Resolver) ListAccessEntriesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListAccessEntriesInput) (*mcp.CallToolResult, types.ListAccessEntriesOutput, error) {
r.MustAuthorize(ctx, input.CampaignID, probo.ActionAccessEntryList)
scope := coredata.NewScopeFromObjectID(input.CampaignID)
pageOrderBy := page.OrderBy[coredata.AccessEntryOrderField]{
Field: coredata.AccessEntryOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if input.OrderBy != nil {
pageOrderBy = page.OrderBy[coredata.AccessEntryOrderField]{
Field: input.OrderBy.Field,
Direction: input.OrderBy.Direction,
}
}
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
var filter *coredata.AccessEntryFilter
if input.Filter != nil {
filter = &coredata.AccessEntryFilter{
Decision: input.Filter.Decision,
Flag: input.Filter.Flag,
IncrementalTag: input.Filter.IncrementalTag,
IsAdmin: input.Filter.IsAdmin,
AuthMethod: input.Filter.AuthMethod,
}
}
var p *page.Page[*coredata.AccessEntry, coredata.AccessEntryOrderField]
if input.AccessSourceID != nil {
var err error
p, err = r.accessReview.Entries(scope).ListForCampaignIDAndSourceID(
ctx,
input.CampaignID,
*input.AccessSourceID,
cursor,
filter,
)
if err != nil {
panic(fmt.Errorf("cannot list access entries: %w", err))
}
} else {
var err error
p, err = r.accessReview.Entries(scope).ListForCampaignID(ctx, input.CampaignID, cursor, filter)
if err != nil {
panic(fmt.Errorf("cannot list access entries: %w", err))
}
}
return nil, types.NewListAccessEntriesOutput(p), nil
}
// GetAccessReviewCampaignStatisticsTool handles the getAccessReviewCampaignStatistics tool
// Get statistics for an access review campaign
func (r *Resolver) GetAccessReviewCampaignStatisticsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetAccessReviewCampaignStatisticsInput) (*mcp.CallToolResult, types.GetAccessReviewCampaignStatisticsOutput, error) {
r.MustAuthorize(ctx, input.CampaignID, probo.ActionAccessReviewCampaignGet)
scope := coredata.NewScopeFromObjectID(input.CampaignID)
stats, err := r.accessReview.Entries(scope).Statistics(ctx, input.CampaignID)
if err != nil {
panic(fmt.Errorf("cannot get campaign statistics: %w", err))
}
return nil, types.GetAccessReviewCampaignStatisticsOutput{
Statistics: types.NewAccessEntryStatistics(stats),
}, nil
}
// RecordAccessEntryDecisionTool handles the recordAccessEntryDecision tool
// Record a decision on an access entry
func (r *Resolver) RecordAccessEntryDecisionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.RecordAccessEntryDecisionInput) (*mcp.CallToolResult, types.RecordAccessEntryDecisionOutput, error) {
r.MustAuthorize(ctx, input.AccessEntryID, probo.ActionAccessEntryDecide)
scope := coredata.NewScopeFromObjectID(input.AccessEntryID)
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, types.RecordAccessEntryDecisionOutput{}, fmt.Errorf("no identity in context")
}
decisionReq := accessreview.RecordAccessEntryDecisionRequest{
EntryID: input.AccessEntryID,
Decision: input.Decision,
DecisionNote: input.DecisionNote,
}
organizationID, err := r.accessReview.ResolveEntryOrganizationID(ctx, input.AccessEntryID)
if err == nil {
profile, err := r.iamSvc.OrganizationService.GetProfileForIdentityAndOrganization(ctx, identity.ID, organizationID)
if err == nil {
decisionReq.DecidedByID = &profile.ID
}
}
entry, err := r.accessReview.Entries(scope).RecordDecision(ctx, decisionReq)
if err != nil {
return nil, types.RecordAccessEntryDecisionOutput{}, fmt.Errorf("cannot record decision: %w", err)
}
return nil, types.RecordAccessEntryDecisionOutput{
AccessEntry: types.NewAccessEntry(entry),
}, nil
}
// RecordAccessEntryDecisionsTool handles the recordAccessEntryDecisions tool
// Record decisions on multiple access entries in a single batch
func (r *Resolver) RecordAccessEntryDecisionsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.RecordAccessEntryDecisionsInput) (*mcp.CallToolResult, types.RecordAccessEntryDecisionsOutput, error) {
if len(input.Decisions) == 0 {
return nil, types.RecordAccessEntryDecisionsOutput{
AccessEntries: []*types.AccessEntry{},
}, nil
}
const maxBatchSize = 100
if len(input.Decisions) > maxBatchSize {
return nil, types.RecordAccessEntryDecisionsOutput{}, fmt.Errorf("cannot record decisions: batch size %d exceeds maximum of %d", len(input.Decisions), maxBatchSize)
}
// Authorize each entry individually to prevent cross-org bypass.
for _, d := range input.Decisions {
r.MustAuthorize(ctx, d.AccessEntryID, probo.ActionAccessEntryDecide)
}
scope := coredata.NewScopeFromObjectID(input.Decisions[0].AccessEntryID)
identity := authn.IdentityFromContext(ctx)
if identity == nil {
return nil, types.RecordAccessEntryDecisionsOutput{}, fmt.Errorf("no identity in context")
}
// Cache profile lookups per organization so we resolve the correct
// decidedByID for each entry even when a batch spans multiple orgs.
profileCache := make(map[gid.GID]*gid.GID)
decisions := make([]accessreview.RecordAccessEntryDecisionRequest, len(input.Decisions))
for i, d := range input.Decisions {
var decidedByID *gid.GID
organizationID, err := r.accessReview.ResolveEntryOrganizationID(ctx, d.AccessEntryID)
if err == nil {
if cached, ok := profileCache[organizationID]; ok {
decidedByID = cached
} else {
profile, err := r.iamSvc.OrganizationService.GetProfileForIdentityAndOrganization(ctx, identity.ID, organizationID)
if err == nil {
decidedByID = &profile.ID
}
profileCache[organizationID] = decidedByID
}
}
decisions[i] = accessreview.RecordAccessEntryDecisionRequest{
EntryID: d.AccessEntryID,
Decision: d.Decision,
DecisionNote: d.DecisionNote,
DecidedByID: decidedByID,
}
}
entries, err := r.accessReview.Entries(scope).RecordDecisions(ctx, decisions)
if err != nil {
return nil, types.RecordAccessEntryDecisionsOutput{}, fmt.Errorf("cannot record decisions: %w", err)
}
accessEntries := make([]*types.AccessEntry, len(entries))
for i, e := range entries {
accessEntries[i] = types.NewAccessEntry(e)
}
return nil, types.RecordAccessEntryDecisionsOutput{
AccessEntries: accessEntries,
}, nil
}
// CloseAccessReviewCampaignTool handles the closeAccessReviewCampaign tool
// Close an access review campaign
func (r *Resolver) CloseAccessReviewCampaignTool(ctx context.Context, req *mcp.CallToolRequest, input *types.CloseAccessReviewCampaignInput) (*mcp.CallToolResult, types.CloseAccessReviewCampaignOutput, error) {
r.MustAuthorize(ctx, input.CampaignID, probo.ActionAccessReviewCampaignClose)
scope := coredata.NewScopeFromObjectID(input.CampaignID)
campaign, err := r.accessReview.Campaigns(scope).Close(ctx, input.CampaignID)
if err != nil {
return nil, types.CloseAccessReviewCampaignOutput{}, fmt.Errorf("cannot close campaign: %w", err)
}
return nil, types.CloseAccessReviewCampaignOutput{
Campaign: types.NewAccessReviewCampaign(campaign),
}, nil
}
// ListAccessSourcesTool handles the listAccessSources tool
// List access sources for an organization
func (r *Resolver) ListAccessSourcesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListAccessSourcesInput) (*mcp.CallToolResult, types.ListAccessSourcesOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionAccessSourceList)
scope := coredata.NewScopeFromObjectID(input.OrganizationID)
pageOrderBy := page.OrderBy[coredata.AccessSourceOrderField]{
Field: coredata.AccessSourceOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if input.OrderBy != nil {
pageOrderBy = page.OrderBy[coredata.AccessSourceOrderField]{
Field: input.OrderBy.Field,
Direction: input.OrderBy.Direction,
}
}
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
p, err := r.accessReview.Sources(scope).ListForOrganizationID(ctx, input.OrganizationID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list access sources: %w", err))
}
return nil, types.NewListAccessSourcesOutput(p), nil
}
// CreateAccessSourceTool handles the createAccessSource tool
// Create a new access source for an organization
func (r *Resolver) CreateAccessSourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.CreateAccessSourceInput) (*mcp.CallToolResult, types.CreateAccessSourceOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionAccessSourceCreate)
scope := coredata.NewScopeFromObjectID(input.OrganizationID)
source, err := r.accessReview.Sources(scope).Create(ctx, accessreview.CreateAccessSourceRequest{
OrganizationID: input.OrganizationID,
ConnectorID: input.ConnectorID,
Name: input.Name,
Category: coredata.AccessSourceCategorySaaS,
CsvData: input.CsvData,
})
if err != nil {
return nil, types.CreateAccessSourceOutput{}, fmt.Errorf("cannot create access source: %w", err)
}
return nil, types.CreateAccessSourceOutput{
AccessSource: types.NewAccessSource(source),
}, nil
}
// UpdateAccessSourceTool handles the updateAccessSource tool
// Update an existing access source
func (r *Resolver) UpdateAccessSourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateAccessSourceInput) (*mcp.CallToolResult, types.UpdateAccessSourceOutput, error) {
r.MustAuthorize(ctx, input.AccessSourceID, probo.ActionAccessSourceUpdate)
scope := coredata.NewScopeFromObjectID(input.AccessSourceID)
updateReq := accessreview.UpdateAccessSourceRequest{
AccessSourceID: input.AccessSourceID,
Name: input.Name,
}
if rawConnectorID := UnwrapOmittable(input.ConnectorID); rawConnectorID != nil {
if *rawConnectorID != nil {
id, err := gid.ParseGID(**rawConnectorID)
if err != nil {
return nil, types.UpdateAccessSourceOutput{}, fmt.Errorf("cannot parse connector_id: %w", err)
}
idPtr := &id
updateReq.ConnectorID = &idPtr
} else {
var nilGID *gid.GID
updateReq.ConnectorID = &nilGID
}
}
if rawCsvData := UnwrapOmittable(input.CsvData); rawCsvData != nil {
updateReq.CsvData = rawCsvData
}
source, err := r.accessReview.Sources(scope).Update(ctx, updateReq)
if err != nil {
return nil, types.UpdateAccessSourceOutput{}, fmt.Errorf("cannot update access source: %w", err)
}
return nil, types.UpdateAccessSourceOutput{
AccessSource: types.NewAccessSource(source),
}, nil
}
// DeleteAccessSourceTool handles the deleteAccessSource tool
// Delete an access source
func (r *Resolver) DeleteAccessSourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteAccessSourceInput) (*mcp.CallToolResult, types.DeleteAccessSourceOutput, error) {
r.MustAuthorize(ctx, input.AccessSourceID, probo.ActionAccessSourceDelete)
scope := coredata.NewScopeFromObjectID(input.AccessSourceID)
if err := r.accessReview.Sources(scope).Delete(ctx, input.AccessSourceID); err != nil {
return nil, types.DeleteAccessSourceOutput{}, fmt.Errorf("cannot delete access source: %w", err)
}
return nil, types.DeleteAccessSourceOutput{
DeletedAccessSourceID: input.AccessSourceID,
}, nil
}
// CreateAccessReviewCampaignTool handles the createAccessReviewCampaign tool
// Create a new access review campaign for an organization
func (r *Resolver) CreateAccessReviewCampaignTool(ctx context.Context, req *mcp.CallToolRequest, input *types.CreateAccessReviewCampaignInput) (*mcp.CallToolResult, types.CreateAccessReviewCampaignOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionAccessReviewCampaignCreate)
scope := coredata.NewScopeFromObjectID(input.OrganizationID)
var description string
if input.Description != nil {
description = *input.Description
}
campaign, err := r.accessReview.Campaigns(scope).Create(ctx, accessreview.CreateAccessReviewCampaignRequest{
OrganizationID: input.OrganizationID,
Name: input.Name,
Description: description,
FrameworkControls: input.FrameworkControls,
AccessSourceIDs: input.AccessSourceIds,
})
if err != nil {
return nil, types.CreateAccessReviewCampaignOutput{}, fmt.Errorf("cannot create access review campaign: %w", err)
}
return nil, types.CreateAccessReviewCampaignOutput{
Campaign: types.NewAccessReviewCampaign(campaign),
}, nil
}
// UpdateAccessReviewCampaignTool handles the updateAccessReviewCampaign tool
// Update an existing access review campaign
func (r *Resolver) UpdateAccessReviewCampaignTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateAccessReviewCampaignInput) (*mcp.CallToolResult, types.UpdateAccessReviewCampaignOutput, error) {
r.MustAuthorize(ctx, input.CampaignID, probo.ActionAccessReviewCampaignUpdate)
scope := coredata.NewScopeFromObjectID(input.CampaignID)
updateReq := accessreview.UpdateAccessReviewCampaignRequest{
CampaignID: input.CampaignID,
Name: input.Name,
Description: input.Description,
}
if rawControls := UnwrapOmittable(input.FrameworkControls); rawControls != nil {
if *rawControls != nil {
controls := make([]string, 0, len(**rawControls))
for _, v := range **rawControls {
if s, ok := v.(string); ok {
controls = append(controls, s)
}
}
updateReq.FrameworkControls = &controls
} else {
empty := []string{}
updateReq.FrameworkControls = &empty
}
}
campaign, err := r.accessReview.Campaigns(scope).Update(ctx, updateReq)
if err != nil {
return nil, types.UpdateAccessReviewCampaignOutput{}, fmt.Errorf("cannot update access review campaign: %w", err)
}
return nil, types.UpdateAccessReviewCampaignOutput{
Campaign: types.NewAccessReviewCampaign(campaign),
}, nil
}
// DeleteAccessReviewCampaignTool handles the deleteAccessReviewCampaign tool
// Delete an access review campaign
func (r *Resolver) DeleteAccessReviewCampaignTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteAccessReviewCampaignInput) (*mcp.CallToolResult, types.DeleteAccessReviewCampaignOutput, error) {
r.MustAuthorize(ctx, input.CampaignID, probo.ActionAccessReviewCampaignDelete)
scope := coredata.NewScopeFromObjectID(input.CampaignID)
if err := r.accessReview.Campaigns(scope).Delete(ctx, input.CampaignID); err != nil {
return nil, types.DeleteAccessReviewCampaignOutput{}, fmt.Errorf("cannot delete access review campaign: %w", err)
}
return nil, types.DeleteAccessReviewCampaignOutput{
DeletedCampaignID: input.CampaignID,
}, nil
}
// StartAccessReviewCampaignTool handles the startAccessReviewCampaign tool
// Start an access review campaign
func (r *Resolver) StartAccessReviewCampaignTool(ctx context.Context, req *mcp.CallToolRequest, input *types.StartAccessReviewCampaignInput) (*mcp.CallToolResult, types.StartAccessReviewCampaignOutput, error) {
r.MustAuthorize(ctx, input.CampaignID, probo.ActionAccessReviewCampaignStart)
scope := coredata.NewScopeFromObjectID(input.CampaignID)
campaign, err := r.accessReview.Campaigns(scope).Start(ctx, input.CampaignID)
if err != nil {
return nil, types.StartAccessReviewCampaignOutput{}, fmt.Errorf("cannot start access review campaign: %w", err)
}
return nil, types.StartAccessReviewCampaignOutput{
Campaign: types.NewAccessReviewCampaign(campaign),
}, nil
}
// CancelAccessReviewCampaignTool handles the cancelAccessReviewCampaign tool
// Cancel an in-progress access review campaign
func (r *Resolver) CancelAccessReviewCampaignTool(ctx context.Context, req *mcp.CallToolRequest, input *types.CancelAccessReviewCampaignInput) (*mcp.CallToolResult, types.CancelAccessReviewCampaignOutput, error) {
r.MustAuthorize(ctx, input.CampaignID, probo.ActionAccessReviewCampaignCancel)
scope := coredata.NewScopeFromObjectID(input.CampaignID)
campaign, err := r.accessReview.Campaigns(scope).Cancel(ctx, input.CampaignID)
if err != nil {
return nil, types.CancelAccessReviewCampaignOutput{}, fmt.Errorf("cannot cancel access review campaign: %w", err)
}
return nil, types.CancelAccessReviewCampaignOutput{
Campaign: types.NewAccessReviewCampaign(campaign),
}, nil
}
// AddAccessReviewCampaignScopeSourceTool handles the addAccessReviewCampaignScopeSource tool
// Add an access source to an access review campaign's scope
func (r *Resolver) AddAccessReviewCampaignScopeSourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddAccessReviewCampaignScopeSourceInput) (*mcp.CallToolResult, types.AddAccessReviewCampaignScopeSourceOutput, error) {
r.MustAuthorize(ctx, input.CampaignID, probo.ActionAccessReviewCampaignAddScopeSource)
scope := coredata.NewScopeFromObjectID(input.CampaignID)
campaign, err := r.accessReview.Campaigns(scope).AddScopeSource(ctx, accessreview.AddCampaignScopeSourceRequest{
CampaignID: input.CampaignID,
AccessSourceID: input.AccessSourceID,
})
if err != nil {
return nil, types.AddAccessReviewCampaignScopeSourceOutput{}, fmt.Errorf("cannot add scope source to access review campaign: %w", err)
}
return nil, types.AddAccessReviewCampaignScopeSourceOutput{
Campaign: types.NewAccessReviewCampaign(campaign),
}, nil
}
// RemoveAccessReviewCampaignScopeSourceTool handles the removeAccessReviewCampaignScopeSource tool
// Remove an access source from an access review campaign's scope
func (r *Resolver) RemoveAccessReviewCampaignScopeSourceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.RemoveAccessReviewCampaignScopeSourceInput) (*mcp.CallToolResult, types.RemoveAccessReviewCampaignScopeSourceOutput, error) {
r.MustAuthorize(ctx, input.CampaignID, probo.ActionAccessReviewCampaignRemoveScopeSource)
scope := coredata.NewScopeFromObjectID(input.CampaignID)
campaign, err := r.accessReview.Campaigns(scope).RemoveScopeSource(ctx, accessreview.RemoveCampaignScopeSourceRequest{
CampaignID: input.CampaignID,
AccessSourceID: input.AccessSourceID,
})
if err != nil {
return nil, types.RemoveAccessReviewCampaignScopeSourceOutput{}, fmt.Errorf("cannot remove scope source from access review campaign: %w", err)
}
return nil, types.RemoveAccessReviewCampaignScopeSourceOutput{
Campaign: types.NewAccessReviewCampaign(campaign),
}, nil
}
// FlagAccessEntryTool handles the flagAccessEntry tool
// Flag an access entry during review
func (r *Resolver) FlagAccessEntryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.FlagAccessEntryInput) (*mcp.CallToolResult, types.FlagAccessEntryOutput, error) {
r.MustAuthorize(ctx, input.AccessEntryID, probo.ActionAccessEntryFlag)
scope := coredata.NewScopeFromObjectID(input.AccessEntryID)
entry, err := r.accessReview.Entries(scope).FlagEntry(ctx, accessreview.FlagAccessEntryRequest{
EntryID: input.AccessEntryID,
Flags: input.Flags,
FlagReasons: input.FlagReasons,
})
if err != nil {
return nil, types.FlagAccessEntryOutput{}, fmt.Errorf("cannot flag access entry: %w", err)
}
return nil, types.FlagAccessEntryOutput{
AccessEntry: types.NewAccessEntry(entry),
}, 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
}
func (r *Resolver) ArchiveDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ArchiveDocumentInput) (*mcp.CallToolResult, types.ArchiveDocumentOutput, error) {
r.MustAuthorize(ctx, input.ID, probo.ActionDocumentArchive)
@@ -3250,18 +3759,16 @@ func (r *Resolver) UpdateOrganizationContextTool(ctx context.Context, req *mcp.C
}, 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)
func (r *Resolver) GetAuditLogEntryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetAuditLogEntryInput) (*mcp.CallToolResult, types.GetAuditLogEntryOutput, error) {
r.MustAuthorize(ctx, input.ID, iam.ActionAuditLogEntryGet)
prb := r.ProboService(ctx, input.ID)
url, err := prb.Audits.GenerateReportURL(ctx, input.ID, 15*time.Minute)
entry, err := r.iamSvc.OrganizationService.GetAuditLogEntry(ctx, input.ID)
if err != nil {
return nil, types.GetAuditReportUrlOutput{}, fmt.Errorf("cannot generate audit report URL: %w", err)
panic(fmt.Errorf("cannot get audit log entry: %w", err))
}
return nil, types.GetAuditReportUrlOutput{
URL: *url,
return nil, types.GetAuditLogEntryOutput{
AuditLogEntry: types.NewAuditLogEntry(entry),
}, nil
}
@@ -3299,19 +3806,6 @@ func (r *Resolver) ListAuditLogEntriesTool(ctx context.Context, req *mcp.CallToo
return nil, types.NewListAuditLogEntriesOutput(p), nil
}
func (r *Resolver) GetAuditLogEntryTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetAuditLogEntryInput) (*mcp.CallToolResult, types.GetAuditLogEntryOutput, error) {
r.MustAuthorize(ctx, input.ID, iam.ActionAuditLogEntryGet)
entry, err := r.iamSvc.OrganizationService.GetAuditLogEntry(ctx, input.ID)
if err != nil {
panic(fmt.Errorf("cannot get audit log entry: %w", err))
}
return nil, types.GetAuditLogEntryOutput{
AuditLogEntry: types.NewAuditLogEntry(entry),
}, nil
}
func (r *Resolver) RequestDocumentVersionApprovalTool(ctx context.Context, req *mcp.CallToolRequest, input *types.RequestDocumentVersionApprovalInput) (*mcp.CallToolResult, types.RequestDocumentVersionApprovalOutput, error) {
r.MustAuthorize(ctx, input.DocumentID, probo.ActionDocumentVersionRequestApproval)

View File

@@ -6412,6 +6412,843 @@ components:
$ref: "#/components/schemas/GID"
description: Deleted applicability statement ID
AccessReviewCampaignStatus:
type: string
enum:
- DRAFT
- IN_PROGRESS
- PENDING_ACTIONS
- FAILED
- COMPLETED
- CANCELLED
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AccessReviewCampaignStatus
AccessEntryDecision:
type: string
enum:
- PENDING
- APPROVED
- REVOKE
- DEFER
- ESCALATE
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AccessEntryDecision
AccessEntryFlag:
type: string
enum:
- NONE
- ORPHANED
- INACTIVE
- EXCESSIVE
- ROLE_MISMATCH
- NEW
- DORMANT
- TERMINATED_USER
- CONTRACTOR_EXPIRED
- SOD_CONFLICT
- PRIVILEGED_ACCESS
- ROLE_CREEP
- NO_BUSINESS_JUSTIFICATION
- OUT_OF_DEPARTMENT
- SHARED_ACCOUNT
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AccessEntryFlag
AccessEntryIncrementalTag:
type: string
enum:
- NEW
- REMOVED
- UNCHANGED
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AccessEntryIncrementalTag
AccessEntryAuthMethod:
type: string
enum:
- SSO
- PASSWORD
- API_KEY
- SERVICE_ACCOUNT
- UNKNOWN
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AccessEntryAuthMethod
AccessEntryAccountType:
type: string
enum:
- USER
- SERVICE_ACCOUNT
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AccessEntryAccountType
MFAStatus:
type: string
enum:
- ENABLED
- DISABLED
- UNKNOWN
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.MFAStatus
AccessReviewCampaignOrderField:
type: string
enum:
- CREATED_AT
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AccessReviewCampaignOrderField
AccessReviewCampaignOrderBy:
type: object
required:
- field
- direction
properties:
field:
$ref: "#/components/schemas/AccessReviewCampaignOrderField"
description: Order field
direction:
$ref: "#/components/schemas/OrderDirection"
description: Order direction
AccessEntryOrderField:
type: string
enum:
- CREATED_AT
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AccessEntryOrderField
AccessEntryOrderBy:
type: object
required:
- field
- direction
properties:
field:
$ref: "#/components/schemas/AccessEntryOrderField"
description: Order field
direction:
$ref: "#/components/schemas/OrderDirection"
description: Order direction
AccessReviewCampaign:
type: object
required:
- id
- organization_id
- name
- status
- created_at
- updated_at
properties:
id:
$ref: "#/components/schemas/GID"
description: Campaign ID
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
name:
type: string
description: Campaign name
description:
type: string
description: Campaign description
status:
$ref: "#/components/schemas/AccessReviewCampaignStatus"
description: Campaign status
started_at:
type:
- string
- "null"
format: date-time
description: Campaign start time
completed_at:
type:
- string
- "null"
format: date-time
description: Campaign completion time
framework_controls:
type: array
items:
type: string
description: Framework controls
created_at:
type: string
format: date-time
description: Creation timestamp
updated_at:
type: string
format: date-time
description: Update timestamp
AccessEntry:
type: object
required:
- id
- campaign_id
- access_source_id
- email
- full_name
- role
- job_title
- is_admin
- mfa_status
- auth_method
- account_type
- external_id
- incremental_tag
- flags
- decision
- created_at
- updated_at
properties:
id:
$ref: "#/components/schemas/GID"
description: Entry ID
campaign_id:
$ref: "#/components/schemas/GID"
description: Campaign ID
access_source_id:
$ref: "#/components/schemas/GID"
description: Access source ID
email:
type: string
description: User email
full_name:
type: string
description: User full name
role:
type: string
description: User role in the system
job_title:
type: string
description: User job title
is_admin:
type: boolean
description: Whether the user has admin privileges
mfa_status:
$ref: "#/components/schemas/MFAStatus"
description: MFA status
auth_method:
$ref: "#/components/schemas/AccessEntryAuthMethod"
description: Authentication method
account_type:
$ref: "#/components/schemas/AccessEntryAccountType"
description: Account type (user or service account)
last_login:
type:
- string
- "null"
format: date-time
description: Last login time
account_created_at:
type:
- string
- "null"
format: date-time
description: Account creation time
external_id:
type: string
description: External ID in the source system
incremental_tag:
$ref: "#/components/schemas/AccessEntryIncrementalTag"
description: Change tag compared to previous campaign
flags:
type: array
items:
$ref: "#/components/schemas/AccessEntryFlag"
description: Risk flags
flag_reasons:
type: array
items:
type: string
description: Reasons for the flags
decision:
$ref: "#/components/schemas/AccessEntryDecision"
description: Review decision
decision_note:
type:
- string
- "null"
description: Decision justification
decided_by:
anyOf:
- $ref: "#/components/schemas/GID"
- type: "null"
description: Profile ID of the decision maker
decided_at:
type:
- string
- "null"
format: date-time
description: Decision timestamp
created_at:
type: string
format: date-time
description: Creation timestamp
updated_at:
type: string
format: date-time
description: Update timestamp
AccessEntryStatistics:
type: object
required:
- total_count
- decision_counts
- flag_counts
- incremental_tag_counts
properties:
total_count:
type: integer
description: Total number of entries
decision_counts:
type: object
additionalProperties:
type: integer
description: Count of entries per decision status
flag_counts:
type: object
additionalProperties:
type: integer
description: Count of entries per flag type
incremental_tag_counts:
type: object
additionalProperties:
type: integer
description: Count of entries per incremental tag
ListAccessReviewCampaignsInput:
type: object
required:
- organization_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
order_by:
$ref: "#/components/schemas/AccessReviewCampaignOrderBy"
description: Order by
size:
type: integer
description: Page size
cursor:
$ref: "#/components/schemas/CursorKey"
description: Page cursor
ListAccessReviewCampaignsOutput:
type: object
required:
- campaigns
properties:
next_cursor:
$ref: "#/components/schemas/CursorKey"
description: Next cursor
campaigns:
type: array
items:
$ref: "#/components/schemas/AccessReviewCampaign"
ListAccessEntriesInput:
type: object
required:
- campaign_id
properties:
campaign_id:
$ref: "#/components/schemas/GID"
description: Campaign ID
access_source_id:
$ref: "#/components/schemas/GID"
description: Filter by access source ID
order_by:
$ref: "#/components/schemas/AccessEntryOrderBy"
description: Order by
size:
type: integer
description: Page size
cursor:
$ref: "#/components/schemas/CursorKey"
description: Page cursor
filter:
type: object
properties:
decision:
$ref: "#/components/schemas/AccessEntryDecision"
description: Filter by decision status
flag:
$ref: "#/components/schemas/AccessEntryFlag"
description: Filter by flag
incremental_tag:
$ref: "#/components/schemas/AccessEntryIncrementalTag"
description: Filter by incremental tag
is_admin:
type: boolean
description: Filter by admin status
auth_method:
$ref: "#/components/schemas/AccessEntryAuthMethod"
description: Filter by auth method
account_type:
$ref: "#/components/schemas/AccessEntryAccountType"
description: Filter by account type
ListAccessEntriesOutput:
type: object
required:
- entries
properties:
next_cursor:
$ref: "#/components/schemas/CursorKey"
description: Next cursor
entries:
type: array
items:
$ref: "#/components/schemas/AccessEntry"
GetAccessReviewCampaignStatisticsInput:
type: object
required:
- campaign_id
properties:
campaign_id:
$ref: "#/components/schemas/GID"
description: Campaign ID
GetAccessReviewCampaignStatisticsOutput:
type: object
required:
- statistics
properties:
statistics:
$ref: "#/components/schemas/AccessEntryStatistics"
RecordAccessEntryDecisionMCPInput:
type: object
required:
- access_entry_id
- decision
properties:
access_entry_id:
$ref: "#/components/schemas/GID"
description: Access entry ID
decision:
$ref: "#/components/schemas/AccessEntryDecision"
description: Decision (APPROVED, REVOKE, DEFER, ESCALATE)
decision_note:
type: string
description: Decision justification (required for non-APPROVED decisions)
RecordAccessEntryDecisionMCPOutput:
type: object
required:
- access_entry
properties:
access_entry:
$ref: "#/components/schemas/AccessEntry"
RecordAccessEntryDecisionsMCPInput:
type: object
required:
- decisions
properties:
decisions:
type: array
items:
type: object
required:
- access_entry_id
- decision
properties:
access_entry_id:
$ref: "#/components/schemas/GID"
description: Access entry ID
decision:
$ref: "#/components/schemas/AccessEntryDecision"
description: Decision (APPROVED, REVOKE, DEFER, ESCALATE)
decision_note:
type: string
description: Decision justification (required for non-APPROVED decisions)
RecordAccessEntryDecisionsMCPOutput:
type: object
required:
- access_entries
properties:
access_entries:
type: array
items:
$ref: "#/components/schemas/AccessEntry"
FlagAccessEntryMCPInput:
type: object
required:
- access_entry_id
- flags
properties:
access_entry_id:
$ref: "#/components/schemas/GID"
description: Access entry ID
flags:
type: array
items:
$ref: "#/components/schemas/AccessEntryFlag"
description: Flags to set (ORPHANED, INACTIVE, EXCESSIVE, ROLE_MISMATCH, NEW, DORMANT, TERMINATED_USER, CONTRACTOR_EXPIRED, SOD_CONFLICT, PRIVILEGED_ACCESS, ROLE_CREEP, NO_BUSINESS_JUSTIFICATION, OUT_OF_DEPARTMENT, SHARED_ACCOUNT)
flag_reasons:
type: array
items:
type: string
description: Reasons for flagging
FlagAccessEntryMCPOutput:
type: object
required:
- access_entry
properties:
access_entry:
$ref: "#/components/schemas/AccessEntry"
CloseAccessReviewCampaignMCPInput:
type: object
required:
- campaign_id
properties:
campaign_id:
$ref: "#/components/schemas/GID"
description: Campaign ID
CloseAccessReviewCampaignMCPOutput:
type: object
required:
- campaign
properties:
campaign:
$ref: "#/components/schemas/AccessReviewCampaign"
AccessSourceCategory:
type: string
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AccessSourceCategory
enum:
- SAAS
- CLOUD_INFRA
- SOURCE_CODE
- OTHER
AccessSourceOrderField:
type: string
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.AccessSourceOrderField
enum:
- CREATED_AT
AccessSourceOrderBy:
type: object
required:
- field
- direction
properties:
field:
$ref: "#/components/schemas/AccessSourceOrderField"
description: Order field
direction:
$ref: "#/components/schemas/OrderDirection"
description: Order direction
AccessSource:
type: object
required:
- id
- organization_id
- name
- created_at
- updated_at
properties:
id:
$ref: "#/components/schemas/GID"
description: Access source ID
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
connector_id:
anyOf:
- $ref: "#/components/schemas/GID"
- type: "null"
description: Connector ID
name:
type: string
description: Access source name
csv_data:
type:
- string
- "null"
description: CSV data for manual sources
created_at:
type: string
format: date-time
description: Creation timestamp
updated_at:
type: string
format: date-time
description: Update timestamp
ListAccessSourcesInput:
type: object
required:
- organization_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
order_by:
$ref: "#/components/schemas/AccessSourceOrderBy"
description: Order by
size:
type: integer
description: Page size
cursor:
$ref: "#/components/schemas/CursorKey"
description: Page cursor
ListAccessSourcesOutput:
type: object
required:
- access_sources
properties:
next_cursor:
$ref: "#/components/schemas/CursorKey"
description: Next cursor
access_sources:
type: array
items:
$ref: "#/components/schemas/AccessSource"
CreateAccessSourceMCPInput:
type: object
required:
- organization_id
- name
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
connector_id:
$ref: "#/components/schemas/GID"
description: Connector ID (optional)
name:
type: string
description: Access source name
csv_data:
type: string
description: CSV data for manual sources (optional)
CreateAccessSourceMCPOutput:
type: object
required:
- access_source
properties:
access_source:
$ref: "#/components/schemas/AccessSource"
UpdateAccessSourceMCPInput:
type: object
required:
- access_source_id
properties:
access_source_id:
$ref: "#/components/schemas/GID"
description: Access source ID
name:
type: string
description: New name
connector_id:
type:
- string
- "null"
go.probo.inc/mcpgen/omittable: true
description: Connector ID (set to null to remove)
csv_data:
type:
- string
- "null"
go.probo.inc/mcpgen/omittable: true
description: CSV data for manual sources (set to null to remove)
UpdateAccessSourceMCPOutput:
type: object
required:
- access_source
properties:
access_source:
$ref: "#/components/schemas/AccessSource"
DeleteAccessSourceMCPInput:
type: object
required:
- access_source_id
properties:
access_source_id:
$ref: "#/components/schemas/GID"
description: Access source ID
DeleteAccessSourceMCPOutput:
type: object
required:
- deleted_access_source_id
properties:
deleted_access_source_id:
$ref: "#/components/schemas/GID"
description: Deleted access source ID
CreateAccessReviewCampaignMCPInput:
type: object
required:
- organization_id
- name
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
name:
type: string
description: Campaign name
description:
type: string
description: Campaign description
framework_controls:
type: array
items:
type: string
description: Framework control references
access_source_ids:
type: array
items:
$ref: "#/components/schemas/GID"
description: Access source IDs to include in scope
CreateAccessReviewCampaignMCPOutput:
type: object
required:
- campaign
properties:
campaign:
$ref: "#/components/schemas/AccessReviewCampaign"
UpdateAccessReviewCampaignMCPInput:
type: object
required:
- campaign_id
properties:
campaign_id:
$ref: "#/components/schemas/GID"
description: Campaign ID
name:
type: string
description: New campaign name
description:
type: string
description: New campaign description
framework_controls:
type:
- array
- "null"
items:
type: string
go.probo.inc/mcpgen/omittable: true
description: Framework control references (set to null to clear)
UpdateAccessReviewCampaignMCPOutput:
type: object
required:
- campaign
properties:
campaign:
$ref: "#/components/schemas/AccessReviewCampaign"
DeleteAccessReviewCampaignMCPInput:
type: object
required:
- campaign_id
properties:
campaign_id:
$ref: "#/components/schemas/GID"
description: Campaign ID
DeleteAccessReviewCampaignMCPOutput:
type: object
required:
- deleted_campaign_id
properties:
deleted_campaign_id:
$ref: "#/components/schemas/GID"
description: Deleted campaign ID
StartAccessReviewCampaignMCPInput:
type: object
required:
- campaign_id
properties:
campaign_id:
$ref: "#/components/schemas/GID"
description: Campaign ID
StartAccessReviewCampaignMCPOutput:
type: object
required:
- campaign
properties:
campaign:
$ref: "#/components/schemas/AccessReviewCampaign"
CancelAccessReviewCampaignMCPInput:
type: object
required:
- campaign_id
properties:
campaign_id:
$ref: "#/components/schemas/GID"
description: Campaign ID
CancelAccessReviewCampaignMCPOutput:
type: object
required:
- campaign
properties:
campaign:
$ref: "#/components/schemas/AccessReviewCampaign"
AddAccessReviewCampaignScopeSourceMCPInput:
type: object
required:
- campaign_id
- access_source_id
properties:
campaign_id:
$ref: "#/components/schemas/GID"
description: Campaign ID
access_source_id:
$ref: "#/components/schemas/GID"
description: Access source ID to add to scope
AddAccessReviewCampaignScopeSourceMCPOutput:
type: object
required:
- campaign
properties:
campaign:
$ref: "#/components/schemas/AccessReviewCampaign"
RemoveAccessReviewCampaignScopeSourceMCPInput:
type: object
required:
- campaign_id
- access_source_id
properties:
campaign_id:
$ref: "#/components/schemas/GID"
description: Campaign ID
access_source_id:
$ref: "#/components/schemas/GID"
description: Access source ID to remove from scope
RemoveAccessReviewCampaignScopeSourceMCPOutput:
type: object
required:
- campaign
properties:
campaign:
$ref: "#/components/schemas/AccessReviewCampaign"
OrganizationContext:
type: object
required:
@@ -7732,6 +8569,156 @@ tools:
$ref: "#/components/schemas/DeleteApplicabilityStatementInput"
outputSchema:
$ref: "#/components/schemas/DeleteApplicabilityStatementOutput"
- name: listAccessReviewCampaigns
description: List access review campaigns for an organization
hints:
readonly: true
idempotent: true
inputSchema:
$ref: "#/components/schemas/ListAccessReviewCampaignsInput"
outputSchema:
$ref: "#/components/schemas/ListAccessReviewCampaignsOutput"
- name: listAccessEntries
description: List access entries for a campaign with optional filters (decision, flag, incremental_tag, is_admin, auth_method, account_type)
hints:
readonly: true
idempotent: true
inputSchema:
$ref: "#/components/schemas/ListAccessEntriesInput"
outputSchema:
$ref: "#/components/schemas/ListAccessEntriesOutput"
- name: getAccessReviewCampaignStatistics
description: Get statistics for an access review campaign including counts by decision, flag, and incremental tag
hints:
readonly: true
idempotent: true
inputSchema:
$ref: "#/components/schemas/GetAccessReviewCampaignStatisticsInput"
outputSchema:
$ref: "#/components/schemas/GetAccessReviewCampaignStatisticsOutput"
- name: recordAccessEntryDecision
description: Record a decision on an access entry (APPROVED, REVOKE, DEFER, or ESCALATE). Non-APPROVED decisions require a decision_note.
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/RecordAccessEntryDecisionMCPInput"
outputSchema:
$ref: "#/components/schemas/RecordAccessEntryDecisionMCPOutput"
- name: recordAccessEntryDecisions
description: Record decisions on multiple access entries in a single batch. Non-APPROVED decisions require a decision_note.
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/RecordAccessEntryDecisionsMCPInput"
outputSchema:
$ref: "#/components/schemas/RecordAccessEntryDecisionsMCPOutput"
- name: flagAccessEntry
description: Flag an access entry with one or more flags during review (ORPHANED, INACTIVE, EXCESSIVE, ROLE_MISMATCH, NEW, etc.). Optionally provide reasons.
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/FlagAccessEntryMCPInput"
outputSchema:
$ref: "#/components/schemas/FlagAccessEntryMCPOutput"
- name: closeAccessReviewCampaign
description: Close an access review campaign. All entries must have been decided (no PENDING entries).
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/CloseAccessReviewCampaignMCPInput"
outputSchema:
$ref: "#/components/schemas/CloseAccessReviewCampaignMCPOutput"
- name: listAccessSources
description: List access sources for an organization
hints:
readonly: true
idempotent: true
inputSchema:
$ref: "#/components/schemas/ListAccessSourcesInput"
outputSchema:
$ref: "#/components/schemas/ListAccessSourcesOutput"
- name: createAccessSource
description: Create a new access source for an organization
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/CreateAccessSourceMCPInput"
outputSchema:
$ref: "#/components/schemas/CreateAccessSourceMCPOutput"
- name: updateAccessSource
description: Update an existing access source
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/UpdateAccessSourceMCPInput"
outputSchema:
$ref: "#/components/schemas/UpdateAccessSourceMCPOutput"
- name: deleteAccessSource
description: Delete an access source
hints:
readonly: false
destructive: true
inputSchema:
$ref: "#/components/schemas/DeleteAccessSourceMCPInput"
outputSchema:
$ref: "#/components/schemas/DeleteAccessSourceMCPOutput"
- name: createAccessReviewCampaign
description: Create a new access review campaign for an organization
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/CreateAccessReviewCampaignMCPInput"
outputSchema:
$ref: "#/components/schemas/CreateAccessReviewCampaignMCPOutput"
- name: updateAccessReviewCampaign
description: Update an existing access review campaign
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/UpdateAccessReviewCampaignMCPInput"
outputSchema:
$ref: "#/components/schemas/UpdateAccessReviewCampaignMCPOutput"
- name: deleteAccessReviewCampaign
description: Delete an access review campaign
hints:
readonly: false
destructive: true
inputSchema:
$ref: "#/components/schemas/DeleteAccessReviewCampaignMCPInput"
outputSchema:
$ref: "#/components/schemas/DeleteAccessReviewCampaignMCPOutput"
- name: startAccessReviewCampaign
description: Start an access review campaign. Triggers data fetching from all configured scope sources.
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/StartAccessReviewCampaignMCPInput"
outputSchema:
$ref: "#/components/schemas/StartAccessReviewCampaignMCPOutput"
- name: cancelAccessReviewCampaign
description: Cancel an in-progress access review campaign
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/CancelAccessReviewCampaignMCPInput"
outputSchema:
$ref: "#/components/schemas/CancelAccessReviewCampaignMCPOutput"
- name: addAccessReviewCampaignScopeSource
description: Add an access source to an access review campaign's scope
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/AddAccessReviewCampaignScopeSourceMCPInput"
outputSchema:
$ref: "#/components/schemas/AddAccessReviewCampaignScopeSourceMCPOutput"
- name: removeAccessReviewCampaignScopeSource
description: Remove an access source from an access review campaign's scope
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/RemoveAccessReviewCampaignScopeSourceMCPInput"
outputSchema:
$ref: "#/components/schemas/RemoveAccessReviewCampaignScopeSourceMCPOutput"
- name: getOrganizationContext
description: Get the organization context containing structured sections about the company
hints:

View File

@@ -0,0 +1,161 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/page"
)
func NewAccessSource(s *coredata.AccessSource) *AccessSource {
return &AccessSource{
ID: s.ID,
OrganizationID: s.OrganizationID,
ConnectorID: s.ConnectorID,
Name: s.Name,
CsvData: s.CsvData,
CreatedAt: s.CreatedAt,
UpdatedAt: s.UpdatedAt,
}
}
func NewListAccessSourcesOutput(
p *page.Page[*coredata.AccessSource, coredata.AccessSourceOrderField],
) ListAccessSourcesOutput {
sources := make([]*AccessSource, 0, len(p.Data))
for _, s := range p.Data {
sources = append(sources, NewAccessSource(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 ListAccessSourcesOutput{
NextCursor: nextCursor,
AccessSources: sources,
}
}
func NewAccessReviewCampaign(c *coredata.AccessReviewCampaign) *AccessReviewCampaign {
return &AccessReviewCampaign{
ID: c.ID,
OrganizationID: c.OrganizationID,
Name: c.Name,
Description: &c.Description,
Status: c.Status,
StartedAt: c.StartedAt,
CompletedAt: c.CompletedAt,
FrameworkControls: c.FrameworkControls,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
func NewAccessEntry(e *coredata.AccessEntry) *AccessEntry {
entry := &AccessEntry{
ID: e.ID,
CampaignID: e.AccessReviewCampaignID,
AccessSourceID: e.AccessSourceID,
Email: e.Email,
FullName: e.FullName,
Role: e.Role,
JobTitle: e.JobTitle,
IsAdmin: e.IsAdmin,
MfaStatus: e.MFAStatus,
AuthMethod: e.AuthMethod,
AccountType: e.AccountType,
LastLogin: e.LastLogin,
AccountCreatedAt: e.AccountCreatedAt,
ExternalID: e.ExternalID,
IncrementalTag: e.IncrementalTag,
Flags: e.Flags,
FlagReasons: e.FlagReasons,
Decision: e.Decision,
DecisionNote: e.DecisionNote,
DecidedBy: e.DecidedBy,
DecidedAt: e.DecidedAt,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
return entry
}
func NewListAccessReviewCampaignsOutput(
p *page.Page[*coredata.AccessReviewCampaign, coredata.AccessReviewCampaignOrderField],
) ListAccessReviewCampaignsOutput {
campaigns := make([]*AccessReviewCampaign, 0, len(p.Data))
for _, c := range p.Data {
campaigns = append(campaigns, NewAccessReviewCampaign(c))
}
var nextCursor *page.CursorKey
if len(p.Data) > 0 {
cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field)
nextCursor = &cursorKey
}
return ListAccessReviewCampaignsOutput{
NextCursor: nextCursor,
Campaigns: campaigns,
}
}
func NewListAccessEntriesOutput(
p *page.Page[*coredata.AccessEntry, coredata.AccessEntryOrderField],
) ListAccessEntriesOutput {
entries := make([]*AccessEntry, 0, len(p.Data))
for _, e := range p.Data {
entries = append(entries, NewAccessEntry(e))
}
var nextCursor *page.CursorKey
if len(p.Data) > 0 {
cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field)
nextCursor = &cursorKey
}
return ListAccessEntriesOutput{
NextCursor: nextCursor,
Entries: entries,
}
}
func NewAccessEntryStatistics(s *coredata.AccessEntryStatistics) *AccessEntryStatistics {
decisionCounts := make(map[string]any, len(s.DecisionCounts))
for k, v := range s.DecisionCounts {
decisionCounts[string(k)] = v
}
flagCounts := make(map[string]any, len(s.FlagCounts))
for k, v := range s.FlagCounts {
flagCounts[string(k)] = v
}
incrementalTagCounts := make(map[string]any, len(s.IncrementalTagCounts))
for k, v := range s.IncrementalTagCounts {
incrementalTagCounts[string(k)] = v
}
return &AccessEntryStatistics{
TotalCount: s.TotalCount,
DecisionCounts: decisionCounts,
FlagCounts: flagCounts,
IncrementalTagCounts: incrementalTagCounts,
}
}

View File

@@ -22,6 +22,7 @@ import (
"github.com/modelcontextprotocol/go-sdk/mcp"
"go.gearno.de/kit/log"
mcpgenmcp "go.probo.inc/mcpgen/mcp"
"go.probo.inc/probo/pkg/accessreview"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/probo"
@@ -34,16 +35,17 @@ func (r *Resolver) ProboService(ctx context.Context, objectID gid.GID) *probo.Te
return r.proboSvc.WithTenant(objectID.TenantID())
}
func NewMux(logger *log.Logger, proboSvc *probo.Service, iamSvc *iam.Service, tokenSecret string) *chi.Mux {
func NewMux(logger *log.Logger, proboSvc *probo.Service, iamSvc *iam.Service, accessReviewSvc *accessreview.Service, tokenSecret string) *chi.Mux {
logger = logger.Named("mcp.v1")
logger.Info("initializing MCP server")
// server.AddReceivingMiddleware(mcputils.LoggingMiddleware(logger))
resolver := &Resolver{
proboSvc: proboSvc,
iamSvc: iamSvc,
logger: logger,
proboSvc: proboSvc,
iamSvc: iamSvc,
accessReview: accessReviewSvc,
logger: logger,
}
mcpServer := server.New(resolver)