discovery

package
v0.6.7 Latest Latest
Warning

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

Go to latest
Published: Dec 19, 2025 License: MIT Imports: 5 Imported by: 0

README

Discovery Package

The discovery package provides tool discovery for MCP servers. It allows you to have a large tool library without sending all tool definitions to the LLM, reducing context window usage.

Overview

When you have many tools, sending all their definitions to an LLM consumes significant tokens. This package provides:

  1. Searchable Tools: Register tools that are hidden from tools/list but can be discovered via search
  2. Dynamic Tool Providers: Load tools from external sources (scripts, databases, APIs)
  3. Request-Scoped Providers: Per-user or per-tenant tools via context
  4. Fuzzy Search: Find tools by name or description using fuzzy matching
  5. execute_tool Wrapper: Call hidden tools through a visible tool (required for MCP client compatibility)

Installation

import "github.com/paularlott/mcp/discovery"

Quick Start

package main

import (
    "context"
    "log"
    "net/http"

    "github.com/paularlott/mcp"
    "github.com/paularlott/mcp/discovery"
)

func main() {
    // Create MCP server
    server := mcp.NewServer("my-server", "1.0.0")

    // Create tool registry
    registry := discovery.NewToolRegistry()

    // Register searchable tools (hidden from tools/list)
    registry.RegisterTool(
        mcp.NewTool("send_email", "Send an email to a recipient",
            mcp.String("to", "Recipient email", mcp.Required()),
            mcp.String("subject", "Email subject", mcp.Required()),
            mcp.String("body", "Email body", mcp.Required()),
        ),
        func(ctx context.Context, req *mcp.ToolRequest) (*mcp.ToolResponse, error) {
            to, _ := req.String("to")
            return mcp.NewToolResponseText("Email sent to " + to), nil
        },
        "email", "communication", "smtp", // keywords for search
    )

    // Attach registry to server (registers tool_search, execute_tool)
    registry.Attach(server)

    // Start server
    http.HandleFunc("/mcp", server.HandleRequest)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Core Types

ToolRegistry

The central registry for searchable tools and dynamic providers. ToolRegistry also implements the ToolProvider interface, so it can be used as a request-scoped provider:

registry := discovery.NewToolRegistry()

// Use as request-scoped provider
ctx := discovery.WithRequestProviders(r.Context(), registry)
ToolMetadata

Describes a tool for search and discovery:

type ToolMetadata struct {
    Name        string   // Unique tool name
    Description string   // Human-readable description
    Keywords    []string // Searchable keywords
}
ToolProvider

Interface for external tool sources:

type ToolProvider interface {
    // ListToolMetadata returns metadata for all tools from this provider
    ListToolMetadata(ctx context.Context) ([]ToolMetadata, error)

    // GetTool returns the full tool definition by name (nil, nil if not found)
    GetTool(ctx context.Context, name string) (*mcp.MCPTool, error)

    // CallTool executes a tool (ErrToolNotFound if not found)
    CallTool(ctx context.Context, name string, args map[string]interface{}) (*mcp.ToolResponse, error)
}

Registering Tools

registry.RegisterTool(
    tool,       // *mcp.ToolBuilder - tool definition with schema
    handler,    // func(context.Context, *mcp.ToolRequest) (*mcp.ToolResponse, error)
    keywords... // string variadic - searchable keywords
)

Example:

registry.RegisterTool(
    mcp.NewTool("sql_query", "Execute SQL queries",
        mcp.String("query", "SQL query to execute", mcp.Required()),
        mcp.String("database", "Database name"),
    ),
    func(ctx context.Context, req *mcp.ToolRequest) (*mcp.ToolResponse, error) {
        query, _ := req.String("query")
        return mcp.NewToolResponseText("Executed: " + query), nil
    },
    "database", "sql", "query", // keywords
)

Dynamic Tool Providers

Global Providers

Global providers are available to all requests:

type ScriptToolProvider struct {
    scripts map[string]*Script
}

func (p *ScriptToolProvider) ListToolMetadata(ctx context.Context) ([]discovery.ToolMetadata, error) {
    var metadata []discovery.ToolMetadata
    for _, script := range p.scripts {
        metadata = append(metadata, discovery.ToolMetadata{
            Name:        script.Name,
            Description: script.Description,
            Keywords:    script.Tags,
        })
    }
    return metadata, nil
}

func (p *ScriptToolProvider) GetTool(ctx context.Context, name string) (*mcp.MCPTool, error) {
    script, ok := p.scripts[name]
    if !ok {
        return nil, nil // not found
    }
    return &mcp.MCPTool{
        Name:        script.Name,
        Description: script.Description,
        InputSchema: script.Schema,
    }, nil
}

func (p *ScriptToolProvider) CallTool(ctx context.Context, name string, args map[string]interface{}) (*mcp.ToolResponse, error) {
    script, ok := p.scripts[name]
    if !ok {
        return nil, discovery.ErrToolNotFound
    }
    // Execute script...
    return mcp.NewToolResponseText("Script executed"), nil
}

// Register globally
registry.AddProvider(scriptProvider)
Request-Scoped Providers

For per-user or per-tenant tools, add providers to the request context:

// Middleware that adds user-specific tools
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    userID := r.Header.Get("X-User-ID")
    if userID != "" {
        userProvider := NewUserToolProvider(userID)
        ctx := discovery.WithRequestProviders(r.Context(), userProvider)
        r = r.WithContext(ctx)
    }
    server.HandleRequest(w, r)
})

