internal/mcptools/register.go
Rama main · réplica de lectura del repositorio original.
package mcptools
import (
"context"
"xprem/config"
"xprem/internal/services"
"xprem/internal/types"
mcpprot "github.com/modelcontextprotocol/go-sdk/mcp"
)
// Fallback is what gates a tool when roles are not enforced; the MIT twin of
// the rbac fallback the composition root maps it to.
type Fallback int
const (
FallbackAdminOnly Fallback = iota + 1
FallbackAnyMember
)
// Access declares who may see and call a gated tool: the rbac permission once
// roles are enforced, the fallback until then. An empty Perm means no grant
// can ever carry it, so the fallback alone decides: {Fallback:
// FallbackAdminOnly} reads as an admin-only tool.
type Access struct {
Perm string
Fallback Fallback
}
// AppPermissions is the whole authorization truth of one account on one app:
// every catalog permission, held or not.
type AppPermissions struct {
AppID string `json:"appId"`
Granted []string `json:"granted"`
Denied []string `json:"denied"`
}
// AccountPermissions is the account's full permission picture, one entry per
// app it can see; the MIT twin of the rbac description wire maps onto it.
type AccountPermissions struct {
Role string `json:"role" jsonschema:"admin or member; admins hold every permission on every app"`
RolesEnforced bool `json:"rolesEnforced" jsonschema:"whether per-app roles are enforced; when false, the community defaults decide"`
Apps []AppPermissions `json:"apps" jsonschema:"one entry per app this account can see; apps not listed are invisible to it"`
}
// The data interfaces below are consumer-side slices of the MIT services;
// wire injects the services as-is.
// AppVisibilityFunc is the visibility decision the ee rbac service provides:
// restricted=false means every app is visible.
type AppVisibilityFunc func(ctx context.Context, principal *services.DashboardPrincipal) (restricted bool, visible map[string]bool, err error)
type AppLister interface {
GetApps(ctx context.Context) ([]config.AppDescriptor, error)
}
type BranchLister interface {
GetBranches(ctx context.Context, appId string) ([]types.BranchMapping, error)
GetRuntimeVersionsWithUpdateStats(ctx context.Context, appId string, branchName string) ([]types.RuntimeVersionWithStats, error)
}
type ChannelLister interface {
GetChannels(ctx context.Context, appId string) ([]types.ChannelMapping, error)
}
type UpdateFeedReader interface {
GetUpdateFeed(ctx context.Context, appId string, query types.UpdateFeedQuery) ([]types.UpdateFeedItem, error)
}
type UpdateRolloutReader interface {
GetUpdateRollout(ctx context.Context, appId string, branchName string, runtimeVersion string) ([]types.RolloutUpdate, error)
}
type BundlePatchReader interface {
ListPatches(ctx context.Context, appId, branch, updateId string) ([]types.BundlePatch, error)
}
type CertificateReader interface {
RetrieveAppCertificate(ctx context.Context, appId string) (string, error)
}
type BranchWriter interface {
CreateBranch(ctx context.Context, appId string, branchName string) (int64, error)
DeleteBranch(ctx context.Context, branchName string, appId string) error
}
type ChannelWriter interface {
CreateChannel(ctx context.Context, appId string, branchName *string, channelName string) (int64, error)
DeleteChannel(ctx context.Context, channelName string, appId string) error
}
type DeploymentWriter interface {
CreateRollback(ctx context.Context, appId string, platform types.Platform, commitHash, runtimeVersion, branchName, message string) (*types.Update, error)
RepublishUpdateByID(ctx context.Context, appId, branchName, runtimeVersion, updateId string) (*types.Update, error)
RepublishPublishGroup(ctx context.Context, appId, branchName, runtimeVersion, publishGroup string) (*services.GroupOperationResult, error)
}
// Deps carries what tools need from the composition root: MIT data access
// injected as-is, and decision functions whose implementations live in ee.
// Every field is a plain method value; wire assembles the struct without a
// line of logic, and tools compose data with decisions.
type Deps struct {
// Apps is the app store; visibility is decided by VisibleApps, never here.
Apps AppLister
Branches BranchLister
Channels ChannelLister
UpdateFeed UpdateFeedReader
UpdateRollouts UpdateRolloutReader
BundlePatches BundlePatchReader
Certificates CertificateReader
BranchWriter BranchWriter
ChannelWriter ChannelWriter
Deployments DeploymentWriter
// SSOEnabled reports whether enterprise SSO is active; get_server_config
// surfaces it.
SSOEnabled func(ctx context.Context) bool
// VisibleApps is the visibility decision: restricted=false means every
// app is visible to the principal.
VisibleApps AppVisibilityFunc
// CanUseSomewhere decides tool visibility at session creation: whether
// the principal may use an access-gated tool on at least one app.
CanUseSomewhere func(ctx context.Context, principal *services.DashboardPrincipal, access Access) bool
// Authorize gates one tool execution on one app, with the same semantics
// as a route guard.
Authorize func(ctx context.Context, principal *services.DashboardPrincipal, appID string, access Access) error
// DescribePermissions answers whoami: the account's full permission
// picture over the given apps, granted and denied.
DescribePermissions func(ctx context.Context, principal *services.DashboardPrincipal, appIDs []string) (AccountPermissions, error)
}
// registrations is the tool table: one line per tool, the MCP twin of a
// routes_*.go file. access nil means any authenticated account.
var registrations = []struct {
register func(*mcpprot.Server, Deps)
access *Access
}{
{register: registerWhoami},
{register: registerGetApps},
{register: registerGetBranches},
{register: registerGetRuntimeVersions},
{register: registerGetChannels},
{register: registerGetUpdates},
{register: registerGetChannelRollouts},
{register: registerGetUpdateRollout},
{register: registerGetUpdatePatches},
{register: registerGetCertificate, access: &certificateAccess},
{register: registerGetServerConfig},
{register: registerCreateBranch, access: &branchCreateAccess},
{register: registerDeleteBranch, access: &branchDeleteAccess},
{register: registerCreateChannel, access: &channelCreateAccess},
{register: registerDeleteChannel, access: &channelDeleteAccess},
{register: registerRollback, access: &publishAccess},
{register: registerRepublish, access: &publishAccess},
}
// DeclaredPermissions lists the permission strings the tool table gates on.
// This package is MIT and cannot import the ee catalog those strings belong
// to, so ee validates them against it at boot (see rbac.MustValidateMCPTools):
// a typo here would silently make a tool admin-only instead of failing.
func DeclaredPermissions() []string {
perms := make([]string, 0, len(registrations))
for _, registration := range registrations {
if registration.access != nil && registration.access.Perm != "" {
perms = append(perms, registration.access.Perm)
}
}
return perms
}
// Configurator returns what NewMCPService needs: a function that populates
// one session's server with the tools its principal may use. Gated tools are
// simply absent from a session that may not use them anywhere; execution
// still re-checks the specific app through Deps.Authorize.
func Configurator(deps Deps) func(ctx context.Context, principal *services.DashboardPrincipal, server *mcpprot.Server) {
return func(ctx context.Context, principal *services.DashboardPrincipal, server *mcpprot.Server) {
canUse := MemoizeVisibility(deps.CanUseSomewhere)
for _, registration := range registrations {
if registration.access != nil && !canUse(ctx, principal, *registration.access) {
continue
}
registration.register(server, deps)
}
}
}
// MemoizeVisibility answers each distinct access once per session build.
// Several tools share a permission, and the decision behind it reads the
// account's grants every time; the principal cannot change mid-build.
func MemoizeVisibility(canUse func(context.Context, *services.DashboardPrincipal, Access) bool) func(context.Context, *services.DashboardPrincipal, Access) bool {
answered := map[Access]bool{}
return func(ctx context.Context, principal *services.DashboardPrincipal, access Access) bool {
if allowed, known := answered[access]; known {
return allowed
}
allowed := canUse(ctx, principal, access)
answered[access] = allowed
return allowed
}
}