# I asked my AI agent for a private LLM client/server. It built one out of MQTT and 1,000 lines

> Source: <https://dev.to/gtanyware/i-asked-my-ai-agent-for-a-private-llm-clientserver-it-built-one-out-of-mqtt-and-1000-lines-34gg>
> Published: 2026-09-27 10:41:23+00:00

I have a PC in my home office with an Nvidia GPU and Ollama on it, and I wanted to ask it questions from my phone while I am out. Every guide I found said roughly the same thing: run something in Docker, put a reverse proxy in front of it, forward a port on the router or install a VPN, provision TLS, add an auth layer, and keep it patched. All of that works. It is also more moving parts than I want between me and a chat box — and if I hand that list to an AI agent, most of its effort goes into YAML rather than into the thing I actually wanted.

So I asked for something different: the smallest possible surface. What we ended up with is a browser page, a small service on the PC, and one rented Linux box running an MQTT broker. Neither the page nor the PC accepts an incoming connection from the internet. Nothing in my house has a port open. Here's what it looks like:

MQTT is a publish/subscribe message bus. Clients connect to a **broker**, subscribe to **topics**, and receive whatever is published to those topics. It is a decades-old piece of infrastructure with excellent servers, and it has a property I wanted: it is a meeting point that both ends can reach without either end being reachable.

```
   phone / laptop                    broker                    PC with the model
  ┌──────────────┐            ┌──────────────────┐          ┌──────────────────────┐
  │ the page     │  ask ─────▶│                  │─────────▶│ mqttchat-server.as   │
  │ (static)     │            │  <topic>/chat    │          │  └ as_chat.py        │
  │              │  ◀─ chunks │                  │◀────────��│     └ Ollama         │
  └──────────────┘            └──────────────────┘          └──────────────────────┘
        wss://443                     tls://8883                    http://11434
```

The phone publishes a question. The PC, which subscribed, is handed it, asks Ollama, and publishes the answer back chunk by chunk — so it types out as the model produces it. The only thing on the internet that knows my house exists is the broker, and all it does is pub/sub.

Being fair to the alternatives: **Tailscale** would also have given me zero open ports, and it is free. What tipped it for me is that it needs a client on every device — I wanted to hand someone a URL, not an app install — and I wanted the phone's traffic to go through machinery I already understood. **Cloudflare Tunnel** is free and needs no client either, but it exposes an HTTP service, so I would have been building the authentication layer myself rather than relying on broker accounts and an ACL. Port forwarding plus a reverse proxy also works, but puts the thing that is actually precious — the GPU box — on the internet, which is the part I did not want.

This is the bit that surprised me, so it's the bit I would check first if I were you.

| The page (UI, streaming, token handling, PWA) | **420 lines** , of which 252 are code | 
| The service on the PC | **94 lines** , of which 46 are code | 
| A Python plugin (token, Ollama, GPU hand-back) | **536 lines** , comments included | 
| Everything the web server serves | **10 files, 71 KiB** — of which 51 KiB is icons | 
| All the broker's own configuration | **15 lines** , plus one 6-line nginx location | 
| The server | less than the price of a takeaway per month | 

That is the whole system. There is no container in it, no reverse proxy in front of the app, no auth service, no database, and nothing to port-forward. The infrastructure I rent runs mosquitto and nginx, and it does several other jobs for me besides this one. I'm not paying for this project; I'm paying for a small server I keep finding uses for.

*Disclosure, because it belongs at the top of the argument rather than hidden at the bottom: this project is written in AllSpeak, a language designed to work efficiently with AI, where code is meant to read as English and every section carries prose explaining why it exists. I did not choose it to sell you anything — I wanted a chat box, and this is the language I build in — but you should read this post with that bias in mind. The line counts above are the honest argument: the parts that are usually painful (a mobile UI that streams, a service that hands the GPU back when it detects intensive work such as video editing happening on the PC, token handling) came out short enough to review in one sitting.*

I wrote very little of this by hand. I pointed my agent at a repository and reviewed what came back — the workflow this language is designed around, where every section of code carries a doc block saying *why* it exists, and the prose gets reviewed by a human who does not have to trust the code. Three things went wrong in interesting ways, and they are the reason I would not publish a "clean" tutorial version of this post:

**The broker silently ate my test messages.** AllSpeak's MQTT plugin frames every MQTT payload — `!last!<total> <json>` when it fits in one chunk, `!part!<n> <total> <json>` for all but the last when it does not — and the receiver *drops* anything unframed while warning loudly in its log. My hand-rolled test publisher sent bare JSON, so for twenty minutes the system looked broken when it was working perfectly. If you test this by hand with `mosquitto_pub`, frame your payload — or you will chase a bug that is not there.

**The PC's sshd refused my key for an hour, without telling the client why.** The home directory was mode 777, and OpenSSH's `StrictModes` silently ignores every key in a home directory that other users can write to. The client just says "Permission denied (publickey,password)". The server knows why; `journalctl -u ssh` says `Authentication refused: bad ownership or modes`. If you have ever shared a home directory over Samba, you may well have seen this problem too.

**Some conditions in the AllSpeak runtime do not mean what they look like.** The test `is object` never holds for a dictionary, and a `clear` on a text variable leaves the boolean `false` rather than an empty string — so an early draft of the answer panel began with the word "falseHello". Both were caught by tests rather than by reading, which is the point I would make to anyone doing this: the agent writes quickly, so the verification is where your attention belongs. Ours is a headless harness that runs the page against a stubbed DOM, MQTT and network; unit tests for the plugin with a fake Ollama; and a live end-to-end run against the real broker with a stub model, which is how my agent knew the answer path worked before a single browser was involved.

The page's MQTT credentials are **public**. The page has to connect to the broker itself, so anyone who loads it can read the username and password out of the traffic. This is fine because of two decisions:

Rotating that token cuts off a device. Rotating the page's broker password cuts off a leaked copy of the page. Neither of those is a substitute for thinking about it, but it is a much smaller thing to think about than an exposed inference service.

Also, to be straight about the limits: one question at a time, one shared token rather than logins, and the model is a 4B one on a 6 GB GPU because that is what fits and answers in a fraction of a second. It is a personal tool, not a product.

The repository — [https://github.com/easycoder/mqttchat](https://github.com/easycoder/mqttchat) — is written to be handed to an agent rather than to a reader: a `SETTING-UP.md` that explains MQTT in plain terms, why a free public broker is the wrong choice, the two-account ACL arrangement, and a checklist of exactly what the agent will need to ask you for — broker hostname, topic prefix, the page's MQTT account, the access token, and which model Ollama has pulled.

Point your agent at it, answer its questions, and expect it to be done in an afternoon. That is what happened here.

Everything this post leans on, in one place:

`pip install allspeak-ai`: Title photo by [Kelly Sikkema](https://unsplash.com/@kellysikkema?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/photos/a-black-square-with-a-white-speech-bubble-on-it-fZzXB6RZI2I?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)
