Skip to content

Extension catalogs

Register tools, agent profiles, sub-agents, providers, MCP servers and skills, and edit any catalog item, built-ins included, with replayable transforms.

A catalog is a list the app builds the agent from: tools, agent profiles and sub-agents, models, providers, MCP servers and skills. An extension adds items with register and edits the whole list with transform. ctx.plugins loads Agent Plugins into the skills and MCP catalogs, and ctx.instructions adds text to the system prompt.

Use register for something the extension brings. Use transform to change what is already there: rename a model, hide a tool, give an MCP server a longer timeout, change the default profile.

Register#

Each register returns a Disposable. Disposing it removes the item. Unloading the extension removes them all.

TypeScript
ctx.tools.register('kitchen_count', {
  spec: {
    name: 'kitchen_count',
    description: 'Read the counter, or bump it by "bump".',
    inputSchema: { type: 'object', properties: { bump: { type: 'integer', minimum: 1 } }, additionalProperties: false },
  },
  isConcurrencySafe: input => input.bump === undefined,
  async execute(input) {
    return JSON.stringify({ count: typeof input.bump === 'number' ? input.bump : 0 })
  },
})
CatalogRegisterItem
Toolsctx.tools.register(name, def)A ToolDef (tools)
Agent profilesctx.agents.register(profile)An AgentProfile (profiles)
Sub-agentsctx.agents.subagents.register(def)A sub-agent type for the spawn tool
Providersctx.providers.register(descriptor)A ProviderDescriptor
MCP serversctx.mcp.register(config)An McpServerConfig (MCP)
Skillsctx.skills.register(skill)A SkillConfig (skills)

Models have no register: a provider's descriptor lists its models.

A registration with the id of an item already in the catalog replaces that item, and Settings records what it replaced. Pick ids that name your extension.

Transforms#

transform(fn) registers a function that edits the catalog through an editor. The app replays every transform, in extension load order, onto a fresh copy of the catalog whenever a registration changes or an extension calls reload().

TypeScript
ctx.mcp.transform((editor) => {
  for (const server of editor.list()) {
    if (server.name.startsWith('kitchen'))
      editor.update(server.name, (s) => { s.toolTimeout = 60_000 })
  }
})

A transform that throws is rolled back as a whole: its edits are dropped, the error is logged against the extension, and the next transform runs.

A transform must be synchronous and pure: it reads the editor and what the extension already holds, and writes only through the editor. To base it on data from the network or a setting, load the data first, then call reload() when it changes. The kitchen-sink example replays its transforms when a setting changes (examples/extension-kitchen-sink/catalogs.ts):

TypeScript
const tweak = (): boolean => ctx.settings.get('tweakCatalogs') === true

ctx.models.transform((editor) => {
  const current = editor.default.get()
  if (tweak() && current && editor.get(current.providerId, current.modelId))
    editor.update(current.providerId, current.modelId, (model) => { model.name = `${model.name} (tuned)` })
})

ctx.settings.onChange('tweakCatalogs', () => {
  void Promise.all([ctx.agents.reload(), ctx.models.reload()])
})

Every catalog with a key has the same editor:

MemberDoes
list()Every item, after the transforms that ran before this one
get(id)One item
origin(id)Who put it there: { kind: 'builtin' }, { kind: 'config' }, { kind: 'mcp', server } or { kind: 'extension', id }
add(item)Adds an item, or replaces the one with the same id
update(id, edit)Edits a copy of the item in edit, then stores it
remove(id)Removes the item

Each catalog domain also has:

MemberDoes
transform(fn)Registers a transform. Returns a Disposable
reload()Replays every transform of this catalog. Resolves when done
list()The catalog after every transform

Provenance#

A transform may edit or remove any item, built-in ones included. Settings shows which extension changed an item. Turning the extension off brings the item back as it was, because the next replay runs without that extension's transforms.

AppShows
Terminal appA badge on each MCP server and skill row an extension changed. An item an extension removed stays listed
Desktop app"Changed by extensions" in Settings › Extensions
Web appThe same list in its extension settings

Tools#

ctx.tools.register(name, def) adds a tool the model can call. def is a ToolDef:

FieldDoes
spec{ name, description, inputSchema }. name is what the model calls. Use the same value as name
execute(input, toolCtx)Runs the call. Returns a string or content blocks. A throw is attributed to the extension and counts toward its error budget
isConcurrencySafetrue, or a function of the input, when the call only reads. Safe calls run in parallel. Default false

