Documentation
¶
Overview ¶
Package skill is the gridctl typed Skill SDK. Skills are typed Go or TypeScript handlers that the gateway exposes as MCP tools — the same envelope an upstream client sees for any other MCP tool. The surface here is the typed flavor of pkg/mcp.AgentClient.CallTool, not a sibling of it: a registered skill becomes a tool on the registry's MCP server and is callable through Gateway.CallTool exactly like any tool from a downstream server.
The package has two layers:
Definition / Registry — the runtime-facing surface that crosses package boundaries. Definitions carry a name, description, an inferred JSON Schema for input, and an Invoker that takes an untyped argument map and returns *mcp.ToolCallResult. The registry server lifts Definitions into mcp.Tool entries and dispatches CallTool to them.
Define[I, O] — the typed authoring surface skill authors use. Inputs and outputs are Go structs with `json` and `jsonschema` tags; the helper infers the input schema, marshals/unmarshals across the boundary, and returns a Definition.
Recursive composability is non-negotiable: local execution (gridctl run <skill>) and remote execution (an upstream client invoking via the gateway) share one code path. The Invoker signature mirrors AgentClient.CallTool so a gridctl instance pointed at another over MCP gets the same shape it would if the skill ran in-process.
Index ¶
- Variables
- func WithSkillBody(parent context.Context, body string) context.Context
- type Definition
- type Invoker
- type Registry
- func (r *Registry) CallTool(ctx context.Context, name string, arguments map[string]any) (*mcp.ToolCallResult, error)
- func (r *Registry) Get(name string) (*Definition, bool)
- func (r *Registry) List() []*Definition
- func (r *Registry) Register(def *Definition) error
- func (r *Registry) Tools() []mcp.Tool
- type RunContext
- type TypedRunner
Constants ¶
This section is empty.
Variables ¶
var ErrSkillNotRegistered = errors.New("skill: not registered")
ErrSkillNotRegistered is returned by Registry.CallTool when no definition is registered under the requested name. The registry server (pkg/registry) tests for this with errors.Is so it can fall through to the TS-dispatcher path without coupling to error wording.
Functions ¶
func WithSkillBody ¶
WithSkillBody returns a context whose downstream RunContext.SkillBody() resolves to body instead of the value captured at Define time. The override is read-only (a context value), so concurrent invocations of the same Definition each see the body wired by their own caller's context. In-process registrations have no reason to call this — they pass body to Define directly.
Types ¶
type Definition ¶
type Definition struct {
// Name is the skill's stable identifier. It becomes the unprefixed
// MCP tool name the gateway prefixes with the registry server name.
// Names are constrained to non-empty strings; the registry rejects
// duplicates.
Name string
// Description is the human-readable summary the model sees when
// deciding whether to call the skill. Treat it as a docstring: the
// model will pattern-match on it.
Description string
// InputSchema is the JSON Schema describing the skill's argument
// object. For skills built via Define[I, O] the schema is inferred
// from I; for hand-built definitions, the schema MUST validate as
// a JSON object. An empty schema reads as "any object" — which is
// permissive but legal.
InputSchema json.RawMessage
// Invoker executes the skill. Errors returned by Invoker propagate
// to the caller verbatim; the runtime does not retry, log, or
// translate them. Use *mcp.ToolCallResult with IsError=true for
// "the skill ran but the result is an error" semantics.
Invoker Invoker
}
Definition is the runtime-facing skill descriptor. The registry server lifts Definitions into mcp.Tool entries the gateway exposes over the wire and dispatches CallTool to Invoker.
Build Definitions with Define[I, O] for typed Go skills, or construct directly when wrapping a non-Go handler (the TS dispatcher in pkg/agent/sandbox does the latter).
func Define ¶
func Define[I any, O any](name, description, body string, run TypedRunner[I, O]) (*Definition, error)
Define wraps a typed runner as a Definition. The input schema is inferred from I via reflectInputSchema (jsonschema struct tags); the returned Definition's Invoker:
- Re-marshals the argument map back to JSON (the gateway hands skills the decoded shape; the typed boundary needs the raw bytes to feed json.Unmarshal into a Go value of type I).
- Decodes into a fresh I.
- Constructs a RunContext closing over the body and name passed to Define so ctx.SkillBody() / ctx.SkillName() resolve without per-call I/O.
- Invokes the runner.
- Renders the typed output O back to MCP content as a single JSON text block. Skills that need richer content shapes (multi-part, image, etc.) should construct a Definition by hand.
body is the post-frontmatter SKILL.md markdown. Pass an empty string for programmatic registrations that don't have a SKILL.md sibling (tests, hand-built fixtures); the runner's ctx.SkillBody() reads as "" and the hybrid pattern degrades gracefully.
Decode failures and runner errors flow back to the caller; the runner's err is wrapped, never swallowed.
func MustDefine ¶
func MustDefine[I any, O any](name, description, body string, run TypedRunner[I, O]) *Definition
MustDefine is the panicking variant of Define. Use it in package-init code where a malformed skill is a programming error and the binary has nothing useful to do without it. Library code should call Define and propagate the error.
func (*Definition) Tool ¶
func (d *Definition) Tool() mcp.Tool
Tool returns the mcp.Tool envelope the registry exposes for this definition. The returned schema is a copy so callers cannot mutate the Definition's InputSchema by mutating the Tool.
type Invoker ¶
Invoker is the runtime-facing handler signature. It takes the same argument map shape pkg/mcp.AgentClient.CallTool receives so the registry server can hand calls straight through without translation.
Invokers MUST honor ctx cancellation: an Invoker that ignores ctx blocks the gateway's deadline propagation and breaks any caller that relies on context-scoped timeouts.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry is a concurrent-safe collection of Definitions. The registry server (pkg/registry) reads from a Registry to populate Tools() and dispatch CallTool, so the same Registry that hosts a programmatically-registered Go skill also serves it to upstream MCP clients through the gateway.
func (*Registry) CallTool ¶
func (r *Registry) CallTool(ctx context.Context, name string, arguments map[string]any) (*mcp.ToolCallResult, error)
CallTool dispatches a registered skill by unprefixed name. The signature matches mcp.AgentClient.CallTool so the registry server can delegate without translation. An unknown skill returns an error that wraps ErrSkillNotRegistered so callers can distinguish "no such skill" from "the skill ran and erred."
func (*Registry) Get ¶
func (r *Registry) Get(name string) (*Definition, bool)
Get returns the Definition registered under name, or false when no such skill exists. The returned pointer is the registry's own — callers MUST NOT mutate it.
func (*Registry) List ¶
func (r *Registry) List() []*Definition
List returns every registered Definition in name-sorted order. The slice is a snapshot; mutations after the call do not affect it.
func (*Registry) Register ¶
func (r *Registry) Register(def *Definition) error
Register installs a Definition. Returns an error if the definition is malformed or its name is already taken — the registry refuses duplicates rather than silently overwriting so registration order stays observable.
type RunContext ¶
type RunContext interface {
context.Context
// SkillBody returns the post-frontmatter markdown body the
// registry parsed from SKILL.md. Empty string for skills whose
// SKILL.md has no body — and for skills constructed via Define
// without one (programmatic registrations, tests).
SkillBody() string
// SkillName returns the registered skill name. The same string
// the gateway exposes as the unprefixed MCP tool name.
SkillName() string
}
RunContext is the typed runner's first argument. It embeds context.Context — cancellation, deadlines, and request-scoped values flow through unchanged — and surfaces two skill-scoped accessors so authors can drive the hybrid pattern: feed the SKILL.md body straight into an llm.Generate System slot, key per-skill state on SkillName.
Body and Name are captured at Define-time and read with no per-call I/O. The runtime resolves the body from the registry store before constructing the Definition, so RunContext readers never reach back into the registry on the call path.
type TypedRunner ¶
type TypedRunner[I any, O any] func(ctx RunContext, input I) (O, error)
TypedRunner is the typed authoring signature skill authors write. I and O are Go structs; the SDK marshals across the boundary so the handler body works in typed Go and the wire form stays JSON. The first argument is RunContext rather than context.Context so the runner can read the SKILL.md body and the registered name without reaching back into the registry.