Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ argocd-mcp-server --transport=http --argocd-url=<url> --argocd-token=<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=<url> -e ARGOCD_MCP_TOKEN=<token> -e ARGOCD_MCP_DEBUG=<true|false> -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=<url> -e ARGOCD_MCP_TOKEN=<token> -e ARGOCD_MCP_DEBUG=<true|false> -p 8080:8080 argocd-mcp-server:latest
```

Edit your `~/.cursor/mcp.json` file with the following contents:
Expand Down
10 changes: 8 additions & 2 deletions cmd/start_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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")
Expand All @@ -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())
Comment on lines +86 to +87
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we usually expose the metrics using it's own server and different port , but I guess this is fine for the mcp server that is not exposed publicly.

Copy link
Copy Markdown
Collaborator Author

@xcoulon xcoulon Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened a discussion on Slack for this specific question


// 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
Expand Down
1 change: 0 additions & 1 deletion compose.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
name: argocd-mcp-server-e2e

version: '3.8'
services:
argocd-server-mock:
image: argocd-server-mock
Expand Down
17 changes: 10 additions & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
17 changes: 12 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
28 changes: 28 additions & 0 deletions internal/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -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"},
)
Comment on lines +11 to +17
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have separate metrics for tool calls and method calls.
For example as https://github.com/codeready-toolchain/mcp-server-devsandbox/blob/master/pkg/metrics/metrics.go

  • the mcp_method_calls_total will count invocations to standard MCP protocol operations like : initialize , ping, tools/list
  • the mcp_tool_calls_total will count calls per tool

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it would be nice to have mcp_tool_call_errors_total counting errors per tool . So that we can have insights on tools that are failing ( maybe because the llm can't figure out how to call them properly or for any other reason .. )

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but since we have labels for the mcp_calls_total, we can filter by method (initialize, tools/call, etc.) and then for tools, by name (unhealthyApplications, unhealthyApplicationResources etc.), and also by success (true, false).
I mean, I don't see the benefits of having multiple metrics.

Also, that's what is usually done with HTTP metrics: you have labels for the method (get, post) and the return code (200, 404, 500, etc.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought that having separated metrics for tools and methods would help with creating separate dashboards , since I guess we would be mostly interested into looking at the tools charts ( more than the methods ones ), but maybe I'm wrong.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we ca have a single metric and use the labels to focus on tool calls per name, etc.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me 👍


// 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"},
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
)
38 changes: 38 additions & 0 deletions internal/server/middleware.go
Original file line number Diff line number Diff line change
@@ -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
}
}
}
1 change: 1 addition & 0 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading