Skip to content

introduce VMMCP CRD - #2740

Open
megative wants to merge 7 commits into
VictoriaMetrics:masterfrom
megative:feat/mcp
Open

megative wants to merge 7 commits into
VictoriaMetrics:masterfrom
megative:feat/mcp

Conversation

@megative

@megative megative commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds the VMMCP CRD (operator.victoriametrics.com/v1) for MCP server for VictoriaMetrics, which gives AI assistants access to VictoriaMetrics data and documentation over Model Context Protocol.

Part of #2739. VLMCP and VTMCP for MCP servers of VictoriaLogs and VictoriaTraces are prepared on top of this PR and will be opened one by one after it, to keep each PR small.

Design

  • One CRD per official helm chart: VMMCP mirrors victoria-metrics-mcp, objects are named vmmcp-<name>, as kindOverride in the chart. The common part of the spec (CommonMCPSpec) and the internal/controller/operator/factory/mcp package are shared, so VLMCP and VTMCP only add settings of their instances.
  • For each CR the operator creates a Deployment, Service, ServiceAccount and VMServiceScrape for /metrics. Optional: PodDisruptionBudget, VerticalPodAutoscaler, NetworkPolicy, additional service, updateStrategy/rollingUpdate.
  • The MCP server is configured only with env vars, so spec.extraArgs is rejected by validation, and env vars from spec.extraEnvs override env vars set by the operator.
  • The backend is a URL at spec.entrypoint (VMSingle or vmselect) with spec.instanceType: single|cluster, as in the chart, or spec.cloudAPIKey for VictoriaMetrics Cloud. Bearer token and Cloud API key are Secret references passed via valueFrom.secretKeyRef, the operator doesn't read them. Custom headers, default tenant, passthrough headers, disabled tools and resources, heartbeat interval and logging are supported.
  • spec.serverMode: http (default, Streamable HTTP at /mcp) or sse (/sse and /message). stdio isn't allowed, it can't be used in Kubernetes.
  • Probes: readiness at /health/readiness, liveness at /health/liveness and a TCP startup probe with failureThreshold: 60. The MCP server loads an in-memory index of the built-in documentation before it starts listening; measured start time: 11s with 1 CPU, 22s with 0.5 CPU, 85s with 0.2 CPU.
  • Default resources are based on measured memory of the image: ~700Mi RSS with the documentation index (13Mi with the documentation tool disabled), so requests are 100m / 768Mi and limits are 500m / 1Gi.
  • The default version is controlled by the new VM_MCP_METRICS_VERSION env var (v1.20.2) with a renovate annotation. It uses the github-releases datasource, since ghcr.io/victoriametrics/mcp-victoriametrics has a mistaken v1.120.1 tag (built from the same commit as v1.20.1), which the docker datasource would pick as the latest version.
  • The image is published only to ghcr.io, so VM_CONTAINERREGISTRY doesn't apply to it, VM_VMMCPDEFAULT_IMAGE can point to a mirror.

Review notes

Part Lines
generated (CRD overlays, typed client, deepcopy, docs/api.md, docs/env.md, RBAC) ~3.4k
tests ~0.8k
docs and examples ~0.3k
code ~1.3k

Core logic:

  • api/operator/v1/mcp_types.go, api/operator/v1/vmmcp_types.go - API types and validation
  • internal/controller/operator/factory/mcp/ - reconciliation and env vars of the MCP server
  • internal/controller/operator/factory/build/defaults.go, internal/config/config.go - defaults
  • internal/controller/operator/factory/finalize/mcp.go, internal/controller/operator/vmmcp_controller.go, internal/webhook/operator/v1/vmmcp_webhook.go

Follow-ups

  • VLMCP and VTMCP - prepared, will be opened after this PR.
  • VMUser targetRef.crd kinds for MCP servers: MCP servers don't authenticate clients, so the docs recommend putting VMAuth in front of them.
  • helm-converter support for the MCP charts.
  • Upgrade tests after the first release with these CRDs.

Testing

