Documentation
¶
Overview ¶
Package index provides a global registry and search layer for tools.
This package implements tool registration, storage, retrieval, and search capabilities. It supports multiple index backends and pluggable search strategies.
Index Types ¶
The package provides a built-in index implementation:
- InMemoryIndex: Fast, in-memory storage with optional custom searcher
Usage ¶
Create and populate an index:
idx := index.NewInMemoryIndex()
tool := model.Tool{
Tool: mcp.Tool{
Name: "calculator",
Description: "Performs arithmetic operations",
},
Namespace: "math",
Tags: []string{"arithmetic", "math"},
}
backend := model.ToolBackend{
Kind: model.BackendKindMCP,
MCP: &model.MCPBackend{ServerName: "math-server"},
}
err := idx.RegisterTool(tool, backend)
Search for tools:
results, err := idx.Search("arithmetic", 10)
Pluggable Search ¶
The index accepts a custom Searcher for advanced search capabilities:
type MySearcher struct{}
func (s *MySearcher) Search(query string, limit int, docs []index.SearchDoc) ([]index.Summary, error) {
// Custom search implementation
}
idx := index.NewInMemoryIndex(index.WithSearcher(&MySearcher{}))
Progressive Disclosure ¶
Tools support progressive disclosure through Summary objects that contain only essential information for display and discovery:
- ID: Canonical tool identifier (namespace:name)
- Name: Tool name
- Namespace: Optional namespace for grouping
- ShortDescription: Truncated description (max 120 chars)
- Summary: Short summary (mirrors ShortDescription)
- Category: Optional category label
- InputModes: Supported input media types
- OutputModes: Supported output media types
- SecuritySummary: Short auth summary
- Tags: Associated tags for filtering
Pagination ¶
Search and list operations support cursor-based pagination:
results, nextCursor, err := idx.SearchPage("query", 10, "")
if nextCursor != "" {
moreResults, nextCursor, err = idx.SearchPage("query", 10, nextCursor)
}
Change Notifications ¶
The index supports change notifications for reactive updates:
unsub := idx.OnChange(func(event index.ChangeEvent) {
// Handle tool added/removed/updated
})
defer unsub()
Migration Note ¶
This package was migrated from github.com/jonwraymond/toolindex as part of the ApertureStack consolidation (PRD-130).
Package index provides a global registry and search layer for tools. It ingests model.Tool and model.ToolBackend and provides progressive discovery (summaries + namespaces) and canonical lookup by tool ID.
Index ¶
- Constants
- Variables
- func DefaultBackendSelector(backends []model.ToolBackend) model.ToolBackend
- type BackendSelector
- type ChangeEvent
- type ChangeListener
- type ChangeNotifier
- type ChangeType
- type DeterministicSearcher
- type InMemoryIndex
- func (idx *InMemoryIndex) GetAllBackends(id string) ([]model.ToolBackend, error)
- func (idx *InMemoryIndex) GetTool(id string) (model.Tool, model.ToolBackend, error)
- func (idx *InMemoryIndex) ListNamespaces() ([]string, error)
- func (idx *InMemoryIndex) ListNamespacesPage(limit int, cursor string) ([]string, string, error)
- func (idx *InMemoryIndex) OnChange(listener ChangeListener) func()
- func (idx *InMemoryIndex) Refresh() uint64
- func (idx *InMemoryIndex) RegisterTool(tool model.Tool, backend model.ToolBackend) error
- func (idx *InMemoryIndex) RegisterTools(regs []ToolRegistration) error
- func (idx *InMemoryIndex) RegisterToolsFromMCP(serverName string, tools []model.Tool) error
- func (idx *InMemoryIndex) Search(query string, limit int) ([]Summary, error)
- func (idx *InMemoryIndex) SearchPage(query string, limit int, cursor string) ([]Summary, string, error)
- func (idx *InMemoryIndex) UnregisterBackend(toolID string, kind model.BackendKind, backendID string) error
- func (idx *InMemoryIndex) Version() uint64
- type Index
- type IndexOptions
- type Refresher
- type SearchDoc
- type Searcher
- type Summary
- type ToolRegistration
Examples ¶
Constants ¶
const MaxShortDescriptionLen = 120
MaxShortDescriptionLen is the maximum length of the ShortDescription field in Summary.
Variables ¶
var ( ErrNotFound = errors.New("tool not found") ErrInvalidTool = errors.New("invalid tool") ErrInvalidBackend = errors.New("invalid backend") ErrInvalidCursor = errors.New("invalid cursor") ErrNonDeterministicSearcher = errors.New("searcher is non-deterministic") )
Error values for consistent error handling by callers.
Functions ¶
func DefaultBackendSelector ¶
func DefaultBackendSelector(backends []model.ToolBackend) model.ToolBackend
DefaultBackendSelector implements the default priority: local > provider > mcp. Exported so other modules (for example, toolrun) can match the same policy.
Types ¶
type BackendSelector ¶
type BackendSelector func([]model.ToolBackend) model.ToolBackend
BackendSelector is a function that selects the default backend from a list.
type ChangeEvent ¶
type ChangeEvent struct {
Type ChangeType
ToolID string
Backend model.ToolBackend
Version uint64
}
ChangeEvent captures a mutation in the index for reactive integration.
type ChangeListener ¶
type ChangeListener func(ChangeEvent)
ChangeListener receives change events from an Index implementation.
type ChangeNotifier ¶
type ChangeNotifier interface {
OnChange(listener ChangeListener) (unsubscribe func())
}
ChangeNotifier is an optional interface for receiving change events.
Contract: - OnChange must be safe for concurrent use. - It must return a non-nil unsubscribe function that is safe to call multiple times. - Passing a nil listener must return a no-op unsubscribe function.
type ChangeType ¶
type ChangeType string
ChangeType describes a mutation event in the index.
const ( ChangeRegistered ChangeType = "registered" ChangeUpdated ChangeType = "updated" ChangeBackendRemoved ChangeType = "backend_removed" ChangeToolRemoved ChangeType = "tool_removed" ChangeRefreshed ChangeType = "refreshed" )
type DeterministicSearcher ¶
DeterministicSearcher indicates whether a searcher provides deterministic ordering.
Contract: - Deterministic reports whether Search ordering is stable for identical inputs. - Implementations must not return true if ordering is non-deterministic.
type InMemoryIndex ¶
type InMemoryIndex struct {
// contains filtered or unexported fields
}
InMemoryIndex is the default in-memory implementation of Index.
func NewInMemoryIndex ¶
func NewInMemoryIndex(opts ...IndexOptions) *InMemoryIndex
NewInMemoryIndex creates a new in-memory tool index.
Example ¶
package main
import (
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/jonwraymond/tooldiscovery/index"
"github.com/jonwraymond/toolfoundation/model"
)
func main() {
idx := index.NewInMemoryIndex()
tool := model.Tool{
Tool: mcp.Tool{
Name: "search",
Description: "Search for files in the filesystem",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"query": map[string]any{"type": "string"},
},
},
},
Namespace: "files",
Tags: []string{"search", "filesystem"},
}
backend := model.NewMCPBackend("files-server")
_ = idx.RegisterTool(tool, backend)
results, _ := idx.Search("search", 10)
fmt.Println("Found:", len(results))
}
Output: Found: 1
func (*InMemoryIndex) GetAllBackends ¶
func (idx *InMemoryIndex) GetAllBackends(id string) ([]model.ToolBackend, error)
GetAllBackends returns all backends for a tool.
func (*InMemoryIndex) GetTool ¶
func (idx *InMemoryIndex) GetTool(id string) (model.Tool, model.ToolBackend, error)
GetTool returns the full tool and its default backend.
Example ¶
package main
import (
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/jonwraymond/tooldiscovery/index"
"github.com/jonwraymond/toolfoundation/model"
)
func main() {
idx := index.NewInMemoryIndex()
tool := model.Tool{
Tool: mcp.Tool{
Name: "read_file",
Description: "Read contents of a file",
InputSchema: map[string]any{"type": "object"},
},
Namespace: "files",
}
backend := model.NewMCPBackend("files-server")
_ = idx.RegisterTool(tool, backend)
// Retrieve the tool by its canonical ID
retrieved, retrievedBackend, err := idx.GetTool("files:read_file")
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Name:", retrieved.Name)
fmt.Println("Backend:", retrievedBackend.MCP.ServerName)
}
Output: Name: read_file Backend: files-server
func (*InMemoryIndex) ListNamespaces ¶
func (idx *InMemoryIndex) ListNamespaces() ([]string, error)
ListNamespaces returns all namespaces in alphabetical order.
Example ¶
package main
import (
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/jonwraymond/tooldiscovery/index"
"github.com/jonwraymond/toolfoundation/model"
)
func main() {
idx := index.NewInMemoryIndex()
// Register tools in different namespaces
namespaces := []string{"git", "docker", "kubectl"}
for _, ns := range namespaces {
tool := model.Tool{
Tool: mcp.Tool{
Name: "tool",
Description: "A tool",
InputSchema: map[string]any{"type": "object"},
},
Namespace: ns,
}
_ = idx.RegisterTool(tool, model.NewMCPBackend("server"))
}
// List all namespaces (alphabetically sorted)
nsList, _ := idx.ListNamespaces()
for _, ns := range nsList {
fmt.Println(ns)
}
}
Output: docker git kubectl
func (*InMemoryIndex) ListNamespacesPage ¶
ListNamespacesPage returns namespaces with cursor pagination.
func (*InMemoryIndex) OnChange ¶
func (idx *InMemoryIndex) OnChange(listener ChangeListener) func()
OnChange registers a listener for index mutations. Returns an unsubscribe function.
Example ¶
package main
import (
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/jonwraymond/tooldiscovery/index"
"github.com/jonwraymond/toolfoundation/model"
)
func main() {
idx := index.NewInMemoryIndex()
// Subscribe to changes
unsubscribe := idx.OnChange(func(event index.ChangeEvent) {
fmt.Printf("Event: %s for %s\n", event.Type, event.ToolID)
})
defer unsubscribe()
tool := model.Tool{
Tool: mcp.Tool{
Name: "my_tool",
Description: "A tool",
InputSchema: map[string]any{"type": "object"},
},
}
// This triggers a change event
_ = idx.RegisterTool(tool, model.NewMCPBackend("server"))
}
Output: Event: registered for my_tool
func (*InMemoryIndex) Refresh ¶
func (idx *InMemoryIndex) Refresh() uint64
Refresh rebuilds the search docs cache and emits a refresh event.
func (*InMemoryIndex) RegisterTool ¶
func (idx *InMemoryIndex) RegisterTool(tool model.Tool, backend model.ToolBackend) error
RegisterTool registers a single tool with its backend.
func (*InMemoryIndex) RegisterTools ¶
func (idx *InMemoryIndex) RegisterTools(regs []ToolRegistration) error
RegisterTools registers multiple tools in batch.
func (*InMemoryIndex) RegisterToolsFromMCP ¶
func (idx *InMemoryIndex) RegisterToolsFromMCP(serverName string, tools []model.Tool) error
RegisterToolsFromMCP is a convenience method for registering tools from an MCP server.
Example ¶
package main
import (
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/jonwraymond/tooldiscovery/index"
"github.com/jonwraymond/toolfoundation/model"
)
func main() {
idx := index.NewInMemoryIndex()
// Simulate receiving tools from an MCP server
mcpTools := []model.Tool{
{
Tool: mcp.Tool{
Name: "list_files",
Description: "List files in directory",
InputSchema: map[string]any{"type": "object"},
},
},
{
Tool: mcp.Tool{
Name: "read_file",
Description: "Read file contents",
InputSchema: map[string]any{"type": "object"},
},
},
}
// Register all tools from the MCP server
_ = idx.RegisterToolsFromMCP("filesystem-server", mcpTools)
results, _ := idx.Search("file", 10)
fmt.Println("File tools:", len(results))
}
Output: File tools: 2
func (*InMemoryIndex) Search ¶
func (idx *InMemoryIndex) Search(query string, limit int) ([]Summary, error)
Search performs a search over the indexed tools.
Example ¶
package main
import (
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/jonwraymond/tooldiscovery/index"
"github.com/jonwraymond/toolfoundation/model"
)
func main() {
idx := index.NewInMemoryIndex()
// Register multiple tools
tools := []model.Tool{
{
Tool: mcp.Tool{
Name: "git_status",
Description: "Show the working tree status",
InputSchema: map[string]any{"type": "object"},
},
Namespace: "git",
Tags: []string{"vcs"},
},
{
Tool: mcp.Tool{
Name: "git_commit",
Description: "Record changes to the repository",
InputSchema: map[string]any{"type": "object"},
},
Namespace: "git",
Tags: []string{"vcs"},
},
{
Tool: mcp.Tool{
Name: "docker_ps",
Description: "List running containers",
InputSchema: map[string]any{"type": "object"},
},
Namespace: "docker",
Tags: []string{"containers"},
},
}
backend := model.NewMCPBackend("dev-tools")
for _, tool := range tools {
_ = idx.RegisterTool(tool, backend)
}
// Search for git-related tools
results, _ := idx.Search("git", 10)
fmt.Println("Git tools found:", len(results))
// Search for containers
results, _ = idx.Search("containers", 10)
fmt.Println("Container tools found:", len(results))
}
Output: Git tools found: 2 Container tools found: 1
func (*InMemoryIndex) SearchPage ¶
func (idx *InMemoryIndex) SearchPage(query string, limit int, cursor string) ([]Summary, string, error)
SearchPage performs a search over the indexed tools with cursor pagination.
Example ¶
package main
import (
"fmt"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/jonwraymond/tooldiscovery/index"
"github.com/jonwraymond/toolfoundation/model"
)
func main() {
idx := index.NewInMemoryIndex()
// Register several tools
for i := 0; i < 5; i++ {
tool := model.Tool{
Tool: mcp.Tool{
Name: fmt.Sprintf("tool_%d", i),
Description: "A sample tool",
InputSchema: map[string]any{"type": "object"},
},
}
_ = idx.RegisterTool(tool, model.NewMCPBackend("server"))
}
// Paginate through results
var cursor string
page := 1
for {
results, nextCursor, _ := idx.SearchPage("", 2, cursor)
fmt.Printf("Page %d: %d results\n", page, len(results))
if nextCursor == "" {
break
}
cursor = nextCursor
page++
}
}
Output: Page 1: 2 results Page 2: 2 results Page 3: 1 results
func (*InMemoryIndex) UnregisterBackend ¶
func (idx *InMemoryIndex) UnregisterBackend(toolID string, kind model.BackendKind, backendID string) error
UnregisterBackend removes a specific backend from a tool. If the last backend is removed, the tool is also removed.
For provider backends, backendID must be in the format "providerID:toolID". For MCP backends, backendID is the server name. For local backends, backendID is the handler name.
func (*InMemoryIndex) Version ¶ added in v0.2.2
func (idx *InMemoryIndex) Version() uint64
Version returns the current index version.
type Index ¶
type Index interface {
// Registration
RegisterTool(tool model.Tool, backend model.ToolBackend) error
RegisterTools(regs []ToolRegistration) error
RegisterToolsFromMCP(serverName string, tools []model.Tool) error
// Unregistration
UnregisterBackend(toolID string, kind model.BackendKind, backendID string) error
// Lookup
GetTool(id string) (model.Tool, model.ToolBackend, error)
GetAllBackends(id string) ([]model.ToolBackend, error)
// Discovery
Search(query string, limit int) ([]Summary, error)
SearchPage(query string, limit int, cursor string) ([]Summary, string, error)
ListNamespaces() ([]string, error)
ListNamespacesPage(limit int, cursor string) ([]string, string, error)
}
Index defines the interface for a tool registry.
Contract:
- Concurrency: implementations must be safe for concurrent use.
- Errors: validation failures should return ErrInvalidTool/ErrInvalidBackend; missing tools/backends should return ErrNotFound; cursor issues should return ErrInvalidCursor; pagination with non-deterministic searchers should return ErrNonDeterministicSearcher. Callers must use errors.Is.
- Ownership: returned slices are caller-owned; elements are read-only and may be shared.
- Determinism: Search/List methods must return stable ordering for identical inputs.
- Nil/zero: empty inputs are treated as no-ops; SearchPage requires limit > 0.
- Atomicity: batch registration is not guaranteed to be atomic on error.
type IndexOptions ¶
type IndexOptions struct {
BackendSelector BackendSelector
Searcher Searcher
// RequireDeterministicSearcher enforces deterministic ordering for pagination.
// When true, SearchPage returns ErrNonDeterministicSearcher if the configured
// searcher does not declare deterministic ordering.
RequireDeterministicSearcher *bool
}
IndexOptions configures the behavior of an Index implementation.
type Refresher ¶
type Refresher interface {
Refresh() uint64
}
Refresher is an optional interface for forcing a refresh of cached search docs.
Contract: - Refresh returns a monotonic version for the search doc cache. - Refresh must be safe for concurrent use.
type SearchDoc ¶
type SearchDoc struct {
ID string // Canonical tool ID
DocText string // Lowercased concatenation of name/namespace/description/tags
Summary Summary // Prebuilt summary for fast return
}
SearchDoc is the internal/exported struct used by Searcher implementations. It contains precomputed search data for efficient querying.
type Searcher ¶
type Searcher interface {
// Search returns summaries ordered by relevance.
// When used with SearchPage, the ordering must be deterministic for the same
// docs/query input to guarantee cursor stability.
Search(query string, limit int, docs []SearchDoc) ([]Summary, error)
}
Searcher is the interface for search implementations.
Contract: - Concurrency: implementations should be safe for concurrent use or document otherwise. - Ownership: docs and summaries are read-only and must not be mutated. - Determinism: identical inputs must yield stable ordering; tie-break deterministically. - Nil/zero: limit <= 0 must return an empty result set with nil error.
type Summary ¶
type Summary struct {
ID string `json:"id"`
Name string `json:"name"`
Namespace string `json:"namespace,omitempty"`
ShortDescription string `json:"shortDescription,omitempty"`
Summary string `json:"summary,omitempty"`
Category string `json:"category,omitempty"`
InputModes []string `json:"inputModes,omitempty"`
OutputModes []string `json:"outputModes,omitempty"`
SecuritySummary string `json:"securitySummary,omitempty"`
Tags []string `json:"tags,omitempty"`
}
Summary represents a lightweight view of a tool for search results. It contains only the essential information for display and discovery, without the full schema payloads.
type ToolRegistration ¶
type ToolRegistration struct {
Tool model.Tool
Backend model.ToolBackend
}
ToolRegistration pairs a tool with its backend for batch registration.