Skip to content

Commit 3f447ea

Browse files
authored
feat: expose metrics for MCP calls (#38)
the `mcp_calls_total` and `mcp_call_duration_seconds` metrics are collected via the SDK's `mcp.Middleware` and exposed on the `/metrics` endpoint of the server --------- Signed-off-by: Xavier Coulon <xcoulon@redhat.com>
1 parent bfed00c commit 3f447ea

10 files changed

Lines changed: 199 additions & 20 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ argocd-mcp-server --transport=http --argocd-url=<url> --argocd-token=<token> --d
117117
Or start the Argo CD MCP server as a container after running `task build-image`:
118118

119119
```bash
120-
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
120+
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
121121
```
122122

123123
Edit your `~/.cursor/mcp.json` file with the following contents:

cmd/start_server.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/codeready-toolchain/argocd-mcp-server/internal/argocd"
1313
"github.com/codeready-toolchain/argocd-mcp-server/internal/server"
1414
"github.com/modelcontextprotocol/go-sdk/mcp"
15+
"github.com/prometheus/client_golang/prometheus/promhttp"
1516
"github.com/spf13/cobra"
1617
)
1718

@@ -58,7 +59,7 @@ var startServerCmd = &cobra.Command{
5859
logger := slog.New(slog.NewTextHandler(cmd.ErrOrStderr(), &slog.HandlerOptions{
5960
Level: lvl,
6061
}))
61-
logger.Info("starting the Argo CD MCP server", "transport", transport, "url", argocdURL, "insecure", argocdInsecure, "debug", debug)
62+
logger.Info("starting the Argo CD MCP server", "transport", transport, "argocd-url", argocdURL, "insecure", argocdInsecure, "debug", debug)
6263
if debug {
6364
lvl.Set(slog.LevelDebug)
6465
logger.Debug("debug mode enabled")
@@ -76,11 +77,16 @@ var startServerCmd = &cobra.Command{
7677
}
7778
default:
7879
mux := http.NewServeMux()
80+
7981
// MCP endpoint
8082
mux.Handle("/mcp", mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
8183
return srv
8284
}, nil))
83-
// HealthCheck endpoint.
85+
86+
// Metrics endpoint
87+
mux.Handle("/metrics", promhttp.Handler())
88+
89+
// HealthCheck endpoint
8490
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
8591
w.Header().Set("Content-Type", "application/json")
8692
json.NewEncoder(w).Encode(map[string]string{ //nolint:errcheck

compose.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
name: argocd-mcp-server-e2e
22

3-
version: '3.8'
43
services:
54
argocd-server-mock:
65
image: argocd-server-mock

go.mod

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ go 1.24.11
55
require (
66
github.com/argoproj/argo-cd/v3 v3.1.11
77
github.com/argoproj/gitops-engine v0.7.1-0.20250905160054-e48120133eec
8+
github.com/codeready-toolchain/toolchain-e2e v0.0.0-20260112152755-eb77a8e22ccb
89
github.com/google/jsonschema-go v0.3.0
910
github.com/h2non/gock v1.2.0
1011
github.com/modelcontextprotocol/go-sdk v1.2.0
12+
github.com/prometheus/client_golang v1.22.0
1113
github.com/spf13/cobra v1.10.1
1214
github.com/spf13/pflag v1.0.9
13-
github.com/stretchr/testify v1.10.0
15+
github.com/stretchr/testify v1.11.0
1416
k8s.io/apimachinery v0.33.6
17+
k8s.io/client-go v0.33.6
1518
)
1619

1720
require (
@@ -103,9 +106,8 @@ require (
103106
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
104107
github.com/pkg/errors v0.9.1 // indirect
105108
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
106-
github.com/prometheus/client_golang v1.22.0 // indirect
107109
github.com/prometheus/client_model v0.6.2 // indirect
108-
github.com/prometheus/common v0.64.0 // indirect
110+
github.com/prometheus/common v0.65.0 // indirect
109111
github.com/prometheus/procfs v0.16.1 // indirect
110112
github.com/redis/go-redis/v9 v9.8.0 // indirect
111113
github.com/robfig/cron/v3 v3.0.2-0.20210106135023-bc59245fe10e // indirect
@@ -122,6 +124,8 @@ require (
122124
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
123125
go.opentelemetry.io/otel v1.36.0 // indirect
124126
go.opentelemetry.io/otel/trace v1.36.0 // indirect
127+
go.yaml.in/yaml/v2 v2.4.2 // indirect
128+
go.yaml.in/yaml/v3 v3.0.3 // indirect
125129
golang.org/x/crypto v0.46.0 // indirect
126130
golang.org/x/net v0.47.0 // indirect
127131
golang.org/x/oauth2 v0.30.0 // indirect
@@ -139,17 +143,16 @@ require (
139143
gopkg.in/yaml.v2 v2.4.0 // indirect
140144
gopkg.in/yaml.v3 v3.0.1 // indirect
141145
k8s.io/api v0.33.6 // indirect
142-
k8s.io/apiextensions-apiserver v0.33.1 // indirect
146+
k8s.io/apiextensions-apiserver v0.33.4 // indirect
143147
k8s.io/apiserver v0.33.6 // indirect
144148
k8s.io/cli-runtime v0.33.6 // indirect
145-
k8s.io/client-go v0.33.6 // indirect
146149
k8s.io/component-base v0.33.6 // indirect
147150
k8s.io/component-helpers v0.33.6 // indirect
148151
k8s.io/controller-manager v0.33.1 // indirect
149152
k8s.io/klog/v2 v2.130.1 // indirect
150153
k8s.io/kube-aggregator v0.33.1 // indirect
151154
k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a // indirect
152-
k8s.io/kubectl v0.33.1 // indirect
155+
k8s.io/kubectl v0.33.4 // indirect
153156
k8s.io/kubernetes v1.33.1 // indirect
154157
k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect
155158
oras.land/oras-go/v2 v2.6.0 // indirect
@@ -158,7 +161,7 @@ require (
158161
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
159162
sigs.k8s.io/randfill v1.0.0 // indirect
160163
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect
161-
sigs.k8s.io/yaml v1.4.0 // indirect
164+
sigs.k8s.io/yaml v1.6.0 // indirect
162165
)
163166

164167
// see https://github.com/argoproj/argo-cd/blob/v3.0.12/go.mod

go.sum

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
7070
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
7171
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
7272
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
73+
github.com/codeready-toolchain/toolchain-e2e v0.0.0-20260112152755-eb77a8e22ccb h1:LXVIwIO9uOkkmWiT9/zm2CJXCSz2Twc/aOl+fEzMRlQ=
74+
github.com/codeready-toolchain/toolchain-e2e v0.0.0-20260112152755-eb77a8e22ccb/go.mod h1:cLET5HRrRWpYdnSpzWVlm0hqYvdYBG/lb9hO/HNZR84=
7375
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
7476
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
7577
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
@@ -306,8 +308,8 @@ github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/
306308
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
307309
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
308310
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
309-
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
310-
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
311+
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
312+
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
311313
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
312314
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
313315
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/
347349
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
348350
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
349351
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
350-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
351-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
352+
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
353+
github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
352354
github.com/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI=
353355
github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q=
354356
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=
384386
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
385387
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
386388
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
389+
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
390+
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
391+
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
392+
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
387393
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
388394
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
389395
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=
568574
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
569575
sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI=
570576
sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
571-
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
572577
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
578+
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
579+
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=

internal/metrics/metrics.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package metrics
2+
3+
import (
4+
"github.com/prometheus/client_golang/prometheus"
5+
"github.com/prometheus/client_golang/prometheus/promauto"
6+
)
7+
8+
var (
9+
10+
// MCPCallsTotal counts total MCP calls by method, name (for `tools/call`) and success
11+
MCPCallsTotal = promauto.NewCounterVec(
12+
prometheus.CounterOpts{
13+
Name: "mcp_calls_total",
14+
Help: "Total number of MCP calls",
15+
},
16+
[]string{"method", "name", "success"},
17+
)
18+
19+
// MCPCallDuration measures the duration of MCP calls by method, name (for `tools/call`) and success
20+
MCPCallDuration = promauto.NewHistogramVec(
21+
prometheus.HistogramOpts{
22+
Name: "mcp_call_duration_seconds",
23+
Help: "Duration of MCP calls in seconds",
24+
Buckets: prometheus.DefBuckets,
25+
},
26+
[]string{"method", "name", "success"},
27+
)
28+
)

internal/server/middleware.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"log/slog"
6+
"strconv"
7+
"time"
8+
9+
"github.com/codeready-toolchain/argocd-mcp-server/internal/metrics"
10+
"github.com/modelcontextprotocol/go-sdk/mcp"
11+
)
12+
13+
func NewMetricsMiddleware(logger *slog.Logger) mcp.Middleware {
14+
return func(next mcp.MethodHandler) mcp.MethodHandler {
15+
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
16+
logger.Debug("metrics-middleware: received request", "method", method, "params", req.GetParams())
17+
// measure the duration of the request
18+
start := time.Now()
19+
// call the next middleware
20+
result, err := next(ctx, method, req)
21+
// measure the duration of the request
22+
duration := time.Since(start)
23+
var tool string
24+
if p, ok := req.GetParams().(*mcp.CallToolParamsRaw); ok {
25+
tool = p.Name
26+
}
27+
success := err == nil
28+
if r, ok := result.(*mcp.CallToolResult); ok {
29+
logger.Debug("metrics-middleware: call tool result", "is-error", r.IsError)
30+
success = success && !r.IsError
31+
}
32+
// increment/update the metrics
33+
metrics.MCPCallsTotal.WithLabelValues(method, tool, strconv.FormatBool(success)).Inc()
34+
metrics.MCPCallDuration.WithLabelValues(method, tool, strconv.FormatBool(success)).Observe(float64(duration.Seconds()))
35+
return result, err
36+
}
37+
}
38+
}

internal/server/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func New(logger *slog.Logger, cl *argocd.Client) *mcp.Server {
2424
)
2525

2626
s.AddPrompt(argocd.UnhealthyResourcesPrompt, argocd.UnhealthyApplicationResourcesPromptHandle(logger, cl))
27+
s.AddReceivingMiddleware(NewMetricsMiddleware(logger))
2728
mcp.AddTool(s, argocd.UnhealthyApplicationsTool, argocd.UnhealthyApplicationsToolHandle(logger, cl))
2829
mcp.AddTool(s, argocd.UnhealthyApplicationResourcesTool, argocd.UnhealthyApplicationResourcesToolHandle(logger, cl))
2930
return s

taskfile.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ tasks:
1414
- $GOPATH/bin/golangci-lint run -v -c .golangci.yml ./...
1515

1616
# E2E Tests
17-
install:
17+
build:
1818
cmds:
1919
- go build -o $GOPATH/bin/argocd-mcp-server main.go
2020

21+
podman-compose-build:
22+
cmds:
23+
- podman-compose build
24+
2125
podman-compose-up:
2226
cmds:
2327
- podman-compose up -d
@@ -28,7 +32,7 @@ tasks:
2832

2933
test-e2e:
3034
deps:
31-
- install
35+
- build
3236
- podman-compose-up
3337
cmds:
3438
# The idiomatic way to disable test caching explicitly is to use -count=1

0 commit comments

Comments
 (0)