Generic Kubernetes MCP servers see an Agones cluster as opaque custom resources. They don't know that an Allocated GameServer has a live match with real players on it, that Fleets scale down by reaping Ready servers first, or that a FleetAutoscaler at its ceiling means players are about to queue. This server speaks Agones natively, so an AI assistant can investigate incidents, answer capacity questions, and operate fleets safely.
Example prompts
- "Why can't EU players get into matches?"
- "Which fleets are close to their autoscaler ceiling?"
- "Show me every Unhealthy server and what happened to it"
- "Scale the practice fleet down to 10 without touching live matches"
- "Allocate me a server from the ranked fleet"
A real example, "How full is the ranked fleet?", calling fleet_capacity:
{"fleets":[{"fleet":"ranked","namespace":"default","allocated":3,"ready":2,"utilizationPct":60,"autoscalerCeiling":6,"atCeiling":false}]}
→ "The ranked fleet is at 60% utilization: 3 matches in progress, 2 servers free, autoscaler can grow up to 6 before it's out of room."
Tools
Every tool above except list_clusters also accepts an optional cluster argument (see Multi-cluster). Every write tool except allocate_gameserver accepts dryRun: true for a server-side validation pass that persists nothing, so an agent can show its work before the real call. The three open_match_* tools additionally require Open Match connectivity to be configured (see Open Match) and return a clear error if it isn't.
All 27 tools are exercised against a live Agones cluster (kind + Agones 1.57), not just unit-tested against fakes - and not on the honor system: the same 45-check suite runs in public CI on every push (test/e2e/mcp_e2e.py, e2e.yml), driving the real binary over MCP stdio as a least-privilege ServiceAccount against a fresh kind cluster. Highlights of what it covers, plus one-off deep verifications done during development:
delete_gameserver's safety gate: refusal withoutforce, success with it, and a race test proving the refusal still holds if a server is allocated between the check and the delete.scale_fleetscale-down/up leaving live allocations untouched, including under a simulated concurrent write (the FleetAutoscaler writesFleet.Spec.Replicastoo).- A real
update_fleet_imagerollout thatrollout_statuscorrectly reported as stalled at 50% while two old-version servers still had live matches. - A full
create_fleet→create_autoscaler→update_autoscaler→delete_autoscaler→delete_fleetround trip, including a call deliberately shaped to violate Agones's own admission-webhook rule (minReplicasmust be 0 or>= bufferSize) to confirm it's rejected client-side with a clear message instead of a raw webhook error. update_fleet_resourcesandupdate_fleet_health, each called twice in sequence to confirm a partial update (setting only one field) leaves the fields set by the first call untouched - checked against rawkubectl get fleet -o jsonpath, not just the tool's own reported output.- Counters/Lists end to end on a real fleet:
create_fleetdeclaring an initial Counter and List,list_gameserversshowing that initial state on both Ready servers,allocate_gameserverapplying a Counter increment and a List append at allocation time, then a secondallocate_gameservercall with a Counter selector correctly skipping the now-lower-capacity server and picking the other one. - Multi-cluster routing (both the happy path and the unknown-cluster error) against a live server started with
AGONES_MCP_CLUSTERSset, including the loud failure on a typo'dAGONES_MCP_CONTEXT. - A full Open Match round trip against a live Open Match deployment:
open_match_create_ticketcreated real tickets, a deployed match function and director matched and assigned them,open_match_ticket_statusobserved the resulting connection assignment, andopen_match_cancel_ticketremoved an unmatched ticket (confirmed by a subsequentNotFoundon status). - Every read and write tool re-run end to end over MCP stdio under
deploy/rbac.yamlexactly as committed, bound to a fresh, otherwise-empty ServiceAccount: the full fleet lifecycle,gameserver_logsincluded, plus the negative paths (invalid state filter rejected, empty image rejected, log reads of non-GameServer pods refused). Nothing failed for lack of permission, and a matrix check confirmed every verb the role does NOT grant is denied.
Safety model
- Every destructive operation is a separate, explicitly-named tool, and the dangerous ones fail closed.
delete_gameserverrefuses to delete anAllocatedserver (live players) unlessforce=trueis passed, and reports the disconnection consequence when forced. Its refusal survives races: aResourceVersionprecondition catches a server that becomes Allocated between the check and the delete.delete_fleetapplies the same live-match refusal, with one honest caveat: the check spans multiple objects, so no single precondition can close its race window completely. An allocation landing in the same instant as the delete can still be disrupted.scale_fleetrelies on Agones scheduling semantics: scale-down removesReadyservers first and never disruptsAllocatedones.- Tool outputs are compact summaries, not raw Kubernetes objects, so agents reason over signal instead of
managedFieldsnoise. namespaceis optional (omit for all namespaces) on tools that list multiple resources (list_fleets,list_gameservers,list_autoscalers,fleet_capacity). It's required everywhere else, because every other tool targets one specific named resource. "All namespaces" isn't a meaningful answer to "which Fleet do you mean."deploy/rbac.yamlgrants exactly the verbs the tools use, nothing more. See Permissions.- Counter/List changes only happen through
allocate_gameserver'scounterActions/listActions, the same allocation-time mechanism Agones's own aggregated allocation API exposes externally. There's deliberately no "set a counter on any running GameServer" tool: a live GameServer's SDK sidecar holds its own in-memory copy of that state and periodically syncs it toStatus, so an external patch outside of allocation would race that sync and could be silently overwritten a moment later. gameserver_logsandgameserver_eventsreturn raw text from inside the cluster (container logs, event messages), which an agent reads as part of its context. Attacker-influenced content there could try to steer the agent toward a write call, and no server can reliably tell real log output from injected instructions. What this one does: caps log and event content independently of what was asked for, marks both outputs as untrusted data before returning them, and refuses to read logs of anything that isn't a GameServer. The real boundary is architectural: your MCP host should require approval on every write-tool call (Claude Code does this by default). That approval gate, not this server, is what stops injected content from causing a write.
Requirements
- Kubernetes cluster with Agones installed (supported Kubernetes: 1.33-1.35)
- kubeconfig with access to Agones resources
- Go 1.25+ to install or build
- Open Match (optional, only for the
open_match_*tools) with a match function and director deployed
No cluster yet? kind plus the Agones install guide gets you a local one in a few minutes:
kind create cluster --name agones-dev
helm repo add agones https://agones.dev/chart/stable
helm install agones agones/agones --namespace agones-system --create-namespace
kubectl apply -f https://raw.githubusercontent.com/googleforgames/agones/release-1.57.0/examples/simple-game-server/fleet.yaml
Install
go install github.com/nicklesimba/agones-conductor-mcp@latest
The binary lands in $(go env GOPATH)/bin (add it to your PATH if it isn't already). Or build from source:
git clone https://github.com/nicklesimba/agones-conductor-mcp
cd agones-conductor-mcp
go build -o agones-conductor-mcp .
On Windows, build with -o agones-conductor-mcp.exe, and use the full path with escaped backslashes in JSON configs (e.g. "C:\\tools\\agones-conductor-mcp.exe").
Configure
The server authenticates using your kubeconfig (the file kubectl already uses to talk to your cluster(s), normally ~/.kube/config), or in-cluster credentials automatically when deployed inside a cluster. A kubeconfig can define multiple contexts (named cluster+user+namespace combinations); if yours has more than one and kubectl config current-context isn't the right one, set AGONES_MCP_CONTEXT to the context name you want.
Claude Code:
claude mcp add agones -- /path/to/agones-conductor-mcp
Claude Desktop / Cursor (mcpServers config):
{
"mcpServers": {
"agones": {
"command": "/path/to/agones-conductor-mcp",
"env": { "AGONES_MCP_CONTEXT": "my-cluster" }
}
}
}
Running headless (claude -p, CI, scripts): the client prompts for per-tool approval on first use, and a non-interactive session has no TTY to answer that prompt, so it just hangs. Pass --allowedTools with the specific mcp__agones__* tool names you need.
Multi-cluster
By default the server targets a single cluster (in-cluster config, or the kubeconfig context named by AGONES_MCP_CONTEXT), and tool calls never need a cluster argument. To manage several clusters from one server, set AGONES_MCP_CLUSTERS to a comma-separated list of kubeconfig context names:
{
"mcpServers": {
"agones": {
"command": "/path/to/agones-conductor-mcp",
"env": { "AGONES_MCP_CLUSTERS": "us-west,us-east,eu-central" }
}
}
}
The first name listed is the default (override with AGONES_MCP_CONTEXT). Every tool then accepts an optional cluster argument naming one of these contexts; omitting it targets the default, so single-cluster prompts keep working unchanged. Use list_clusters to see what's configured.
Open Match
Skip this section unless you already run Open Match. It's a separate matchmaking system, not something Agones needs. The open_match_* tools are off by default; without configuration they just return a clear "not configured" error.
If you do run it, these tools connect to its Frontend service over gRPC without TLS. The connection is plaintext, matching Open Match's own default in-cluster deployment, so keep the Frontend unexposed to untrusted networks: use in-cluster DNS or a kubectl port-forward rather than exposing it publicly. AGONES_MCP_OPEN_MATCH_FRONTEND needs a host:port this server can actually reach. Concretely, one of:
- In-cluster DNS name (e.g.
open-match-frontend.open-match.svc.cluster.local:50504) - works ifagones-conductor-mcpitself runs as a Pod in the same cluster. - NodePort or LoadBalancer - a Kubernetes Service type that exposes something inside the cluster to the outside; use this if
agones-conductor-mcpruns outside the cluster. - A
kubectl port-forwardyou've started yourself, forwarding a local port to the Frontend service - the quickest option for local testing.
Set AGONES_MCP_OPEN_MATCH_FRONTEND to whichever of these applies:
{
"mcpServers": {
"agones": {
"command": "/path/to/agones-conductor-mcp",
"env": { "AGONES_MCP_OPEN_MATCH_FRONTEND": "open-match-frontend.open-match.svc.cluster.local:50504" }
}
}
}
In multi-cluster mode, use AGONES_MCP_OPEN_MATCH_FRONTENDS instead: a comma-separated clusterName=host:port list, matching the names in AGONES_MCP_CLUSTERS. Clusters without an entry simply have Open Match tools disabled for them.
This server doesn't do any matchmaking itself. Open Match's own architecture needs two other pieces to actually turn tickets into matches: a match function (your matchmaking logic - which players belong together) and a director (polls Open Match for proposed matches and finalizes them). Those are things you write and deploy separately, following Open Match's own docs. agones-conductor-mcp only creates, checks, and cancels tickets against whatever pipeline you've already got running - allocate_gameserver has the identical relationship to Agones's own allocator, calling it rather than deciding server placement itself.
Permissions
Don't run this server with broad cluster-admin-style access. RBAC (Role-Based Access Control) is how Kubernetes scopes what an identity is allowed to do, and deploy/rbac.yaml defines the minimal set this server actually needs (see that file's own comments for the exact list). A ClusterRole by itself grants nothing: it's a named bundle of permissions that only takes effect once bound to an identity via a ClusterRoleBinding. If this server runs inside the cluster as a Pod, bind it to that Pod's ServiceAccount; if it runs externally via kubeconfig, apply an equivalent grant to whichever identity that kubeconfig authenticates as.
Concretely, for a ServiceAccount named agones-conductor-mcp in namespace default:
kubectl apply -f deploy/rbac.yaml
kubectl create serviceaccount agones-conductor-mcp -n default
kubectl create clusterrolebinding agones-conductor-mcp --clusterrole=agones-conductor-mcp --serviceaccount=default:agones-conductor-mcp
Every tool has been re-run against a real cluster under this exact role, bound to a fresh ServiceAccount with no other access. Nothing needs more than what's granted here, and a permission matrix confirmed the excess verbs (pod reads, secret access, gameserver writes, and more) are denied.
Development
scripts/simulate-churn.sh [fleet] allocates and tears down GameServers on a timer, simulating matches starting and ending, so there's live state to observe while testing tools interactively. Defaults to context kind-agones-dev, namespace default, fleet simple-fleet; override with AGONES_CONTEXT/AGONES_NAMESPACE env vars or a fleet name argument.
Contributing
Issues and PRs welcome. This project aims to be useful to anyone running Agones, regardless of scale or game genre. If a tool here makes an assumption that doesn't fit your setup, that's a bug worth filing.
License
Apache-2.0. See LICENSE.