Documentation
¶
Overview ¶
Package set provides composable tool collection building.
Toolset enables curated, filtered, and access-controlled tool surfaces from multiple sources. It is pure data composition with no I/O, execution, or network dependencies.
Core Concepts ¶
- Toolset: Thread-safe collection of canonical tools
- Builder: Fluent API for constructing toolsets from sources
- FilterFunc: Predicates for filtering tools (AND-composed)
- Policy: Access control decisions (applied after filters)
- Exposure: Export to MCP/OpenAI/Anthropic via adapter
Basic Usage ¶
ts, err := set.NewBuilder("github-readonly").
FromRegistry(myRegistry).
WithNamespace("github").
WithTags([]string{"read"}).
WithPolicy(set.DenyTags("deprecated")).
Build()
if err != nil {
log.Fatal(err)
}
// Export to OpenAI format
exp := set.NewExposure(ts, adapter.NewOpenAIAdapter())
tools, warnings, errs := exp.ExportWithWarnings()
Filter Composition ¶
Filters are AND-composed: a tool must pass ALL filters to be included. Use custom FilterFunc for OR logic:
orFilter := func(t *adapter.CanonicalTool) bool {
return set.NamespaceFilter("ns1")(t) || set.NamespaceFilter("ns2")(t)
}
ts, _ := set.NewBuilder("custom").
FromTools(tools).
WithFilter(orFilter).
Build()
Built-in Filters ¶
- NamespaceFilter(ns...): Match tools in any listed namespace (OR)
- TagsAny(tags...): Match tools with ANY of the tags (OR)
- TagsAll(tags...): Match tools with ALL tags (AND)
- TagsNone(tags...): Match tools with NONE of the tags
- CategoryFilter(categories...): Match tools in any category (OR)
- AllowIDs(ids...): Include only listed tool IDs
- DenyIDs(ids...): Exclude listed tool IDs
Policies ¶
Policies provide access control after filtering:
- AllowAll(): Allow all non-nil tools
- DenyAll(): Deny all tools
- AllowNamespaces(ns...): Allow only listed namespaces
- DenyTags(tags...): Deny tools with any forbidden tag
- AllowScopes(scopes...): Allow tools requiring only allowed scopes
Thread Safety ¶
- Toolset: Safe for concurrent use (RWMutex protected)
- Builder: NOT safe for concurrent use (single-goroutine construction)
- Policy: Safe after construction (immutable closures)
- FilterFunc: Must be safe if Toolset is shared across goroutines
Integration ¶
The set package integrates with:
- github.com/jonwraymond/toolfoundation/adapter for protocol conversion
- github.com/jonwraymond/tooldiscovery/index as a Registry source
Error Handling ¶
The package defines sentinel errors for programmatic handling:
- ErrNoSource: Builder.Build() called without FromTools or FromRegistry
- ErrNilAdapter: Exposure created with nil adapter
- ErrNilToolset: Operation requires non-nil Toolset
Use errors.Is() for reliable error checking:
if errors.Is(err, set.ErrNoSource) {
// handle missing source
}
Index ¶
- Variables
- type Builder
- func (b *Builder) Build() (*Toolset, error)
- func (b *Builder) ExcludeTools(ids []string) *Builder
- func (b *Builder) FromRegistry(r Registry) *Builder
- func (b *Builder) FromTools(tools []*adapter.CanonicalTool) *Builder
- func (b *Builder) WithCategories(categories []string) *Builder
- func (b *Builder) WithFilter(fn FilterFunc) *Builder
- func (b *Builder) WithNamespace(ns string) *Builder
- func (b *Builder) WithNamespaces(ns []string) *Builder
- func (b *Builder) WithPolicy(p Policy) *Builder
- func (b *Builder) WithTags(tags []string) *Builder
- func (b *Builder) WithTools(ids []string) *Builder
- type ConversionError
- type Exposure
- type FilterFunc
- func AllowIDs(ids ...string) FilterFunc
- func CategoryFilter(categories ...string) FilterFunc
- func DenyIDs(ids ...string) FilterFunc
- func NamespaceFilter(namespaces ...string) FilterFunc
- func TagsAll(tags ...string) FilterFunc
- func TagsAny(tags ...string) FilterFunc
- func TagsNone(tags ...string) FilterFunc
- type Policy
- type PolicyFunc
- type Registry
- type Toolset
- func (ts *Toolset) Add(tool *adapter.CanonicalTool)
- func (ts *Toolset) Count() int
- func (ts *Toolset) Filter(fn FilterFunc) *Toolset
- func (ts *Toolset) Get(id string) (*adapter.CanonicalTool, bool)
- func (ts *Toolset) IDs() []string
- func (ts *Toolset) Name() string
- func (ts *Toolset) Remove(id string) bool
- func (ts *Toolset) Tools() []*adapter.CanonicalTool
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoSource is returned when Build() is called without a source. ErrNoSource = errors.New("set: no source provided (call FromTools or FromRegistry)") // ErrNilAdapter is returned when an Exposure is created with a nil adapter. ErrNilAdapter = errors.New("set: adapter is nil") // ErrNilToolset is returned when an operation requires a non-nil Toolset. ErrNilToolset = errors.New("set: toolset is nil") )
Sentinel errors for the set package.
Functions ¶
This section is empty.
Types ¶
type Builder ¶
type Builder struct {
// contains filtered or unexported fields
}
Builder constructs Toolsets with filtering.
Thread-safety: Builder is NOT safe for concurrent use. Create and configure Builders in a single goroutine, then call Build() once. The resulting Toolset IS thread-safe.
Execution flow:
- Source tools gathered from FromTools or FromRegistry
- Filters applied in order (AND-composed)
- Policy applied last (if set)
- Resulting tools added to new Toolset
func NewBuilder ¶
NewBuilder creates a new Builder with the given toolset name.
Example ¶
package main
import (
"fmt"
"github.com/jonwraymond/toolcompose/set"
"github.com/jonwraymond/toolfoundation/adapter"
)
func main() {
tools := []*adapter.CanonicalTool{
{Name: "create_issue", Namespace: "github", Tags: []string{"write", "issues"}},
{Name: "list_repos", Namespace: "github", Tags: []string{"read", "repos"}},
{Name: "get_user", Namespace: "github", Tags: []string{"read", "users"}},
}
ts, err := set.NewBuilder("readonly").
FromTools(tools).
WithTags([]string{"read"}).
Build()
if err != nil {
panic(err)
}
fmt.Println("Count:", ts.Count())
fmt.Println("IDs:", ts.IDs())
}
Output: Count: 2 IDs: [github:get_user github:list_repos]
Example (WithNamespace) ¶
package main
import (
"fmt"
"github.com/jonwraymond/toolcompose/set"
"github.com/jonwraymond/toolfoundation/adapter"
)
func main() {
tools := []*adapter.CanonicalTool{
{Name: "create_issue", Namespace: "github"},
{Name: "list_files", Namespace: "filesystem"},
{Name: "list_repos", Namespace: "github"},
}
ts, _ := set.NewBuilder("github-only").
FromTools(tools).
WithNamespace("github").
Build()
fmt.Println("IDs:", ts.IDs())
}
Output: IDs: [github:create_issue github:list_repos]
Example (WithPolicy) ¶
package main
import (
"fmt"
"github.com/jonwraymond/toolcompose/set"
"github.com/jonwraymond/toolfoundation/adapter"
)
func main() {
tools := []*adapter.CanonicalTool{
{Name: "create_issue", Namespace: "github", Tags: []string{"write"}},
{Name: "delete_repo", Namespace: "github", Tags: []string{"write", "dangerous"}},
{Name: "list_repos", Namespace: "github", Tags: []string{"read"}},
}
ts, _ := set.NewBuilder("safe-tools").
FromTools(tools).
WithPolicy(set.DenyTags("dangerous")).
Build()
fmt.Println("IDs:", ts.IDs())
}
Output: IDs: [github:create_issue github:list_repos]
func (*Builder) ExcludeTools ¶
ExcludeTools excludes listed tool IDs.
func (*Builder) FromRegistry ¶
FromRegistry sets a registry as the source.
func (*Builder) FromTools ¶
func (b *Builder) FromTools(tools []*adapter.CanonicalTool) *Builder
FromTools sets tools as the source.
func (*Builder) WithCategories ¶
WithCategories filters to tools with ANY category.
func (*Builder) WithFilter ¶
func (b *Builder) WithFilter(fn FilterFunc) *Builder
WithFilter adds a custom filter.
func (*Builder) WithNamespace ¶
WithNamespace filters to a single namespace.
func (*Builder) WithNamespaces ¶
WithNamespaces filters to multiple namespaces.
func (*Builder) WithPolicy ¶
WithPolicy sets the access control policy (applied after filters).
type ConversionError ¶
ConversionError represents a tool that failed to convert.
func (*ConversionError) Error ¶
func (e *ConversionError) Error() string
func (*ConversionError) Unwrap ¶
func (e *ConversionError) Unwrap() error
type Exposure ¶
type Exposure struct {
// contains filtered or unexported fields
}
Exposure exports a Toolset to protocol-specific formats.
func NewExposure ¶
NewExposure creates an Exposure for the given toolset and adapter.
func (*Exposure) ExportWithWarnings ¶
func (e *Exposure) ExportWithWarnings() ([]any, []adapter.FeatureLossWarning, []error)
ExportWithWarnings converts tools and returns feature loss warnings and conversion errors. Unlike Export, this method continues on conversion errors and collects them for reporting. Callers should check the errors slice to detect tools that failed to convert.
type FilterFunc ¶
type FilterFunc func(*adapter.CanonicalTool) bool
FilterFunc is a predicate for filtering tools.
Contract:
- Must not call methods on the Toolset being filtered (would deadlock)
- Must not mutate the tool (treat as read-only)
- Must be safe for concurrent calls if Toolset is shared
- Must return false for nil tools
func AllowIDs ¶
func AllowIDs(ids ...string) FilterFunc
AllowIDs returns a filter matching only the listed tool IDs.
func CategoryFilter ¶
func CategoryFilter(categories ...string) FilterFunc
CategoryFilter returns a filter matching tools in any of the categories.
func DenyIDs ¶
func DenyIDs(ids ...string) FilterFunc
DenyIDs returns a filter excluding the listed tool IDs.
func NamespaceFilter ¶
func NamespaceFilter(namespaces ...string) FilterFunc
NamespaceFilter returns a filter matching tools in any of the namespaces.
func TagsAll ¶
func TagsAll(tags ...string) FilterFunc
TagsAll returns a filter matching tools with ALL of the tags.
func TagsAny ¶
func TagsAny(tags ...string) FilterFunc
TagsAny returns a filter matching tools with ANY of the tags.
func TagsNone ¶
func TagsNone(tags ...string) FilterFunc
TagsNone returns a filter matching tools with NONE of the tags.
type Policy ¶
type Policy interface {
Allow(tool *adapter.CanonicalTool) bool
}
Policy decides whether a tool is allowed.
Contract: - Concurrency: implementations must be safe for concurrent use after construction. - Errors: implementations must encode deny via false; no panic for invalid input. - Ownership: implementations must not mutate the tool; treat it as read-only. - Determinism: for a given tool, Allow returns a stable result. - Nil handling: if tool is nil, Allow must return false.
func AllowNamespaces ¶
AllowNamespaces returns a policy allowing only listed namespaces.
func AllowScopes ¶
AllowScopes returns a policy allowing tools requiring only allowed scopes.
Example ¶
package main
import (
"fmt"
"github.com/jonwraymond/toolcompose/set"
"github.com/jonwraymond/toolfoundation/adapter"
)
func main() {
tools := []*adapter.CanonicalTool{
{Name: "read_file", Namespace: "fs", RequiredScopes: []string{"read"}},
{Name: "write_file", Namespace: "fs", RequiredScopes: []string{"read", "write"}},
{Name: "delete_file", Namespace: "fs", RequiredScopes: []string{"read", "write", "delete"}},
}
// User only has read scope
ts, _ := set.NewBuilder("user-tools").
FromTools(tools).
WithPolicy(set.AllowScopes("read")).
Build()
fmt.Println("Tools with read scope:", ts.IDs())
}
Output: Tools with read scope: [fs:read_file]
type PolicyFunc ¶
type PolicyFunc func(*adapter.CanonicalTool) bool
PolicyFunc adapts a function to the Policy interface.
func (PolicyFunc) Allow ¶
func (f PolicyFunc) Allow(t *adapter.CanonicalTool) bool
Allow implements Policy.
type Registry ¶
type Registry interface {
Tools() []*adapter.CanonicalTool
}
Registry provides tools for the builder.
Contract: - Concurrency: Tools may be called concurrently; implementations must be safe or document otherwise. - Ownership: returned slice is caller-owned; tools are shared and read-only. - Determinism: ordering should be deterministic for identical registry state. - Nil handling: returning nil is treated as empty.
type Toolset ¶
type Toolset struct {
// contains filtered or unexported fields
}
Toolset is a thread-safe collection of canonical tools.
All methods are safe for concurrent use. Tools are stored by reference; callers should not mutate tools after adding them to a Toolset. Results from IDs() and Tools() are deterministically sorted by ID.
func (*Toolset) Add ¶
func (ts *Toolset) Add(tool *adapter.CanonicalTool)
Add adds a tool. Nil tools are silently ignored.
func (*Toolset) Filter ¶
func (ts *Toolset) Filter(fn FilterFunc) *Toolset
Filter returns a new Toolset with tools matching fn. The original Toolset is not modified.
Example ¶
package main
import (
"fmt"
"github.com/jonwraymond/toolcompose/set"
"github.com/jonwraymond/toolfoundation/adapter"
)
func main() {
ts := set.New("all-tools")
ts.Add(&adapter.CanonicalTool{Name: "tool1", Namespace: "ns1", Tags: []string{"fast"}})
ts.Add(&adapter.CanonicalTool{Name: "tool2", Namespace: "ns1", Tags: []string{"slow"}})
ts.Add(&adapter.CanonicalTool{Name: "tool3", Namespace: "ns2", Tags: []string{"fast"}})
filtered := ts.Filter(set.TagsAny("fast"))
fmt.Println("Original count:", ts.Count())
fmt.Println("Filtered count:", filtered.Count())
fmt.Println("Filtered IDs:", filtered.IDs())
}
Output: Original count: 3 Filtered count: 2 Filtered IDs: [ns1:tool1 ns2:tool3]
func (*Toolset) Get ¶
func (ts *Toolset) Get(id string) (*adapter.CanonicalTool, bool)
Get retrieves a tool by ID. Returns (nil, false) if not found.
func (*Toolset) Tools ¶
func (ts *Toolset) Tools() []*adapter.CanonicalTool
Tools returns all tools sorted lexicographically by ID.