Wiring Model Context Protocol (MCP) Agents into GitLab and Argo CD for Pre-Flight Validation
By the SynapseTel Cloud principal engineers · AI Agents & MCP Integration · Published October 18, 2026 · 15 min read
A reference design for MCP-backed GitLab checks and Argo CD deployment gates, with revision-bound reports, fail-closed Python examples and limits for OpenShift admission checks.
What the validation gate needs to establish
A manifest can pass a schema check and still fail when it reaches the target cluster. A required custom resource definition may be absent, an admission policy may reject the workload, or the running configuration may differ from the baseline assumed by the change. Static validation remains useful; it answers a different question from a check against current cluster state.
Model Context Protocol (MCP) provides a tool interface through which an agent or a fixed CI client can request those checks. It does not supply Kubernetes validation, authorization or a release-approval policy by itself. The design here keeps the deployment verdict in deterministic validators. An AI agent can explain findings and assemble context, but generated prose cannot turn a failed or incomplete check into a pass.
This is an engineering reference, not a report of a customer deployment. The examples use the MCP 2025-11-25 tools and transport model, the Python SDK v1 client interface, and OpenShift Container Platform 4.20 documentation for SCC discussion. Lock and test actual SDK, GitLab, Argo CD and cluster versions before adoption; these are reference baselines, not a supported-version commitment.
Separate review, observation and deployment authority
Use unprivileged merge-request jobs for rendering, schema checks and tests. Run checks needing production-adjacent credentials in a separate trusted context after review. A normal GitLab merge-request pipeline tests the source branch; a merged-results pipeline tests a temporary combination with the target branch, where that feature is available. Neither result should be assumed to identify a later deployment revision without checking it.
The MCP validation service maps an approved target key to a fixed cluster, namespace and policy. Version that mapping with the policy, and verify the actual target identity in the adapters: echoing a target key is not enough. The caller cannot supply arbitrary API URLs, shell commands or a different kubeconfig. Keep the service credentials separate from Argo CD deployment credentials. An agent may request permitted tools and summarize redacted results; it has no route to patch resources, approve a merge or start synchronization through this interface.
Merge request --> unprivileged render, schema and test jobs
|
Reviewed protected revision --> trusted GitLab pre-flight job
|
authenticated MCP service
|
deterministic validation adapters
render / live-state diff / admission
|
revision- and bundle-bound report
|
approved GitOps promotion --> Argo CD
|
fresh PreSync gate --> workload sync
|
rollout and application checksDefine checks that can return an honest verdict
For the example policy, require three checks: render, live-diff and admission. The render check validates the complete non-secret workload bundle against the expected API schemas. The live-diff check compares that candidate with the target using the same ownership, normalization and ignored-field rules used for deployment. A replica-count mismatch alone is not a drift detector: autoscalers and other controllers may legitimately own fields. Record intentional candidate changes separately from unexpected changes already present in the cluster.
The admission adapter evaluates the applicable API and policy decisions. If a CRD is missing, an API call is forbidden, a webhook cannot be evaluated or a dependency observation is stale, return fail or unknown with a reason. Do not return clean for an unsupported resource kind or treat every 404 as successful creation. Pre-flight validation does not prove that images can be pulled, Pods can be scheduled or applications will become healthy.
A telecom dependency check needs its own approved adapter, identifiers and expected result. For example, confirming an ENM management-interface dependency is different from checking a Kubernetes Service. Add that check to the trusted policy and its required-check list when the engagement needs it; MCP does not provide a universal Ericsson ENM tool.
Use an explicit MCP tool and report contract
Implement a custom preflight_validate tool with an input schema for sourceRevision, targetKey, policyRevision, renderSha256 and manifestYaml. Reject unknown targets, oversized inputs and disallowed resource kinds. Recompute the bundle hash on the server, authorize the caller for the mapped target, and run the registered validators. Do not accept a caller-supplied verdict or a list of checks to skip.
Declare an outputSchema and return the report in structuredContent. The check names and report fields below are our example contract, not standard MCP tools. A tool execution error uses isError; a successful tools/call exchange can still carry a failed validation verdict. Both layers must be checked. An AI summary is optional and must remain outside the fields used to approve deployment.
observedAt represents the oldest live observation used by the decision, not the time a cached report was reissued. reportId identifies server-side evidence. The following values are illustrative and are not production results.
{
"schemaVersion": 1,
"reportId": "example-only-01",
"verdict": "pass",
"sourceRevision": "1111111111111111111111111111111111111111",
"renderSha256": "2222222222222222222222222222222222222222222222222222222222222222",
"targetKey": "staging-network-tools",
"policyRevision": "preflight-policy-v1",
"observedAt": "2026-10-18T13:30:00Z",
"checks": [
{
"id": "render",
"status": "pass"
},
{
"id": "live-diff",
"status": "pass"
},
{
"id": "admission",
"status": "pass"
}
]
}Reject incomplete, stale or mismatched reports
The client checks the report against values supplied by its trusted job context. This small policy rejects a missing check, duplicate check, unknown status, different target, changed policy or mismatched revision and bundle hash. It also rejects old or implausibly future-dated observations. The five-minute freshness window and 30-second clock allowance are example policy choices, not service targets; synchronize clocks and choose limits for the environment.
These checks establish consistency with the declared contract, not the honesty of a compromised validation server. Protect the validator implementation and evidence store, validate its full output schema, and authenticate the service. A SHA-256 digest identifies content; it is not a digital signature or proof that a cluster was examined.
# report_policy.py -- Python 3.11+
from datetime import datetime, timezone
REQUIRED = {"render", "live-diff", "admission"}
def accepted(report, revision, digest, target, policy, now=None):
if not isinstance(report, dict):
return False
expected = {
"schemaVersion": 1, "verdict": "pass",
"sourceRevision": revision, "renderSha256": digest,
"targetKey": target, "policyRevision": policy,
}
if any(type(report.get(k)) is not type(v) or report[k] != v
for k, v in expected.items()):
return False
report_id = report.get("reportId")
if not isinstance(report_id, str) or not 1 <= len(report_id) <= 128:
return False
try:
observed = datetime.fromisoformat(report["observedAt"].replace("Z", "+00:00"))
if observed.tzinfo is None:
return False
age = ((now or datetime.now(timezone.utc)) - observed).total_seconds()
except (KeyError, TypeError, ValueError, AttributeError):
return False
if not -30 <= age <= 300:
return False
checks = report.get("checks")
if not isinstance(checks, list) or len(checks) != len(REQUIRED):
return False
if any(not isinstance(c, dict) or not isinstance(c.get("id"), str)
or c.get("status") != "pass" for c in checks):
return False
return {c["id"] for c in checks} == REQUIREDCall the service from a bounded MCP client
Package check.py and report_policy.py in a reviewed runner image with an exact dependency lock for an MCP Python SDK v1 release and its compatible HTTPX version. The three-value transport tuple below is the v1 interface; do not assume another SDK major uses the same API. The runner must already contain the dependencies. This example does not install packages from a merge request.
The authenticated HTTPS endpoint must implement the custom tool and validators described above. Provision MCP_ENDPOINT and a short-lived MCP_ACCESS_TOKEN through the approved credential system, with an audience and permissions for that service. Restrict endpoint selection and network egress in the trusted runner configuration. HTTP MCP and a stdio-only server are different transports; starting stdio_server does not create a network endpoint.
For this example, bundle must contain only workload.yaml, holding the complete pre-rendered application as a multi-document YAML bundle. Reject symlinks, Secret payloads and other sensitive content; use secret references where appropriate. Argo CD must consume those same bytes. Applications using several files, remote bases, Helm or Kustomize need a canonical bundle and matching deployment-rendering procedure instead. The client writes only a small decision receipt. Detailed findings stay in the access-controlled evidence store. A transport failure or malformed response exits nonzero without logging credentials, manifests or raw tool output.
# check.py -- package this and report_policy.py in a reviewed runner image.
import asyncio
import hashlib
import json
import os
from pathlib import Path
from urllib.parse import urlsplit
import httpx
import mcp
from mcp.client.streamable_http import streamable_http_client
from report_policy import accepted
async def main():
receipt = {"verdict": "unknown", "reason": "validation unavailable or rejected"}
ok = False
try:
bundle = Path("bundle")
candidate = bundle / "workload.yaml"
if bundle.is_symlink() or candidate.is_symlink():
raise ValueError("symlinked bundle rejected")
if {p.name for p in bundle.iterdir()} != {"workload.yaml"}:
raise ValueError("expected one complete workload bundle")
raw = candidate.read_bytes()
if len(raw) > 1_000_000:
raise ValueError("bundle too large")
digest = hashlib.sha256(raw).hexdigest()
revision = os.environ["CI_COMMIT_SHA"]
target = os.environ["PREFLIGHT_TARGET"]
policy = os.environ["PREFLIGHT_POLICY"]
endpoint = os.environ["MCP_ENDPOINT"]
if urlsplit(endpoint).scheme != "https":
raise ValueError("HTTPS required")
headers = {"Authorization": "Bearer " + os.environ["MCP_ACCESS_TOKEN"]}
async with asyncio.timeout(60):
async with httpx.AsyncClient(headers=headers, timeout=30, trust_env=False) as http:
async with streamable_http_client(endpoint, http_client=http) as streams:
read, write, _ = streams # MCP Python SDK v1 interface
async with mcp.ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("preflight_validate", {
"sourceRevision": revision, "targetKey": target,
"policyRevision": policy, "renderSha256": digest,
"manifestYaml": raw.decode("utf-8"),
})
report = result.structuredContent
ok = not result.isError and accepted(
report, revision, digest, target, policy
)
if ok:
receipt = {"verdict": "pass", "reportId": report["reportId"],
"sourceRevision": revision, "renderSha256": digest,
"targetKey": target, "policyRevision": policy}
except Exception:
# Do not print exception bodies, tokens, manifests or raw tool output.
ok = False
Path("preflight.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8")
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(asyncio.run(main()))Run the credentialed GitLab job only in a trusted context
The job below deliberately runs on the protected default branch after review. It is a pre-deployment gate, not a claim that an arbitrary merge-request pipeline can safely receive cluster credentials. Configure that branch, a protected runner, protected credential variables and the reviewed image digest. A job rule or a runner tag alone does not establish the trust boundary.
Keep merge-request jobs without those credentials. GitLab applies specific conditions to access protected variables and runners; fork and ordinary feature-branch pipelines cannot be assumed to qualify. Review changes to CI configuration and validator code as security-sensitive changes. The fixed /opt/preflight entry point prevents a repository script from silently becoming the validator, but approved repository content and runner configuration still need protection.
Merge this fragment with the existing pipeline rather than replacing its stages. GitLab 18.4 introduced the maintainer artifact-access value used here; adapt it to the installed version. Configure job-token visibility separately, because artifact UI/API access restrictions do not cover every access path. Seven days is an example retention setting, not an audit retention policy.
# Merge into the existing .gitlab-ci.yml; retain its other stages and rules.
stages: [validate]
mcp_preflight:
stage: validate
# Protected project variable: reviewed image pinned by its sha256 digest.
image:
name: $PREFLIGHT_RUNNER_IMAGE
entrypoint: [""]
tags: [trusted-preflight]
timeout: 3m
allow_failure: false
variables:
PREFLIGHT_TARGET: "staging-network-tools"
PREFLIGHT_POLICY: "preflight-policy-v1"
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_REF_PROTECTED == "true"'
- when: never
script:
- python /opt/preflight/check.py
artifacts:
when: always
access: maintainer
expire_in: 7 days
paths: [preflight.json]Check actual OpenShift admission behavior
For OpenShift, evaluate the Pod that the workload controller would create, including init containers, service account and namespace context. A dry-run update to a Deployment is not the same as admitting the later Pod. Under an SCC using MustRunAsRange, an omitted runAsUser can be defaulted from the namespace range; interpreting an absent UID as zero produces a false result. restricted-v2 also does not by itself require a read-only root filesystem. Inspect the policies and SCC selection in the actual cluster rather than assuming one default across releases and upgraded installations.
Kubernetes server-side dry-run passes a request through relevant validation and admission stages without persisting it. It needs the same API authorization as the corresponding real request. Do not call a service account read-only if it can create or patch resources merely because its client usually sends dryRun=All. Isolate any admission broker, limit its scope, and enforce its permitted operations server-side. Confirm the admission identity reflects the intended workload; a more privileged checker can otherwise produce a misleading result.
Make Argo CD consume the approved revision
A successful GitLab job does not automatically stop an Argo CD application that tracks a moving branch with auto-sync enabled. Promote a specific reviewed Git revision and the hash of the rendered bundle through a controlled release record. In this example, Argo CD reads the same pre-rendered bundle directory without a second Helm or Kustomize rendering step. Do not place the CI receipt or policy JSON in that manifest directory.
The Application below uses an approved full commit SHA and disables automatic sync. Replace the example repository and revision, and configure the named AppProject and destination before use. Restrict who can edit the Application, change its source revision or synchronize a different revision. If an ApplicationSet manages it, enforce the setting in the generating configuration rather than relying on a child edit.
This configuration supports a controlled approval workflow; it is not a complete machine-enforced gate on its own. Before an authorized sync, rerun validation for that revision and target. A prior pipeline result may no longer describe current cluster state. An automated promotion controller or PreSync hook can enforce that revalidation when implemented and tested for the environment.
# Standalone Application example; replace the repository and approved revision.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: network-tools
namespace: argocd
spec:
project: reviewed-workloads
source:
repoURL: https://gitlab.example.invalid/platform/network-tools.git
targetRevision: REPLACE_WITH_APPROVED_FULL_COMMIT_SHA
path: bundle
directory:
recurse: false
destination:
server: https://kubernetes.default.svc
namespace: network-tools
syncPolicy:
automated:
enabled: falseUse PreSync hooks and waves for their actual responsibilities
A PreSync Job can request fresh validation and return nonzero when the report is missing, stale or rejected. Its trusted checker must obtain the actual application revision and candidate bundle through an authenticated release mechanism, then compare them with the report. A manually typed revision environment variable is not sufficient binding. Provision the hook namespace, service account, credentials and network access before the sync; ordinary Sync-phase resources cannot satisfy a first-run PreSync dependency.
Place the hook annotation on the Job and wave annotations on the resources whose order is being controlled. A wave annotation on an Application does not order all resources inside that application. Phases take precedence over wave numbers. Use a completion or readiness check for a newly installed CRD or external dependency rather than treating an ordering delay as proof of readiness.
Argo CD does not run hooks during a selective resource sync. Restrict or otherwise cover bypass paths, test the exact sync modes in your installed version, and keep admission enforcement in place. Avoid treating similarly named sync options as interchangeable across versions. Preserve failed-hook evidence before cleanup. Neither waves nor hooks guarantee a successful rollout or an automatic recovery path.
# argo-presync-compliance-gate.yaml
# PreSync hook for the network-tools Application. It re-runs validation for the
# revision Argo CD is about to sync and writes the report to the evidence sink.
# A nonzero exit blocks the sync. Argo CD skips hooks on selective syncs, so
# restrict those separately.
apiVersion: batch/v1
kind: Job
metadata:
name: network-tools-presync-gate
namespace: network-tools
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
# Example mapping only; agree identifiers and evidence format with the audit owner.
compliance.example.invalid/control-mapping: "SOC2-CC8.1,NIST-SP800-53-CM-3,3GPP-TS33.117"
spec:
backoffLimit: 0
activeDeadlineSeconds: 180
template:
metadata:
annotations:
openshift.io/required-scc: restricted-v2
spec:
restartPolicy: Never
serviceAccountName: preflight-gate
# The checker talks to the validation service, not the Kubernetes API.
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: preflight-gate
# Same reviewed runner image as the GitLab job, pinned by digest.
image: registry.example.invalid/secops/preflight-runner@sha256:REPLACE_WITH_REVIEWED_DIGEST
command: ["python", "/opt/preflight/check.py"]
workingDir: /work
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
env:
- name: PREFLIGHT_TARGET
value: "staging-network-tools"
- name: PREFLIGHT_POLICY
value: "preflight-policy-v1"
# The checker resolves the approved revision and candidate bundle from
# the release record; nothing the hook receives is trusted on its own.
- name: RELEASE_RECORD
value: "REPLACE_WITH_RELEASE_RECORD_ID"
- name: EVIDENCE_SINK
value: "s3://REPLACE_WITH_EVIDENCE_BUCKET/preflight"
- name: MCP_ENDPOINT
valueFrom:
secretKeyRef: {name: preflight-gate, key: endpoint}
- name: MCP_ACCESS_TOKEN
valueFrom:
secretKeyRef: {name: preflight-gate, key: token}
volumeMounts:
- name: work
mountPath: /work
resources:
requests: {cpu: 100m, memory: 128Mi}
limits: {cpu: 500m, memory: 256Mi}
volumes:
- name: work
emptyDir: {}Keep agent context separate from release authorization
An AI agent can use approved tools to summarize a diff, link a failed admission check to a policy explanation or prepare questions for a reviewer. Treat repository text, log content and tool descriptions as untrusted input to the model. Do not let those inputs select new endpoints, expand permissions, suppress checks or authorize deployment.
Authenticate callers, authorize each target, bound tool inputs and sanitize outputs. The MCP service should use its own scoped downstream credentials rather than pass a client token through to a Kubernetes or telecom API. Retain the source revision, bundle digest, validator image or build identity, policy revision, target identity, observation times, check outcomes and reviewer decision. Sign or attest that evidence only through an actual configured signing system; JSON reports alone do not establish regulatory compliance.
Test rejection paths before enabling deployment
Begin with one application and a non-production target. Confirm that a missing check, duplicate check, unknown verdict, expired observation, wrong namespace mapping, changed manifest and changed Git revision each block promotion. Then test MCP timeout, authorization denial, unavailable admission webhook and a policy-version change. The report must not stay green when a validator did not run.
Exercise Argo CD full sync, any permitted selective sync, first installation and repeated synchronization. Change live state after the CI check to confirm that deployment-time revalidation catches stale assumptions. Complete rollout and application-specific checks after synchronization; a pre-flight pass is not an availability or performance result.
We provide MCP integration and AI-assisted NOC workflow services, with DevSecOps work within another engagement. Bring the delivery path, cluster versions, approval model and intended validation checks to the discussion. The implementation, operational responsibilities and support scope need to be agreed for your environment.