Skip to content

Toolsets Reference

A toolset is the selector list an agent declares to say which of the gateway's tools it is composed with. muster evaluates it per request, statelessly, as the third filter on the caller's catalogue — after server-authentication visibility and workflow availability — so list_tools, filter_tools, describe_tool, list_core_tools, the resource and prompt accessors and call_tool all answer within it.

A toolset never widens access. The catalogue it filters is already the caller's own: a tool from a server the session has not signed in to is not in the catalogue, so a selector naming it matches nothing. The invoking human's identity and the backends' own authorization remain the boundary; a toolset bounds what the model can discover and call.

Declaring a toolset

The X-muster-Toolset request header

X-muster-Toolset: preset:read-only, workflow:incident-triage

A comma-separated list of inline selectors; whitespace around each selector is trimmed. The header is read on every request. Nothing is bound to the MCP session: two requests on the same session with different headers get different catalogues, and a request without the header is unscoped — exactly today's behaviour. muster's session for a forwarded token is derived from the token, so all the agents one person drives share one session; per-request evaluation is what keeps agent A's toolset from applying to agent B.

The platform's Generic agent chart renders this header from its toolset value onto the agent's muster tool entry (headersFrom), so an agent declares its toolset once, on its own release.

The toolset bound to a Session

A request that authenticates with Substrate's actor JWT (the actor bearer) is served as the person its Session binding names, and within the toolset that binding carries: the toolset the component starting the Session sent with PUT /session-bindings, the agent's toolset at the Session's start. It applies to every such request, with or without the header:

  • without X-muster-Toolset, the request resolves within the bound toolset;
  • with it, the header resolves within the bound toolset: it narrows it and never widens it, and a header asking for more gets the bound toolset and no error;
  • a binding without a toolset bounds nothing: the request is scoped by its header alone, as for a request that authenticates as the person;
  • preset:none resolves to nothing; an unknown preset in a binding fails every meta-tool call of the Session with the error naming it, as in a header.

A refusal of a name outside the bound toolset names it, for example tool "x_k8s_delete" is outside the toolset [preset:read-only] bound to the session; filter_tools echoes it as toolset_bound, and the actor bearer's audit line carries it (toolset=[...]). When a header and the bound toolset differ, muster logs toolset header resolved within the bound toolset with both. A request that authenticates with a person's bearer is never bound.

Inline selector grammar

Selector Selects
preset:<name> A preset from toolsetPresets or a built-in (below).
server:<name> Every tool of the MCPServer <name>. For a family, the family name selects the family surface on every member; a member's name selects it on that member only, so a call naming another member with the instance argument is refused.
workflow:<name> The workflow's execution tool (workflow_<name>).
tool:<name> One tool by its exposed name (x_<server>_<tool>, workflow_<name>, core_*).

Names are exact and case-sensitive (regex ^(preset|server|workflow|tool):[^\s,]+$). At most 32 selectors inline — larger selections are presets. Core tools (core_*) are selectable by name (tool:core_workflow_list inline, pattern: core_* in a preset) and through their annotations (the built-in read-only preset includes the read-only core tools). There is no core pseudo-server.

Rejected inline, with an error naming the toolset and the selector:

Input Error
empty header / empty list toolset [] is empty; use "preset:none" for an agent without tools
toolset:<name> … selector "toolset:x" is reserved for shared toolsets
label:<k>=<v> … selector "label:…" is allowed inside presets only
anything else … selector "…" is malformed; expected preset:<name>, server:<name>, workflow:<name> or tool:<name>
more than 32 toolset [...] has 33 selectors, more than the 32 allowed inline; define a preset
unknown preset toolset [preset:foo] names unknown preset "foo"; known presets: read-only, none, full, …

These are error results on every meta-tool call — including get_resource and get_prompt — never a silent fall-back to the unscoped catalogue and never silent degradation to the selectors that did parse. They are not sticky: the next request without the header is unscoped again.

Presets

Presets are muster configuration (toolsetPresets: in config.yaml; chart value muster.toolsetPresets, rendered into the ConfigMap). They are GitOps-managed by construction and evaluated against the live catalogue on each request, so a preset selecting by server or by annotation stays correct when a server adds tools.

