Skip to main content
Glama
tool.go6.97 kB
package _default import ( "bytes" "context" "embed" "fmt" "sync" "text/template" "time" "github.com/distribution/reference" "github.com/inspektor-gadget/inspektor-gadget/pkg/gadget-service/api" metadatav1 "github.com/inspektor-gadget/inspektor-gadget/pkg/metadata/v1" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "gopkg.in/yaml.v3" "github.com/inspektor-gadget/ig-mcp-server/pkg/cache" "github.com/inspektor-gadget/ig-mcp-server/pkg/discoverer" "github.com/inspektor-gadget/ig-mcp-server/pkg/gadgetmanager" ) //go:embed templates var templates embed.FS type gadgetInfoResult struct { img string info *api.GadgetInfo err error } func GetTools(ctx context.Context, mgr gadgetmanager.GadgetManager, env string, gadgets []discoverer.Gadget) []server.ServerTool { // load cache version, err := mgr.GetVersion() if err != nil { log.Warn("Could not get gadget manager version, proceeding without cache", "error", err) } cachedInfos, err := cache.LoadCache(version, env) if err != nil { log.Debug("No valid cache found, proceeding without cache", "error", err) } if len(cachedInfos) == 0 { log.Info("Fetching gadget information without cache. Initial load may take several seconds.") } // prepare tools gadgetInfos := fetchGadgetInfosConcurrently(ctx, mgr, gadgets, cachedInfos) tools := buildToolsFromGadgetInfos(env, mgr, gadgetInfos) // save cache if needed if len(cachedInfos) != len(gadgetInfos) { err = cache.SaveCache(version, env, gadgetInfos) if err != nil { log.Warn("Could not save cache", "error", err) } } return tools } func fetchGadgetInfosConcurrently(ctx context.Context, mgr gadgetmanager.GadgetManager, gadgets []discoverer.Gadget, cachedInfos map[string]*api.GadgetInfo) map[string]*api.GadgetInfo { const maxConcurrency = 10 sem := make(chan struct{}, maxConcurrency) var wg sync.WaitGroup resultsChan := make(chan gadgetInfoResult, len(gadgets)) // Start goroutines to fetch gadget info for _, gadget := range gadgets { select { case <-ctx.Done(): log.Warn("Context cancelled, stopping gadget info fetch") return nil default: } wg.Add(1) sem <- struct{}{} go fetchSingleGadgetInfo(ctx, mgr, versionedImage(mgr, gadget.Image), cachedInfos, &wg, sem, resultsChan) } // Close results channel when all goroutines complete go func() { wg.Wait() close(resultsChan) }() // Collect results gadgetInfos := make(map[string]*api.GadgetInfo) for result := range resultsChan { if result.err != nil { log.Warn("Skipping gadget image due to error", "image", result.img, "error", result.err) continue } gadgetInfos[result.img] = result.info } return gadgetInfos } func versionedImage(mgr gadgetmanager.GadgetManager, img string) string { version, err := mgr.GetVersion() if err != nil { return img } named, err := reference.ParseNamed(img) if err != nil { return img } return named.Name() + ":v" + version } func fetchSingleGadgetInfo(ctx context.Context, mgr gadgetmanager.GadgetManager, image string, cachedInfos map[string]*api.GadgetInfo, wg *sync.WaitGroup, sem chan struct{}, resultsChan chan gadgetInfoResult) { defer func() { wg.Done() <-sem }() // Check cache first if cachedInfos != nil { if cachedInfo, ok := cachedInfos[image]; ok { log.Debug("Using cached gadget info", "image", image) resultsChan <- gadgetInfoResult{img: image, info: cachedInfo, err: nil} return } } // Fetch with retries info, err := fetchGadgetInfoWithRetries(ctx, mgr, image) resultsChan <- gadgetInfoResult{img: image, info: info, err: err} } func fetchGadgetInfoWithRetries(ctx context.Context, mgr gadgetmanager.GadgetManager, image string) (*api.GadgetInfo, error) { const maxRetries = 3 const retryDelay = 2 * time.Second for attempt := 0; attempt < maxRetries; attempt++ { info, err := mgr.GetInfo(ctx, image) if err == nil { return info, nil } log.Warn("Failed to get gadget info, retrying", "image", image, "attempt", attempt+1, "error", err) if attempt < maxRetries-1 { time.Sleep(retryDelay) } } return nil, fmt.Errorf("failed to get gadget info after %d attempts", maxRetries) } func buildToolsFromGadgetInfos(env string, mgr gadgetmanager.GadgetManager, gadgetInfos map[string]*api.GadgetInfo) []server.ServerTool { var tools []server.ServerTool for image, info := range gadgetInfos { tool, err := gadgetsTool(env, info) if err != nil { log.Warn("Skipping gadget due to error creating tool", "image", image, "error", err) continue } handler := gadgetHandler(mgr, info) serverTool := server.ServerTool{ Tool: tool, Handler: handler, } tools = append(tools, serverTool) } return tools } func gadgetsTool(env string, info *api.GadgetInfo) (mcp.Tool, error) { var metadata metadatav1.GadgetMetadata err := yaml.Unmarshal(info.Metadata, &metadata) if err != nil { return mcp.Tool{}, fmt.Errorf("unmarshalling gadget metadata: %w", err) } description, err := generateToolDescription(env, &metadata, info) if err != nil { return mcp.Tool{}, fmt.Errorf("generating tool description: %w", err) } toolParams := make(map[string]interface{}) for _, p := range info.Params { toolParams[p.Prefix+p.Key] = map[string]interface{}{ "type": "string", "description": p.Description, } } tool := createMCPTool(metadata.Name, description, toolParams) return tool, nil } func generateToolDescription(env string, metadata *metadatav1.GadgetMetadata, info *api.GadgetInfo) (string, error) { tmpl, err := template.ParseFS(templates, "templates/toolDescription.tmpl") if err != nil { return "", fmt.Errorf("parsing template: %w", err) } var fields []FieldData // TODO: Support multiple data sources if len(info.DataSources) > 0 { for _, field := range info.DataSources[0].Fields { fields = append(fields, FieldData{ Name: field.FullName, Description: field.Annotations[metadatav1.DescriptionAnnotation], PossibleValues: field.Annotations[metadatav1.ValueOneOfAnnotation], }) } } toolData := ToolData{ Name: normalizeToolName(metadata.Name), Description: metadata.Description, Environment: env, Fields: fields, } var out bytes.Buffer if err = tmpl.Execute(&out, toolData); err != nil { return "", fmt.Errorf("executing template for gadget %s: %w", info.ImageName, err) } return out.String(), nil } func createMCPTool(name, description string, params map[string]interface{}) mcp.Tool { opts := []mcp.ToolOption{ mcp.WithDescription(description), mcp.WithReadOnlyHintAnnotation(true), mcp.WithObject("params", mcp.Required(), mcp.Description("key-value pairs of parameters to pass to the gadget"), mcp.Properties(params), ), mcp.WithNumber("duration", mcp.Description("Duration in seconds to run the gadget. Use 0 to run in background/continuously."), ), } return mcp.NewTool(normalizeToolName(name), opts...) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/inspektor-gadget/ig-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server