Self-hosting MCP servers
Owning your data while staying free to swap the model that reads it
One of the more mundane realities of our AI future (in the form of Large Language Models, anyway) is that every software developer seems hell-bent on crowbarring in some form of AI and charging a premium to cover the model costs. This reduces a lot of software, and especially a lot of apps, to being glorified wrappers around frontier models that can’t be run without having a series of lake-evaporating data centers around the globe. The friction is (setting the lake-evaporating thing aside for a minute) that many of us already have access to these models through paid or free-tier subscriptions. And I, for one, am not really keen on financing the pivot to becoming an ✨AI first✨ company for my goddamned to-do list app.
It’s not just about being cheap though, but also about owning your own data and controlling where it goes. Every time an app asks you to connect your xyz account and hand over a token to your mail, calendar, fitness or financial data, you’re now handing over data to both them and a third party LLM provider.
I do love the convenience and sometimes genuinely impressive results that LLMs offer in my day to day and I’m willing to sacrifice some data privacy to our new AI overlords. On several occasions I’ve shared data like running statistics or even financial data with my clanker of choice (Claude by Anthropic) to get back plans and patterns I wouldn’t have come up with myself (always double check, never trust a clanker on their word though) On top of that, the more consistent and detailed data I send, the better the plans and patterns I get back are.
That’s how I found myself in the situation of having multiple long running chats with Claude, sharing specific data about specific topics and creating plans or artifacts out of them. This was far from perfect, as for my running data I was just sharing screenshots of my runs in Apple Fitness, and for my workouts I exported an excel sheet with workout data and fed it to the same chat. Besides the clumsy logistics, it also gave too much or too little data, was hard to keep track of and created chats with larger and larger contexts.
What MCP actually changed
Model Context Protocol (MCP) is the de facto standard for connecting AI applications to external systems. This allows us to split the AI model from the integration by handing the model a bunch of tools which, when called, perform a specific action like direct database read/writes or retrieving data from an internal/external API. So instead of handfeeding Apple Fitness screenshots to a Claude chat, we can connect our Claude client with a training MCP which provides get_recent_runs as a tool, and the recent runs will be inserted into the chat in a structured way, with all the data I’d like it to include. The great thing about this, is that an MCP doesn’t have to run on a cloud network, you can easily self-host them.
This way of working turned out better than I expected. I use both Claude Desktop and Claude web as clients. I don’t expose everything to Claude web, but the connection to Claude Desktop is flawless.
The set-up
A couple of years ago I bought this Beelink N100 mini-pc for like 200 bucks to serve as a media-server. This was before every piece of memory hardware became 4x the price because of AI developments (it’s a vicious circle), but I think they’re still out there for reasonable prices. I run Ubuntu Server on it and it’s always on, so the perfect candidate to host some applications.
Each of my servers has the same kind of base. A small FastMCP application speaking MCP over stdio with a bunch of typed tool functions. Supergateway wraps it into HTTP so remote clients can reach it. I’ve got one Supergateway install that serves my entire MCP fleet. Because I was repeating the same code so much, I built a small mcp-template: one command generates the project, the config, and the unit. A new integration takes under an hour: write the tools, drop in credentials, systemctl --user enable --now.
I’ve got ten running at the time of writing: tasks, bookmarks, media, workouts, health metrics, two brokerages, a finance aggregator, Gmail. For a complete, real example you can have a look at my training-api which ships its MCP server alongside the REST API it wraps.
The clients
As I’ve only got a Claude subscription, my clients are Claude Desktop and Claude web. Claude Desktop was surprisingly easy to set-up: Tailscale solved almost everything. The servers listen only on the tailnet; my MacBook reaches them over WireGuard from anywhere. No open ports, no authentication, no certificates. Not reachable over the network by default. As far as Claude Desktop is concerned, my servers run on localhost. If a desktop client is all you want to use, things are genuinely easy.
claude.ai and the mobile apps were a bit more complicated The web client can’t join your tailnet. It needs a public HTTPS endpoint with real OAuth in front, so here’s what we (my clanker and I) came up with:
claude.ai ──HTTPS──▶ Cloudflare Tunnel ──▶ OAuth proxy ──▶ MCP server
(no inbound ports) (Google sign-in, (localhost)
me and only me)
The tunnel keeps every router port closed. The connection is outbound from the box and the proxy requires a Google login pinned to my account before anything reaches a server. This part was far from smooth: I had to patch an open-source auth proxy for token lifetimes the web client didn’t want to refresh (this might be patched at the time of you reading this), kept on running into SSE stream issues under Cloudflare’s idle timeout and created a whole bunch of duplicate connections that needed deduplicating. None of it was super difficult but it was time consuming and probably the part I need to put in some more effort to make it better than hobby grade. Desktop-over-Tailscale however is solid as a rock.
How I use it
- Daily briefings and EOD reviews: Every day a scheduled task in Claude Desktop gathers my emails, my todo-list items, my calendar, my training schedule and my media server watch lists and tells me what is overdue, what should have priority and what can be rescheduled. If I want to send out emails, reschedule calendar items or other to-do’s, I can do so right from the chat. At the end of the day we do the same so we’ve got everything organized for the next day. This one really feels like having an actual assistant that reminds you anything that might’ve flown under the radar otherwise.
- Training/health evaluation: This is one of my favorites. I’ve got one API (and accompanying app which I’ll write about later) which gathers all my running stats and syncs my relevant Apple health data here as well. Based on this I can evaluate my progress, find explanations for performance and create new training plans based on previous data.
- Finance API: I’ve gathered all my financial data from different brokers and banks in one place and made an MCP with limited access so that Claude can give me overviews and predictions on my financial status. It also connects to my brokers so if I want to make investments on some sort of hypothesis I can have Claude keep an eye on the stocks and create a proper plan.
The levers
These were just a few examples, but the bottom line is the same for every implementation: I control my data locally and the model only exposes what I want to expose. Some servers are read only, some allow for more permissive actions. Some share tools for all the data whilst others expose only a very limited subset to retain a (sense of) privacy and data control. Credentials stay in-house as well. The model gets results, never tokens. On the mini-PC, sensitive logins are sealed to the TPM and decrypted into memory at service start, so no plaintext secrets on disk either. Aggregation stays on disk, and that’s something I care about a lot. The sensitive data collection isn’t a single account for some app, it’s a combination of aggregated data I fully control. That in turn lets me decide how much the LLM knows: enough to be helpful, never enough to get a full picture if I’m not comfortable sharing it. Last but not least, it’s fully auditable. I’ve got one process and one log stream per integration, and when I want to know what data a model actually requested last Tuesday, my logs can tell me without having any vendor dashboards involved
(AI) Assistants are useful in proportion to the context you share with them, which is not always comfortable. The more useful the assistant, the more of your life is shared with some connector. A lot of people choose to either not share anything, or surrender it all. Self-hosted data and MCP is a nice third option. The models remain third party as long as I can’t run a proper one myself, but memory, credentials, aggregation and all the levers live on a small media pc in my apartment. My data to grant, scope and revoke.
As MCP is an open protocol, the model stays swappable. Today my client is Claude but there’s nothing preventing me from swapping to another model provider or even use local models in case that would make sense. The integration layer wouldn’t change one thing.