Automated

  • Unit tests: API validation, naming and URLs; env vars including Secret references, VictoriaMetrics Cloud and extraEnvs overrides; default probes and resources; deployment params; optional objects creation and removal; finalizer. Coverage of the factory package is 84%.
  • e2e (test/e2e/mcp_test.go, label vmmcp): VMSingle and VMMCP with strict security are created, a real MCP session (initialize + tools/call of the query tool) is made through the Kubernetes API server proxy and returns data of VMSingle; then logLevel and podDisruptionBudget are updated, and objects are removed after CR deletion.

Manual, at local kind cluster

The operator image was built from this branch (dd1be89) and deployed to kind via config/default. The stand already had VMSingle + VMAgent, which scrapes all VMServiceScrape objects.

export KUBECONFIG=~/.kube/kind-vme
TAG=mcp-pr1 make docker-build
kind load docker-image docker.io/victoriametrics/operator:mcp-pr1 --name vme
bin/kustomize-v5.8.3 build config/default | sed 's|image: manager$|image: docker.io/victoriametrics/operator:mcp-pr1|' | kubectl apply --server-side --force-conflicts -f -

VMMCP with default settings was created for the existing VMSingle:

apiVersion: operator.victoriametrics.com/v1
kind: VMMCP
metadata: {name: demo, namespace: default}
spec:
  entrypoint: http://vmsingle-example.default.svc:8428

The MCP server was reached via kubectl port-forward and called over Streamable HTTP with curl (initialize, then tools/call with the Mcp-Session-Id):

check result
creation operational in 27s, since the MCP server starts listening after loading the documentation index. Deployment, Service, ServiceAccount and VMServiceScrape are created, the container has the default resources (requests 100m / 768Mi, limits 500m / 1Gi), a TCP startup probe, liveness at /health/liveness and readiness at /health/readiness. The pod uses 713Mi of memory
query tool, up 7 series, all up=1, including job="vmmcp-demo": the MCP server itself is scraped via the created VMServiceScrape. Its first scrape is up=0, since vmagent discovers the pod while the documentation index is loading
documentation tool, retention period 30 results
Claude Code connected with claude mcp add --transport http, question "what targets are in VictoriaMetrics" answered via the MCP server with the same 7 targets, all up
serverMode: sse, then back to http the Deployment is rolled out, /sse returns event: endpoint
VMMCP deletion all vmmcp-* objects are removed
Response of the query tool, labels trimmed
{"metric": {"__name__": "up", "job": "vmagent-example", "pod": "vmagent-example-67cc47cbcc-xh2dr"}, "value": [1792357571, "1"]}
{"metric": {"__name__": "up", "job": "vmagent-example-reloader-http", "pod": "vmagent-example-67cc47cbcc-xh2dr"}, "value": [1792357571, "1"]}
{"metric": {"__name__": "up", "job": "vmestimator-example", "pod": "vmestimator-select-example-6b76455f9f-c7574"}, "value": [1792357571, "1"]}
{"metric": {"__name__": "up", "job": "vmestimator-storage-example", "pod": "vmestimator-storage-example-0"}, "value": [1792357571, "1"]}
{"metric": {"__name__": "up", "job": "vmestimator-storage-example", "pod": "vmestimator-storage-example-1"}, "value": [1792357571, "1"]}
{"metric": {"__name__": "up", "job": "vmmcp-demo", "pod": "vmmcp-demo-65ffd4b88c-wxrwn"}, "value": [1792357571, "1"]}
{"metric": {"__name__": "up", "job": "vmsingle-example", "pod": "vmsingle-example-d8d859bbb-gzhfc"}, "value": [1792357571, "1"]}

🤖 Generated with Claude Code

The CRD deploys the MCP server for VictoriaMetrics, the same as the
official victoria-metrics-mcp helm chart. For each VMMCP the operator
creates a Deployment, Service, ServiceAccount and VMServiceScrape,
optionally PodDisruptionBudget, VPA and NetworkPolicy.

