API reference¶
Every endpoint is under /api/v1 and speaks JSON over HTTP. The examples assume
the server is at http://localhost:8080 and your key is in $API_KEY.
Endpoints¶
| Method | Path | Key needed | Calls Claude |
|---|---|---|---|
GET |
/api/v1/health |
no | no |
GET |
/api/v1/health/live |
no | no |
GET |
/api/v1/health/ready |
no | no |
GET |
/api/v1/namespaces |
yes | no |
GET |
/api/v1/resources/{type} |
yes | no |
GET |
/api/v1/resources/{type}/{name} |
yes | no |
GET |
/api/v1/events |
yes | no |
GET |
/api/v1/namespaces/{namespace}/topology |
yes | no |
GET |
/api/v1/namespaces/{namespace}/graph |
yes | no |
GET |
/api/v1/namespaces/{namespace}/resources |
yes | no |
GET |
/api/v1/namespaces/{namespace}/analysis |
yes | yes, unless ?narrative=false |
GET |
/api/v1/argocd/applications |
yes | no |
GET |
/api/v1/argocd/applications/{name} |
yes | no |
GET |
/api/v1/gitlab/projects |
yes | no |
GET |
/api/v1/gitlab/projects/{projectId}/pipelines |
yes | no |
POST |
/api/v1/mcp/resource |
yes | yes |
POST |
/api/v1/mcp/troubleshoot |
yes | only with a query |
POST |
/api/v1/mcp/mergeRequest |
yes | yes |
POST |
/api/v1/mcp |
yes | yes |
POST |
/api/v1/mcp/commit |
yes | not implemented |
Endpoints that call Claude typically take tens of seconds. Make sure your
client's timeout, any proxy in between, and the server's own
writeTimeout all allow for that.
None of these endpoints change anything in Kubernetes, ArgoCD or GitLab.
Authentication¶
Every endpoint except the three health endpoints needs the key from
server.auth.apiKey. Send it either way:
curl -H "X-API-Key: $API_KEY" http://localhost:8080/api/v1/namespaces
curl -H "Authorization: Bearer $API_KEY" http://localhost:8080/api/v1/namespaces
If both headers are present, X-API-Key is the one checked. A failed check
returns 401 with one of these bodies:
error |
Cause |
|---|---|
Authentication required |
Neither header was sent. |
Invalid authorization format |
Authorization isn't exactly Bearer <key>. |
Invalid API key |
The key doesn't match. |
If the server has no key configured, every request fails this check, whatever it sends. Keys are compared in constant time.
Cross-origin requests. Every response carries
Access-Control-Allow-Origin: *, and CORS preflight (OPTIONS) requests are
answered with 204 and allow the Content-Type, Authorization and
X-API-Key headers, so a browser on any origin can call the API. That's safe
because the key is a header the page has to set itself, not a cookie another
site could ride on. Still, a key embedded in a web page is a published key:
call the API from a backend unless the page's users should hold the key.
Before v1.4.0, preflights got a 404 or 405 and no browser could call the
API.
Errors¶
Errors are JSON with an error message and, when there's an underlying cause,
details:
{
"details": "failed to get deployment shop/web: deployments.apps \"web\" not found",
"error": "Failed to get resource"
}
| Status | When |
|---|---|
400 |
The body isn't valid JSON, or a required field is missing. |
401 |
The API key is missing or wrong. |
500 |
Anything else that fails, including an object that doesn't exist. A missing object is a 500, not a 404; read details. |
503 |
ArgoCD refused the server's token (ArgoCD endpoints), or readiness failed. |
details carries the underlying error as-is, which can include part of an
upstream response body. An unknown path returns a plain-text
404 page not found. A known path called with the wrong method returns 404 or
405, depending on the route.
There's no pagination: list endpoints return everything in one response.
Health¶
GET /health¶
Checks Kubernetes, ArgoCD and GitLab live on every call and always answers
200. Read status for the verdict. Because it calls out to ArgoCD and GitLab,
don't use it as a Kubernetes probe; use /health/live and
/health/ready.
{
"status": "degraded",
"services": {
"argocd": "unauthorized",
"claude": "assumed available",
"gitlab": "available",
"kubernetes": "available"
},
"components": {
"argocd": {
"status": "denied",
"reachable": true,
"authorized": false,
"checkedAt": "2026-10-20T14:03:11.402918Z",
"message": "the ArgoCD token authenticates but lacks applications:get; check argocd-rbac-cm"
}
}
}
| Field | Values |
|---|---|
status |
ok, or degraded when Kubernetes isn't available or ArgoCD refused the token. An unconfigured or unreachable ArgoCD or GitLab doesn't make it degraded. |
services.kubernetes |
available or unavailable. |
services.argocd |
available; unavailable when it can't be reached or isn't configured; unauthorized when it's reachable but refused the token. |
services.gitlab |
available or unavailable. |
services.claude |
Always assumed available. Health checks never call Claude. |
components.argocd |
The ArgoCD authorization probe. See below. |
The ArgoCD authorization probe¶
Reaching ArgoCD doesn't prove the server can read anything from it. ArgoCD
silently leaves out Applications the caller isn't allowed to get, so a token
that has lost its RBAC grant still connects, and simply sees an empty list. To
catch that, the server asks ArgoCD's
GET /api/v1/account/can-i/applications/get/*/* at startup and every 60
seconds, and reports the cached answer here:
status |
Meaning | reachable |
authorized |
|---|---|---|---|
authorized |
The token may get Applications in every project. | true |
true |
denied |
The token is valid but lacks applications, get. Check ArgoCD's argocd-rbac-cm. |
true |
false |
unauthenticated |
ArgoCD rejected the token itself (HTTP 401): it's revoked, expired, or its account is disabled. |
true |
false |
unavailable |
ArgoCD couldn't be reached or gave no usable answer. | false |
null |
unknown |
No probe has finished yet, or ArgoCD isn't configured. | false |
null |
checkedAt is when the last probe ran, and is left out until the first one
finishes. message appears only for denied and unauthenticated, and names
the fix. A change of state is logged once, at ERROR for denied and
unauthenticated.
GET /health/live¶
Liveness. Answers 200 whenever the process can respond:
GET /health/ready¶
Readiness. Checks only that the Kubernetes API is reachable, and adds the cached ArgoCD probe result without calling ArgoCD:
{
"status": "ready",
"ready": true,
"checks": { "kubernetes": true },
"components": {
"argocd": { "status": "authorized", "reachable": true, "authorized": true, "checkedAt": "2026-10-20T14:03:11.402918Z" }
}
}
| Situation | HTTP | status |
ready |
|---|---|---|---|
| Kubernetes reachable | 200 |
ready |
true |
| Kubernetes reachable, ArgoCD refused the token | 200 |
degraded |
true |
| Kubernetes unreachable | 503 |
not ready |
false |
ArgoCD never fails readiness on purpose. A failing readiness probe takes the Pod out of its Service, which would take down every Kubernetes and GitLab endpoint along with the ArgoCD ones.
Kubernetes¶
GET /namespaces¶
GET /resources/{type}¶
Lists objects of one type, as full Kubernetes objects.
| Parameter | In | Notes |
|---|---|---|
type |
path | A resource type. These short names always work: pod, deployment, service, configmap, secret, statefulset, daemonset, job, cronjob, ingress, namespace, node, pv, pvc. Any other plural or singular name the cluster serves also works, including custom resources (certificates, application). Case doesn't matter. |
namespace |
query | Optional. Leave it out to list across all namespaces. |
curl -s -H "X-API-Key: $API_KEY" \
"http://localhost:8080/api/v1/resources/deployment?namespace=shop"
{
"resources": [
{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": { "name": "web", "namespace": "shop", "...": "..." },
"spec": { "...": "..." },
"status": { "...": "..." }
}
]
}
An unknown type returns 500 with details unknown resource type: <type>.
GET /resources/{type}/{name}¶
Returns one object, as-is. Pass ?namespace= for namespaced types; without it,
the lookup fails.
curl -s -H "X-API-Key: $API_KEY" \
"http://localhost:8080/api/v1/resources/deployment/web?namespace=shop"
GET /events¶
Events about one object, newest first.
| Parameter | Notes |
|---|---|
namespace |
The object's namespace. Leave it out for cluster-scoped objects. |
resource |
The object's kind, spelled exactly as Kubernetes does: Pod, Deployment. Kubernetes compares it exactly, so pod matches nothing. |
name |
The object's name. |
curl -s -H "X-API-Key: $API_KEY" \
"http://localhost:8080/api/v1/events?namespace=shop&resource=Pod&name=web-7d9c6b5f4-x2k8p"
{
"events": [
{
"reason": "BackOff",
"message": "Back-off restarting failed container web in pod web-7d9c6b5f4-x2k8p_shop(…)",
"type": "Warning",
"count": 42,
"firstTime": "2026-10-20T13:02:10Z",
"lastTime": "2026-10-20T13:41:55Z",
"object": { "kind": "Pod", "name": "web-7d9c6b5f4-x2k8p", "namespace": "shop" }
}
]
}
events is null when there are none. Kubernetes keeps events for an hour by
default. Events recorded through the newer events API may show
0001-01-01T00:00:00Z for firstTime and lastTime.
GET /namespaces/{namespace}/topology¶
A map of the namespace: which objects exist, how they relate, and a quick health read of each. The server lists every namespaced resource type it's allowed to list and skips the ones it isn't.
{
"namespace": "shop",
"resources": {
"Deployment": ["web"],
"Pod": ["web-7d9c6b5f4-x2k8p"],
"Service": ["web"]
},
"relationships": [
{
"sourceKind": "Service", "sourceName": "web", "sourceNamespace": "shop",
"targetKind": "Pod", "targetName": "web-7d9c6b5f4-x2k8p", "targetNamespace": "shop",
"relationType": "selects"
}
],
"metrics": {
"Deployment": { "count": 1 },
"Pod": { "count": 1 },
"Service": { "count": 1 }
},
"health": {
"Deployment": { "web": "healthy" },
"Pod": { "web-7d9c6b5f4-x2k8p": "healthy" },
"Service": { "web": "healthy" }
}
}
relationType is one of:
| Value | Meaning |
|---|---|
owns |
The source is listed in the target's ownerReferences (Deployment → ReplicaSet → Pod). |
selects |
A Service's selector matches a Pod. |
mounts |
A Pod mounts a ConfigMap or Secret as a volume. |
configures |
A Pod reads a ConfigMap or Secret through env or envFrom. |
binds |
A PersistentVolumeClaim is bound to a PersistentVolume. |
routes |
An Ingress sends traffic to a Service. |
health values are healthy, progressing, unhealthy or unknown. They
come from a quick read of each object's status, not a full check:
- A Pod reads
healthywhile its phase isRunning, which includes a container that's crash-looping. The analysis endpoint'sissuescatch that. - Deployments, StatefulSets and ReplicaSets compare available replicas with
spec.replicas. One scaled to zero, such as an old ReplicaSet left by a rollout, readsunhealthy, and so does every DaemonSet.
GET /namespaces/{namespace}/graph¶
The topology as nodes and edges, for drawing:
{
"nodes": [
{ "id": 0, "kind": "Service", "name": "web", "health": "healthy", "group": "Service" },
{ "id": 1, "kind": "Pod", "name": "web-7d9c6b5f4-x2k8p", "health": "healthy", "group": "Pod" }
],
"edges": [
{ "source": 0, "target": 1, "relationship": "selects" }
]
}
source and target are node ids. A relationship whose other end is outside
the namespace, such as a PersistentVolume, has no edge.
GET /namespaces/{namespace}/resources¶
Every object in the namespace, grouped by kind, as full Kubernetes objects, with a count per kind. This can be a large response.
{
"namespace": "shop",
"resources": {
"Deployment": [ { "apiVersion": "apps/v1", "kind": "Deployment", "...": "..." } ]
},
"stats": { "Deployment": 1, "Pod": 1, "Service": 1 }
}
GET /namespaces/{namespace}/analysis¶
Looks for problems in a namespace and, by default, asks Claude to write them up.
| Parameter | Notes |
|---|---|
narrative |
false skips Claude and returns the findings alone, usually within seconds. Any other value, or none, includes Claude's analysis. |
The findings come from two places:
- Warning events in the namespace. Each becomes an issue.
- CronJobs whose last scheduled run didn't succeed. A failing CronJob leaves every Deployment and Pod healthy, and its warning event expires within the hour, so the server reads the CronJob's own status instead: it counts as failing when its last scheduled run is newer than its last success (or it has never succeeded), it isn't suspended, and no run is in progress. The issue carries the end of the failed run's Pod log when that Pod still exists.
A trimmed response with ?narrative=false:
{
"namespace": "shop",
"resourceCounts": { "CronJob": 1, "Deployment": 1, "Job": 1, "Pod": 2, "ReplicaSet": 1, "Service": 1 },
"healthStatus": {
"CronJob": { "unknown": 1 },
"Deployment": { "healthy": 1 },
"Job": { "unhealthy": 1 },
"Pod": { "healthy": 1, "unhealthy": 1 },
"ReplicaSet": { "healthy": 1 },
"Service": { "healthy": 1 }
},
"resourceRelationships": [
{
"sourceKind": "Deployment", "sourceName": "web", "sourceNamespace": "shop",
"targetKind": "ReplicaSet", "targetName": "web-7d9c6b5f4", "targetNamespace": "shop",
"relationType": "owns"
}
],
"issues": [
{
"title": "Scheduled job failing: nightly-sync",
"category": "CronJobFailing",
"severity": "Warning",
"source": "Kubernetes",
"description": "Scheduled job nightly-sync (0 3 * * *) did not succeed on its last run at 2026-10-20T03:00:00Z; last success 2026-10-17T03:00:00Z. End of the failed run's log:\nconnecting to db...\nError: connect ECONNREFUSED 10.0.12.4:5432"
}
],
"recommendations": [],
"analysis": "1 issue(s) found in shop:\n\n- Scheduled job failing: nightly-sync: Scheduled job nightly-sync ..."
}
| Field | What it holds |
|---|---|
resourceCounts |
Objects per kind. |
healthStatus |
Per kind, how many objects read healthy, progressing, unhealthy or unknown (see topology for how that's judged). |
resourceRelationships |
As in the topology. null when there are none. |
issues |
The findings. See Issue categories. |
recommendations |
Up to 10 lines taken from Claude's analysis. Empty with narrative=false. |
analysis |
Claude's write-up. With narrative=false, or if the Claude call fails, a plain summary of the issues instead (No issues found in shop. when there are none). |
If Claude is unavailable, this endpoint still answers 200 with the findings.
A namespace that doesn't exist isn't an error: it has no resources and no
issues.
ArgoCD¶
These need ArgoCD configured.
GET /argocd/applications¶
{
"applications": [
{
"metadata": { "name": "web", "namespace": "argocd", "labels": { "team": "shop" } },
"spec": {
"source": { "repoURL": "https://gitlab.com/acme/deploy.git", "path": "apps/web", "targetRevision": "main" },
"destination": { "server": "https://kubernetes.default.svc", "namespace": "shop" }
},
"status": {
"sync": { "status": "Synced", "revision": "4f1c2d9" },
"health": { "status": "Healthy" },
"resources": [
{
"group": "apps", "version": "v1", "kind": "Deployment", "namespace": "shop", "name": "web",
"status": "Synced", "health": { "status": "Healthy", "message": "" }
}
]
},
"name": ""
}
]
}
Each Application is a subset of ArgoCD's own object: the fields shown above.
Read the name from metadata.name; the top-level name field is always empty.
An Application with several sources (spec.sources) shows an empty
spec.source.
If ArgoCD returns no Applications while the authorization probe
says it refused the token, this endpoint answers 503 instead of an empty list,
because the empty list would only reflect the refusal:
{
"details": "the ArgoCD token authenticates but lacks applications:get; check argocd-rbac-cm",
"error": "ArgoCD denied applications:get for this server's token"
}
ArgoCD rejecting the token outright (401) gets the same 503, with
details ArgoCD rejected the token (HTTP 401); check ARGOCD_TOKEN and that its account is enabled,
even before the first probe has run. A non-empty list is always returned as-is.
Other failures are 500.
GET /argocd/applications/{name}¶
One Application, in the same shape. A 401 from ArgoCD returns the same 503
as above. Any other ArgoCD error, including a 403 or 404 for an
Application that doesn't exist or that the token can't see, is a 500 with
ArgoCD's status in details.
GitLab¶
These need GitLab configured.
GET /gitlab/projects¶
Up to 100 projects the token's user is a member of, most recently updated first:
{
"projects": [
{
"id": 4821,
"name": "deploy",
"path": "deploy",
"path_with_namespace": "acme/deploy",
"web_url": "https://gitlab.com/acme/deploy",
"default_branch": "main",
"visibility": "private"
}
]
}
GET /gitlab/projects/{projectId}/pipelines¶
The project's 20 most recent pipelines, newest first. projectId is the
numeric project ID.
{
"pipelines": [
{
"id": 99812,
"status": "failed",
"ref": "main",
"sha": "4f1c2d9e7b...",
"web_url": "https://gitlab.com/acme/deploy/-/pipelines/99812",
"created_at": "2026-10-20T12:58:03.114Z",
"updated_at": "2026-10-20T13:04:41.870Z"
}
]
}
Claude-backed endpoints¶
These gather context, send it to Claude with your question, and return Claude's answer. How it works describes what goes into the context.
Request fields¶
All of them take a JSON body built from these fields:
| Field | Type | Used by |
|---|---|---|
resource |
string | The object's kind, such as Deployment. Spell it as Kubernetes does, so the event lookup matches. |
name |
string | The object's name. |
namespace |
string | The object's namespace. Leave it out for cluster-scoped objects. |
query |
string | Your question. Optional, but Claude's answer is only as focused as the question. |
projectId |
string | A GitLab project: the numeric ID as a string ("4821") or its path ("acme/deploy"). |
mergeRequestIid |
integer | A merge request's number within its project (the 42 in !42). |
context |
string | Optional. When set on a resource question, the server skips its own data collection and sends this text to Claude as the context. |
action |
string | /mcp only. |
Response¶
{
"success": true,
"message": "Successfully processed queryResource request in 11.204718321s",
"analysis": "## Summary\n\nThe deployment `web` has 0 of 3 replicas available...",
"context": { "kind": "", "name": "", "namespace": "", "apiVersion": "" }
}
analysis is Claude's answer, usually in Markdown. context is always present
and always empty; ignore it. Some endpoints add a field, noted below.
POST /mcp/resource¶
Ask Claude about one object. resource and name are required; without them
you get 400 Resource and name are required.
curl -s -X POST http://localhost:8080/api/v1/mcp/resource \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d '{
"resource": "Deployment",
"name": "web",
"namespace": "shop",
"query": "Why are no replicas available?"
}'
The server reads the object and its events, and tries to find the ArgoCD
Application that manages it, along with that Application's sync history and
GitLab project, last pipeline and recent commits. For a Deployment it also
pulls out the replica counts and each container's image and resource settings.
A lookup that fails doesn't fail the request: Claude is told what couldn't be
collected. So a mistyped name still returns 200, with Claude saying it found
nothing.
A whole namespace. With "resource": "namespace", name is the namespace.
The server runs the full namespace analysis,
then asks your question with a summary of the namespace's resources,
relationships, health and recent events. The response adds a
namespaceAnalysis field with the analysis result. This makes two Claude
calls, so it's the slowest request.
POST /mcp/troubleshoot¶
Runs rule-based checks on one object. resource and name are required.
Without a query, the result comes back directly and Claude isn't called:
{
"resourceContext": {
"kind": "Pod",
"name": "web-7d9c6b5f4-x2k8p",
"namespace": "shop",
"apiVersion": "v1",
"events": [
{
"reason": "BackOff",
"message": "Back-off restarting failed container web in pod web-7d9c6b5f4-x2k8p_shop(…)",
"type": "Warning",
"count": 42,
"firstTime": "2026-10-20T13:02:10Z",
"lastTime": "2026-10-20T13:41:55Z",
"object": { "kind": "Pod", "name": "web-7d9c6b5f4-x2k8p", "namespace": "shop" }
}
],
"errors": [
"Failed to find ArgoCD applications: failed to list applications: failed to add authentication: no valid ArgoCD credentials available"
]
},
"issues": [
{
"title": "Container web CrashLoopBackOff",
"category": "CrashLoopBackOff",
"severity": "Error",
"source": "Kubernetes",
"description": "Container is waiting: CrashLoopBackOff - back-off 5m0s restarting failed container=web pod=web-7d9c6b5f4-x2k8p_shop(…)"
},
{
"title": "Container web Frequent Restarts",
"category": "FrequentRestarts",
"severity": "Warning",
"source": "Kubernetes",
"description": "Container has restarted 12 times"
}
],
"recommendations": [
"Check container logs for errors.",
"Verify environment variables and configuration."
]
}
That example is trimmed; a crash-looping Pod also reports its failing
readiness conditions. resourceContext can also carry argoApplication,
argoSyncStatus, argoHealthStatus, argoSyncHistory, gitlabProject,
lastPipeline, lastDeployment and recentCommits when the server finds them.
errors lists what it couldn't collect; with ArgoCD or GitLab not configured,
those lookups always appear there. recommendations are fixed advice for each
issue category, in no particular order.
With a query, the server also asks Claude for specific kubectl commands to
investigate and fix the issues, and returns the
standard response with the result added as troubleshootResult.
The checks are listed under Issue categories.
POST /mcp/mergeRequest¶
Ask Claude about a GitLab merge request's effect on the cluster. projectId and
mergeRequestIid are required; without them you get 400
Project ID and merge request IID are required.
curl -s -X POST http://localhost:8080/api/v1/mcp/mergeRequest \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d '{
"projectId": "acme/deploy",
"mergeRequestIid": 42,
"query": "What will this change do to the running workloads?"
}'
The server reads the merge request's changes. If no changed file looks like a
Helm chart (Chart.yaml, values.yaml, templates/*.yaml) or a Kubernetes
manifest, Claude gets no cluster context and answers from your question alone.
Otherwise the server looks for ArgoCD Applications whose source is this
project and whose path contains a changed file, and adds the objects they
manage. This needs ArgoCD: if listing Applications fails, the request fails
with 500.
POST /mcp¶
The generic form. Set action to choose what to do:
action |
Same as |
|---|---|
queryResource |
/mcp/resource, except resource and name aren't checked and a namespace isn't handled specially. |
queryMergeRequest |
/mcp/mergeRequest, without the field checks. |
Any other value returns 500 with details unsupported action: <action>.
Prefer the specific endpoints.
POST /mcp/commit¶
Registered, but not implemented. With projectId and commitSha set
it returns 500 with details unsupported action: queryCommit; without them,
400.
Issue categories¶
issues in the namespace analysis and the troubleshoot result share one shape:
| Field | Values |
|---|---|
title |
A short label. |
category |
One of the categories below. |
severity |
Warning or Error. |
source |
Kubernetes, ArgoCD or GitLab. |
description |
The detail, often the event or status message. |
From Warning events (both endpoints), matched on the event's reason and message:
| Category | Matches |
|---|---|
ImagePullError |
Reason contains Failed and message contains ImagePull. |
HealthCheckFailure |
Reason contains Unhealthy (a failing probe). |
ResourceIssue |
Message mentions memory or cpu. |
CrashLoopBackOff |
Reason contains BackOff. |
OtherWarning |
Any other Warning event. |
From CronJob status (namespace analysis only): CronJobFailing.
From the object's status (troubleshoot only):
| Category | Checked on | Raised when |
|---|---|---|
PodNotRunning |
Pod | Phase is Pending or Failed. |
SchedulingIssue, InitializationIssue, ContainerReadinessIssue, PodNotReady |
Pod | The PodScheduled, Initialized, ContainersReady or Ready condition isn't True. |
CrashLoopBackOff, ImagePullError, PodInitializing, ContainerWaiting |
Pod | A container or init container is waiting, by its reason. |
ContainerTerminated |
Pod | A container exited with a non-zero code. |
FrequentRestarts |
Pod | A container has restarted more than 3 times. |
VolumeIssue |
Pod | A mounted PersistentVolumeClaim is missing or not Bound. |
DeploymentNotAvailable, DeploymentNotReady, DeploymentNotProgressing |
Deployment | Fewer available or ready replicas than desired, or the Available/Progressing condition isn't True. |
UnknownIssue |
Pod, Deployment | Nothing else matched but the object isn't healthy. |
SyncIssue, HealthIssue, SyncFailure |
the managing ArgoCD Application | Not Synced, not Healthy, or a recent failed sync. |
PipelineIssue, DeploymentIssue |
the GitLab project | The last pipeline or deployment didn't succeed. |