Documentation
¶
Overview ¶
Package actioncatalog provides the canonical GitLab action catalog shared by catalog-backed MCP tool surfaces.
The catalog is the intermediate action core between typed GitLab handlers and the public meta and dynamic tool surfaces. It stores executable actions as deterministic groups, preserving route metadata such as input schemas, output schemas, destructive flags, action aliases, tags, icons, descriptions, and formatter hooks.
Action IDs use the stable domain.action form derived from the backing meta-tool name and action name. For example, gitlab_project/create becomes project.create, and gitlab_merge_request/list becomes merge_request.list. Dynamic mode uses these IDs directly, while meta-tools expose the same routes through action dispatch inside domain tools.
This package is not the registry for individual MCP tools. Individual tools are still registered directly by internal/tools.RegisterAll for compatibility. Meta-tools and dynamic tools consume this catalog through adapters such as internal/tools.RegisterMetaCatalog and internal/tools/dynamic.NewRegistryFromCatalog.
Invariants ¶
Catalog construction must be deterministic. Groups preserve explicit action order for user-facing descriptions, cloning avoids mutable alias/schema sharing between surfaces, and validation rejects duplicate action IDs or ambiguous aliases before tools are registered.
Index ¶
- Variables
- func ClassicNeedOf(id string, readOnly bool) finegrained.ClassicScope
- func DomainFromToolName(toolName string) string
- func RewriteSeeAlso(description string, resolve SeeAlsoResolver) string
- func ServedDescription(action Action, resolve SeeAlsoResolver) string
- func ToActionMaps(catalog *Catalog) map[string]toolutil.ActionMap
- type Action
- type ActionID
- type Catalog
- func (c *Catalog) Action(id ActionID) (Action, bool)
- func (c *Catalog) ActionMaps() map[string]toolutil.ActionMap
- func (c *Catalog) Actions() []Action
- func (c *Catalog) AddAction(toolName string, action Action, groupOptions ...GroupOptions) error
- func (c *Catalog) AddGroup(group Group) error
- func (c *Catalog) BindTo(client *gitlabclient.Client) *Catalog
- func (c *Catalog) Clone() *Catalog
- func (c *Catalog) CountActions() int
- func (c *Catalog) CountGroups() int
- func (c *Catalog) ExcludedActionIDs(excludeTools []string) []string
- func (c *Catalog) Filter(opts FilterOptions) *Catalog
- func (c *Catalog) FilterAllowedToolNames(toolNames []string) *Catalog
- func (c *Catalog) FilterExcludedToolNames(excludeTools []string) (filtered *Catalog, unmatched []string)
- func (c *Catalog) FilterExcludedTools(excludeTools []string) *Catalog
- func (c *Catalog) FilterReachableWith(token finegrained.ClassicScope) *Catalog
- func (c *Catalog) FilterReadOnlyActions() *Catalog
- func (c *Catalog) FilterReadOnlyGroups() *Catalog
- func (c *Catalog) Group(toolName string) (Group, bool)
- func (c *Catalog) Groups() []Group
- func (c *Catalog) MarkShared()
- func (c *Catalog) SharedOrigin() *Catalog
- func (c *Catalog) Validate() error
- func (c *Catalog) WithSafeModePreviews() *Catalog
- type CatalogGroupSpec
- type FilterOptions
- type Group
- type GroupOptions
- type ScopeWithheld
- type SeeAlsoResolver
- type SurfaceKind
- type SurfaceToolSpec
Constants ¶
This section is empty.
Variables ¶
var SeeAlsoClause = regexp.MustCompile(`See also: ([a-z0-9_.]+(?:, [a-z0-9_.]+)*)\.`)
SeeAlsoClause matches the cross-reference sentence an individual tool's Description ends with, "See also: gitlab_a, gitlab_b.", and, because the character class admits dots, a clause written in canonical IDs or already projected into a surface's entry IDs ("See also: widget.create, gitlab_widget.get."). The terminating literal dot still matches: the greedy class backtracks one character off the final name. The first submatch is the comma-separated list of names.
It is the one definition of the clause, because two readers have to agree on it and disagreeing fails silently. internal/resources rewrites what it matches into the names of the surface a manifest serves, and cmd/audit_action_ids passes over what it matches when it holds a Description to the rule that served prose names no tool, since that rewrite is what makes an individual tool name right there. With a copy in each, a manifest pattern narrowed later would stop rewriting text the gate still skipped, and tool names would reach the dynamic and meta surfaces verbatim with the gate green.
Functions ¶
func ClassicNeedOf ¶ added in v3.2.0
func ClassicNeedOf(id string, readOnly bool) finegrained.ClassicScope
ClassicNeedOf is Action.ClassicNeed for an action known by its canonical ID and classification alone, which is how a standalone surface tool is held to the same rule as the catalog's actions.
func DomainFromToolName ¶
DomainFromToolName returns the canonical dynamic domain for a meta-tool name.
func RewriteSeeAlso ¶ added in v3.2.0
func RewriteSeeAlso(description string, resolve SeeAlsoResolver) string
RewriteSeeAlso projects the "See also:" clause of an individual-surface description into the active surface's identifier namespace.
The specs hand-write these clauses once, in individual-tool names; on the dynamic and meta surfaces those names are not invocable, and the manifest instructions tell the model to pass entry IDs, so emitting the individual names there contradicts the same document two lines later. The standalone surface tools (the guided flows and project discovery) write theirs in canonical IDs instead, because their description reaches every surface's tools/list and find results verbatim, which this projection never sees. An ID is rewritten here like a tool name is, which only the dynamic manifest does to theirs, where an ID maps to itself: the meta and individual surfaces register those tools beside a catalog that does not carry them, so their manifests list them as direct entries and serve the description, the clause's canonical IDs included, verbatim, and gitlab://tools/{id} resolves those IDs on every surface.
A name the resolver does not know is dropped, not passed through: on one instance the catalog is tier-filtered, so a Free-tier server legitimately cannot resolve a reference to a Premium action, and a name that resolves to nothing on the whole instance is not a reference, it is noise. Stale names cannot hide behind this: the guard test in internal/resources checks the hand-written clauses against the full unfiltered catalog, where only a genuinely wrong name fails. A clause left empty is removed whole, and the spaces and line breaks it leaves at the end of the text with it.
func ServedDescription ¶ added in v3.2.0
func ServedDescription(action Action, resolve SeeAlsoResolver) string
ServedDescription is the description gitlab://tools serves for an action on a surface whose "See also:" names resolve through resolve: the individual tool's Description with its clause rewritten by RewriteSeeAlso, or, for an action with no Description, its Usage line verbatim.
It is the one definition of that text, because two readers have to agree on it: internal/resources serves it, and cmd/gen_tool_reference publishes it on the site as the description the dynamic surface serves. A copy in the generator had already come to rewrite a Usage line the server serves as it is, with nothing to notice, since its gate compares the generator with its own output.
Types ¶
type Action ¶
type Action struct {
ID ActionID
ToolName string
Domain string
Name string
Route toolutil.ActionRoute
SchemaURI string
Aliases []string
Tags []string
Usage string
RelatedActions []string
Compatibility toolutil.CompatibilityPolicy
ReadOnly bool
Edition string
GitLabDotComOnly bool
OwnerPackage string
IndividualTool toolutil.IndividualToolSpec
ContentKind string
NotFoundPolicy string
EmbeddedResourcePolicy string
EmbeddedResource string
RichResultPolicy string
SchemaValidationNotes []string
RuntimeValidationNotes []string
SpecBacked bool
Destructive bool
Idempotent bool
OpenWorld bool
// FineGrained is what a fine-grained personal access token needs for the
// action, read from the generated table by canonical ID when the action
// joins a catalog; nil when the table holds no row for it. It points into
// the one table, so every catalog shares it.
FineGrained *finegrained.Requirement
}
Action describes one executable GitLab action in the canonical catalog.
func ActionsFromSpecs ¶
func ActionsFromSpecs(specs []toolutil.ActionSpec) ([]Action, error)
ActionsFromSpecs projects canonical action specs into catalog actions.
func (Action) ClassicNeed ¶ added in v3.2.0
func (a Action) ClassicNeed() finegrained.ClassicScope
ClassicNeed is the classic scope the action needs: what the generated table derived from the requests it sends (finegrained.Requirement.Classic), or, for an action the table holds no known scope for, the one its read or write classification gives, read_api for a read and api for anything else. R-GRANT fails the build on an action with no row, so the fallback is what an action added since the last generation is judged by until the table is regenerated.
type ActionID ¶
type ActionID string
ActionID is the stable dynamic identifier for one GitLab action.
type Catalog ¶
type Catalog struct {
// contains filtered or unexported fields
}
Catalog stores deterministic groups and action lookup indexes. A Catalog is intended to be mutated during single-threaded initialization and then shared read-only; concurrent mutation is not supported.
What a catalog shares ¶
Every accessor returns copies of the Group and Action values, so a caller may set fields and append to slices without reaching the catalog. What the copies do not include is the route metadata: the InputSchema, OutputSchema and ParameterGuidance maps of every route are frozen and shared, between the copies, the catalog, and every other catalog derived from it by filtering or binding. The accessors used to deep-copy them, and in the HTTP pool, where a catalog is consulted several times for each of a thousand credentials, those copies were half of the process's heap. A consumer that must change a schema derives its own through toolutil.DeriveSchema.
Shared and bound catalogs ¶
A catalog built once per configuration and cached for the process is marked with Catalog.MarkShared; its handlers are bound to no credential. Each server gets Catalog.BindTo of it: the same groups and actions, with the handlers rebuilt for that server's client. A bound catalog remembers the shared one it came from as its origin, and consumers that build something client-independent from a catalog (the dynamic search index, the tool manifest, the telemetry identifier) key it on Catalog.SharedOrigin so every server of one configuration shares one.
func FromActionMaps ¶
FromActionMaps converts legacy route maps into a canonical catalog.
func FromActionMapsWithError ¶
FromActionMapsWithError converts legacy route maps into a canonical catalog and reports invalid groups instead of panicking.
func (*Catalog) Action ¶
Action returns a copy of one action by canonical ID, sharing the route schemas as described on Catalog.
func (*Catalog) ActionMaps ¶
ActionMaps returns a legacy route snapshot keyed by tool and action. The maps are the caller's; the routes share their schemas with the catalog.
func (*Catalog) Actions ¶
Actions returns copies of all actions sorted by canonical ID, sharing the route schemas as described on Catalog.
func (*Catalog) AddAction ¶
func (c *Catalog) AddAction(toolName string, action Action, groupOptions ...GroupOptions) error
AddAction adds one action to an existing or newly-created group. When the group does not exist, callers may provide GroupOptions so the synthesized group carries the same metadata as a normal catalog group.
func (*Catalog) BindTo ¶
func (c *Catalog) BindTo(client *gitlabclient.Client) *Catalog
BindTo returns this catalog with every handler rebuilt for client, through toolutil.ActionRoute.BindTo. The groups, the actions and the route metadata are the same; only the handlers are new, which is the whole per-credential cost of a surface built from a shared catalog. The result remembers this catalog's Catalog.SharedOrigin as its own.
func (*Catalog) Clone ¶
Clone returns a copy of the catalog that may be added to and filtered without reaching the original, sharing the route schemas as described on Catalog. The copy is a new catalog with no shared origin: what is added to it afterwards makes it a different action set.
func (*Catalog) CountActions ¶
CountActions returns the number of actions in the catalog.
func (*Catalog) CountGroups ¶
CountGroups returns the number of groups in the catalog.
func (*Catalog) ExcludedActionIDs ¶
ExcludedActionIDs returns the canonical action IDs excludeTools removes from this catalog, sorted.
The tool surface is not the only request path to a GitLab object: a resource template returns the same data through the same credential, and its registration knows nothing about tool names. Resolving the operator's entries — group name, individual tool name or action ID alike — to canonical IDs here is what lets that other surface apply the same narrowing from one table keyed by one kind of name, instead of repeating this matcher.
func (*Catalog) Filter ¶
func (c *Catalog) Filter(opts FilterOptions) *Catalog
Filter applies all catalog-level filters in a deterministic order.
Its exclusion step is Catalog.FilterExcludedTools, whose warning is computed against this catalog alone, so it is not how the server applies or reports --exclude-tools; see that method for why. The server calls tools.ExcludeFromCatalog instead.
func (*Catalog) FilterAllowedToolNames ¶
FilterAllowedToolNames returns a cloned catalog with only explicitly allowed tools.
func (*Catalog) FilterExcludedToolNames ¶
func (c *Catalog) FilterExcludedToolNames(excludeTools []string) (filtered *Catalog, unmatched []string)
FilterExcludedToolNames returns a cloned catalog without the tools and actions named by excludeTools, together with the entries that matched nothing in this catalog.
An entry is matched against three names, because three names reach the same action depending on which surface is registered:
- a group's meta-tool name ("gitlab_issue"), which removes the group and every action in it;
- an action's individual tool name ("gitlab_issue_delete"), which removes that one action;
- the canonical action ID ("issue.delete"), which the dynamic surface takes directly, likewise.
Only the group name matched before. The default surface is dynamic, where the two visible tools reach every action by canonical ID, so an operator who hardened a deployment with EXCLUDE_TOOLS=gitlab_issue_delete got a server that still executed it and a startup log that said nothing was excluded.
Aliases are deliberately not matched. Alias resolution is fuzzy by design, and an exclusion that removes more than the operator named is a different defect rather than a fix for this one.
A group whose every action was excluded is dropped: a dispatcher that can only refuse is worse than no tool at all. A group that was already empty is left alone so catalog validation can still report it.
func (*Catalog) FilterExcludedTools ¶
FilterExcludedTools returns a cloned catalog without the tools and actions named by excludeTools.
Entries that name nothing in this catalog are logged at WARN rather than refused: one configuration is routinely reused across Free, Premium and Ultimate instances, and a lower-tier catalog legitimately lacks tools the same file names. The warning is deliberately operator-facing only: the excluded actions must stay out of the client-facing withheld list, or the dynamic registry would name back the very actions the operator removed.
It is not how the server reports a deployment's exclusions. The standalone utilities (gitlab_discover_project, the gitlab_interactive_* flows) are in none of the catalogs the surfaces filter, yet an exclusion removes them on every surface, so a warning computed here names an entry that works. The server calls Catalog.FilterExcludedToolNames instead, from tools.ExcludeFromCatalog, and reports only what neither the catalog nor a standalone utility answers.
func (*Catalog) FilterReachableWith ¶ added in v3.2.0
func (c *Catalog) FilterReachableWith(token finegrained.ClassicScope) *Catalog
FilterReachableWith returns a catalog of the actions a token whose strongest classic scope is token reaches (Action.ClassicNeed), with the groups left empty dropped. A kept group is read-only only when every action it keeps is, since a token carrying read_api reaches some actions that write (one that writes a file on the server's machine, one GitLab authenticates by another credential the caller passes) and the meta tool's annotation must not claim otherwise.
func (*Catalog) FilterReadOnlyActions ¶
FilterReadOnlyActions returns a cloned catalog containing only read-only actions, filtered at action granularity rather than group granularity.
A group that mixes reads and writes keeps its read-only actions instead of disappearing entirely, which is what read-only mode promises: mutating operations are removed, reads keep working. Groups left without a single read-only action are dropped, and every surviving group is marked ReadOnly so derived tool annotations agree with the actions the group actually exposes (otherwise read-only tool pruning would remove the very tools this filter just built).
func (*Catalog) FilterReadOnlyGroups ¶
FilterReadOnlyGroups returns a cloned catalog containing only read-only groups.
func (*Catalog) Group ¶
Group returns a copy of one group by tool name, sharing the route schemas as described on Catalog.
func (*Catalog) Groups ¶
Groups returns copies of all groups sorted by tool name, sharing the route schemas as described on Catalog.
func (*Catalog) MarkShared ¶
func (c *Catalog) MarkShared()
MarkShared records that this catalog is the process-lived copy of its configuration and registers every route schema in it with toolutil.ShareSchema, so the transforms every server derives from them are built once. Call it only from a cache that keeps the catalog for the rest of the process: a registered schema is never collected, and a catalog's identity may key other caches from then on.
func (*Catalog) SharedOrigin ¶
SharedOrigin returns the shared catalog this one is, or was bound from, and nil for a catalog that is neither. It is the identity to key a client-independent derivation on: every server of one configuration binds the same shared catalog, and a catalog nobody shared has no identity worth caching under.
func (*Catalog) Validate ¶
Validate verifies that the catalog has a consistent, executable action index.
func (*Catalog) WithSafeModePreviews ¶
WithSafeModePreviews returns a cloned catalog in which every mutating action is replaced by a handler returning a toolutil.SafeModePreview naming that action, leaving read-only actions untouched.
Safe Mode is a per-action policy, but dispatcher surfaces (meta-tools and the dynamic execute tool) expose one tool covering many actions, so intercepting at the tool level would block that tool's reads too. Rewriting the catalog instead keeps the interception exactly as granular as the policy: reads execute, writes preview, and the preview names the action rather than the dispatcher. Destructive is cleared on rewritten actions because nothing is executed, so there is nothing to confirm.
type CatalogGroupSpec ¶
type CatalogGroupSpec struct {
ToolName string
Title string
Description string
ReadOnly bool
Icons []mcp.Icon
BaseDomain string
EnterpriseOnly bool
GitLabDotComOnly bool
CapabilityRequirements []string
FormatResult toolutil.FormatResultFunc
Actions []toolutil.ActionSpec
OwnerPackage string
SurfaceKind SurfaceKind
}
CatalogGroupSpec is the canonical metadata contract for one catalog group.
func CloneCatalogGroupSpec ¶
func CloneCatalogGroupSpec(spec CatalogGroupSpec) CatalogGroupSpec
CloneCatalogGroupSpec returns a defensive copy of group metadata.
func (CatalogGroupSpec) GroupOptions ¶
func (spec CatalogGroupSpec) GroupOptions() GroupOptions
GroupOptions returns catalog group options projected from the group spec.
func (CatalogGroupSpec) Validate ¶
func (spec CatalogGroupSpec) Validate() error
Validate verifies group-level catalog invariants before runtime projection.
type FilterOptions ¶
FilterOptions describes catalog-level filtering inputs.
type Group ¶
type Group struct {
ToolName string
Title string
Description string
Icons []mcp.Icon
ReadOnly bool
FormatResult toolutil.FormatResultFunc
BaseDomain string
EnterpriseOnly bool
GitLabDotComOnly bool
CapabilityRequirements []string
OwnerPackage string
SurfaceKind SurfaceKind
Actions map[string]Action
ActionOrder []string
}
Group describes all actions exposed through one logical meta-tool group.
func GroupFromSpecs ¶
func GroupFromSpecs(opts GroupOptions, specs []toolutil.ActionSpec) (Group, error)
GroupFromSpecs builds a catalog group from canonical action specs.
func NewGroup ¶
func NewGroup(opts GroupOptions) Group
NewGroup creates an action group with initialized maps.
func (*Group) ActionMap ¶
ActionMap returns a legacy route map for this group. The map is the caller's; the routes in it share their schemas with the group, as every accessor of this package does (see Catalog).
func (*Group) ActionsInOrder ¶
ActionsInOrder returns group actions in deterministic action-name order.
type GroupOptions ¶
type GroupOptions struct {
ToolName string
Title string
Description string
Icons []mcp.Icon
ReadOnly bool
FormatResult toolutil.FormatResultFunc
BaseDomain string
EnterpriseOnly bool
GitLabDotComOnly bool
CapabilityRequirements []string
OwnerPackage string
SurfaceKind SurfaceKind
}
GroupOptions contains metadata for creating a catalog group.
type ScopeWithheld ¶ added in v3.2.0
type ScopeWithheld struct {
ID string
// ByGitLab is the classic API scope GitLab requires for what the action
// sends that the credential lacks: api, or nothing.
ByGitLab []string
// ByServer are the scopes the action's catalog group demands before this
// server serves it that the credential lacks: admin_mode, or nothing.
ByServer []string
}
ScopeWithheld is one action key a credential's scopes withheld, with the scopes it would need and does not carry, kept apart by who asks for them: GitLab, which requires a classic API scope for what the action sends, and this server, which serves a catalog group that demands a scope (admin_mode) only to a credential carrying it. The answer to a withheld action words the two apart, because only the first is GitLab's rule: GitLab serves some actions of an admin_mode group to any authenticated token, and an instance with Admin Mode off asks admin_mode of none. The key is a canonical action ID or an alias that resolved to one, as a caller may ask for either.
func (ScopeWithheld) Missing ¶ added in v3.2.0
func (w ScopeWithheld) Missing() []string
Missing is every scope the credential lacks for the action, sorted and each once, which is what a caller reauthorizes with.
type SeeAlsoResolver ¶ added in v3.2.0
SeeAlsoResolver maps a name a "See also:" clause spells, an individual-surface tool name or a canonical action ID, to the identifier the active surface's entries are invoked by, reporting whether the name is known. A nil resolver leaves descriptions untouched (the individual surface, whose namespace a domain action's clause already uses, and where a canonical ID resolves as it does on every surface).
type SurfaceKind ¶
type SurfaceKind string
SurfaceKind classifies the runtime surface represented by a catalog group.
const ( // SurfaceKindGitLabAction identifies ordinary GitLab API actions. SurfaceKindGitLabAction SurfaceKind = "gitlab-action" // SurfaceKindMetaGroup identifies visible domain meta-tool dispatchers. SurfaceKindMetaGroup SurfaceKind = "meta-group" // SurfaceKindDynamicController identifies Dynamic controller tools. SurfaceKindDynamicController SurfaceKind = "dynamic-controller" // SurfaceKindRuntimeUtility identifies non-GitLab runtime helper tools. SurfaceKindRuntimeUtility SurfaceKind = "runtime-utility" // SurfaceKindInteractiveUtility identifies tools that require MCP elicitation. SurfaceKindInteractiveUtility SurfaceKind = "interactive-utility" )
type SurfaceToolSpec ¶
type SurfaceToolSpec struct {
Name string
Title string
// Description is this one action's own description, the text the tool
// serves for it on the surfaces that register it as a tool.
Description string
// Usage is the action's Usage line, which the dynamic surface serves in
// its find and describe results.
//
// It is its own field, projected as the Usage, rather than the Description
// served again under that name. A standalone surface tool is registered
// on meta and on individual alike and runs through the dynamic surface
// too, so its text is served on all three, and cmd/audit_action_ids holds
// a Usage line to that whole, folding one assembled at run time. It holds
// a Description only where the Description is a constant: its dotted IDs
// to the published-ID rule, and its tool names to the served-prose rule
// everywhere but its "See also" clause. A Description assembled at run
// time, as the flows' is from the usage handed in, is read by neither.
// Projecting the Description as the Usage put the flows' descriptions on
// every surface while the rule read them nowhere, and every one of them
// named a tool two surfaces lack. The standalone specs write their text
// once, as the Usage and as the Description both.
//
// A spec without one projects an action without one. The dynamic
// surface's two controllers are the only such specs, and they are
// registered as tools of their own and never projected into a catalog.
Usage string
// GroupDescription is what the group tool this action belongs to says
// about itself, which is not the same sentence and must not be derived
// from one action's.
//
// It is carried on every spec of the group and read from whichever one
// happens to be seen first, because a group is assembled from its actions
// and has no record of its own. Before this field existed, the group's
// description was taken from that first action instead: `gitlab_interactive`
// introduced itself with 1650 characters about creating an issue, while
// also creating merge requests, projects and releases.
GroupDescription string
GroupToolName string
BaseDomain string
ActionName string
SurfaceKind SurfaceKind
Route toolutil.ActionRoute
Aliases []string
Tags []string
RelatedActions []string
Compatibility toolutil.CompatibilityPolicy
Icons []mcp.Icon
CapabilityRequirements []string
FormatResult toolutil.FormatResultFunc
SafeModePolicy string
ReadOnlyPolicy string
OwnerPackage string
ReadOnly bool
Destructive bool
Idempotent bool
OpenWorld bool
}
SurfaceToolSpec is the canonical metadata contract for visible MCP tools that are not ordinary GitLab API meta-tool groups.
func CloneSurfaceToolSpec ¶
func CloneSurfaceToolSpec(spec SurfaceToolSpec) SurfaceToolSpec
CloneSurfaceToolSpec returns a defensive copy of surface tool metadata.
func (SurfaceToolSpec) ActionSpec ¶
func (spec SurfaceToolSpec) ActionSpec() (toolutil.ActionSpec, error)
ActionSpec projects a surface tool spec into the shared ActionSpec model.
func (SurfaceToolSpec) Validate ¶
func (spec SurfaceToolSpec) Validate() error
Validate verifies that the surface spec has enough metadata for runtime registration and catalog projection.