The MCP server is configured with env vars: the queried instance is set
at spec.entrypoint, credentials are passed as Secret references and
spec.extraEnvs override env vars set by the operator. Default resources
and the startup probe account for the in-memory documentation index,
which takes ~700Mi and slows down the start with low CPU limits.

The common part of the spec and the factory are shared, so MCP servers
for VictoriaLogs and VictoriaTraces can be added on top of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@megative megative changed the title introduce VMMCP, VLMCP and VTMCP CRDs introduce VMMCP CRD Oct 18, 2026
@megative
megative marked this pull request as ready for review October 18, 2026 21:12

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 42 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="internal/webhook/operator/v1/vmmcp_webhook.go">

<violation number="1" location="internal/webhook/operator/v1/vmmcp_webhook.go:38">
P1: This marker is not accompanied by a deployed `ValidatingWebhookConfiguration` rule for `vmmcps`, so installs using the tracked webhook manifests will never call this validator on VMMCP create/update requests. Regenerate and commit the webhook manifest so the new path and resource rule are included.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Complete()
}

// +kubebuilder:webhook:path=/validate-operator-victoriametrics-com-v1-vmmcp,mutating=false,failurePolicy=fail,sideEffects=None,groups=operator.victoriametrics.com,resources=vmmcps,verbs=create;update,versions=v1,name=vvmmcps-v1.kb.io,admissionReviewVersions=v1

@cubic-dev-ai cubic-dev-ai Bot Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: This marker is not accompanied by a deployed ValidatingWebhookConfiguration rule for vmmcps, so installs using the tracked webhook manifests will never call this validator on VMMCP create/update requests. Regenerate and commit the webhook manifest so the new path and resource rule are included.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At internal/webhook/operator/v1/vmmcp_webhook.go, line 38:

<comment>This marker is not accompanied by a deployed `ValidatingWebhookConfiguration` rule for `vmmcps`, so installs using the tracked webhook manifests will never call this validator on VMMCP create/update requests. Regenerate and commit the webhook manifest so the new path and resource rule are included.</comment>

<file context>
@@ -0,0 +1,68 @@
+		Complete()
+}
+
+// +kubebuilder:webhook:path=/validate-operator-victoriametrics-com-v1-vmmcp,mutating=false,failurePolicy=fail,sideEffects=None,groups=operator.victoriametrics.com,resources=vmmcps,verbs=create;update,versions=v1,name=vvmmcps-v1.kb.io,admissionReviewVersions=v1
+type VMMCPCustomValidator struct{}
+
</file context>
Fix with cubic

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, the VMMCP validator is in config/webhook/manifests.yaml now. make manifests doesn't write that file, so the entry is added by hand in the same format as the others.

Comment thread internal/controller/operator/factory/mcp/mcp_test.go
Comment thread test/e2e/mcp_test.go Outdated
Comment thread test/e2e/mcp_test.go
Comment thread api/operator/v1/vmmcp_types.go
Comment thread docs/resources/vmmcp.md Outdated
Comment thread docs/resources/vmmcp.md
Comment thread internal/controller/operator/factory/finalize/mcp.go Outdated
Comment thread internal/controller/operator/vmmcp_controller.go
Comment thread docs/resources/vmmcp.md
Comment thread api/operator/v1/mcp_types.go Outdated
return s.Validate()
}

// validateMCPListItems checks items of the list, which is passed to the MCP server as a comma-separated env var value

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// validateMCPListItems checks items of the list, which is passed to the MCP server as a comma-separated env var value
// validateMCPListItems checks items of the list - headers or tools, which is passed to the MCP server as a comma-separated env var value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied, thanks.