The tools editor adds two fields to the common editor:

FieldHolds
phase'pre-run' when the host lists tools before a run (a tool picker), 'run' when it builds a run
sessionIdThe session the list is for
TypeScript
ctx.tools.transform((editor) => {
  if (editor.phase === 'run' && editor.get('web_search'))
    editor.remove('web_search')
})

The tools transform runs per run, with the run's tools as its base: built-in tools, MCP tools under their mcp_<server>_<tool> names, and extension tools. Its origin for an MCP tool is { kind: 'mcp', server }.

Tool metadata#

ctx.tools.metadata(namespace, spec) claims a namespace of tool-result metadata. A tool.result interceptor writes under it. The context panel reads it.

FieldDoes
contextRows({ namespace, entries })Rows { label, value } for the context panel, from every entry. Without it, the panel shows a count
summarize(metadata)Reduces one result's metadata to a compact JSON summary
TypeScript
ctx.tools.metadata('kitchen', {
  contextRows: ({ entries }) => [{ label: 'Kitchen results', value: String(entries.length) }],
})

Agents#

ctx.agents.register(profile) adds a profile to the agent picker. Profiles, families and switch notes are on the overview.

ctx.agents.subagents.register(def) adds a sub-agent type the spawn tool offers:

TypeScript
ctx.agents.subagents.register({
  name: 'kitchen-taster',
  description: 'Reads a file and says, in one line, what it is about.',
  system: 'You read the file you are given and describe it in one line.',
  tools: ['read_file'],
  readonly: true,
})
FieldDoes
nameRequired. What the model passes as subagent_type
descriptionShown to the model, to pick a type
systemThe sub-agent's system prompt
model, thinkingDefault model and thinking level
toolsTool names the sub-agent keeps, from the parent's tools
readonlyKeeps only read_file, grep, glob and list_files. tools wins when both are set

The agents editor adds:

MemberDoes
default.get(), default.set(id)The default profile
subagentsAn editor over sub-agent types, with override(name, fn): fn gets the current definition and returns the new one, or undefined to remove it
TypeScript
ctx.agents.transform((editor) => {
  editor.subagents.override('kitchen-taster', def => (def ? { ...def, thinking: 'low' } : def))
})

Models#

The models catalog holds the models of the providers the user signed in to. Each ModelInfo has providerId, id, name, enabled, contextWindow, maxOutput, capabilities (tools, images, reasoning) and cost (input, output).

The models editor is keyed by provider and model:

MemberDoes
list(providerId?)Every model, or one provider's
get(providerId, modelId)One model
update(providerId, modelId, edit)Edits a copy of the model
remove(providerId, modelId)Hides the model
default.get(), default.set(providerId, modelId)The default model

ctx.models.default() returns the default after every transform. For a provider whose accounts have lists of their own, ctx.models.list() and the editor list the active account's models.

Providers#

ctx.providers.register(descriptor) adds a provider to the pickers. The main fields:

FieldDoes
keyRequired. The registry key
label, descriptionThe name and one line, in pickers
factory(options)Required. Builds a Provider for a session
models, defaultModelThe models it serves. A getter reads them when the picker opens
envKeyThe environment variable that holds an API key. undefined for a provider that only signs in
integrationThe integration it signs in with. Pickers, "Sign in" and usage follow it
accountsThe factory honors options.account: pickers offer an account per session
usageA subscription usage probe

The providers editor lists every descriptor, inactive ones included, keyed by key. Its models member edits a provider's model list: set(providerId, models, { account? }), update(providerId, modelId, edit) and remove(providerId, modelId).

TypeScript
ctx.providers.transform((editor) => {
  for (const provider of editor.list()) {
    if (editor.origin(provider.key)?.kind === 'builtin' && provider.description === undefined)
      editor.update(provider.key, (p) => { p.description = 'Checked by kitchen-sink.' })
  }
})

To wrap a built provider's requests, use the provider and http.request interceptors.

Models per account#

One account of a provider can serve other models than another: a team plan, a region, a key with fewer rights. models.set takes an account, and then replaces that account's list only:

TypeScript
import type { ModelInfo } from 'zidane/extensions'

const byAccount = new Map<string, ModelInfo[]>() // each account's models, fetched with its credential

ctx.providers.transform((editor) => {
  for (const [account, models] of byAccount)
    editor.models.set('acme', models, { account })
})

