Add SCIM tools to MCP API

The SCIM operations (configuration, bridge, events) were only
available through the GraphQL Connect API. This adds the
equivalent MCP tools so MCP clients can manage SCIM
provisioning: get/create/delete configuration, regenerate
token, get/update bridge, and list events.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-04 18:22:49 +04:00
parent 5328a1ff5b
commit 8ce1ab8a53
3 changed files with 604 additions and 0 deletions

View File

@@ -5107,3 +5107,117 @@ func (r *Resolver) PublishRiskListTool(ctx context.Context, req *mcp.CallToolReq
DocumentVersionID: documentVersion.ID,
}, nil
}
func (r *Resolver) GetSCIMConfigurationTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetSCIMConfigurationInput) (*mcp.CallToolResult, types.GetSCIMConfigurationOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, iam.ActionSCIMConfigurationGet)
config, err := r.iamSvc.OrganizationService.GetSCIMConfiguration(ctx, input.OrganizationID)
if err != nil {
var errNotFound *iam.ErrNoSCIMConfigurationFound
if errors.As(err, &errNotFound) {
return nil, types.GetSCIMConfigurationOutput{}, fmt.Errorf("SCIM configuration not found for organization %s", input.OrganizationID)
}
panic(fmt.Errorf("cannot get SCIM configuration: %w", err))
}
return nil, types.GetSCIMConfigurationOutput{ScimConfiguration: types.NewSCIMConfiguration(config)}, nil
}
func (r *Resolver) CreateSCIMConfigurationTool(ctx context.Context, req *mcp.CallToolRequest, input *types.CreateSCIMConfigurationInput) (*mcp.CallToolResult, types.CreateSCIMConfigurationOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, iam.ActionSCIMConfigurationCreate)
config, token, err := r.iamSvc.OrganizationService.CreateSCIMConfiguration(ctx, input.OrganizationID)
if err != nil {
return nil, types.CreateSCIMConfigurationOutput{}, fmt.Errorf("cannot create SCIM configuration: %w", err)
}
output := types.CreateSCIMConfigurationOutput{
ScimConfiguration: types.NewSCIMConfiguration(config),
Token: token,
}
if input.ConnectorID != nil {
bridge, err := r.iamSvc.OrganizationService.CreateSCIMBridge(ctx, input.OrganizationID, config.ID, *input.ConnectorID)
if err != nil {
return nil, types.CreateSCIMConfigurationOutput{}, fmt.Errorf("cannot create SCIM bridge: %w", err)
}
output.ScimBridge = types.NewSCIMBridge(bridge)
}
return nil, output, nil
}
func (r *Resolver) DeleteSCIMConfigurationTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteSCIMConfigurationInput) (*mcp.CallToolResult, types.DeleteSCIMConfigurationOutput, error) {
r.MustAuthorize(ctx, input.OrganizationID, iam.ActionSCIMConfigurationDelete)
err := r.iamSvc.OrganizationService.DeleteSCIMConfiguration(ctx, input.OrganizationID, input.ScimConfigurationID)
if err != nil {
return nil, types.DeleteSCIMConfigurationOutput{}, fmt.Errorf("cannot delete SCIM configuration: %w", err)
}
return nil, types.DeleteSCIMConfigurationOutput{DeletedScimConfigurationID: input.ScimConfigurationID}, nil
}
func (r *Resolver) RegenerateSCIMTokenTool(ctx context.Context, req *mcp.CallToolRequest, input *types.RegenerateSCIMTokenInput) (*mcp.CallToolResult, types.RegenerateSCIMTokenOutput, error) {
r.MustAuthorize(ctx, input.ScimConfigurationID, iam.ActionSCIMConfigurationUpdate)
config, token, err := r.iamSvc.OrganizationService.RegenerateSCIMToken(ctx, input.OrganizationID, input.ScimConfigurationID)
if err != nil {
return nil, types.RegenerateSCIMTokenOutput{}, fmt.Errorf("cannot regenerate SCIM token: %w", err)
}
return nil, types.RegenerateSCIMTokenOutput{
ScimConfiguration: types.NewSCIMConfiguration(config),
Token: token,
}, nil
}
func (r *Resolver) GetSCIMBridgeTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetSCIMBridgeInput) (*mcp.CallToolResult, types.GetSCIMBridgeOutput, error) {
r.MustAuthorize(ctx, input.ID, iam.ActionSCIMBridgeGet)
bridge, err := r.iamSvc.OrganizationService.GetSCIMBridgeByID(ctx, input.ID)
if err != nil {
var errNotFound *iam.ErrSCIMBridgeNotFound
if errors.As(err, &errNotFound) {
return nil, types.GetSCIMBridgeOutput{}, fmt.Errorf("SCIM bridge %s not found", input.ID)
}
panic(fmt.Errorf("cannot get SCIM bridge: %w", err))
}
return nil, types.GetSCIMBridgeOutput{ScimBridge: types.NewSCIMBridge(bridge)}, nil
}
func (r *Resolver) UpdateSCIMBridgeTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateSCIMBridgeInput) (*mcp.CallToolResult, types.UpdateSCIMBridgeOutput, error) {
r.MustAuthorize(ctx, input.ScimBridgeID, iam.ActionSCIMBridgeUpdate)
bridge, err := r.iamSvc.OrganizationService.UpdateSCIMBridge(ctx, input.OrganizationID, input.ScimBridgeID, input.ExcludedUserNames)
if err != nil {
return nil, types.UpdateSCIMBridgeOutput{}, fmt.Errorf("cannot update SCIM bridge: %w", err)
}
return nil, types.UpdateSCIMBridgeOutput{ScimBridge: types.NewSCIMBridge(bridge)}, nil
}
func (r *Resolver) ListSCIMEventsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListSCIMEventsInput) (*mcp.CallToolResult, types.ListSCIMEventsOutput, error) {
r.MustAuthorize(ctx, input.ScimConfigurationID, iam.ActionSCIMEventList)
pageOrderBy := page.OrderBy[coredata.SCIMEventOrderField]{
Field: coredata.SCIMEventOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if input.OrderBy != nil {
pageOrderBy = page.OrderBy[coredata.SCIMEventOrderField]{
Field: input.OrderBy.Field,
Direction: input.OrderBy.Direction,
}
}
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
p, err := r.iamSvc.OrganizationService.ListSCIMEventsByConfigID(ctx, input.ScimConfigurationID, cursor)
if err != nil {
panic(fmt.Errorf("cannot list SCIM events: %w", err))
}
return nil, types.NewListSCIMEventsOutput(p), nil
}

View File

@@ -9879,6 +9879,344 @@ components:
cookie_consent_record:
$ref: "#/components/schemas/CookieConsentRecord"
SCIMBridgeType:
type: string
enum:
- GOOGLE_WORKSPACE
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.SCIMBridgeType
SCIMBridgeState:
type: string
enum:
- PENDING
- ACTIVE
- SYNCING
- FAILED
- DISABLED
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.SCIMBridgeState
SCIMEventOrderField:
type: string
enum:
- CREATED_AT
go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.SCIMEventOrderField
SCIMEventOrderBy:
type: object
required:
- field
- direction
properties:
field:
$ref: "#/components/schemas/SCIMEventOrderField"
description: SCIM event order field
direction:
$ref: "#/components/schemas/OrderDirection"
description: SCIM event order direction
SCIMConfiguration:
type: object
required:
- id
- organization_id
- created_at
- updated_at
properties:
id:
$ref: "#/components/schemas/GID"
description: SCIM configuration ID
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
bridge_id:
type:
- string
- "null"
description: Associated SCIM bridge ID, if any
created_at:
type: string
format: date-time
description: Creation timestamp
updated_at:
type: string
format: date-time
description: Last update timestamp
SCIMBridge:
type: object
required:
- id
- organization_id
- scim_configuration_id
- type
- state
- excluded_user_names
- created_at
- updated_at
properties:
id:
$ref: "#/components/schemas/GID"
description: SCIM bridge ID
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
scim_configuration_id:
$ref: "#/components/schemas/GID"
description: SCIM configuration ID
connector_id:
type:
- string
- "null"
description: Connector ID, if any
type:
$ref: "#/components/schemas/SCIMBridgeType"
description: Bridge type
state:
$ref: "#/components/schemas/SCIMBridgeState"
description: Bridge state
excluded_user_names:
type: array
items:
type: string
description: User names excluded from SCIM sync
last_synced_at:
type:
- string
- "null"
format: date-time
description: Last successful sync timestamp
created_at:
type: string
format: date-time
description: Creation timestamp
updated_at:
type: string
format: date-time
description: Last update timestamp
SCIMEvent:
type: object
required:
- id
- organization_id
- scim_configuration_id
- method
- path
- status_code
- user_name
- ip_address
- created_at
properties:
id:
$ref: "#/components/schemas/GID"
description: SCIM event ID
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
scim_configuration_id:
$ref: "#/components/schemas/GID"
description: SCIM configuration ID
method:
type: string
description: HTTP method
path:
type: string
description: Request path
status_code:
type: integer
description: HTTP status code
request_body:
type:
- string
- "null"
description: Request body
response_body:
type:
- string
- "null"
description: Response body
error_message:
type:
- string
- "null"
description: Error message, if any
user_name:
type: string
description: SCIM user name
ip_address:
type: string
description: Client IP address
created_at:
type: string
format: date-time
description: Creation timestamp
GetSCIMConfigurationInput:
type: object
required:
- organization_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
GetSCIMConfigurationOutput:
type: object
required:
- scim_configuration
properties:
scim_configuration:
$ref: "#/components/schemas/SCIMConfiguration"
CreateSCIMConfigurationInput:
type: object
required:
- organization_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
connector_id:
$ref: "#/components/schemas/GID"
description: Optional connector ID to create a SCIM bridge
CreateSCIMConfigurationOutput:
type: object
required:
- scim_configuration
- token
properties:
scim_configuration:
$ref: "#/components/schemas/SCIMConfiguration"
scim_bridge:
$ref: "#/components/schemas/SCIMBridge"
description: SCIM bridge, if a connector ID was provided
token:
type: string
description: Plaintext SCIM bearer token (only returned at creation time)
DeleteSCIMConfigurationInput:
type: object
required:
- organization_id
- scim_configuration_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
scim_configuration_id:
$ref: "#/components/schemas/GID"
description: SCIM configuration ID to delete
DeleteSCIMConfigurationOutput:
type: object
required:
- deleted_scim_configuration_id
properties:
deleted_scim_configuration_id:
$ref: "#/components/schemas/GID"
description: Deleted SCIM configuration ID
RegenerateSCIMTokenInput:
type: object
required:
- organization_id
- scim_configuration_id
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
scim_configuration_id:
$ref: "#/components/schemas/GID"
description: SCIM configuration ID
RegenerateSCIMTokenOutput:
type: object
required:
- scim_configuration
- token
properties:
scim_configuration:
$ref: "#/components/schemas/SCIMConfiguration"
token:
type: string
description: New plaintext SCIM bearer token
GetSCIMBridgeInput:
type: object
required:
- id
properties:
id:
$ref: "#/components/schemas/GID"
description: SCIM bridge ID
GetSCIMBridgeOutput:
type: object
required:
- scim_bridge
properties:
scim_bridge:
$ref: "#/components/schemas/SCIMBridge"
UpdateSCIMBridgeInput:
type: object
required:
- organization_id
- scim_bridge_id
- excluded_user_names
properties:
organization_id:
$ref: "#/components/schemas/GID"
description: Organization ID
scim_bridge_id:
$ref: "#/components/schemas/GID"
description: SCIM bridge ID
excluded_user_names:
type: array
items:
type: string
description: User names to exclude from SCIM sync
UpdateSCIMBridgeOutput:
type: object
required:
- scim_bridge
properties:
scim_bridge:
$ref: "#/components/schemas/SCIMBridge"
ListSCIMEventsInput:
type: object
required:
- scim_configuration_id
properties:
scim_configuration_id:
$ref: "#/components/schemas/GID"
description: SCIM configuration ID
order_by:
$ref: "#/components/schemas/SCIMEventOrderBy"
description: SCIM event order by
size:
type: integer
description: Page size
cursor:
$ref: "#/components/schemas/CursorKey"
description: Page cursor
ListSCIMEventsOutput:
type: object
required:
- scim_events
properties:
next_cursor:
$ref: "#/components/schemas/CursorKey"
description: Next cursor
scim_events:
type: array
items:
$ref: "#/components/schemas/SCIMEvent"
tools:
- name: listOrganizations
description: List all organizations the user has access to
@@ -11769,3 +12107,63 @@ tools:
$ref: "#/components/schemas/GetCookieConsentRecordInput"
outputSchema:
$ref: "#/components/schemas/GetCookieConsentRecordOutput"
- name: getSCIMConfiguration
description: Get the SCIM configuration for an organization
hints:
readonly: true
idempotent: true
inputSchema:
$ref: "#/components/schemas/GetSCIMConfigurationInput"
outputSchema:
$ref: "#/components/schemas/GetSCIMConfigurationOutput"
- name: createSCIMConfiguration
description: Create a SCIM configuration for an organization. Optionally provide a connector ID to also create a SCIM bridge.
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/CreateSCIMConfigurationInput"
outputSchema:
$ref: "#/components/schemas/CreateSCIMConfigurationOutput"
- name: deleteSCIMConfiguration
description: Delete a SCIM configuration and its associated bridge
hints:
readonly: false
destructive: true
inputSchema:
$ref: "#/components/schemas/DeleteSCIMConfigurationInput"
outputSchema:
$ref: "#/components/schemas/DeleteSCIMConfigurationOutput"
- name: regenerateSCIMToken
description: Regenerate the bearer token for a SCIM configuration
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/RegenerateSCIMTokenInput"
outputSchema:
$ref: "#/components/schemas/RegenerateSCIMTokenOutput"
- name: getSCIMBridge
description: Get a SCIM bridge by ID
hints:
readonly: true
idempotent: true
inputSchema:
$ref: "#/components/schemas/GetSCIMBridgeInput"
outputSchema:
$ref: "#/components/schemas/GetSCIMBridgeOutput"
- name: updateSCIMBridge
description: Update a SCIM bridge's excluded user names
hints:
readonly: false
inputSchema:
$ref: "#/components/schemas/UpdateSCIMBridgeInput"
outputSchema:
$ref: "#/components/schemas/UpdateSCIMBridgeOutput"
- name: listSCIMEvents
description: List SCIM events for a configuration
hints:
readonly: true
idempotent: true
inputSchema:
$ref: "#/components/schemas/ListSCIMEventsInput"
outputSchema:
$ref: "#/components/schemas/ListSCIMEventsOutput"

View File

@@ -0,0 +1,92 @@
// 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 NewSCIMConfiguration(c *coredata.SCIMConfiguration) *SCIMConfiguration {
var bridgeID *string
if c.BridgeID != nil {
s := c.BridgeID.String()
bridgeID = &s
}
return &SCIMConfiguration{
ID: c.ID,
OrganizationID: c.OrganizationID,
BridgeID: bridgeID,
CreatedAt: c.CreatedAt,
UpdatedAt: c.UpdatedAt,
}
}
func NewSCIMBridge(b *coredata.SCIMBridge) *SCIMBridge {
var connectorID *string
if b.ConnectorID != nil {
s := b.ConnectorID.String()
connectorID = &s
}
return &SCIMBridge{
ID: b.ID,
OrganizationID: b.OrganizationID,
ScimConfigurationID: b.ScimConfigurationID,
ConnectorID: connectorID,
Type: b.Type,
State: b.State,
ExcludedUserNames: b.ExcludedUserNames,
LastSyncedAt: b.LastSyncedAt,
CreatedAt: b.CreatedAt,
UpdatedAt: b.UpdatedAt,
}
}
func NewSCIMEvent(e *coredata.SCIMEvent) *SCIMEvent {
return &SCIMEvent{
ID: e.ID,
OrganizationID: e.OrganizationID,
ScimConfigurationID: e.SCIMConfigurationID,
Method: e.Method,
Path: e.Path,
StatusCode: e.StatusCode,
RequestBody: e.RequestBody,
ResponseBody: e.ResponseBody,
ErrorMessage: e.ErrorMessage,
UserName: e.UserName,
IPAddress: e.IPAddress.String(),
CreatedAt: e.CreatedAt,
}
}
func NewListSCIMEventsOutput(p *page.Page[*coredata.SCIMEvent, coredata.SCIMEventOrderField]) ListSCIMEventsOutput {
events := make([]*SCIMEvent, 0, len(p.Data))
for _, e := range p.Data {
events = append(events, NewSCIMEvent(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 ListSCIMEventsOutput{
NextCursor: nextCursor,
ScimEvents: events,
}
}