From 51567a0bdfd403cb1a6a81106eb87c6471a13c93 Mon Sep 17 00:00:00 2001 From: Deluan Date: Sat, 19 Apr 2025 11:59:34 -0400 Subject: [PATCH] feat: add proof-of-concept MCP agent Add a new agent implementation MCPAgent in core/agents/mcp.\n\nThis agent interacts with an external Model Context Protocol (MCP) server\nusing the github.com/metoro-io/mcp-golang library via stdio.\n\nIt currently implements the ArtistBiographyRetriever interface to fetch\nbiographies by calling the 'get_artist_biography' tool on the MCP server.\nThe path to the MCP server executable is hardcoded for this PoC.\n\nIncludes basic Ginkgo test setup for the new agent. --- core/agents/mcp/mcp_agent.go | 139 ++++++++++++++++++++++++++++++ core/agents/mcp/mcp_agent_test.go | 55 ++++++++++++ core/agents/mcp/mcp_suite_test.go | 17 ++++ core/external/provider.go | 1 + go.mod | 11 +++ go.sum | 25 ++++++ 6 files changed, 248 insertions(+) create mode 100644 core/agents/mcp/mcp_agent.go create mode 100644 core/agents/mcp/mcp_agent_test.go create mode 100644 core/agents/mcp/mcp_suite_test.go diff --git a/core/agents/mcp/mcp_agent.go b/core/agents/mcp/mcp_agent.go new file mode 100644 index 000000000..31c67422e --- /dev/null +++ b/core/agents/mcp/mcp_agent.go @@ -0,0 +1,139 @@ +package mcp + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" + + mcp "github.com/metoro-io/mcp-golang" + "github.com/metoro-io/mcp-golang/transport/stdio" + + "github.com/navidrome/navidrome/core/agents" + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/model" +) + +const ( + mcpAgentName = "mcp" + // Hardcoded path for PoC + mcpServerPath = "/Users/deluan/Development/navidrome/plugins-mcp/mcp-server" + mcpToolName = "get_artist_biography" +) + +// MCPAgent interacts with an external MCP server for metadata retrieval. +type MCPAgent struct { + // ds model.DataStore // Not needed for this PoC +} + +func mcpConstructor(ds model.DataStore) agents.Interface { + // Check if the MCP server executable exists + if _, err := os.Stat(mcpServerPath); os.IsNotExist(err) { + log.Warn("MCP server executable not found, disabling agent", "path", mcpServerPath, "error", err) + return nil + } + log.Info("MCP Agent initialized", "serverPath", mcpServerPath) + return &MCPAgent{} +} + +func (a *MCPAgent) AgentName() string { + return mcpAgentName +} + +// getArtistBiographyArgs defines the structure for the MCP tool arguments. +// IMPORTANT: Field names MUST be exported (start with uppercase) for JSON marshalling, +// but the `json` tags determine the actual field names sent over MCP. +type getArtistBiographyArgs struct { + ID string `json:"id"` + Name string `json:"name"` + Mbid string `json:"mbid,omitempty"` // Use omitempty if MBID might be absent +} + +// GetArtistBiography retrieves the artist biography by calling the external MCP server. +func (a *MCPAgent) GetArtistBiography(ctx context.Context, id, name, mbid string) (string, error) { + log.Debug(ctx, "Calling MCP agent GetArtistBiography", "id", id, "name", name, "mbid", mbid) + + // Prepare command with context for cancellation handling + cmd := exec.CommandContext(ctx, mcpServerPath) + + // Get pipes for stdin and stdout + stdin, err := cmd.StdinPipe() + if err != nil { + log.Error(ctx, "Failed to get stdin pipe for MCP server", "error", err) + return "", fmt.Errorf("failed to get stdin pipe: %w", err) + } + stdout, err := cmd.StdoutPipe() + if err != nil { + log.Error(ctx, "Failed to get stdout pipe for MCP server", "error", err) + return "", fmt.Errorf("failed to get stdout pipe: %w", err) + } + // Capture stderr for debugging, but don't block on it + var stderr strings.Builder + cmd.Stderr = &stderr + + // Start the MCP server process + if err := cmd.Start(); err != nil { + log.Error(ctx, "Failed to start MCP server process", "path", mcpServerPath, "error", err) + return "", fmt.Errorf("failed to start MCP server: %w", err) + } + // Ensure the process is killed eventually + defer func() { + if err := cmd.Process.Kill(); err != nil { + log.Error(ctx, "Failed to kill MCP server process", "pid", cmd.Process.Pid, "error", err) + } else { + log.Debug(ctx, "Killed MCP server process", "pid", cmd.Process.Pid) + } + // Log stderr if it contains anything + if stderr.Len() > 0 { + log.Warn(ctx, "MCP server stderr output", "pid", cmd.Process.Pid, "stderr", stderr.String()) + } + }() + + log.Debug(ctx, "MCP server process started", "pid", cmd.Process.Pid) + + // Create and initialize the MCP client + transport := stdio.NewStdioServerTransportWithIO(stdout, stdin) + client := mcp.NewClient(transport) + + if _, err := client.Initialize(ctx); err != nil { + log.Error(ctx, "Failed to initialize MCP client", "error", err) + return "", fmt.Errorf("failed to initialize MCP client: %w", err) + } + log.Debug(ctx, "MCP client initialized") + + // Prepare arguments for the tool call + args := getArtistBiographyArgs{ + ID: id, + Name: name, + Mbid: mbid, + } + + // Call the tool + log.Debug(ctx, "Calling MCP tool", "tool", mcpToolName, "args", args) + response, err := client.CallTool(ctx, mcpToolName, args) + if err != nil { + log.Error(ctx, "Failed to call MCP tool", "tool", mcpToolName, "error", err) + return "", fmt.Errorf("failed to call MCP tool '%s': %w", mcpToolName, err) + } + + // Process the response + if response == nil || len(response.Content) == 0 || response.Content[0].TextContent == nil { + log.Warn(ctx, "MCP tool returned empty or invalid response", "tool", mcpToolName) + return "", agents.ErrNotFound // Or a more specific error? + } + + bio := response.Content[0].TextContent.Text + log.Debug(ctx, "Received biography from MCP agent", "tool", mcpToolName, "bioLength", len(bio)) + + // Return the biography text + return bio, nil +} + +// Ensure MCPAgent implements the required interface +var _ agents.ArtistBiographyRetriever = (*MCPAgent)(nil) + +func init() { + agents.Register(mcpAgentName, mcpConstructor) + log.Info("Registered MCP Agent") +} diff --git a/core/agents/mcp/mcp_agent_test.go b/core/agents/mcp/mcp_agent_test.go new file mode 100644 index 000000000..e6485da87 --- /dev/null +++ b/core/agents/mcp/mcp_agent_test.go @@ -0,0 +1,55 @@ +package mcp_test + +import ( + "context" + + "github.com/navidrome/navidrome/core/agents" + "github.com/navidrome/navidrome/core/agents/mcp" + "github.com/navidrome/navidrome/tests" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("MCPAgent", func() { + var ( + ctx context.Context + // ds model.DataStore // Not needed yet for PoC + agent agents.ArtistBiographyRetriever + ) + + BeforeEach(func() { + ctx = context.Background() + // Use ctx to avoid unused variable error + _ = ctx + ds := &tests.MockDataStore{} + // Use ds to avoid unused variable error + _ = ds + // Directly instantiate for now, assuming constructor logic is minimal for PoC + // In a real scenario, you might use the constructor or mock dependencies + + // The constructor is not exported, we need to access it differently or make it testable. + // For PoC, let's assume we can get an instance. We might need to adjust mcp_agent.go later + // For now, comment out the direct constructor call for simplicity in test setup phase. + // constructor := mcp.mcpConstructor // This won't work as it's unexported + + // Placeholder: Create a simple MCPAgent instance directly for testing its existence. + // This bypasses the constructor logic (like the file check), which is fine for a basic test. + agent = &mcp.MCPAgent{} + + Expect(agent).NotTo(BeNil()) + }) + + It("should be created", func() { + Expect(agent).NotTo(BeNil()) + }) + + // TODO: Add PoC test case that calls GetArtistBiography + // This will likely require the actual MCP server to be running + // or mocking the exec.Command part. + It("should call GetArtistBiography (placeholder)", func() { + // bio, err := agent.GetArtistBiography(ctx, "artist-id", "Artist Name", "mbid-123") + // Expect(err).ToNot(HaveOccurred()) + // Expect(bio).ToNot(BeEmpty()) + Skip("Skipping actual MCP call for initial PoC test setup") + }) +}) diff --git a/core/agents/mcp/mcp_suite_test.go b/core/agents/mcp/mcp_suite_test.go new file mode 100644 index 000000000..f43960ac8 --- /dev/null +++ b/core/agents/mcp/mcp_suite_test.go @@ -0,0 +1,17 @@ +package mcp_test + +import ( + "testing" + + "github.com/navidrome/navidrome/log" + "github.com/navidrome/navidrome/tests" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMCPAgent(t *testing.T) { + tests.Init(t, false) + log.SetLevel(log.LevelFatal) + RegisterFailHandler(Fail) + RunSpecs(t, "MCP Agent Test Suite") +} diff --git a/core/external/provider.go b/core/external/provider.go index f27ded11b..1a62b423c 100644 --- a/core/external/provider.go +++ b/core/external/provider.go @@ -13,6 +13,7 @@ import ( "github.com/navidrome/navidrome/core/agents" _ "github.com/navidrome/navidrome/core/agents/lastfm" _ "github.com/navidrome/navidrome/core/agents/listenbrainz" + _ "github.com/navidrome/navidrome/core/agents/mcp" _ "github.com/navidrome/navidrome/core/agents/spotify" "github.com/navidrome/navidrome/log" "github.com/navidrome/navidrome/model" diff --git a/go.mod b/go.mod index d907aea0e..7229553f6 100644 --- a/go.mod +++ b/go.mod @@ -68,7 +68,9 @@ require ( require ( github.com/aymerick/douceur v0.2.0 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect github.com/cespare/reflex v0.3.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/creack/pty v1.1.11 // indirect @@ -85,6 +87,7 @@ require ( github.com/gorilla/css v1.0.1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/jsonschema v0.12.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect @@ -96,9 +99,12 @@ require ( github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/option v1.0.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/metoro-io/mcp-golang v0.11.0 // indirect github.com/mfridman/interpolate v0.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ogier/pflag v0.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.62.0 // indirect @@ -113,6 +119,11 @@ require ( github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/go.sum b/go.sum index 864c9fd32..ffc354925 100644 --- a/go.sum +++ b/go.sum @@ -8,12 +8,16 @@ github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kk github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/cespare/reflex v0.3.1 h1:N4Y/UmRrjwOkNT0oQQnYsdr6YBxvHqtSfPB4mqOyAKk= github.com/cespare/reflex v0.3.1/go.mod h1:I+0Pnu2W693i7Hv6ZZG76qHTY0mgUa7uCIfCtikXojE= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -104,8 +108,11 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI= +github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc= github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX60= @@ -142,12 +149,16 @@ github.com/lestrrat-go/jwx/v2 v2.1.4 h1:uBCMmJX8oRZStmKuMMOFb0Yh9xmEMgNJLgjuKKt4 github.com/lestrrat-go/jwx/v2 v2.1.4/go.mod h1:nWRbDFR1ALG2Z6GJbBXzfQaYyvn751KuuyySN2yR6is= github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU= github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE= github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU= github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/metoro-io/mcp-golang v0.11.0 h1:1k+VSE9QaeMTLn0gJ3FgE/DcjsCBsLFnz5eSFbgXUiI= +github.com/metoro-io/mcp-golang v0.11.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= @@ -167,6 +178,8 @@ github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -234,8 +247,20 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbWU= github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=