Comment thread config/examples/vmmcp.yaml Outdated
entrypoint: http://vmselect-example:8481
instanceType: cluster
defaultTenantID: "0"
replicaCount: 2

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Lets use a more realistic scenario - single replica (I'm not even sure whether two replicas make sense at all)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, the example runs a single replica now. The High availability section of the docs describes when several replicas work.

Comment on lines +18 to +29
// mcpCR is implemented by CRDs of MCP servers
type mcpCR interface {
client.Object
crObject
PrefixedName() string
GetServiceAccountName() string
}

// OnVMMCPDelete deletes all VMMCP related resources
func OnVMMCPDelete(ctx context.Context, rclient client.Client, cr *vmv1.VMMCP) error {
return onMCPDelete(ctx, rclient, cr, &cr.Spec.CommonMCPSpec)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We don't need an extra wrapper and this interface

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, OnVMMCPDelete removes the finalizers directly now, like the other finalizers.

crMeta := metav1.ObjectMeta{
Name: "test",
Namespace: "default",
Finalizers: []string{vmv1beta1.FinalizerName},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Set OwnerRefs here too and check that those are retained on finalize

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, the test sets owner references on the VMMCP and its child objects and checks that they're retained.

"github.com/VictoriaMetrics/operator/internal/controller/operator/factory/k8stools"
)

func quantityBytes(s string) int64 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use resource.MustParse instead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. It compares with resource.MustParse(...).Equal(), since quantities read back from the fake client keep their string form and don't match with assert.Equal.

// +kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmmcps,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmmcps/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=operator.victoriametrics.com,resources=vmmcps/finalizers,verbs=update
// +kubebuilder:rbac:groups="networking.k8s.io",resources=networkpolicies,verbs=*

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
// +kubebuilder:rbac:groups="networking.k8s.io",resources=networkpolicies,verbs=*
// +kubebuilder:rbac:groups="networking.k8s.io",resources=networkpolicies,verbs=get;create;update;patch;delete

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied, thanks.

# Conflicts:
#	config/crd/overlay/crd.descriptionless.yaml
#	config/crd/overlay/crd.specless.yaml
#	config/crd/overlay/crd.yaml
#	docs/CHANGELOG.md
- OnVMMCPDelete removes finalizers directly, without an interface and a wrapper
- the finalizer test checks that owner references are retained
- the examples run a single replica
- tests compare memory with resource.MustParse
- the networkpolicies RBAC lists the verbs the controller uses
- spec.entrypoint must have a host, since the CRD pattern checks only the scheme
- the unit test checks that the NetworkPolicy is created and removed
- the e2e test sends notifications/initialized after initialize, as MCP requires,
  and checks that all child objects are deleted
- docs: when the ServiceAccount and VMServiceScrape aren't created, VMAuth in
  the security example, session affinity with serviceSpec.useAsDefault
@megative
megative requested a review from vrutkovs October 20, 2026 19:03

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All reported issues were addressed across 11 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread docs/resources/vmmcp.md Outdated

@vrutkovs vrutkovs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Three things left to fix:

Comment thread config/rbac/role.yaml
resources:
- ingresses/finalizers
verbs:
- '*'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
- '*'
- get
- create
- update
- patch
- delete

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

config/rbac/role.yaml is generated by make manifests (Makefile) from the +kubebuilder:rbac markers, so a manual change here gets reverted:

$ git diff --stat config/rbac/role.yaml   # after applying the suggestion
 config/rbac/role.yaml | 6 +++++-
$ make manifests
$ git diff --stat config/rbac/role.yaml   # no changes: the suggestion is gone

This '*' is for ingresses/finalizers and comes from the marker in finalize/common.go. networkpolicies get '*' from the markers of 13 other controllers, e.g. vmagent and vmauth. Should I change those markers to explicit verbs too, or keep this PR to the VMMCP marker?

…ld objects

- the ValidatingWebhookConfiguration calls the VMMCP validator on create and update
- deleting a VMMCP doesn't touch a ServiceAccount set in spec.serviceAccountName
- the controller watches the ServiceAccount, PodDisruptionBudget, NetworkPolicy,
  VMServiceScrape and, with VPA API enabled, VerticalPodAutoscaler, so it restores
  deleted or changed objects right away
- the e2e test checks that a deleted PodDisruptionBudget is restored before
  the periodic resync
# Conflicts:
#	docs/CHANGELOG.md
#	docs/env.md

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants