diff --git a/README.md b/README.md index af67d23..c02fce4 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ argocd-mcp-server --transport=http --argocd-url= --argocd-token= --d Or start the Argo CD MCP server as a container after running `task build-image`: ```bash -podman run -d --name argocd-mcp-server --transport http -e ARGOCD_MCP_URL= -e ARGOCD_MCP_TOKEN= -e ARGOCD_MCP_DEBUG= -p 8080:8080 argocd-mcp-server:latest +podman run -d --name argocd-mcp-server --transport http -e ARGOCD_MCP_SERVER_LISTEN_HOST=0.0.0.0 -e ARGOCD_MCP_URL= -e ARGOCD_MCP_TOKEN= -e ARGOCD_MCP_DEBUG= -p 8080:8080 argocd-mcp-server:latest ``` Edit your `~/.cursor/mcp.json` file with the following contents: diff --git a/cmd/start_server.go b/cmd/start_server.go index c3338cf..e3c9463 100644 --- a/cmd/start_server.go +++ b/cmd/start_server.go @@ -12,6 +12,7 @@ import ( "github.com/codeready-toolchain/argocd-mcp-server/internal/argocd" "github.com/codeready-toolchain/argocd-mcp-server/internal/server" "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/spf13/cobra" ) @@ -58,7 +59,7 @@ var startServerCmd = &cobra.Command{ logger := slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{ Level: lvl, })) - logger.Info("starting the Argo CD MCP server", "transport", transport, "url", argocdURL, "insecure", argocdInsecure, "debug", debug) + logger.Info("starting the Argo CD MCP server", "transport", transport, "argocd-url", argocdURL, "insecure", argocdInsecure, "debug", debug) if debug { lvl.Set(slog.LevelDebug) logger.Debug("debug mode enabled") @@ -76,11 +77,16 @@ var startServerCmd = &cobra.Command{ } default: mux := http.NewServeMux() + // MCP endpoint mux.Handle("/mcp", mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return srv }, nil)) - // HealthCheck endpoint. + + // Metrics endpoint + mux.Handle("/metrics", promhttp.Handler()) + + // HealthCheck endpoint mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]string{ //nolint:errcheck diff --git a/compose.yaml b/compose.yaml index 40abe27..1eb0f17 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,6 +1,5 @@ name: argocd-mcp-server-e2e -version: '3.8' services: argocd-server-mock: image: argocd-server-mock diff --git a/go.mod b/go.mod index cb426fb..51618b9 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,16 @@ go 1.24.11 require ( github.com/argoproj/argo-cd/v3 v3.1.11 github.com/argoproj/gitops-engine v0.7.1-0.20250905160054-e48120133eec + github.com/codeready-toolchain/toolchain-e2e v0.0.0-20260112152755-eb77a8e22ccb github.com/google/jsonschema-go v0.3.0 github.com/h2non/gock v1.2.0 github.com/modelcontextprotocol/go-sdk v1.2.0 + github.com/prometheus/client_golang v1.22.0 github.com/spf13/cobra v1.10.1 github.com/spf13/pflag v1.0.9 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.0 k8s.io/apimachinery v0.33.6 + k8s.io/client-go v0.33.6 ) require ( @@ -103,9 +106,8 @@ require ( github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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_golang v1.22.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.64.0 // indirect + github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/redis/go-redis/v9 v9.8.0 // indirect github.com/robfig/cron/v3 v3.0.2-0.20210106135023-bc59245fe10e // indirect @@ -122,6 +124,8 @@ require ( github.com/yosida95/uritemplate/v3 v3.0.2 // indirect go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.3 // indirect golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect @@ -139,17 +143,16 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.33.6 // indirect - k8s.io/apiextensions-apiserver v0.33.1 // indirect + k8s.io/apiextensions-apiserver v0.33.4 // indirect k8s.io/apiserver v0.33.6 // indirect k8s.io/cli-runtime v0.33.6 // indirect - k8s.io/client-go v0.33.6 // indirect k8s.io/component-base v0.33.6 // indirect k8s.io/component-helpers v0.33.6 // indirect k8s.io/controller-manager v0.33.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-aggregator v0.33.1 // indirect k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a // indirect - k8s.io/kubectl v0.33.1 // indirect + k8s.io/kubectl v0.33.4 // indirect k8s.io/kubernetes v1.33.1 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect oras.land/oras-go/v2 v2.6.0 // indirect @@ -158,7 +161,7 @@ require ( sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) // see https://github.com/argoproj/argo-cd/blob/v3.0.12/go.mod diff --git a/go.sum b/go.sum index 78b6887..70824b6 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/codeready-toolchain/toolchain-e2e v0.0.0-20260112152755-eb77a8e22ccb h1:LXVIwIO9uOkkmWiT9/zm2CJXCSz2Twc/aOl+fEzMRlQ= +github.com/codeready-toolchain/toolchain-e2e v0.0.0-20260112152755-eb77a8e22ccb/go.mod h1:cLET5HRrRWpYdnSpzWVlm0hqYvdYBG/lb9hO/HNZR84= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -306,8 +308,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/ github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4= -github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= @@ -347,8 +349,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= +github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= @@ -384,6 +386,10 @@ go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -568,5 +574,6 @@ sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go new file mode 100644 index 0000000..27da97b --- /dev/null +++ b/internal/metrics/metrics.go @@ -0,0 +1,28 @@ +package metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + + // MCPCallsTotal counts total MCP calls by method, name (for `tools/call`) and success + MCPCallsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "mcp_calls_total", + Help: "Total number of MCP calls", + }, + []string{"method", "name", "success"}, + ) + + // MCPCallDuration measures the duration of MCP calls by method, name (for `tools/call`) and success + MCPCallDuration = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "mcp_call_duration_seconds", + Help: "Duration of MCP calls in seconds", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "name", "success"}, + ) +) diff --git a/internal/server/middleware.go b/internal/server/middleware.go new file mode 100644 index 0000000..4e90fcb --- /dev/null +++ b/internal/server/middleware.go @@ -0,0 +1,38 @@ +package server + +import ( + "context" + "log/slog" + "strconv" + "time" + + "github.com/codeready-toolchain/argocd-mcp-server/internal/metrics" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +func NewMetricsMiddleware(logger *slog.Logger) mcp.Middleware { + return func(next mcp.MethodHandler) mcp.MethodHandler { + return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) { + logger.Debug("metrics-middleware: received request", "method", method, "params", req.GetParams()) + // measure the duration of the request + start := time.Now() + // call the next middleware + result, err := next(ctx, method, req) + // measure the duration of the request + duration := time.Since(start) + var tool string + if p, ok := req.GetParams().(*mcp.CallToolParamsRaw); ok { + tool = p.Name + } + success := err == nil + if r, ok := result.(*mcp.CallToolResult); ok { + logger.Debug("metrics-middleware: call tool result", "is-error", r.IsError) + success = success && !r.IsError + } + // increment/update the metrics + metrics.MCPCallsTotal.WithLabelValues(method, tool, strconv.FormatBool(success)).Inc() + metrics.MCPCallDuration.WithLabelValues(method, tool, strconv.FormatBool(success)).Observe(float64(duration.Seconds())) + return result, err + } + } +} diff --git a/internal/server/server.go b/internal/server/server.go index 944f13d..46225d7 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -24,6 +24,7 @@ func New(logger *slog.Logger, cl *argocd.Client) *mcp.Server { ) s.AddPrompt(argocd.UnhealthyResourcesPrompt, argocd.UnhealthyApplicationResourcesPromptHandle(logger, cl)) + s.AddReceivingMiddleware(NewMetricsMiddleware(logger)) mcp.AddTool(s, argocd.UnhealthyApplicationsTool, argocd.UnhealthyApplicationsToolHandle(logger, cl)) mcp.AddTool(s, argocd.UnhealthyApplicationResourcesTool, argocd.UnhealthyApplicationResourcesToolHandle(logger, cl)) return s diff --git a/taskfile.yaml b/taskfile.yaml index 96069c0..cdcc2c8 100644 --- a/taskfile.yaml +++ b/taskfile.yaml @@ -14,10 +14,14 @@ tasks: - $GOPATH/bin/golangci-lint run -v -c .golangci.yml ./... # E2E Tests - install: + build: cmds: - go build -o $GOPATH/bin/argocd-mcp-server main.go + podman-compose-build: + cmds: + - podman-compose build + podman-compose-up: cmds: - podman-compose up -d @@ -28,7 +32,7 @@ tasks: test-e2e: deps: - - install + - build - podman-compose-up cmds: # The idiomatic way to disable test caching explicitly is to use -count=1 diff --git a/test/e2e/server_test.go b/test/e2e/server_test.go index 8d6546c..5e26d67 100644 --- a/test/e2e/server_test.go +++ b/test/e2e/server_test.go @@ -3,16 +3,20 @@ package e2etests import ( "context" "encoding/json" + "math" "os/exec" "strconv" "testing" + toolchaintests "github.com/codeready-toolchain/toolchain-e2e/testsupport/metrics" + argocdv3 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" "github.com/codeready-toolchain/argocd-mcp-server/internal/argocd" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" ) // ------------------------------------------------------------------------------------------------ @@ -43,6 +47,17 @@ func TestServer(t *testing.T) { defer session.Close() t.Run("call/unhealthyApplications/ok", func(t *testing.T) { + // get the metrics before the call + var mcpCallsTotalMetricBefore int64 + var mcpCallsDurationSecondsInfBucketBefore int64 + if td.name == "http" { + mcpCallsTotalMetricBefore, mcpCallsDurationSecondsInfBucketBefore = getMetrics(t, "http://localhost:50081", map[string]string{ + "method": "tools/call", + "name": "unhealthyApplications", + "success": "true", + }) + } + // when result, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: "unhealthyApplications", @@ -69,9 +84,31 @@ func TestServer(t *testing.T) { err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.StructuredContent.(map[string]any), &actualStructuredContent) require.NoError(t, err) assert.Equal(t, expectedContent, actualStructuredContent) + // also, check the metrics when the server runs on HTTP + if td.name == "http" { + // get the metrics after the call + mcpCallsTotalMetricAfter, mcpCallsDurationSecondsInfBucketAfter := getMetrics(t, "http://localhost:50081", map[string]string{ + "method": "tools/call", + "name": "unhealthyApplications", + "success": "true", + }) + assert.Equal(t, mcpCallsTotalMetricBefore+1, mcpCallsTotalMetricAfter) + assert.Equal(t, mcpCallsDurationSecondsInfBucketBefore+1, mcpCallsDurationSecondsInfBucketAfter) + } + }) t.Run("call/unhealthyApplicationResources/ok", func(t *testing.T) { + var mcpCallsTotalMetricBefore int64 + var mcpCallsDurationSecondsInfBucketBefore int64 + if td.name == "http" { + mcpCallsTotalMetricBefore, mcpCallsDurationSecondsInfBucketBefore = getMetrics(t, "http://localhost:50081", map[string]string{ + "method": "tools/call", + "name": "unhealthyApplicationResources", + "success": "true", + }) + } + // when result, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: "unhealthyApplicationResources", @@ -130,9 +167,29 @@ func TestServer(t *testing.T) { err = runtime.DefaultUnstructuredConverter.FromUnstructured(result.StructuredContent.(map[string]any), &actualStructuredContent) require.NoError(t, err) assert.Equal(t, expectedContent, actualStructuredContent) + if td.name == "http" { + // get the metrics after the call + mcpCallsTotalMetricAfter, mcpCallsDurationSecondsInfBucketAfter := getMetrics(t, "http://localhost:50081", map[string]string{ + "method": "tools/call", + "name": "unhealthyApplicationResources", + "success": "true", + }) + assert.Equal(t, mcpCallsTotalMetricBefore+1, mcpCallsTotalMetricAfter) + assert.Equal(t, mcpCallsDurationSecondsInfBucketBefore+1, mcpCallsDurationSecondsInfBucketAfter) + } }) t.Run("call/unhealthyApplicationResources/argocd-error", func(t *testing.T) { + var mcpCallsTotalMetricBefore int64 + var mcpCallsDurationSecondsInfBucketBefore int64 + if td.name == "http" { + mcpCallsTotalMetricBefore, mcpCallsDurationSecondsInfBucketBefore = getMetrics(t, "http://localhost:50081", map[string]string{ + "method": "tools/call", + "name": "unhealthyApplicationResources", + "success": "false", + }) + } + // when result, err := session.CallTool(context.Background(), &mcp.CallToolParams{ Name: "unhealthyApplicationResources", @@ -144,6 +201,16 @@ func TestServer(t *testing.T) { // then require.NoError(t, err) assert.True(t, result.IsError) + if td.name == "http" { + // get the metrics after the call + mcpCallsTotalMetricAfter, mcpCallsDurationSecondsInfBucketAfter := getMetrics(t, "http://localhost:50081", map[string]string{ + "method": "tools/call", + "name": "unhealthyApplicationResources", + "success": "false", + }) + assert.Equal(t, mcpCallsTotalMetricBefore+1, mcpCallsTotalMetricAfter) + assert.Equal(t, mcpCallsDurationSecondsInfBucketBefore+1, mcpCallsDurationSecondsInfBucketAfter) + } }) }) } @@ -153,11 +220,11 @@ func TestServer(t *testing.T) { init func(*testing.T) *mcp.ClientSession }{ { - name: "stdio", + name: "stdio-unreachable", init: newStdioSession(true, "http://localhost:50085", "another-token", true), // invalid URL and token for the Argo CD server }, { - name: "http", + name: "http-unreachable", init: newHTTPSession("http://localhost:50082/mcp"), // invalid URL and token for the Argo CD server }, } @@ -183,6 +250,32 @@ func TestServer(t *testing.T) { } } +func getMetrics(t *testing.T, mcpServerURL string, labels map[string]string) (int64, int64) { //nolint:unparam + labelStrings := make([]string, 0, 2*len(labels)) + for k, v := range labels { + labelStrings = append(labelStrings, k) + labelStrings = append(labelStrings, v) + } + var mcpCallsTotalMetric int64 + var mcpCallsDurationSecondsInf int64 + + if value, err := toolchaintests.GetMetricValue(&rest.Config{}, mcpServerURL, `mcp_calls_total`, labelStrings); err == nil { + mcpCallsTotalMetric = int64(value) + } else { + t.Logf("failed to get mcp_calls_total metric, assuming 0: %v", err) + mcpCallsTotalMetric = 0 + } + if buckets, err := toolchaintests.GetHistogramBuckets(&rest.Config{}, mcpServerURL, `mcp_call_duration_seconds`, labelStrings); err == nil { + for _, bucket := range buckets { + if bucket.GetUpperBound() == math.Inf(1) { + mcpCallsDurationSecondsInf = int64(bucket.GetCumulativeCount()) //nolint:gosec + break + } + } + } + return mcpCallsTotalMetric, mcpCallsDurationSecondsInf +} + func newStdioSession(mcpServerDebug bool, argocdURL string, argocdToken string, argocdInsecureURL bool) func(*testing.T) *mcp.ClientSession { return func(t *testing.T) *mcp.ClientSession { ctx := context.Background()