toolsetPresets:
  infrastructure:
    description: Every infrastructure server, without deletes
    include:
      - server: mcp-kubernetes
      - server: mcp-prometheus
    exclude:
      - pattern: "*_delete"
  safe-ops:
    description: The infrastructure preset without its writes, plus one workflow
    include:
      - preset: infrastructure
      - workflow: incident-triage
    exclude:
      - pattern: "*_apply"
      - pattern: "*_patch"

Each preset has an optional description (shown by filter_tools with include_presets), an include list and an optional exclude list. A preset resolves to the union of its includes minus its excludes. include rules are a union, never an intersection: a preset that includes preset: infrastructure and readOnly: true resolves to every infrastructure tool plus every read-only tool in the catalogue, not to the infrastructure's read-only tools. To narrow a server set, include it and exclude the tools you do not want by pattern: or tool:, as safe-ops does above (readOnly: false is not a rule; muster rejects it at startup). Every rule sets exactly one key:

Rule Selects
tool: <name> One tool by exposed name.
pattern: <glob> Tools whose exposed name matches the glob (core_*, x_mcp-kubernetes_*; Go path.Match syntax).
server: <name> Every tool of that server (family name for every member, member name for that member alone).
workflow: <name> The workflow's execution tool.
readOnly: true Every tool annotated readOnlyHint: true, including workflows carrying the derived hint (below).
preset: <name> Composition: everything another preset selects. include only.
label: <key>=<value> / label: <key> Tools of every MCPServer whose resource carries that label — metadata.labels in both storage modes, read live on each request, so a server gaining or losing the label changes the resolution on the next request without a restart. <key>=<value> matches the value, <key> alone matches presence, <key>= matches an empty value. A family tool joins on the members providing it that carry the label. Presets only — inline label: is rejected.

The two platform presets are defined by the tool-group label every platform-shipped MCPServer carries (agent-platform.giantswarm.io/tool-group: infrastructure | agent-platform; see the fleet chart), so a new manager or a fourth infrastructure family joins its preset with no values change:

toolsetPresets:
  infrastructure:
    description: The servers for the infrastructure underneath the platform
    include:
      - label: agent-platform.giantswarm.io/tool-group=infrastructure
  agent-platform:
    description: The platform's own management surface, including muster's core tools
    include:
      - label: agent-platform.giantswarm.io/tool-group=agent-platform
      - pattern: core_*

Built-in presets

Three presets are built in and cannot be redefined — configuration that tries fails muster's startup naming the preset:

Preset Resolves to
read-only Every tool its server annotates readOnlyHint: true — muster's own read-only core tools included, see Core tool annotations — plus every workflow whose step tools are all read-only.
none Nothing. A preset:none header hides and refuses every tool; the Generic chart omits the muster tool entry altogether for an agent whose toolset is exactly ["preset:none"].
full The whole catalogue, core tools included (pattern: "*").

The platform charts ship infrastructure and agent-platform as values (by the agent-platform.giantswarm.io/tool-group label); installations add their own the same way.

Validation at startup

muster serve refuses to start when toolsetPresets redefines a built-in preset, when a rule sets no key or more than one, when a pattern does not compile, when readOnly is false, when exclude composes a preset, when a composition names an unknown preset, or when compositions cycle. Every error names the preset and the rule position.

Workflow read-only derivation

A workflow is read-only when every tool its steps reference — conditions, forEach (nested loops included) and parallel sub-steps and onFailure handlers included, nested workflows followed — resolves in the caller's catalogue to a tool annotated read-only — a downstream server's tool or a core tool by its declared annotation. A step calling a core tool that writes (core_workflow_delete, core_service_stop, …), an unknown tool, or a cycle makes the workflow not read-only. The derived hint fills the workflow tool's readOnlyHint annotation slot, so describe_tool shows it and preset:read-only includes the pure-query workflows.

