Files
registry_mcp/tool_search.go
T
2026-05-09 13:29:32 +02:00

125 lines
3.4 KiB
Go

package main
import (
"context"
"encoding/json"
"github.com/mark3labs/mcp-go/mcp"
"fn-registry/registry"
)
type searchArgs struct {
Query string `json:"query"`
Kind string `json:"kind,omitempty"`
Lang string `json:"lang,omitempty"`
Domain string `json:"domain,omitempty"`
Purity string `json:"purity,omitempty"`
Limit int `json:"limit,omitempty"`
}
type searchHit struct {
ID string `json:"id"`
Name string `json:"name"`
Kind string `json:"kind"`
Lang string `json:"lang"`
Domain string `json:"domain"`
Purity string `json:"purity"`
Signature string `json:"signature,omitempty"`
Description string `json:"description"`
Algebraic string `json:"algebraic,omitempty"`
Entity string `json:"entity"` // "function" | "type"
}
func searchTool() mcp.Tool {
return mcp.NewTool("fn_search",
mcp.WithDescription("Search the registry (functions + types) via FTS5. Free-text query with optional filters. Returns up to `limit` hits ordered by name. Use this BEFORE writing new code to avoid duplicates."),
mcp.WithString("query",
mcp.Required(),
mcp.Description("FTS5 expression or free text. Examples: 'slice', 'name:filter*', 'description:\"single-page\"', 'sqlite OR fts5'."),
),
mcp.WithString("kind",
mcp.Description("Filter by kind: function, pipeline, component."),
mcp.Enum("function", "pipeline", "component"),
),
mcp.WithString("lang",
mcp.Description("Filter by language: go, py, bash, ts, cpp, ps."),
),
mcp.WithString("domain",
mcp.Description("Filter by domain: core, infra, finance, datascience, cybersecurity, shell, tui, pipelines, browser, ..."),
),
mcp.WithString("purity",
mcp.Description("Filter by purity (functions only)."),
mcp.Enum("pure", "impure"),
),
mcp.WithNumber("limit",
mcp.Description("Max hits returned (default 50)."),
mcp.Min(1),
mcp.Max(500),
),
)
}
func (d *deps) handleSearch(ctx context.Context, _ mcp.CallToolRequest, args searchArgs) (*mcp.CallToolResult, error) {
limit := args.Limit
if limit <= 0 {
limit = 50
}
q := sanitizeFTS5(args.Query)
fns, err := d.db.SearchFunctions(q, registry.Kind(args.Kind), registry.Purity(args.Purity), args.Lang, args.Domain)
if err != nil {
return mcp.NewToolResultError("search functions: " + err.Error()), nil
}
var hits []searchHit
for _, f := range fns {
hits = append(hits, searchHit{
ID: f.ID,
Name: f.Name,
Kind: string(f.Kind),
Lang: f.Lang,
Domain: f.Domain,
Purity: string(f.Purity),
Signature: f.Signature,
Description: f.Description,
Entity: "function",
})
if len(hits) >= limit {
break
}
}
// Types: only when no kind filter (kind applies only to functions).
if args.Kind == "" && len(hits) < limit {
ts, err := d.db.SearchTypes(q, args.Lang, args.Domain)
if err != nil {
return mcp.NewToolResultError("search types: " + err.Error()), nil
}
for _, t := range ts {
hits = append(hits, searchHit{
ID: t.ID,
Name: t.Name,
Lang: t.Lang,
Domain: t.Domain,
Algebraic: string(t.Algebraic),
Description: t.Description,
Entity: "type",
})
if len(hits) >= limit {
break
}
}
}
out := map[string]any{
"query": args.Query,
"count": len(hits),
"limit": limit,
"results": hits,
}
b, _ := json.MarshalIndent(out, "", " ")
return mcp.NewToolResultText(string(b)), nil
}