Tool catalog
tool.Catalog contains the tools the model can call. Build a catalog, register
your tools, and pass it to the agent engine.
Register tools
catalog := tool.NewCatalog()
catalog.MustRegister(readTool)
if err := catalog.Register(optionalTool); err != nil {
if errors.Is(err, tool.ErrDuplicateTool) {
// Choose which tool owns the name.
}
return err
}
Register returns ErrDuplicateTool when a name is already present.
MustRegister panics, which is useful when duplicate static registration is a
programming error.
Catalog queries return deterministic, name-sorted results:
| Method | Result |
|---|---|
Lookup(name) | The tool registered under a name |
Tools() | All registered tools |
Available(mode) | Tools available in a permission mode |
Specs(mode) | Full specifications for available tools |
AdvertisedSpecs(mode) | Specifications after progressive disclosure |
Plan mode excludes mutating tools. A tool that implements tool.PlanOnly is
available only in plan mode.
Implement a tool
Every tool implements three methods:
type Tool interface {
Spec() ToolSpec
ReadOnly() bool
Execute(
ctx context.Context,
call session.ToolCall,
env Environment,
) (session.ToolResult, error)
}
Spec provides the name, model-facing description, and JSON Schema for
arguments. Explain when to use the tool, identify its important limits, and keep
the schema as narrow as the implementation.
ReadOnly controls dispatch concurrency and plan-mode availability. Mecatl runs
adjacent read-only calls concurrently, runs mutating calls one at a time, and
offers only read-only tools in plan mode. Return false if the tool changes the
workspace or other shared state.
Execute receives the model's call and the session environment. Return a
ToolResult with IsError: true for a failure the model can address. Mecatl
converts a non-nil Go error into a model-visible tool error and continues the
run, so use it for unexpected implementation failures. The environment's
CommandRunner() can be nil; check it before running a command.
Add a custom tool
package pingtool
import (
"context"
"encoding/json"
"github.com/stacklok/mecatl/engine/session"
"github.com/stacklok/mecatl/engine/tool"
)
type Tool struct{}
func (Tool) Spec() tool.ToolSpec {
return tool.ToolSpec{
Name: "Ping",
Description: "Return pong to verify that tool dispatch is working.",
Schema: json.RawMessage(
`{"type":"object","additionalProperties":false}`,
),
}
}
func (Tool) ReadOnly() bool {
return true
}
func (Tool) Execute(
_ context.Context,
call session.ToolCall,
_ tool.Environment,
) (session.ToolResult, error) {
return session.NewToolResult(call.ID, "pong"), nil
}
Register the tool before constructing the engine:
catalog := tool.NewCatalog()
catalog.MustRegister(pingtool.Tool{})
engine := agent.NewEngine(agent.Deps{
Catalog: catalog,
// Supply the remaining dependencies.
})
Use session.NewToolError for a plain-text model-visible failure.
session.NewToolResultWithParts returns typed content blocks when the tool
needs more than text.
Advertise a lightweight specification
Implement tool.Disclosable when a full tool schema is expensive to include on
every turn:
type Disclosable interface {
Tool
Advertised() ToolSpec
}
Advertised returns a short description and minimal schema. Spec still
returns the complete definition for ToolSearch. Tools without this interface
always advertise their full specification.
Add MCP tools
Mecatl registers tools from connected MCP servers as mcp__<SERVER>__<TOOL>.
Remote tools are treated as mutating unless the MCP server sets its
readOnlyHint.
Core and previously registered tools keep their names when an MCP tool collides. The conflicting MCP tool is skipped and a diagnostic identifies the name.
The shipped applications can connect global MCP servers at startup and accept session-scoped MCP servers from clients. Session-scoped connections close with the session; global connections close with the application.
For connection options, authentication, resources, prompts, and failure behavior, see MCP client.
Provide skills
Skills are instruction bundles. They do not add executable tools or grant permissions.
tool.SkillSource provides skill metadata, instructions, and optional assets:
type SkillSource interface {
ListSkills(
ctx context.Context,
) ([]SkillMeta, error)
SkillBody(
ctx context.Context,
name string,
) (string, error)
ListSkillAssets(
ctx context.Context,
name string,
) ([]SkillAsset, error)
ReadSkillAsset(
ctx context.Context,
skill string,
asset string,
) ([]byte, error)
}
ListSkills returns a name-sorted, unique snapshot. Metadata stays available
for routing, while Mecatl loads the body only when the model activates a skill.
Return ErrSkillNotFound for an unknown skill and ErrSkillAssetNotFound for
an unknown asset.
Assets use logical slash-separated names such as references/api.md. Validate
them with tool.ValidSkillAssetName. The Skill tool reads one bounded textual
asset on demand. It rejects invalid UTF-8 and NUL bytes. It does not write
assets to the workspace or make them available to Shell.
engine/adapter/skillfs implements SkillSource for SKILL.md bundles.
Project-tier sources are included only for trusted workspaces. Filesystem paths
remain private to the adapter.
A skill is also available as a slash command. Local command files take precedence over a skill with the same name, and skills take precedence over driver commands and MCP prompts. See Skills, commands, and soul for file format and discovery behavior.
Validate a custom source with
engine/adapter/sourceconformance.RunSkillSource. The test factory must return
a fresh source that serves exactly sourceconformance.Fixture.
Share catalog state safely
A tool registered in the application catalog is shared by every session. Keep
the tool stateless or make its dependencies safe for concurrent sessions.
Session-specific workspace and command execution are available through the
tool.Environment passed to Execute.
If a tool owns a session-scoped connection or credential, construct that tool in your session engine factory and close its resources when the session ends. Do not close application-wide resources from a session cleanup function.
Record tool calls
port.ToolCallRecorder records tool arguments, results, queue time, and
execution time independently of the live event stream:
type ToolCallRecorder interface {
ToolCall(
id session.SessionID,
call session.ToolCall,
result session.ToolResult,
queued time.Duration,
took time.Duration,
)
}
queued measures time spent waiting in dispatch before execution. took
measures execution. Both are zero when the engine has no Clock.
The JSONL store writes tool-call records to a separate audit sidecar. Implement this port to send structured tool audit data to another system.