What the meta-tools do with a toolset

  • list_tools, filter_tools, describe_tool, list_core_tools read the filtered catalogue. list_tools pages it (limit, default 50, and offset; total is the size of the filtered catalogue and truncated says whether more pages exist), so a 450-tool preset:read-only is nine pages of summaries rather than one 400 KB answer; preset:none is total: 0 and no tools. Like filter_tools, it echoes the header's selectors in toolset. describe_tool of a tool the session can see but the toolset excludes answers tool "<name>" is outside the toolset [<selectors>]; an unknown name is still Tool not found. A family tool is shown with the members the toolset selects it on alone, in its instance argument's enum and its description.
  • call_tool — including workflow execution (workflow_<name>) — of a name outside the toolset is refused with tool "<name>" is outside the toolset [<selectors>] and logged once at info level with the tool, the toolset and the session. A family tool runs on the member its instance argument selects, and the toolset is checked on that member: a call naming a member the toolset does not select the tool on is refused with tool "<name>" on server "<member>" is outside the toolset [<selectors>]. Rules are evaluated per member, so a preset excluding server: <member> keeps the tool on the other members. The tools a workflow's steps call internally are the workflow author's composition and are not re-checked. A tool of a server the session has not signed in to is not in the catalogue, so no selector resolves to it; when the toolset could select it once listed, the call answers auth_required with the sign-in link instead of refusing the name, and runs nothing. The server is the one the call would be routed to: in a family's name space, the member the instance argument selects; elsewhere, the server whose tool prefix is the longest the name carries. The toolset could select it when a tool:<name> or server:<name> selector names it (server: also takes the family name), or a preset's rules could: tool, pattern, server and label rules are decided on the name, the server and its labels, and readOnly, whose annotation is not known before sign-in, counts as a possible include and never as an exclude. When a stored grant connects the session instead, the call is checked against the tool as listed, with its annotations, like any other.
  • Resources and prompts follow the servers: a server is inside the toolset when at least one of its tools is selected. list_resources, filter_resources, describe_resource, list_prompts, filter_prompts, describe_prompt hide the others; get_resource and get_prompt refuse them (resource "<uri>" is outside the toolset […]).
  • servers_requiring_auth (list_tools; filter_tools for the servers its pattern could match) is neither narrowed by the toolset nor paged: it tells the caller which sign-in would make more of the toolset resolve.
  • tools/list — the meta-tools themselves — is unchanged.

filter_tools and toolsets

filter_tools accepts toolset (an array of inline selectors) and include_presets (bool):

{"name": "filter_tools", "arguments": {"toolset": ["preset:read-only", "server:pro"], "include_presets": true}}

The response carries the tools the toolset resolves to for the caller, toolset (the selectors as given), toolset_unmatched (the selectors that selected nothing for the caller — for example a server the caller has not signed in to; preset:none is never reported), toolset_requiring_auth (the servers awaiting the caller's sign-in some of whose tools the toolset could select once listed, by the same rules as call_tool, sorted by name, each with name, auth_tool, tool_prefix and, for a family member, family: the part of the toolset a sign-in would unlock) and presets ([{name, description, built_in}], built-ins first). When the request also carries X-muster-Toolset or a bound toolset, the argument resolves within them and never widens them. Argument errors use the header's texts.

toolset names the toolset the returned tools were resolved within, whichever way it was declared. Without a toolset argument, a request carrying X-muster-Toolset gets the header's selectors echoed (as declared, e.g. ["preset:read-only"]), with toolset_unmatched computed against them — the one place an agent whose toolset is set by its deployment can learn what bounds it, and how an operator reading a transcript sees which toolset a discovery call ran under. With both header and argument the argument is echoed, as it is the narrower of the two. Without either, a bound Session's request gets the bound toolset echoed. A bound Session's response also carries toolset_bound. An unscoped request carries no toolset. presets accompanies the argument or include_presets only: a header-declared toolset alone does not add them, so the discovery calls of an agent scoped by header stay as small as unscoped ones.

Tool information

Every entry of list_tools / filter_tools, and describe_tool, carries:

  • server — the owning MCPServer (the family name for a family tool); omitted for workflows and core tools,
  • kind — tool (served by an MCPServer), workflow or core,
  • annotations — the hints the server declared (readOnlyHint, destructiveHint, idempotentHint, openWorldHint), or the derived readOnlyHint of a workflow; omitted when the tool carries none.

Composition, not authorization

A toolset narrows what an agent reaches within the invoking person's access; it never grants more. Where the request authenticates as the person (a forwarded token), the toolset is declared by the request: a client that omits the header reaches everything the person may reach, which is what it could reach before toolsets existed. Where it authenticates with the actor JWT of a bound Session, the toolset is bound to the Session at its start and no process in the sandbox can widen it; the person's own access and the backend's authorization remain the boundary.