account is the account id the provider's sign-in stores: an integration connection's account, or the id of a stored provider account. An empty account throws. An account without a list of its own uses the provider's list, and set with an empty list gives an account back the provider's list.

  • Pickers list each account's own models under that account. A session pinned to an account runs on its list: with no model picked, on the provider's default when the list has it, else on the list's first model. A model the user picked is never changed.
  • ctx.models.list() follows the active account (the default connection, or the stored account in use).
  • models.update and models.remove reach the model in the provider's list and in every account's list. A model edit (ctx.models update, remove) applies to account lists by model id.
  • The provider transforms run again when the user connects or disconnects an account, or makes another one the default. A token refresh does not count.

MCP#

ctx.mcp registers servers, edits the list, and calls tools outside a run.

MemberDoes
register(config)Adds a server: { name, transport, command, args } for stdio, { name, transport, url, headers } for sse and streamable-http (MCP)
transform(fn), reload()Edits the server configs, keyed by name
list()Every connected server (status)
call(server, tool, args?, { timeoutMs?, signal? })Calls a tool of a server outside a run. Resolves { content, isError }
TypeScript
const result = await ctx.mcp.call('github', 'search_issues', { query: 'is:open label:bug' }, { timeoutMs: 15_000 })
if (!result.isError)
  ctx.logger.info('issues', { content: result.content })

call connects a server the extension registered when it is not connected yet, and closes it after 60 seconds without calls. Use the server's own tool name, not the mcp_<server>_<tool> name.

MCP status#

FieldHolds
nameThe server's name
statusconnected, connecting, failed or disabled
toolsThe server's own tool names
errorWhy it failed
url, commandIts URL (sse, streamable-http) or launch command (stdio)
toolPrefixmcp_<name>_. The agent's name of a tool is <toolPrefix><tool>: what tool.call and tool.result see

list includes servers connected inside sessions, remote ones too.

Skills#

ctx.skills.register(skill) adds a skill. The skills editor adds autoinvoke(name, on): an auto-invoked skill is active at the start of every run.

TypeScript
ctx.skills.register({
  name: 'kitchen-sink-guide',
  description: 'Use when the user asks what the kitchen-sink extension does.',
  instructions: 'The kitchen-sink extension shows every zidane extension API. Its commands start with /kitchen-.',
})
ctx.skills.transform((editor) => {
  editor.autoinvoke('kitchen-sink-guide', ctx.settings.get('tweakCatalogs') === true)
})

To pick skills per run from the prompt or the model, use the skills.activate interceptor.

Agent Plugins#

ctx.plugins.load(spec) loads an Agent Plugin and registers its skills and MCP servers. spec is a folder, relative to the extension's folder, or an object:

FieldDoes
dirThe plugin's folder
skillsLoad its skills. Default true
mcpLoad its MCP servers. Default true
namePrefixA prefix for the names of what it adds

It resolves to the loaded plugin (ok, manifest, skills, mcpServers, diagnostics) plus dispose(). Check ok: a plugin the loader rejected loads nothing.

TypeScript
const plugin = await ctx.plugins.load({ dir: 'plugin', mcp: false })
if (!plugin.ok)
  ctx.logger.warn(`plugin did not load: ${plugin.diagnostics.map(d => d.message).join('; ')}`)

Instructions#

ctx.instructions.add(text, { sessionId?, key? }) adds text to the system prompt. Without sessionId, it applies to every session. A key replaces an earlier entry with the same key. The returned Disposable removes it.

TypeScript
ctx.instructions.add('When the user mentions the kitchen sink, answer with a cooking pun.')
ctx.instructions.add('This session looked at the counter at 3.', { sessionId: 'a-session-id', key: 'last-info' })

Prefer instructions to the system interceptor for fixed text: the prompt stays the same from turn to turn, so the provider's cache keeps working.

Pitfalls#

  • A transform that awaits, fetches or writes state runs at the wrong time, or more than once. Keep it synchronous and pure, and call reload() when its inputs change.
  • A transform runs again on every replay. Do not count or log in it.
  • update edits a copy. Changes to the object get returned are lost.
  • A tool's spec.name and the name passed to register should match: the model calls spec.name.
  • MCP and tool.call names differ: ctx.mcp.call takes the server's tool name, interceptors see mcp_<server>_<tool>.
  • An update must keep the item's id.
  • Hooks: interceptors for decisions made per call.
  • Settings: apply: 'agent-rebuild' for settings that transforms read.
  • Integrations: providers that sign in through an integration.