We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/safedep/vet'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
package ai
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/jedib0t/go-pretty/v6/table"
"github.com/spf13/cobra"
"github.com/safedep/vet/pkg/aitool"
"github.com/safedep/vet/pkg/common/logger"
)
var (
discoverScopes []string
discoverProjectDir string
discoverReportJSON string
discoverSilent bool
)
func newDiscoverCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "discover",
Short: "Discover AI tools usage, MCP servers, and coding agents",
RunE: func(cmd *cobra.Command, args []string) error {
redirectLogOutput(cmd)
return runDiscover(cmd.Context())
},
}
cmd.Flags().StringArrayVar(&discoverScopes, "scope", nil, "Limit to specific scopes (system, project); repeatable, empty for all")
cmd.Flags().StringVarP(&discoverProjectDir, "project-dir", "D", "", "Project root for project-level discovery (default: cwd)")
cmd.Flags().StringVar(&discoverReportJSON, "report-json", "", "Write JSON inventory to file")
cmd.Flags().BoolVarP(&discoverSilent, "silent", "s", false, "Suppress default summary output")
return cmd
}
func runDiscover(ctx context.Context) error {
projectDir := discoverProjectDir
if projectDir == "" {
var err error
projectDir, err = os.Getwd()
if err != nil {
return fmt.Errorf("failed to get working directory: %w", err)
}
}
scope, err := buildDiscoveryScope(discoverScopes)
if err != nil {
return err
}
config := aitool.DiscoveryConfig{
ProjectDir: projectDir,
Scope: scope,
}
registry := aitool.DefaultRegistry()
inventory := aitool.NewAIToolInventory()
err = registry.Discover(ctx, config, func(tool *aitool.AITool) error {
inventory.Add(tool)
return nil
})
if err != nil {
return err
}
if !discoverSilent {
printSummaryTable(inventory)
}
if discoverReportJSON != "" {
if err := writeJSONInventory(inventory, discoverReportJSON); err != nil {
return fmt.Errorf("failed to write JSON report: %w", err)
}
}
return nil
}
func printSummaryTable(inventory *aitool.AIToolInventory) {
apps := inventory.GroupByApp()
fmt.Fprintf(os.Stderr, "\nDiscovered %d AI tool usage(s) across %d app(s)\n\n",
len(inventory.Tools), len(apps))
if len(inventory.Tools) == 0 {
return
}
tbl := table.NewWriter()
tbl.SetOutputMirror(os.Stderr)
tbl.SetStyle(table.StyleLight)
tbl.AppendHeader(table.Row{"TYPE", "NAME", "APP", "SCOPE", "DETAIL"})
for _, tool := range inventory.Tools {
detail := toolDetail(tool)
tbl.AppendRow(table.Row{
tool.Type.DisplayName(),
tool.Name,
tool.AppDisplay,
tool.Scope.DisplayName(),
detail,
})
}
tbl.Render()
fmt.Fprintln(os.Stderr)
}
func toolDetail(tool *aitool.AITool) string {
switch tool.Type {
case aitool.AIToolTypeMCPServer:
if tool.MCPServer != nil {
if tool.MCPServer.Command != "" {
detail := string(tool.MCPServer.Transport) + ": " + tool.MCPServer.Command
if len(tool.MCPServer.Args) > 0 {
for _, arg := range tool.MCPServer.Args {
detail += " " + arg
}
}
return detail
}
if tool.MCPServer.URL != "" {
return string(tool.MCPServer.Transport) + ": " + tool.MCPServer.URL
}
}
return tool.ConfigPath
case aitool.AIToolTypeCLITool:
version := tool.GetMetaString("binary.version")
path := tool.GetMetaString("binary.path")
if version != "" {
return path + " v" + version
}
return path
case aitool.AIToolTypeAIExtension:
id := tool.GetMetaString("extension.id")
version := tool.GetMetaString("extension.version")
ide := tool.GetMetaString("extension.ide")
detail := id
if version != "" {
detail += " v" + version
}
if ide != "" {
detail += " (" + ide + ")"
}
return detail
case aitool.AIToolTypeProjectConfig:
if tool.Agent != nil && len(tool.Agent.InstructionFiles) > 0 {
return strings.Join(fileBaseNames(tool.Agent.InstructionFiles), ", ")
}
return tool.ConfigPath
default:
return tool.ConfigPath
}
}
func writeJSONInventory(inventory *aitool.AIToolInventory, path string) error {
data, err := json.MarshalIndent(inventory, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0o644)
}
func redirectLogOutput(cmd *cobra.Command) {
logFile, _ := cmd.Root().PersistentFlags().GetString("log")
if logFile == "-" {
logger.MigrateTo(os.Stdout)
} else if logFile != "" {
logger.LogToFile(logFile)
} else {
logger.MigrateTo(io.Discard)
}
}
func fileBaseNames(paths []string) []string {
names := make([]string, len(paths))
for i, p := range paths {
names[i] = filepath.Base(p)
}
return names
}
// buildDiscoveryScope converts CLI --scope flag values to a DiscoveryScope.
// Returns nil (all scopes) when no scopes are specified.
func buildDiscoveryScope(scopes []string) (*aitool.DiscoveryScope, error) {
if len(scopes) == 0 {
return nil, nil
}
parsed := make([]aitool.AIToolScope, len(scopes))
for i, s := range scopes {
parsed[i] = aitool.AIToolScope(s)
}
return aitool.NewDiscoveryScope(parsed...)
}