http.Handle("/mcp", handler)

Discovery Tools

When you call registry.Attach(server), two tools are registered:

Search for available tools:

{
    "name": "tool_search",
    "arguments": {
        "query": "email",
        "max_results": 10
    }
}

Returns matching tools with names, descriptions, relevance scores, and full input schemas:

[
    {
        "name": "send_email",
        "description": "Send an email to a recipient",
        "score": 0.95,
        "input_schema": {
            "type": "object",
            "properties": {
                "to": {"type": "string", "description": "Recipient email"},
                "subject": {"type": "string", "description": "Email subject"},
                "body": {"type": "string", "description": "Email body"}
            },
            "required": ["to", "subject", "body"]
        }
    }
]
execute_tool

Execute a hidden tool (required for MCP client compatibility):

{
    "name": "execute_tool",
    "arguments": {
        "name": "send_email",
        "arguments": {
            "to": "user@example.com",
            "subject": "Hello",
            "body": "World"
        }
    }
}

Why execute_tool?

MCP clients validate tool names against the tools/list response before allowing tools/call. Since hidden tools are not in tools/list, they cannot be called directly. The execute_tool wrapper is a visible tool that proxies calls to hidden tools.

Search Algorithm

The search uses fuzzy matching with Levenshtein distance:

  1. Exact name match: Score 1.0
  2. Name prefix: Score 0.9
  3. Name contains: Score 0.8
  4. Keyword exact match: Score 0.85
  5. Keyword contains: Score 0.7
  6. Description word match: Score 0.6
  7. Description contains: Score 0.5
  8. Fuzzy matches: Scaled by similarity

Results are sorted by score and limited to the requested max results (default: 10).

Thread Safety

The ToolRegistry is thread-safe. All operations (registering tools, adding providers, searching, calling) can be performed concurrently from multiple goroutines.

Example

See examples/tool-discovery for a complete working example with:

  • Searchable tools for various domains (database, email, documents)
  • Request-scoped user tool provider
  • HTTP middleware for context injection

Documentation

Overview

Package discovery provides tool discovery functionality for MCP servers. This package allows you to register tools that are hidden from the main tools/list response but can be discovered via search and executed through a wrapper tool.

This is useful when you have many tools and want to reduce context window usage. Instead of sending all tool definitions to the LLM upfront, you can: 1. Register essential tools normally with the MCP server 2. Register specialized tools with a ToolRegistry 3. Attach the registry to the server - it registers tool_search and execute_tool

