set

package
v0.1.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 6, 2026 License: MIT Imports: 4 Imported by: 0

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:

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

Examples

Constants

This section is empty.

Variables

View Source
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:

  1. Source tools gathered from FromTools or FromRegistry
  2. Filters applied in order (AND-composed)
  3. Policy applied last (if set)
  4. Resulting tools added to new Toolset

func NewBuilder

func NewBuilder(name string) *Builder

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) Build

func (b *Builder) Build() (*Toolset, error)

Build creates the Toolset.

func (*Builder) ExcludeTools

func (b *Builder) ExcludeTools(ids []string) *Builder

ExcludeTools excludes listed tool IDs.

func (*Builder) FromRegistry

func (b *Builder) FromRegistry(r Registry) *Builder

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

func (b *Builder) WithCategories(categories []string) *Builder

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

func (b *Builder) WithNamespace(ns string) *Builder

WithNamespace filters to a single namespace.

func (*Builder) WithNamespaces

func (b *Builder) WithNamespaces(ns []string) *Builder

WithNamespaces filters to multiple namespaces.

func (*Builder) WithPolicy

func (b *Builder) WithPolicy(p Policy) *Builder

WithPolicy sets the access control policy (applied after filters).

func (*Builder) WithTags

func (b *Builder) WithTags(tags []string) *Builder

WithTags filters to tools with ALL specified tags.

func (*Builder) WithTools

func (b *Builder) WithTools(ids []string) *Builder

WithTools includes only listed tool IDs.

type ConversionError

type ConversionError struct {
	ToolID string
	Cause  error
}

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

func NewExposure(ts *Toolset, adapter adapter.Adapter) *Exposure

NewExposure creates an Exposure for the given toolset and adapter.

func (*Exposure) Export

func (e *Exposure) Export() ([]any, error)

Export converts all tools to the adapter's format.

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 AllowAll

func AllowAll() Policy

AllowAll returns a policy that allows all tools.

func AllowNamespaces

func AllowNamespaces(ns ...string) Policy

AllowNamespaces returns a policy allowing only listed namespaces.

func AllowScopes

func AllowScopes(allowed ...string) Policy

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]

func DenyAll

func DenyAll() Policy

DenyAll returns a policy that denies all tools.

func DenyTags

func DenyTags(tags ...string) Policy

DenyTags returns a policy denying tools with any of the tags.

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 New

func New(name string) *Toolset

New creates a new Toolset with the given name.

func (*Toolset) Add

func (ts *Toolset) Add(tool *adapter.CanonicalTool)

Add adds a tool. Nil tools are silently ignored.

func (*Toolset) Count

func (ts *Toolset) Count() int

Count returns the number of tools.

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) IDs

func (ts *Toolset) IDs() []string

IDs returns tool IDs sorted lexicographically.

func (*Toolset) Name

func (ts *Toolset) Name() string

Name returns the toolset's name.

func (*Toolset) Remove

func (ts *Toolset) Remove(id string) bool

Remove removes a tool by ID. Returns true if found and removed.

func (*Toolset) Tools

func (ts *Toolset) Tools() []*adapter.CanonicalTool

Tools returns all tools sorted lexicographically by ID.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL