Putting AI-Hands on Routers: Building a GenieACS MCP Server in Go

When a new capability feels like magic, the only reasonable reaction is to ask ‘how can I wire this into my own toys?’”
From a spark in Claude’s doc to a fully-automated Go release pipeline
Back in March Claude dropped its “Model Context Protocol” white-paper—the spec that gives large-language models real-time hands instead of canned function-calling.
I was hooked the moment I read the handshake sequence: initialize → listTools → callTool
. One month later every SaaS out there seemed to have an “/mcp” endpoint… except my long-time companion GenieACS (the open-source TR-069 / USP ACS I packaged into a Docker image that has quietly amassed > 241k pulls).
So I rolled up my sleeves and built GenieACS-MCP: a tiny Go service that turns any GenieACS instance into an MCP v1 server.
Below is the full story—equal parts LLM nerdery, Go learning journey, and DevOps dopamine.
TL;DR
- Goal: Expose GenieACS (TR-069/USP ACS) to large-language models using Claude’s Model Context Protocol (MCP) so an AI can list devices, reboot them, push firmware, etc. in real time.
- What I built: genieacs-mcp—a ~500-line Go side-car that registers 4 resources and 3 tools with MCP v1. Runs as a tiny 5 MB static container; talks to GenieACS only through its public REST/NBI.
- DevOps: GoReleaser + GitHub Actions turn a git tag into multi-arch binaries and Docker images automatically. One
docker run … drumsergio/genieacs-mcp:x.y.z
and you’re live.
1. A very short MCP primer
MCP is a JSON-RPC dialect that:
- Lets an AI model ask “what tools do you have?”
- Describes each tool with a JSON schema (args, types, docs).
- Lets the model call those tools, either over HTTP or a streaming transport.
Think of it as capability-based RPC designed for LLMs: the model can only do what the server explicitly advertises.
For GenieACS that means:
Kind | URI / Tool ID | What it gives the LLM |
---|---|---|
Resource | genieacs://device/{id} |
Full device document |
Resource | genieacs://file/{name} |
Firmware / file metadata |
Resource | genieacs://tasks/{id} |
Historical + pending CWMP tasks |
Resource | genieacs://devices/list |
Lightweight catalogue |
Tool | reboot_device |
Queue a Reboot CWMP task |
Tool | download_firmware |
Push a file to the CPE |
Tool | refresh_parameter |
Force a single parameter read |
That’s just seven entry points—small enough for an LLM to reason about, but powerful enough for 90 % of daily ACS work.
2. Why Go?
I write Python & Bash for a living, but after building first a Redis PoC Operator, the two things I wanted to do were:
- Expand my knowledge with Go
- GoReleaser: build once, ship multi-arch binaries & Docker images with a single tag push.
Building an MCP server in Go let me score both.
3. Anatomy of the project
cmd/server/main.go ← wires everything
internal/resources/* ← read-only resource handlers
internal/tools/* ← tool definitions + handlers
client/acs.go ← tiny HTTP client for GenieACS NBI
config/config.go ← dot-env + env vars
version/version.go ← ldflags at build-time
.github/workflows/release.yml ← GitHub actions releaser
.goreleaser.yaml ← goreleaser definition
Dockerfile ← small Dockerfile
Resources
A resource is either a template ({id}
) or a concrete URI.
tpl := mcp.NewResourceTemplate(
"genieacs://device/{id}",
"GenieACS device JSON",
mcp.WithTemplateDescription("Raw device document as returned by NBI"),
mcp.WithTemplateMIMEType("application/json"),
)
The Inspector UI shows templates separately; you paste an ID and hit “Read”.
Tools
Tools are even simpler:
tool := mcp.NewTool("reboot_device",
mcp.WithDescription("Reboot a CPE via GenieACS"),
mcp.WithString("device_id", mcp.Required()),
)
handler := func(ctx context.Context, req mcp.CallToolRequest)
(*mcp.CallToolResult, error) {
id, _ := req.RequireString("device_id")
body, err := acs.RebootDevice(id)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText(fmt.Sprintf(
"Reboot queued. GenieACS said: %s", body)), nil
}
s.AddTool(tool, handler)
The ACS client
The only “clever” part is client/acs.go
; GenieACS has a REST-ish NBI, so a 60-line helper layer was enough.
4. GitOps all the way down
GoReleaser is the glue that turns a simple git tag into a fully-fledged, reproducible release pipeline: it vendors the Go tool-chain in a container, compiles your code for every target you declare, injects version metadata via ldflags, signs the artifacts, assembles Software-Bill-of-Materials (SBOM) and provenance files, builds OCI images, and finally publishes everything—checksums, release notes and all—to GitHub and Docker Hub. Because the entire recipe lives in a single YAML file checked into the repo, every contributor can audit or tweak the process, and every release is 100 % deterministic: clone → tag → watch the bot do the rest.
- Branch → PR → merge → tag vx.y.z
- GitHub Action runs GoReleaser:
• Cross-compiles Windows, Linux, macOS binaries
• Buildsdrumsergio/genieacs-mcp:{tag}-{arch}
images
• Creates a multi-arch manifest & GitHub release page - The Docker image already has sane defaults, so spinning a new instance is:
docker run -d --name acs-mcp \
-e ACS_URL=http://genieacs:7557 \
-p 8080:8080 drumsergio/genieacs-mcp:latest
…and yes, the entire flow exists just because I wanted to see GoReleaser’s emoji-filled logs fly past 😎.
5. Pros, cons & lessons
Pros
- Tiny binary (static busybox image is ~5 MB).
- Zero coupling with GenieACS internals; only uses the public NBI.
- LLM-friendly interface: seven concise operations.
- Observability out of the box: MCP Inspector shows every JSON-RPC call, perfect for debugging.
Cons / Next steps
- GenieACS’s download task wants the GridFS _id; you still would need to get it manually.
- No streaming yet—SSE transport would allow live progress events like the push firmware operation.
- Strictly stateless; long-running tasks (firmware upgrade) don’t push updates back to the agent.
- Security is still a big lacking topic as highlighted by most MCP enthusiasts, and this first iteration of this server is clearly an example of it
- Improve error handling. Right now, an error even sends a 200 JSON-RPC success envelope
- Tests. There are no tests at all right now
6. What’s next?
- Add an SSE transport.
- Auto-discover firmware names (
genieacs://files/list
resource). - Maybe feed the device schema to the AI so it can ask “what’s the current DSL line rate?” directly.
- Many other features are yet to be implemented. PRs are more than welcome.
7. Test it yourself
docker-compose up -d
the stack ingenieacs-docker
.npx @modelcontextprotocol/inspector http://localhost:8080/mcp
initialize → readResource genieacs://devices/list
- Pick a device ID, call
reboot_device
. - Stare at your router’s LEDs blinking.
Final thoughts
Claude’s MCP spec felt like science fiction in March; by May it is just code.
That’s the fun of this AI-agent moment: if an interface doesn’t exist yet, you can probably write it in a weekend and ship it worldwide with a single tag push.
GenieACS now speaks LLM and GoReleaser earns another ⭐ in my toolbox.
Happy hacking.