The workflow for LLMs becomes:

  1. tool_search(query="email") -> finds tools with full schemas
  2. execute_tool(name="send_email", arguments={...}) -> executes the tool

Index

Constants

This section is empty.

Variables

View Source
var ErrToolNotFound = mcp.ErrUnknownTool

ErrToolNotFound is returned when a tool is not found

Functions

func WithRequestProviders

func WithRequestProviders(ctx context.Context, providers ...ToolProvider) context.Context

WithRequestProviders adds request-scoped tool providers to the context. These providers are only available for the duration of the request. Use this for per-user or per-tenant tool providers.

Types

type SearchResult

type SearchResult struct {
	Name        string      `json:"name"`
	Description string      `json:"description"`
	Score       float64     `json:"score"`
	InputSchema interface{} `json:"inputSchema,omitempty"`
}

SearchResult represents a matched tool from a search

type ToolMetadata

type ToolMetadata struct {
	Name        string   `json:"name"`
	Description string   `json:"description"`
	Keywords    []string `json:"keywords,omitempty"`
}

ToolMetadata contains searchable information about a tool

type ToolProvider

type ToolProvider interface {
	// ListToolMetadata returns metadata for all searchable tools from this provider
	ListToolMetadata(ctx context.Context) ([]ToolMetadata, error)

	// GetTool returns the full tool definition for a specific tool by name
	// Returns nil, nil if the tool doesn't exist in this provider
	GetTool(ctx context.Context, name string) (*mcp.MCPTool, error)

	// CallTool executes a tool by name with the given arguments
	// Returns ErrToolNotFound if the tool doesn't exist in this provider
	CallTool(ctx context.Context, name string, args map[string]interface{}) (*mcp.ToolResponse, error)
}

ToolProvider allows external tool sources (scripts, plugins, databases, etc.)

type ToolRegistry

type ToolRegistry struct {
	// contains filtered or unexported fields
}

ToolRegistry manages searchable tools and provides discovery functionality. Tools registered here are hidden from tools/list but can be discovered via search. Create one instance and attach it to your MCP server.

func NewToolRegistry

func NewToolRegistry() *ToolRegistry

NewToolRegistry creates a new tool registry for searchable tools

func (*ToolRegistry) AddProvider

func (r *ToolRegistry) AddProvider(provider ToolProvider)

AddProvider adds a dynamic tool provider

func (*ToolRegistry) Attach

func (r *ToolRegistry) Attach(server *mcp.Server)

Attach registers the discovery tools (tool_search, execute_tool) with the MCP server

func (*ToolRegistry) CallTool

func (r *ToolRegistry) CallTool(ctx context.Context, name string, args map[string]interface{}) (*mcp.ToolResponse, error)

CallTool attempts to call a registered or dynamic tool

func (*ToolRegistry) GetTool

func (r *ToolRegistry) GetTool(ctx context.Context, name string) (*mcp.MCPTool, error)

GetTool retrieves a registered or dynamic tool by name

func (*ToolRegistry) ListToolMetadata

func (r *ToolRegistry) ListToolMetadata(ctx context.Context) ([]ToolMetadata, error)

ListToolMetadata returns metadata for all tools registered in this registry. This implements the ToolProvider interface, allowing a ToolRegistry to be used as a request-scoped provider via WithRequestProviders.

func (*ToolRegistry) RegisterTool

func (r *ToolRegistry) RegisterTool(tool *mcp.ToolBuilder, handler mcp.ToolHandler, keywords ...string)

RegisterTool registers a searchable tool that won't appear in ListTools but can be discovered and called. Keywords are used for fuzzy search matching.

func (*ToolRegistry) RemoveProvider

func (r *ToolRegistry) RemoveProvider(provider ToolProvider)

RemoveProvider removes a dynamic tool provider

func (*ToolRegistry) Search

func (r *ToolRegistry) Search(ctx context.Context, query string, maxResults int) []SearchResult

Search performs fuzzy search across all registered and dynamic tools. If query is empty, returns all tools.

Jump to

Keyboard shortcuts

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