[{"data":1,"prerenderedAt":457},["ShallowReactive",2],{"blog-post-detail-build-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4":3,"blogs-all-posts-detail-suggestions-en":28,"blog-comments-build-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4-en":393},{"status":4,"source":5,"data":6},"success","markdown-file",{"id":7,"title":8,"slug":9,"lang":10,"category":11,"categorySlug":12,"summary":13,"excerpt":13,"author":14,"date":15,"readTime":16,"image":17,"tags":18,"publishedAt":22,"createdAt":22,"updatedAt":22,"filePath":23,"sourceUrl":24,"content":25,"seoTitle":26,"seoDescription":13,"canonicalUrl":27},"cron-1786956210950-en","Build an MCP Server in Go (Part 1): Designing a diagnostic-grade Kubernetes client","build-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4","en","Kubernetes","kubernetes","This post designs the Kubernetes client. The next post wraps it as an MCP server and wires it to an...","Fer Rios","8\u002F17\u002F2026","6 min read","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpw4b055hh8hxl5unrmvv.png",[11,12,19,20,21],"go","devops","mcp","2026-08-17T08:43:30.949Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fkubernetes\u002Fbuild-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4.md","https:\u002F\u002Fdev.to\u002Fferztyle\u002Fbuild-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-49a2","\n*This post designs the Kubernetes client.* *The next post* *wraps it as an MCP server and wires it to an agent.*\n\n## Asking instead of running\n\nThere's a familiar rhythm to debugging a bad deploy: `kubectl get pods`, spot the `CrashLoopBackOff`, `kubectl logs --previous`, still not obvious, `kubectl describe pod`, scroll to Events, cross-reference with `kubectl get events`, maybe check if the Service even has endpoints. Five or six commands, in your head, in a specific order, because you've done this enough times to know the order.\n\nThat order is exactly what an AI agent can execute for you, if you give it the right tools, ask \"why is `checkout-service` failing?\" and have it chain through the same commands you would have, arriving at an actual answer instead of a wall of YAML. The protocol that makes an agent capable of calling tools like that is the Model Context Protocol (MCP), and building that server is part 2 of this series.\n\nThis post is about what comes first and matters more: the Kubernetes client those tools will eventually sit on top of. An agent is only as good as what it's allowed to ask for. Hand it a client with one `ListPods` method and it can list pods, nothing else. Hand it a client that mirrors what a senior engineer actually checks when something's broken, pod state, events, endpoints, node capacity, rollout history, and it can genuinely diagnose. That client is a real piece of engineering on its own, independent of whether an LLM ever touches it, which is why it gets its own post before MCP enters the picture at all.\n\n### `ferctl` vs. an MCP server: two answers to the same problem\n\n`ferctl` was my first attempt at this: a Cobra CLI, backed by client-go, that wraps common troubleshooting checks into subcommands: `ferctl top`, `ferctl logs`, that shape. It's fast, deterministic, and scriptable. Run `ferctl describe-pod checkout-service`, get the same structured output every time, pipe it into`jq`, drop it into a CI step, no ambiguity about what ran or why. That predictability is exactly what a CLI is good at.\n\nWhat it doesn't do is investigate. `ferctl` runs the one command you gave it, you're still the one who has to know that a `CrashLoopBackOff` means \"check previous logs, then check events\", and you're still typing each step by hand. It's a faster way to run the commands you already know, not a way to skip learning them.\n\nAn MCP server flips that. You ask \"why is `checkout-service` failing\" once, and the agent decides the sequence, list pods, notices the restart count, pulls previous logs, cross-references events, the way a `ferctl` invocation never will, because no single subcommand can adapt its next step to what the last one returned. That's the shift this series is really about: from \"a faster way to run known commands\" to \"something that can figure out which commands to run.\" It's also, frankly, the more current way to build this kind of tooling, a fixed CLI surface is a fine interface for a human who already knows the shape of the problem, but an LLM-driven agent chaining calls dynamically is a better fit for the actual shape of debugging, which rarely follows a script.\n\nNone of that makes `ferctl` obsolete. A CLI is still the right tool when you want guaranteed, repeatable output, a CI health check, a pre-deploy gate, anything where non-determinism is a bug, not a feature. An agent is the right tool when the problem is exploratory, and you don't yet know which three commands you'll need. They're not really competitors; they're two different interfaces that answer \"is my cluster healthy\" in two different situations. And notably, both could sit on top of the exact same `KubeClient` this post builds, the interface doesn't care whether its caller is a Cobra command or an MCP tool handler, which is itself a small argument for designing the client first, independent of either.\n\n* * *\n\n## Project structure\n\n```plaintext\ngo-k8s-mcp-server\u002F\n├── go.mod\n└── internal\u002F\n    └── kubernetes\u002F\n        ├── client.go           ← KubeClient interface, constructor\n        ├── pods.go             ← PodClient implementation\n        ├── workloads.go        ← WorkloadClient implementation\n        ├── nodes.go            ← NodeClient implementation\n        ├── events.go           ← EventClient implementation\n        ├── network.go          ← NetworkClient implementation\n        ├── ingress.go          ← IngressClient implementation\n        ├── gateway.go          ← GatewayClient implementation\n        ├── config.go           ← ConfigClient implementation\n        ├── storage.go          ← StorageClient implementation\n        └── metrics.go          ← MetricsClient implementation\n```\n\nThis is only the client half of the project, next post adds `cmd\u002Fk8s-mcp-server\u002F` and `internal\u002Ftools\u002F` on top of this same `internal\u002Fkubernetes\u002F` package. The `internal\u002F` boundary and one-package-per-domain layout follow the same convention laid out in [Go packages and modules explained](https:\u002F\u002Fferztyle.me\u002Fgo-packages-and-modules-explained): `internal\u002F` for everything the Go toolchain should keep private to this module, one focused file per concern rather than one large one.\n\nIf you haven't set up a client-go connection before, this post assumes that groundwork. I covered kubeconfig loading, the Clientset, and how API groups work in [Talking to Kubernetes from Go: a practical client-go guide](https:\u002F\u002Fferztyle.me\u002Ftalking-to-kubernetes-from-go-a-practical-client-go-guide). Everything below builds directly on that same connection pattern, same `rest.Config` setup, same in-cluster\u002Fout-of-cluster fallback, same \"wrap the Clientset behind an interface\" philosophy. What's new here is the shape of the interface itself.\n\n* * *\n\n## Designing the `KubeClient` interface\n\nA tempting first move is one fat interface with every method the agent might ever need. Resist it. A twenty-method interface is hard to test, hard to fake, and it hides the fact that these methods address genuinely different diagnostic concerns: some are about pods, some are about scheduling, some are about networking. Go rewards small interfaces, so we compose one from several.\n\nIf you're starting this project fresh rather than following along from the client-go post, pin the packages to your cluster's version, same as before, mixing minor versions across them breaks at compile time. `k8s.io\u002Fmetrics` and `sigs.k8s.io\u002Fgateway-api` are included here too, even though neither is used until their respective sub-clients further down, because the `client` struct and `NewClient` constructor right below already need both:\n\n```shell\nmkdir go-k8s-mcp-server && cd go-k8s-mcp-server\ngo mod init github.com\u002FFerRiosCosta\u002Fgo-k8s-mcp-server\n\ngo get k8s.io\u002Fclient-go@v0.36.2\ngo get k8s.io\u002Fapi@v0.36.2\ngo get k8s.io\u002Fapimachinery@v0.36.2\ngo get k8s.io\u002Fmetrics@v0.36.2\ngo get sigs.k8s.io\u002Fgateway-api@v1.3.0\ngo mod tidy\n```\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fclient.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    \"k8s.io\u002Fclient-go\u002Fkubernetes\"\n    \"k8s.io\u002Fclient-go\u002Frest\"\n    \"k8s.io\u002Fclient-go\u002Ftools\u002Fclientcmd\"\n    metricsclientset \"k8s.io\u002Fmetrics\u002Fpkg\u002Fclient\u002Fclientset\u002Fversioned\"\n    gatewayclientset \"sigs.k8s.io\u002Fgateway-api\u002Fpkg\u002Fclient\u002Fclientset\u002Fversioned\"\n)\n\n\u002F\u002F KubeClient is the full set of read-only diagnostic operations the\n\u002F\u002F MCP server exposes as tools. It's composed from smaller, domain-specific\n\u002F\u002F interfaces so each one stays independently testable and each\n\u002F\u002F implementation file has exactly one reason to change.\n\u002F\u002F\n\u002F\u002F Every method here is read-only by design. There is no Delete, Scale,\n\u002F\u002F or Patch anywhere in this interface, that's not an accident, it's\n\u002F\u002F the security boundary. See \"Read-only by construction\" below.\ntype KubeClient interface {\n    PodClient\n    WorkloadClient\n    NodeClient\n    EventClient\n    NetworkClient\n    IngressClient\n    GatewayClient\n    ConfigClient\n    StorageClient\n    MetricsClient\n}\n\n\u002F\u002F client is the concrete implementation every sub-interface method\n\u002F\u002F below is defined on. It holds three Clientsets: the core Clientset\n\u002F\u002F for everything except metrics and Gateway API, a metrics-server\n\u002F\u002F Clientset (the split the client-go post flagged as needed once\n\u002F\u002F k8s.io\u002Fmetrics entered the picture), and a Gateway API Clientset,\n\u002F\u002F Gateway API is CRD-based, so it isn't reachable through the core\n\u002F\u002F Clientset the way Ingress is.\ntype client struct {\n    clientset        kubernetes.Interface\n    metricsClientset metricsclientset.Interface\n    gatewayClientset gatewayclientset.Interface\n}\n\n\u002F\u002F NewClient builds a KubeClient the same way kubectl resolves its\n\u002F\u002F config: in-cluster config if running inside a pod, otherwise\n\u002F\u002F $KUBECONFIG if set, otherwise ~\u002F.kube\u002Fconfig, and whichever\n\u002F\u002F context is marked current-context in that file, automatically.\n\u002F\u002F There's no path parameter here on purpose: hardcoding a path means\n\u002F\u002F this server silently ignores context switches made with\n\u002F\u002F `kubectl config use-context`, which is exactly the surprise you\n\u002F\u002F don't want from a diagnostic tool.\nfunc NewClient() (KubeClient, error) {\n    config, err := buildConfig()\n    if err != nil {\n        return nil, fmt.Errorf(\"build config: %w\", err)\n    }\n\n    clientset, err := kubernetes.NewForConfig(config)\n    if err != nil {\n        return nil, fmt.Errorf(\"create clientset: %w\", err)\n    }\n\n    metricsClientset, err := metricsclientset.NewForConfig(config)\n    if err != nil {\n        return nil, fmt.Errorf(\"create metrics clientset: %w\", err)\n    }\n\n    gatewayClientset, err := gatewayclientset.NewForConfig(config)\n    if err != nil {\n        return nil, fmt.Errorf(\"create gateway clientset: %w\", err)\n    }\n\n    return &client{\n        clientset:        clientset,\n        metricsClientset: metricsClientset,\n        gatewayClientset: gatewayClientset,\n    }, nil\n}\n\nfunc buildConfig() (*rest.Config, error) {\n    if config, err := rest.InClusterConfig(); err == nil {\n        return config, nil\n    }\n\n    \u002F\u002F NewDefaultClientConfigLoadingRules checks $KUBECONFIG first\n    \u002F\u002F (colon-separated on Linux\u002FmacOS, semicolon on Windows, merging\n    \u002F\u002F multiple files if listed), then falls back to ~\u002F.kube\u002Fconfig.\n    \u002F\u002F ConfigOverrides{} is empty on purpose, an empty overrides\n    \u002F\u002F struct means \"use whatever current-context is set,\" the same\n    \u002F\u002F default clientcmd.BuildConfigFromFlags gave us before, just\n    \u002F\u002F without hardcoding the path ourselves.\n    loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()\n    overrides := &clientcmd.ConfigOverrides{}\n    return clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides).ClientConfig()\n}\n```\n\nSame packages as the client-go post's `client.go`, plus two additions: `metricsclientset` from `k8s.io\u002Fmetrics`, and `gatewayclientset` from `sigs.k8s.io\u002Fgateway-api`, both alongside the core Clientset. `gatewayClientset` will show `NewForConfig` succeed even against a cluster with no Gateway API CRDs installed; it's just a REST client pointed at a set of API paths, and only fails once you actually call it against a cluster that doesn't serve them. That failure mode gets called out concretely in the Gateway API section below.\n\nEvery method in the sections below is defined on this same `*client` type, one file per domain.\n\n### Pods, the core of \"why is my app broken\"\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fpods.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"io\"\n\n    corev1 \"k8s.io\u002Fapi\u002Fcore\u002Fv1\"\n    metav1 \"k8s.io\u002Fapimachinery\u002Fpkg\u002Fapis\u002Fmeta\u002Fv1\"\n)\n\n\u002F\u002F PodClient covers the operations that answer \"what's wrong with this pod.\"\ntype PodClient interface {\n    \u002F\u002F ListPods returns pods in namespace. An empty namespace returns\n    \u002F\u002F pods from every namespace, same convention as ListPods in the\n    \u002F\u002F client-go guide.\n    ListPods(ctx context.Context, namespace string) (*corev1.PodList, error)\n\n    \u002F\u002F GetPod returns a single pod by name, used once the agent has\n    \u002F\u002F narrowed down which pod it cares about.\n    GetPod(ctx context.Context, namespace, name string) (*corev1.Pod, error)\n\n    \u002F\u002F GetPodLogs returns log output for one container in a pod.\n    GetPodLogs(ctx context.Context, namespace, name string, opts LogOptions) (string, error)\n}\n\n\u002F\u002F LogOptions bounds a log request. This type exists specifically so an\n\u002F\u002F agent can never accidentally pull megabytes of logs in a single tool\n\u002F\u002F call, TailLines and SinceSeconds are the caps, not suggestions.\ntype LogOptions struct {\n    Container    string \u002F\u002F empty selects the pod's first container\n    Previous     bool   \u002F\u002F true fetches the last terminated instance's logs,\n                         \u002F\u002F essential for CrashLoopBackOff, since the *current*\n                         \u002F\u002F instance often hasn't logged the failure yet\n    TailLines    int64  \u002F\u002F 0 falls back to a safe default, never \"unbounded\"\n    SinceSeconds int64  \u002F\u002F 0 means no time bound\n}\n\nconst defaultLogTailLines = 200\n\nfunc (c *client) ListPods(ctx context.Context, namespace string) (*corev1.PodList, error) {\n    pods, err := c.clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"list pods namespace=%q: %w\", namespace, err)\n    }\n    return pods, nil\n}\n\nfunc (c *client) GetPod(ctx context.Context, namespace, name string) (*corev1.Pod, error) {\n    pod, err := c.clientset.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get pod namespace=%q name=%q: %w\", namespace, name, err)\n    }\n    return pod, nil\n}\n\nfunc (c *client) GetPodLogs(ctx context.Context, namespace, name string, opts LogOptions) (string, error) {\n    tail := opts.TailLines\n    if tail \u003C= 0 {\n        tail = defaultLogTailLines\n    }\n\n    podLogOpts := &corev1.PodLogOptions{\n        Container: opts.Container,\n        Previous:  opts.Previous,\n        TailLines: &tail,\n    }\n    if opts.SinceSeconds > 0 {\n        podLogOpts.SinceSec    }\n\n    req := c.clientset.CoreV1().Pods(namespace).GetLogs(name, podLogOpts)\n    stream, err := req.Stream(ctx)\n    if err != nil {\n        return \"\", fmt.Errorf(\"stream logs namespace=%q pod=%q: %w\", namespace, name, err)\n    }\n    defer stream.Close()\n\n    data, err := io.ReadAll(stream)\n    if err != nil {\n        return \"\", fmt.Errorf(\"read logs namespace=%q pod=%q: %w\", namespace, name, err)\n    }\n    return string(data), nil\n}\n```\n\nNote that `TailLines` defaults to `200`, not `0` meaning unbounded. That single line is the difference between a tool an agent can call freely and a tool that occasionally hands it 40,000 lines of stack traces and blows past its context window on one call.\n\n### Events, often the fastest path to a root cause\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fevents.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    corev1 \"k8s.io\u002Fapi\u002Fcore\u002Fv1\"\n    metav1 \"k8s.io\u002Fapimachinery\u002Fpkg\u002Fapis\u002Fmeta\u002Fv1\"\n)\n\n\u002F\u002F EventClient covers the Events API, which frequently surfaces a root\n\u002F\u002F cause, a failed scheduling attempt, an image pull error, before\n\u002F\u002F anything else does. It's queried by involved object, not by name,\n\u002F\u002F which is different enough from PodClient to earn its own file.\ntype EventClient interface {\n    \u002F\u002F GetEvents returns events in namespace, optionally filtered to\n    \u002F\u002F those involving a specific object name (e.g. a Pod).\n    \u002F\u002F An empty involvedObjectName returns all events in the namespace.\n    GetEvents(ctx context.Context, namespace, involvedObjectName string) ([]corev1.Event, error)\n}\n\nfunc (c *client) GetEvents(ctx context.Context, namespace, involvedObjectName string) ([]corev1.Event, error) {\n    opts := metav1.ListOptions{}\n    if involvedObjectName != \"\" {\n        opts.FieldSelector = \"involvedObject.name=\" + involvedObjectName\n    }\n\n    events, err := c.clientset.CoreV1().Events(namespace).List(ctx, opts)\n    if err != nil {\n        return nil, fmt.Errorf(\"list events namespace=%q object=%q: %w\", namespace, involvedObjectName, err)\n    }\n    return events.Items, nil\n}\n```\n\n### Workloads, rollout and scheduling problems above the pod\n\nNot every failure lives at the pod level. A deployment that's stuck rolling out, or one whose pods belong to two different generations at once, needs a different query shape entirely: you're not asking about one pod anymore, you're asking about the relationship between a Deployment and the ReplicaSets it owns.\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fworkloads.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    appsv1 \"k8s.io\u002Fapi\u002Fapps\u002Fv1\"\n    metav1 \"k8s.io\u002Fapimachinery\u002Fpkg\u002Fapis\u002Fmeta\u002Fv1\"\n)\n\n\u002F\u002F WorkloadClient covers Deployments and the ReplicaSets they own,\n\u002F\u002F the layer above individual pods, where rollout and scheduling\n\u002F\u002F problems that span multiple pods actually live.\ntype WorkloadClient interface {\n    \u002F\u002F GetDeployment returns a Deployment's spec and status, including\n    \u002F\u002F desired vs. available replica counts and rollout conditions.\n    GetDeployment(ctx context.Context, namespace, name string) (*appsv1.Deployment, error)\n\n    \u002F\u002F ListReplicaSets finds every ReplicaSet matching labelSelector in\n    \u002F\u002F a namespace. This is what surfaces orphaned or stuck ReplicaSets,\n    \u002F\u002F the classic \"Deployment says 3\u002F3 but two of those pods belong to\n    \u002F\u002F the old ReplicaSet\" failed-rollout scenario, which GetDeployment\n    \u002F\u002F alone won't show you.\n    ListReplicaSets(ctx context.Context, namespace, labelSelector string) (*appsv1.ReplicaSetList, error)\n}\n\nfunc (c *client) GetDeployment(ctx context.Context, namespace, name string) (*appsv1.Deployment, error) {\n    deploy, err := c.clientset.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get deployment namespace=%q name=%q: %w\", namespace, name, err)\n    }\n    return deploy, nil\n}\n\nfunc (c *client) ListReplicaSets(ctx context.Context, namespace, labelSelector string) (*appsv1.ReplicaSetList, error) {\n    rs, err := c.clientset.AppsV1().ReplicaSets(namespace).List(ctx, metav1.ListOptions{\n        LabelSelector: labelSelector,\n    })\n    if err != nil {\n        return nil, fmt.Errorf(\"list replicasets namespace=%q selector=%q: %w\", namespace, labelSelector, err)\n    }\n    return rs, nil\n}\n```\n\n`ListReplicaSets` takes a label selector rather than a Deployment name because that's what the ReplicaSets API actually indexes on, a Deployment doesn't \"contain\" its ReplicaSets, it selects them by label, the same way a Service selects its pods. To go from \"diagnose this Deployment\" to \"here are its ReplicaSets,\" a tool built on this interface pulls the selector off the Deployment's spec first (`deploy.Spec.Selector`), then passes that into `ListReplicaSets`. That two-step shape is exactly why this stays two methods instead of one convenience method that hides the relationship, the tool layer is where composing them into a single `diagnose_rollout`\\-style tool belongs, not the client.\n\n### Nodes, capacity and scheduling context\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fnodes.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    corev1 \"k8s.io\u002Fapi\u002Fcore\u002Fv1\"\n    metav1 \"k8s.io\u002Fapimachinery\u002Fpkg\u002Fapis\u002Fmeta\u002Fv1\"\n)\n\n\u002F\u002F NodeClient covers cluster capacity, what you need to explain a\n\u002F\u002F \"0\u002F3 nodes are available\" scheduling failure, which is a node-level\n\u002F\u002F question, not a pod-level one.\ntype NodeClient interface {\n    ListNodes(ctx context.Context) (*corev1.NodeList, error)\n\n    \u002F\u002F GetNode returns conditions (MemoryPressure, DiskPressure,\n    \u002F\u002F PIDPressure), taints, and allocatable resources for one node.\n    GetNode(ctx context.Context, name string) (*corev1.Node, error)\n}\n\nfunc (c *client) ListNodes(ctx context.Context) (*corev1.NodeList, error) {\n    nodes, err := c.clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"list nodes: %w\", err)\n    }\n    return nodes, nil\n}\n\nfunc (c *client) GetNode(ctx context.Context, name string) (*corev1.Node, error) {\n    node, err := c.clientset.CoreV1().Nodes().Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get node name=%q: %w\", name, err)\n    }\n    return node, nil\n}\n```\n\n### Network, is anything actually answering\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fnetwork.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    corev1 \"k8s.io\u002Fapi\u002Fcore\u002Fv1\"\n    metav1 \"k8s.io\u002Fapimachinery\u002Fpkg\u002Fapis\u002Fmeta\u002Fv1\"\n)\n\n\u002F\u002F NetworkClient covers Services and Endpoints. A Service existing\n\u002F\u002F doesn't mean anything is listening behind it, that's exactly the\n\u002F\u002F gap GetEndpoints is here to close.\ntype NetworkClient interface {\n    GetService(ctx context.Context, namespace, name string) (*corev1.Service, error)\n\n    \u002F\u002F GetEndpoints checks whether a Service actually has healthy pods\n    \u002F\u002F backing it. Zero endpoints despite matching pods usually means a\n    \u002F\u002F label selector mismatch or a failing readiness probe, one of the\n    \u002F\u002F most common \"works locally, broken in cluster\" bugs.\n    GetEndpoints(ctx context.Context, namespace, serviceName string) (*corev1.Endpoints, error)\n}\n\nfunc (c *client) GetService(ctx context.Context, namespace, name string) (*corev1.Service, error) {\n    svc, err := c.clientset.CoreV1().Services(namespace).Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get service namespace=%q name=%q: %w\", namespace, name, err)\n    }\n    return svc, nil\n}\n\nfunc (c *client) GetEndpoints(ctx context.Context, namespace, serviceName string) (*corev1.Endpoints, error) {\n    \u002F\u002F The Endpoints object shares its name with the Service it backs,\n    \u002F\u002F Kubernetes creates and keeps it in sync automatically, so no\n    \u002F\u002F separate lookup by label is needed here.\n    eps, err := c.clientset.CoreV1().Endpoints(namespace).Get(ctx, serviceName, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get endpoints namespace=%q service=%q: %w\", namespace, serviceName, err)\n    }\n    return eps, nil\n}\n```\n\n### Ingress on EKS, is the ALB even pointing here\n\nA healthy Service with healthy endpoints still means nothing to an external caller if the layer in front of it, the Ingress, and on EKS almost always the AWS Load Balancer Controller (LBC) behind it, never finished provisioning. This is a different failure class from anything `NetworkClient` covers: the Service can be perfectly correct and traffic still never arrives, because the ALB was never created, was created against the wrong subnets, or is pointing at the wrong target group.\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fingress.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    networkingv1 \"k8s.io\u002Fapi\u002Fnetworking\u002Fv1\"\n    metav1 \"k8s.io\u002Fapimachinery\u002Fpkg\u002Fapis\u002Fmeta\u002Fv1\"\n)\n\n\u002F\u002F IngressClient covers classic Ingress resources, on EKS, almost\n\u002F\u002F always reconciled by the AWS Load Balancer Controller into an ALB.\n\u002F\u002F No AWS API calls happen here; everything comes from the Kubernetes\n\u002F\u002F object the controller writes status and events back onto, which is\n\u002F\u002F usually enough to tell you where reconciliation stalled.\ntype IngressClient interface {\n    GetIngress(ctx context.Context, namespace, name string) (*networkingv1.Ingress, error)\n\n    \u002F\u002F ListIngressClasses lets a diagnosis confirm the Ingress's\n    \u002F\u002F ingressClassName actually exists and matches a real controller.\n    \u002F\u002F A typo'd or missing class is a common reason an Ingress just\n    \u002F\u002F sits there, the LBC never picks it up, and nothing in the\n    \u002F\u002F Ingress object itself says why.\n    ListIngressClasses(ctx context.Context) (*networkingv1.IngressClassList, error)\n}\n\nfunc (c *client) GetIngress(ctx context.Context, namespace, name string) (*networkingv1.Ingress, error) {\n    ing, err := c.clientset.NetworkingV1().Ingresses(namespace).Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get ingress namespace=%q name=%q: %w\", namespace, name, err)\n    }\n    return ing, nil\n}\n\nfunc (c *client) ListIngressClasses(ctx context.Context) (*networkingv1.IngressClassList, error) {\n    classes, err := c.clientset.NetworkingV1().IngressClasses().List(ctx, metav1.ListOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"list ingress classes: %w\", err)\n    }\n    return classes, nil\n}\n```\n\nThree things matter when reading an `Ingress` object for diagnosis, worth knowing even before the tool layer wraps this in part 2:\n\n*   `ingress.Status.LoadBalancer.Ingress`: empty means the LBC either hasn't reconciled yet or is stuck. A populated hostname (the ALB's DNS name) means AWS-side provisioning succeeded; an empty one after more than a minute or two almost always means the LBC is failing, not just slow.\n    \n*   `ingress.Spec.IngressClassName`: cross-reference this against `ListIngressClasses` output. A `nil` or misspelled class is the single most common reason an Ingress is silently ignored, no error on the object itself, the LBC just never claims it.\n    \n*   **Events on the Ingress object**: this is where `EventClient.GetEvents`, already built above, becomes directly useful here without any new code: the LBC writes events like `SuccessfullyReconciled` on success, and specific failure reasons (invalid target group, subnet tagging problems, certificate ARN not found) as `Warning` events when reconciliation fails. Composing `GetIngress` with `GetEvents` is exactly the same pattern `describe_pod` used for pods, applied one layer up the stack.\n    \n\n### Gateway API, the newer, cluster-portable entry point\n\nGateway API is the follow-on to Ingress: instead of one annotation-heavy resource, traffic routing splits across a `Gateway` (the listener; where traffic enters, which addresses\u002Fports it accepts) and route resources like `HTTPRoute` (how it's matched and where it goes). EKS supports it the same way it supports Ingress, the AWS Load Balancer Controller reconciles `Gateway`\u002F`HTTPRoute` into an ALB, the same way it reconciles `Ingress`.\n\nThe client-side difference is real, not cosmetic: Gateway API types are CRDs, not part of the core Kubernetes API, so they need their own Clientset, the `gatewayClientset` already added to the `client` struct above.\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fgateway.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    gatewayv1 \"sigs.k8s.io\u002Fgateway-api\u002Fapis\u002Fv1\"\n    metav1 \"k8s.io\u002Fapimachinery\u002Fpkg\u002Fapis\u002Fmeta\u002Fv1\"\n)\n\n\u002F\u002F GatewayClient covers Gateway API's two core resources. Both come\n\u002F\u002F back with rich status.conditions, Gateway API standardizes on\n\u002F\u002F Accepted\u002FProgrammed for Gateways and Accepted\u002FResolvedRefs for\n\u002F\u002F routes, which is a more structured diagnostic signal than Ingress\n\u002F\u002F ever gave you: no need to infer state from events alone.\ntype GatewayClient interface {\n    GetGateway(ctx context.Context, namespace, name string) (*gatewayv1.Gateway, error)\n\n    \u002F\u002F GetHTTPRoute returns the route's status, including\n    \u002F\u002F per-parent-Gateway conditions. ResolvedRefs=False on a route\n    \u002F\u002F almost always means a backendRef points at a Service or port\n    \u002F\u002F that doesn't exist, check it before assuming the problem is\n    \u002F\u002F upstream at the Gateway.\n    GetHTTPRoute(ctx context.Context, namespace, name string) (*gatewayv1.HTTPRoute, error)\n}\n\nfunc (c *client) GetGateway(ctx context.Context, namespace, name string) (*gatewayv1.Gateway, error) {\n    gw, err := c.gatewayClientset.GatewayV1().Gateways(namespace).Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get gateway namespace=%q name=%q: %w\", namespace, name, err)\n    }\n    return gw, nil\n}\n\nfunc (c *client) GetHTTPRoute(ctx context.Context, namespace, name string) (*gatewayv1.HTTPRoute, error) {\n    route, err := c.gatewayClientset.GatewayV1().HTTPRoutes(namespace).Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get httproute namespace=%q name=%q: %w\", namespace, name, err)\n    }\n    return route, nil\n}\n```\n\nDiagnosis here is a two-level check, and skipping the first level is the most common mistake:\n\n*   `gateway.Status.Conditions`: check `Accepted` and `Programmed` first. If the `Gateway` itself isn't `Programmed`, no `HTTPRoute` attached to it can possibly work, no matter how correct that route is. This is the equivalent of checking `describe_pod`'s container state before chasing application-level logs, start at the layer closest to the infrastructure.\n    \n*   `httproute.Status.Parents[].Conditions`: a route can reference multiple Gateways, so status is reported per-parent, not once. `ResolvedRefs: False` specifically means a `backendRef`, the Service and port the route sends traffic to, doesn't resolve. That's diagnosable the same way `check_endpoints` diagnoses a plain Service: the route's `backendRefs[].name` and `.port` are exactly what `GetService`\u002F`GetEndpoints` from earlier need to cross-check.\n    \n*   **A cluster without Gateway API CRDs installed**: surfaces as a real error from `GetGateway`\u002F`GetHTTPRoute`, a `NotFound`\\-shaped error on the CRD's group\u002Fversion, not on the specific object. Worth catching and wrapping with a clearer message at the tool layer in part 2, the same way `pod_metrics` wraps a missing metrics-server as a clearer error than the bare API response gives you.\n    \n\n### Config, keys only, never values\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fconfig.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    metav1 \"k8s.io\u002Fapimachinery\u002Fpkg\u002Fapis\u002Fmeta\u002Fv1\"\n)\n\n\u002F\u002F ConfigClient deliberately returns key names only, for both\n\u002F\u002F ConfigMaps and Secrets. An agent diagnosing a missing env var needs\n\u002F\u002F to know a key exists, never what it contains, see \"Read-only by\n\u002F\u002F construction\" below for why that boundary lives here and not in a\n\u002F\u002F tool description.\ntype ConfigClient interface {\n    GetConfigMapKeys(ctx context.Context, namespace, name string) ([]string, error)\n    GetSecretKeys(ctx context.Context, namespace, name string) ([]string, error)\n}\n\nfunc (c *client) GetConfigMapKeys(ctx context.Context, namespace, name string) ([]string, error) {\n    cm, err := c.clientset.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get configmap namespace=%q name=%q: %w\", namespace, name, err)\n    }\n    keys := make([]string, 0, len(cm.Data))\n    for k := range cm.Data {\n        keys = append(keys, k)\n    }\n    return keys, nil\n}\n\nfunc (c *client) GetSecretKeys(ctx context.Context, namespace, name string) ([]string, error) {\n    secret, err := c.clientset.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get secret namespace=%q name=%q: %w\", namespace, name, err)\n    }\n    \u002F\u002F secret.Data is map[string][]byte, the values are deliberately\n    \u002F\u002F never read here, only the key names.\n    keys := make([]string, 0, len(secret.Data))\n    for k := range secret.Data {\n        keys = append(keys, k)\n    }\n    return keys, nil\n}\n```\n\n### Storage, volumes that can't bind\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fstorage.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    corev1 \"k8s.io\u002Fapi\u002Fcore\u002Fv1\"\n    metav1 \"k8s.io\u002Fapimachinery\u002Fpkg\u002Fapis\u002Fmeta\u002Fv1\"\n)\n\n\u002F\u002F StorageClient surfaces the \"pod stuck Pending because its volume\n\u002F\u002F can't bind\" class of failure, a Pending PVC explains a Pending pod\n\u002F\u002F that otherwise looks like a scheduling mystery.\ntype StorageClient interface {\n    ListPVCs(ctx context.Context, namespace string) (*corev1.PersistentVolumeClaimList, error)\n}\n\nfunc (c *client) ListPVCs(ctx context.Context, namespace string) (*corev1.PersistentVolumeClaimList, error) {\n    pvcs, err := c.clientset.CoreV1().PersistentVolumeClaims(namespace).List(ctx, metav1.ListOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"list pvcs namespace=%q: %w\", namespace, err)\n    }\n    return pvcs, nil\n}\n```\n\n### Metrics, the same extension the client-go post already teed up\n\n`k8s.io\u002Fmetrics` was already installed above alongside the three core packages, since `NewClient` needed `metricsclientset` before this section even started. This is where it actually gets used:\n\n```go\n\u002F\u002F internal\u002Fkubernetes\u002Fmetrics.go\npackage kubernetes\n\nimport (\n    \"context\"\n    \"fmt\"\n\n    metav1 \"k8s.io\u002Fapimachinery\u002Fpkg\u002Fapis\u002Fmeta\u002Fv1\"\n    metricsv1beta1 \"k8s.io\u002Fmetrics\u002Fpkg\u002Fapis\u002Fmetrics\u002Fv1beta1\"\n)\n\n\u002F\u002F MetricsClient reads from the metrics-server extension API, a\n\u002F\u002F separate Clientset from the core one, same as it was flagged in the\n\u002F\u002F client-go post's \"what's next\" for the ferctl top CLI.\ntype MetricsClient interface {\n    GetPodMetrics(ctx context.Context, namespace, name string) (*metricsv1beta1.PodMetrics, error)\n    GetNodeMetrics(ctx context.Context, name string) (*metricsv1beta1.NodeMetrics, error)\n}\n\nfunc (c *client) GetPodMetrics(ctx context.Context, namespace, name string) (*metricsv1beta1.PodMetrics, error) {\n    m, err := c.metricsClientset.MetricsV1beta1().PodMetricses(namespace).Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get pod metrics namespace=%q name=%q: %w\", namespace, name, err)\n    }\n    return m, nil\n}\n\nfunc (c *client) GetNodeMetrics(ctx context.Context, name string) (*metricsv1beta1.NodeMetrics, error) {\n    m, err := c.metricsClientset.MetricsV1beta1().NodeMetricses().Get(ctx, name, metav1.GetOptions{})\n    if err != nil {\n        return nil, fmt.Errorf(\"get node metrics name=%q: %w\", name, err)\n    }\n    return m, nil\n}\n```\n\n`GetPodMetrics` and `GetNodeMetrics` reach through a second Clientset, `c.metricsClientset`, built from `k8s.io\u002Fmetrics` alongside the core one in `NewClient`, the same two-Clientset shape the client-go post flagged as coming in a future post, now realized here instead.\n\n`GetConfigMapKeys` and `GetSecretKeys` are worth pausing on. It would be less code to return the whole object, `map[string]string` and all. Returning only the keys is a security decision made at the interface boundary, before a single tool or prompt exists, an agent that can only ever see \"this Secret has a key called `DATABASE_URL`\" cannot leak what that key contains, no matter how it's prompted. That's a much stronger guarantee than \"the tool description tells the agent not to print secret values.\"\n\n### Read-only by construction\n\n## Summary\n\nEvery method on `KubeClient` is `List`, `Get`, or `Stream`, nothing that mutates the cluster. That boundary, along with the read-only `Secret`\u002F`ConfigMap` key-only design and the bounded `LogOptions`, all live in the Go types themselves rather than in documentation someone has to remember to follow. Three things worth taking away:\n\n*   Compose small, domain-specific interfaces (`PodClient`, `EventClient`, `NetworkClient`, ...) instead of one large `KubeClient` with twenty methods on it, easier to test, easier to fake, and each file has one reason to change.\n    \n*   Design the client before you design anything that calls it. Whether the caller ends up being a CLI, a report generator, or an AI agent, the interface should already answer \"what's actually useful to ask a cluster\" independent of who's asking.\n    \n*   Every safety boundary that matters, read-only access, bounded queries, secret values never leaving the cluster, belongs in the interface's types and method signatures, not in a comment asking future callers to be careful.\n    \n\nPart 2 picks up exactly here: wrapping this `KubeClient` as MCP tools, wiring it to an agent, and running the whole thing end-to-end against a real cluster. [Building a Kubernetes-aware AI agent with a Go MCP server →](https:\u002F\u002Fdev.to\u002Fferztyle\u002Fbuild-an-mcp-server-in-go-part-2-building-the-mcp-tool-layer-4l3m)\n\n---\n## **Let's connect!**\n\nOne of the best parts of writing in public is the people you meet along the way, engineers at different stages of their journey, working on similar problems from completely different angles.\n\nIf something in this post resonated, if you spotted a bug, or if you just want to talk Go, Kubernetes, Platform Engineering, DevOps, or whatever, I'm always happy to hear from you.\n\n*   [**LinkedIn**](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fferrios\u002F)\n    \n*   [**GitHub**](https:\u002F\u002Fgithub.com\u002FFerRiosCosta)\n    \n*   [**Twitter \u002F X**](https:\u002F\u002Fx.com\u002Fferztyle)\n    \n\n*Building from Asunción, Paraguay 🇵🇾*\n\n---\n\n> 🔗 **Original Source**: [Fer Rios](https:\u002F\u002Fdev.to\u002Fferztyle\u002Fbuild-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-49a2)\n","Build an MCP Server in Go (Part 1): Designing a diagnostic-grade Kubernetes client - Intlight Insights","https:\u002F\u002Fintlighttech.com\u002Fblogs\u002Fbuild-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4",{"items":29,"total":356,"page":357,"totalPages":357,"limit":358,"lang":10,"categories":359,"popularTags":373},[30,47,63,79,81,95,108,121,135,149,163,175,188,200,214,227,241,253,267,280,294,307,319,332,344],{"id":31,"title":32,"slug":33,"lang":10,"category":34,"categorySlug":35,"summary":36,"excerpt":36,"author":37,"date":15,"readTime":16,"image":38,"tags":39,"publishedAt":44,"createdAt":44,"updatedAt":44,"filePath":45,"sourceUrl":46},"cron-1786956218725-en","I Thought I'd Lost the Plot. I Was Writing It.","i-thought-id-lost-the-plot-i-was-writing-it-wjvz","AI Agents","ai-agents","I Thought I'd Lost the Plot. I Was Writing It.            I set out to build autonomous...","Joe Black","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1200,height=627,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fa3l9h76kr920p8bm2mjp.png",[34,40,41,42,43],"claudecode","aiagents","developmenttools","autonomousagents","2026-08-17T08:43:38.725Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fai-agents\u002Fi-thought-id-lost-the-plot-i-was-writing-it-wjvz.md","https:\u002F\u002Fdev.to\u002Fjoeblackwaslike\u002Fi-thought-id-lost-the-plot-i-was-writing-it-5fil",{"id":48,"title":49,"slug":50,"lang":10,"category":51,"categorySlug":52,"summary":53,"excerpt":53,"author":54,"date":15,"readTime":16,"image":55,"tags":56,"publishedAt":60,"createdAt":60,"updatedAt":60,"filePath":61,"sourceUrl":62},"cron-1786956216746-en","What Is the Circuit Breaker Pattern? A Practical Guide","what-is-the-circuit-breaker-pattern-a-practical-guide-4j68","Microservices","microservices","What Is the Circuit Breaker Pattern? A Practical Guide for Developers   Imagine your...","Avijit Bera","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6cy1uyqxjd14ikkveev.png",[51,52,57,58,59],"backend","api","Trending","2026-08-17T08:43:36.746Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fmicroservices\u002Fwhat-is-the-circuit-breaker-pattern-a-practical-guide-4j68.md","https:\u002F\u002Fdev.to\u002Favijitbera\u002Fwhat-is-the-circuit-breaker-pattern-a-practical-guide-20i4",{"id":64,"title":65,"slug":66,"lang":10,"category":67,"categorySlug":68,"summary":69,"excerpt":69,"author":70,"date":15,"readTime":16,"image":71,"tags":72,"publishedAt":76,"createdAt":76,"updatedAt":76,"filePath":77,"sourceUrl":78},"cron-1786956214008-en","I attacked my own npm package before launching it. It let the proposer approve their own writes","i-attacked-my-own-npm-package-before-launching-it-it-let-the-proposer-approve-their-own-writes-oi6y","Security","security","My library exists so a human approves an LLM's UPDATE before it runs. It never checked that the approver was somebody other than the proposer — and wrote \\\"approved\\\" into the audit trail anyway.","hyuga","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1200,height=627,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F65yv5n4qvrgn3t6ot4gy.png",[67,73,74,68,75],"opensource","ai","database","2026-08-17T08:43:34.007Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fsecurity\u002Fi-attacked-my-own-npm-package-before-launching-it-it-let-the-proposer-approve-their-own-writes-oi6y.md","https:\u002F\u002Fdev.to\u002Fhyuga611\u002Fi-attacked-my-own-npm-package-before-launching-it-it-let-the-proposer-approve-their-own-writes-4mki",{"id":7,"title":8,"slug":9,"lang":10,"category":11,"categorySlug":12,"summary":13,"excerpt":13,"author":14,"date":15,"readTime":16,"image":17,"tags":80,"publishedAt":22,"createdAt":22,"updatedAt":22,"filePath":23,"sourceUrl":24},[11,12,19,20,21],{"id":82,"title":83,"slug":84,"lang":10,"category":34,"categorySlug":35,"summary":85,"excerpt":85,"author":86,"date":15,"readTime":16,"image":87,"tags":88,"publishedAt":92,"createdAt":92,"updatedAt":92,"filePath":93,"sourceUrl":94},"cron-1786956117904-en","The Write Policy Is the Hard Part: Promotion Pipelines for Agent Memory","the-write-policy-is-the-hard-part-promotion-pipelines-for-agent-memory-4qv9","Storing agent memory is easy. Deciding what earns a permanent write, and keeping the write-path alive through RBAC and network policy, is the real work.","Guatu","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fguatulabs.dev%2Fog%2Fthe-write-policy-is-the-hard-part-promotion-pipelines-for-agent-memory.png",[34,41,89,90,91],"agentmemory","rbac","networkpolicies","2026-08-17T08:41:57.903Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fai-agents\u002Fthe-write-policy-is-the-hard-part-promotion-pipelines-for-agent-memory-4qv9.md","https:\u002F\u002Fdev.to\u002Ffuthgar\u002Fthe-write-policy-is-the-hard-part-promotion-pipelines-for-agent-memory-5mc",{"id":96,"title":97,"slug":98,"lang":10,"category":34,"categorySlug":35,"summary":99,"excerpt":99,"author":100,"date":15,"readTime":101,"image":102,"tags":103,"publishedAt":105,"createdAt":105,"updatedAt":105,"filePath":106,"sourceUrl":107},"cron-1786955347611","I Changed How I Think About AI Memory","i-changed-how-i-think-about-ai-memory-fnpm","I Changed How I Think About AI Memory   When I first built Lean AI Memory, I focused too...","Phúc Phùng","6 phút","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1200,height=627,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0oqod5zshdan74573bau.png",[34,74,41,73,104],"git","2026-08-17T08:29:07.610Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fai-agents\u002Fi-changed-how-i-think-about-ai-memory-fnpm.md","https:\u002F\u002Fdev.to\u002Fphucphungbk\u002Fi-changed-how-i-think-about-ai-memory-4mkd",{"id":109,"title":110,"slug":111,"lang":10,"category":51,"categorySlug":52,"summary":112,"excerpt":112,"author":113,"date":15,"readTime":101,"image":114,"tags":115,"publishedAt":118,"createdAt":118,"updatedAt":118,"filePath":119,"sourceUrl":120},"cron-1786955347443","Real-Life Refactoring Example: ~3x Less Code to Read","real-life-refactoring-example-3x-less-code-to-read-dccm","There is a popular idea that refactoring is making code shorter. It is not entirely wrong....","Valentine Shi","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1200,height=627,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foubl9dlj23byhsak2mqy.png",[51,116,117,57,52],"node","software","2026-08-17T08:29:07.443Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fmicroservices\u002Freal-life-refactoring-example-3x-less-code-to-read-dccm.md","https:\u002F\u002Fdev.to\u002Fvalentineshi-dev\u002Freal-life-refactoring-example-3x-less-code-to-read-3mdl",{"id":122,"title":123,"slug":124,"lang":10,"category":67,"categorySlug":68,"summary":125,"excerpt":125,"author":126,"date":15,"readTime":101,"image":127,"tags":128,"publishedAt":132,"createdAt":132,"updatedAt":132,"filePath":133,"sourceUrl":134},"cron-1786955347034","The Tragedy of the Clean-Handed Auditor","the-tragedy-of-the-clean-handed-auditor-rgoz","\\\"I could save them if they'd only listen...\\\"  Hey, you. Yeah, you: the compliance or governance...","Ben Link","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5z426h3tt6e2b3sthd2f.png",[67,68,129,130,131],"compliance","developers","careerdevelopment","2026-08-17T08:29:07.034Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fsecurity\u002Fthe-tragedy-of-the-clean-handed-auditor-rgoz.md","https:\u002F\u002Fdev.to\u002Flinkbenjamin\u002Fthe-tragedy-of-the-clean-handed-auditor-1253",{"id":136,"title":137,"slug":138,"lang":10,"category":11,"categorySlug":12,"summary":139,"excerpt":139,"author":140,"date":15,"readTime":101,"image":141,"tags":142,"publishedAt":146,"createdAt":146,"updatedAt":146,"filePath":147,"sourceUrl":148},"cron-1786955346614","Building Sluice: QoS-Aware Capacity Governance for Self-Hosted LLM Inference","building-sluice-qos-aware-capacity-governance-for-self-hosted-llm-inference-flbu","📦 Project: https:\u002F\u002Fgithub.com\u002FVampiricCyborg\u002Fsluice           1. The Problem: When Capacity Becomes...","Madhav M S","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1200,height=627,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh3vuie9oakcn3bqzxtbh.png",[11,143,144,12,145],"distributedsystems","llm","systemdesign","2026-08-17T08:29:06.613Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fkubernetes\u002Fbuilding-sluice-qos-aware-capacity-governance-for-self-hosted-llm-inference-flbu.md","https:\u002F\u002Fdev.to\u002Fvampiriccyborg\u002Fbuilding-sluice-qos-aware-capacity-governance-for-self-hosted-llm-inference-13ja",{"id":150,"title":151,"slug":152,"lang":10,"category":34,"categorySlug":35,"summary":153,"excerpt":153,"author":154,"date":15,"readTime":101,"image":155,"tags":156,"publishedAt":160,"createdAt":160,"updatedAt":160,"filePath":161,"sourceUrl":162},"cron-1786954879160","Test What Your AI Agents Must Not Do","test-what-your-ai-agents-must-not-do-fj6y","A Guardrail Without A Negative Test Is Still An Assumption   Most AI agent governance starts...","Bobai Kato","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fres.cloudinary.com%2Fota-run%2Fimage%2Fupload%2Fq_auto%2Ftest-what-your-ai-agents-must-not-do.png",[34,41,157,158,159],"agentsafety","negativetesting","executiongovernance","2026-08-17T08:21:19.160Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fai-agents\u002Ftest-what-your-ai-agents-must-not-do-fj6y.md","https:\u002F\u002Fdev.to\u002Fotaready\u002Ftest-what-your-ai-agents-must-not-do-3e1a",{"id":164,"title":165,"slug":166,"lang":10,"category":51,"categorySlug":52,"summary":167,"excerpt":167,"author":168,"date":15,"readTime":101,"image":169,"tags":170,"publishedAt":172,"createdAt":172,"updatedAt":172,"filePath":173,"sourceUrl":174},"cron-1786954878754","Protecting Microservices: Implementing End-to-End Encryption Across REST APIs","protecting-microservices-implementing-end-to-end-encryption-across-rest-apis-gtfz","End-to-end encryption across REST APIs is the difference between a microservices architecture that...","Fu'ad Husnan","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fba3onadsxu08gb14brer.png",[51,58,52,59,171],"2026","2026-08-17T08:21:18.754Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fmicroservices\u002Fprotecting-microservices-implementing-end-to-end-encryption-across-rest-apis-gtfz.md","https:\u002F\u002Fdev.to\u002Ffuadhusnan_f44f3e13\u002Fprotecting-microservices-implementing-end-to-end-encryption-across-rest-apis-26hb",{"id":176,"title":177,"slug":178,"lang":10,"category":67,"categorySlug":68,"summary":179,"excerpt":179,"author":180,"date":15,"readTime":101,"image":181,"tags":182,"publishedAt":185,"createdAt":185,"updatedAt":185,"filePath":186,"sourceUrl":187},"cron-1786954878272","I Gave My Agent One Signed Permission It Couldn’t Mint Itself","i-gave-my-agent-one-signed-permission-it-couldnt-mint-itself-nm1o","Evidence status. The supervised operator run completed on 2026-08-09. An operator-signed job...","Self-Correcting Systems","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg1gjbzx4muvma5fznpjl.png",[67,183,20,68,184],"machinelearning","agents","2026-08-17T08:21:18.271Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fsecurity\u002Fi-gave-my-agent-one-signed-permission-it-couldnt-mint-itself-nm1o.md","https:\u002F\u002Fdev.to\u002Fkenielzep97\u002Fi-gave-my-agent-one-signed-permission-it-couldnt-mint-itself-2lpc",{"id":189,"title":190,"slug":191,"lang":10,"category":11,"categorySlug":12,"summary":192,"excerpt":192,"author":193,"date":15,"readTime":101,"image":194,"tags":195,"publishedAt":197,"createdAt":197,"updatedAt":197,"filePath":198,"sourceUrl":199},"cron-1786954877848","One GPU, four ways to share it: ten scenarios, and the headline finding I had to retract","one-gpu-four-ways-to-share-it-ten-scenarios-and-the-headline-finding-i-had-to-retract-3y1v","I measured every GPU sharing mode across ten scenarios, published a headline finding that duplicate models are nearly free on unified memory, then failed to replicate it and retracted it. Here is what the controlled replication showed and how the original measurement fooled me.","Christopher Maher","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fllmkube.com%2Fog-gpu-sharing-four-ways-devto.png",[11,12,196,74,20],"gpu","2026-08-17T08:21:17.847Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fkubernetes\u002Fone-gpu-four-ways-to-share-it-ten-scenarios-and-the-headline-finding-i-had-to-retract-3y1v.md","https:\u002F\u002Fdev.to\u002Fdefilan\u002Fone-gpu-four-ways-to-share-it-ten-scenarios-and-the-one-number-that-inverts-on-your-hardware-1bih",{"id":201,"title":202,"slug":203,"lang":10,"category":34,"categorySlug":35,"summary":204,"excerpt":204,"author":86,"date":205,"readTime":101,"image":206,"tags":207,"publishedAt":211,"createdAt":211,"updatedAt":211,"filePath":212,"sourceUrl":213},"cron-1786954563310","FastMCP Agent Mail: RBAC Tokens vs Anonymous Access, and the 403 Errors in Between","fastmcp-agent-mail-rbac-tokens-vs-anonymous-access-and-the-403-errors-in-between-ef0p","Why a FastMCP agent mail server that works anonymously in dev returns 403 behind TLS ingress, and how to wire bearer tokens without leaking them.","17\u002F8\u002F2026","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fguatulabs.dev%2Fog%2Ffastmcp-agent-mail-rbac-token-vs-anonymous-lessons-from-403-errors.png",[34,208,209,41,210],"fastmcp","mcpservers","authentication","2026-08-17T08:16:03.310Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fai-agents\u002Ffastmcp-agent-mail-rbac-tokens-vs-anonymous-access-and-the-403-errors-in-between-ef0p.md","https:\u002F\u002Fdev.to\u002Ffuthgar\u002Ffastmcp-agent-mail-rbac-tokens-vs-anonymous-access-and-the-403-errors-in-between-22pk",{"id":215,"title":216,"slug":217,"lang":10,"category":51,"categorySlug":52,"summary":218,"excerpt":218,"author":219,"date":205,"readTime":101,"image":220,"tags":221,"publishedAt":224,"createdAt":224,"updatedAt":224,"filePath":225,"sourceUrl":226},"cron-1786954562962","eBPF-Powered Request Tracing in Go Microservices Without Instrumentation Tax","ebpf-powered-request-tracing-in-go-microservices-without-instrumentation-tax-k53p","How eBPF uprobes and ring buffers replace manual trace propagation in Go services—mechanics, tradeoffs, and failure modes.","Neeraj Singhi","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1200,height=627,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fws515jitp8rvalxr8d63.png",[51,222,19,52,223],"architecture","performance","2026-08-17T08:16:02.962Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fmicroservices\u002Febpf-powered-request-tracing-in-go-microservices-without-instrumentation-tax-k53p.md","https:\u002F\u002Fdev.to\u002Fneeraj_singhi_golang\u002Febpf-powered-request-tracing-in-go-microservices-without-instrumentation-tax-34kf",{"id":228,"title":229,"slug":230,"lang":10,"category":67,"categorySlug":68,"summary":231,"excerpt":231,"author":232,"date":205,"readTime":101,"image":233,"tags":234,"publishedAt":238,"createdAt":238,"updatedAt":238,"filePath":239,"sourceUrl":240},"cron-1786954562825","How Dopamine Works: The Architecture of a Modern iOS Jailbreak","how-dopamine-works-the-architecture-of-a-modern-ios-jailbreak-woxq","Most developers will never jailbreak a phone. That is fine. This article is not a how-to, and there...","ArshTechPro","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0gnfjvl3fzvb1r1rpvci.png",[67,235,236,68,237],"ios","mobile","programming","2026-08-17T08:16:02.825Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fsecurity\u002Fhow-dopamine-works-the-architecture-of-a-modern-ios-jailbreak-woxq.md","https:\u002F\u002Fdev.to\u002Farshtechpro\u002Fhow-dopamine-works-the-architecture-of-a-modern-ios-jailbreak-2hj3",{"id":242,"title":243,"slug":244,"lang":10,"category":11,"categorySlug":12,"summary":245,"excerpt":245,"author":246,"date":205,"readTime":101,"image":247,"tags":248,"publishedAt":250,"createdAt":250,"updatedAt":250,"filePath":251,"sourceUrl":252},"cron-1786954562468","I got tired of SSHing into 10 VMs a day, so I built a live map of my whole infrastructure","i-got-tired-of-sshing-into-10-vms-a-day-so-i-built-a-live-map-of-my-whole-infrastructure-iqz9","Every day at work looked the same. Something breaks, or I need to push a new image, and I'm SSHing...","ByteStrix","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1200,height=627,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4xdxpc7fsu7nv8usri7a.png",[11,249,20,73,12],"productivity","2026-08-17T08:16:02.468Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fkubernetes\u002Fi-got-tired-of-sshing-into-10-vms-a-day-so-i-built-a-live-map-of-my-whole-infrastructure-iqz9.md","https:\u002F\u002Fdev.to\u002Fbytestrix\u002Fi-got-tired-of-sshing-into-10-vms-a-day-so-i-built-a-live-map-of-my-whole-infrastructure-2iil",{"id":254,"title":255,"slug":256,"lang":10,"category":34,"categorySlug":35,"summary":257,"excerpt":257,"author":258,"date":205,"readTime":101,"image":259,"tags":260,"publishedAt":264,"createdAt":264,"updatedAt":264,"filePath":265,"sourceUrl":266},"cron-1786954558132","The Coordinated Rename Is the Agent's Most Dangerous Refactor","the-coordinated-rename-is-the-agents-most-dangerous-refactor-iazu","Multi-agent rename tooling rewrites two hundred files in ten seconds because it noticed the drift. Half the time the drift was a load-bearing distinction the team encoded on purpose. Vocabulary curation is a real-time review surface now, and senior includes refusing changes that would be technically more consistent because the domain has two concepts the agent has no way to see.","Travis Frisinger","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fwww.tddbuddy.com%2Fimages%2Fcovers%2Fthe-coordinated-rename-is-the-dangerous-refactor.png",[34,261,41,262,263],"vocabulary","domainmodeling","codereview","2026-08-17T08:15:58.132Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fai-agents\u002Fthe-coordinated-rename-is-the-agents-most-dangerous-refactor-iazu.md","https:\u002F\u002Fdev.to\u002Ftmfrisinger\u002Fthe-coordinated-rename-is-the-agents-most-dangerous-refactor-6a",{"id":268,"title":269,"slug":270,"lang":10,"category":51,"categorySlug":52,"summary":271,"excerpt":271,"author":272,"date":205,"readTime":101,"image":273,"tags":274,"publishedAt":277,"createdAt":277,"updatedAt":277,"filePath":278,"sourceUrl":279},"cron-1786954557526","Microservices: Building Applications as Independent, Communicating Services","microservices-building-applications-as-independent-communicating-services-c45k","Microservices: Building Applications as Independent, Communicating Services   A practical,...","Rhuturaj Takle","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8puio1c7flrtyivr04hl.png",[51,52,275,237,276],"dotnet","learning","2026-08-17T08:15:57.526Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fmicroservices\u002Fmicroservices-building-applications-as-independent-communicating-services-c45k.md","https:\u002F\u002Fdev.to\u002Frhuturaj_takle\u002Fmicroservices-building-applications-as-independent-communicating-services-2eo8",{"id":281,"title":282,"slug":283,"lang":10,"category":67,"categorySlug":68,"summary":284,"excerpt":284,"author":285,"date":205,"readTime":101,"image":286,"tags":287,"publishedAt":291,"createdAt":291,"updatedAt":291,"filePath":292,"sourceUrl":293},"cron-1786954556056","From Arduino To Automotive: How I Escaped The IDE And Owned The Bus","from-arduino-to-automotive-how-i-escaped-the-ide-and-owned-the-bus-hj7g","Arduino taught me how to build. Bare metal taught me how the build actually works.  I have a lot of...","v. Splicer","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fabn3138rnp9vm0nyk54b.jpg",[67,288,289,290,68],"esp32","arduino","canbus","2026-08-17T08:15:56.056Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fsecurity\u002Ffrom-arduino-to-automotive-how-i-escaped-the-ide-and-owned-the-bus-hj7g.md","https:\u002F\u002Fdev.to\u002Fnumbpill3d\u002Ffrom-arduino-to-automotive-how-i-escaped-the-ide-and-owned-the-bus-f8f",{"id":295,"title":296,"slug":297,"lang":10,"category":11,"categorySlug":12,"summary":298,"excerpt":298,"author":299,"date":205,"readTime":101,"image":300,"tags":301,"publishedAt":304,"createdAt":304,"updatedAt":304,"filePath":305,"sourceUrl":306},"cron-1786954555663","The Backup Awakens: A Star Wars Story","the-backup-awakens-a-star-wars-story-jzpq","The Quest Begins (The \\\"Why\\\")   Honestly, I used to think backups were the boring chores you...","Timevolt","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1200,height=627,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ynsfxiz14nn4b9ylhn6.png",[11,20,302,12,303],"docker","cicd","2026-08-17T08:15:55.662Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fkubernetes\u002Fthe-backup-awakens-a-star-wars-story-jzpq.md","https:\u002F\u002Fdev.to\u002Ftimevolt\u002Fthe-backup-awakens-a-star-wars-story-1616",{"id":308,"title":309,"slug":310,"lang":10,"category":34,"categorySlug":35,"summary":311,"excerpt":311,"author":258,"date":205,"readTime":101,"image":312,"tags":313,"publishedAt":316,"createdAt":316,"updatedAt":316,"filePath":317,"sourceUrl":318},"cron-1786954321408","Test Deletion Is a Privileged Operation","test-deletion-is-a-privileged-operation-2pfz","The cheapest way for an agent to make a failing test pass is to delete it. That is logical for the agent and catastrophic for the codebase. Tests are append-only by default. Deletion needs a human author, a separate commit, and a separate review.","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fwww.tddbuddy.com%2Fimages%2Fcovers%2Ftest-deletion-is-a-privileged-operation.png",[34,314,41,315,263],"tdd","testdesign","2026-08-17T08:12:01.408Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fai-agents\u002Ftest-deletion-is-a-privileged-operation-2pfz.md","https:\u002F\u002Fdev.to\u002Ftmfrisinger\u002Ftest-deletion-is-a-privileged-operation-264a",{"id":320,"title":321,"slug":322,"lang":10,"category":51,"categorySlug":52,"summary":323,"excerpt":323,"author":324,"date":205,"readTime":101,"image":325,"tags":326,"publishedAt":329,"createdAt":329,"updatedAt":329,"filePath":330,"sourceUrl":331},"cron-1786954321239","You Don't Always Need a Workflow Engine to Roll Back a Failed Checkout","you-dont-always-need-a-workflow-engine-to-roll-back-a-failed-checkout-1iwf","Here's a sequence that shows up in almost every Laravel app that talks to the outside world:   Charge...","Sient","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu6g3j4z4af9tbuozbz2v.png",[51,327,328,222,52],"laravel","php","2026-08-17T08:12:01.239Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fmicroservices\u002Fyou-dont-always-need-a-workflow-engine-to-roll-back-a-failed-checkout-1iwf.md","https:\u002F\u002Fdev.to\u002Fsient\u002Fyou-dont-always-need-a-workflow-engine-to-roll-back-a-failed-checkout-5gop",{"id":333,"title":334,"slug":335,"lang":10,"category":67,"categorySlug":68,"summary":336,"excerpt":336,"author":337,"date":205,"readTime":101,"image":338,"tags":339,"publishedAt":341,"createdAt":341,"updatedAt":341,"filePath":342,"sourceUrl":343},"cron-1786954321092","I Stopped Trusting AI Agents With Tools. So I Built a Gatekeeper.","i-stopped-trusting-ai-agents-with-tools-so-i-built-a-gatekeeper-9i2m","Update 08\u002F15 0.2.0 Released   github.com\u002Fdeghosal-2026\u002Fagent-tooltrust · pip install agent-tooltrust...","Debashish Ghosal","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr97gsrqar0qk7ejjibih.png",[67,74,184,68,340],"gatekeeper","2026-08-17T08:12:01.092Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fsecurity\u002Fi-stopped-trusting-ai-agents-with-tools-so-i-built-a-gatekeeper-9i2m.md","https:\u002F\u002Fdev.to\u002Fdebashish_ghosal\u002Fi-stopped-trusting-ai-agents-with-tools-so-i-built-a-gatekeeper-26fb",{"id":345,"title":346,"slug":347,"lang":10,"category":11,"categorySlug":12,"summary":348,"excerpt":348,"author":349,"date":205,"readTime":101,"image":350,"tags":351,"publishedAt":353,"createdAt":353,"updatedAt":353,"filePath":354,"sourceUrl":355},"cron-1786954320913","I Automated My Entire GitOps Security Stack. The First Thing It Blocked Was My Own Salary.","i-automated-my-entire-gitops-security-stack-the-first-thing-it-blocked-was-my-own-salary-pko8","I Automated My Entire GitOps Security Stack. The First Thing It Blocked Was My Own...","Le Beltagy","https:\u002F\u002Fmedia2.dev.to\u002Fdynamic\u002Fimage\u002Fwidth=1000,height=420,fit=cover,gravity=auto,format=auto\u002Fhttps%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frvjfi7ee0tmi1xp1mem5.png",[11,12,68,352,20],"gitops","2026-08-17T08:12:00.911Z","\u002FUsers\u002Fnguyenanhtuan\u002FCode\u002FNuxtjs\u002Fnextpress\u002Fintlight\u002Fpublic\u002Fcontents\u002Fkubernetes\u002Fi-automated-my-entire-gitops-security-stack-the-first-thing-it-blocked-was-my-own-salary-pko8.md","https:\u002F\u002Fdev.to\u002Fle_beltagy\u002Fi-automated-my-entire-gitops-security-stack-the-first-thing-it-blocked-was-my-own-salary-227e",25,1,50,[360,363,367,369,370,371],{"name":361,"slug":362,"count":356},"All","all",{"name":364,"slug":365,"count":366},"Nuxt 4","nuxt-4",0,{"name":11,"slug":12,"count":368},6,{"name":67,"slug":68,"count":368},{"name":51,"slug":52,"count":368},{"name":34,"slug":35,"count":372},7,[374,375,376,377,378,379,380,381,382,383,385,387,389,390,392],{"name":34,"slug":35,"count":372},{"name":41,"slug":41,"count":372},{"name":68,"slug":68,"count":372},{"name":51,"slug":52,"count":368},{"name":52,"slug":52,"count":368},{"name":67,"slug":68,"count":368},{"name":11,"slug":12,"count":368},{"name":12,"slug":12,"count":368},{"name":20,"slug":20,"count":368},{"name":74,"slug":74,"count":384},4,{"name":73,"slug":73,"count":386},3,{"name":57,"slug":57,"count":388},2,{"name":58,"slug":58,"count":388},{"name":59,"slug":391,"count":388},"trending",{"name":19,"slug":19,"count":388},{"success":394,"slug":9,"lang":10,"items":395,"total":455,"page":357,"limit":456,"hasMore":394,"remaining":368},true,[396,407,417,427,436],{"author":397,"avatar":398,"role":399,"date":400,"createdAt":401,"content":402,"likes":403,"isLiked":404,"replies":405,"id":406},"Hannah Schmidt","H","DevOps & CI\u002FCD Lead","1 day ago","2026-08-16T10:09:00.577Z","Can confirm: automated canary deployments with Argo Rollouts and Prometheus metrics analysis prevented several outages for our payment gateways.",31,false,[],"c-build-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4-en-1",{"author":408,"avatar":409,"role":410,"date":411,"createdAt":412,"content":413,"likes":414,"isLiked":404,"replies":415,"id":416},"Elena Rostova","E","Lead SRE & Platform Architect","45 mins ago","2026-08-17T09:24:00.576Z","The KEDA autoscaling setup with custom Prometheus metrics is production-grade. We observed a 60% compute cost reduction after switching to event-driven pod scaling.",34,[],"c-build-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4-en-2",{"author":418,"avatar":419,"role":420,"date":421,"createdAt":422,"content":423,"likes":424,"isLiked":404,"replies":425,"id":426},"Liam O'Connor","L","Frontend Performance Specialist","3 hours ago","2026-08-17T07:09:00.576Z","Nuxt 4 with selective hydration and zero-JS interactive islands delivers mind-blowing speed. Sub-100ms INP and 99+ Core Web Vitals out of the box.",21,[],"c-build-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4-en-3",{"author":428,"avatar":419,"role":429,"date":430,"createdAt":431,"content":432,"likes":433,"isLiked":404,"replies":434,"id":435},"Lucas Moreau","Cloud Native Developer","12 hours ago","2026-08-16T22:09:00.577Z","Kafka event streaming with schema registry ensures backward compatibility even as payload models evolve across microservice boundaries.",18,[],"c-build-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4-en-4",{"author":437,"avatar":438,"role":439,"date":440,"createdAt":441,"content":442,"likes":443,"isLiked":404,"replies":444,"id":454},"Alexander Wright","A","Principal Systems Architect @ Stripe","20 mins ago","2026-08-17T09:49:00.576Z","Superb architectural breakdown! The hybrid L1 in-memory + L2 distributed Redis cache pattern is crucial for mitigating high-concurrency thundering herd issues.",29,[445],{"author":446,"avatar":447,"role":448,"date":449,"createdAt":450,"content":451,"likes":452,"isLiked":404,"id":453},"David Chen","D","Staff Infrastructure Engineer","12 mins ago","2026-08-17T09:57:00.576Z","Totally agree, Alexander. Pairing that with singleflight request deduplication on the backend virtually eliminates DB spikes.",15,"r-build-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4-en-5-1","c-build-an-mcp-server-in-go-part-1-designing-a-diagnostic-grade-kubernetes-client-p1d4-en-5",13,5,1786961340902]