~/pdfinnigan / home-server

Remember once.
Recall anywhere.

I use Claude, Claude Code, ChatGPT, Codex and Grok every day, and each one kept its own memory of me. So I built a personal MCP server on a 2018 MacBook Pro that all of them share (MCP, the Model Context Protocol, is the standard way AI apps call outside tools), and any new AI can plug into. It has zero open ports, every request passes an authenticated gateway, and it deploys itself when I ask for it in chat.

5AI apps connected
1shared memory
0open ports on the Mac
132automated tests
01 · The problem

Five assistants, five memories.

Each AI app remembered a different slice of me, and none of them could see my existing notes: a git repo of preferences, decisions and project facts that my coding agents already used on my laptop. Web and phone apps can't read files on a laptop.

Before

Separate silos

Telling ChatGPT a preference didn't reach Claude. Claude on my phone couldn't see what Claude Code knew. I repeated myself everywhere.

Claude memoryChatGPT memorylaptop filesGrok: none
After

One source of truth

Every app reads the same git-backed memory through one MCP endpoint, and recalls it automatically at the start of a conversation. New facts land as notes that every app sees next time.

ClaudeClaude CodeChatGPTCodexGrok
02 · Architecture

Follow a request.

Press play to trace one tool call from an app on my phone to the Mac and back. Every hop adds one check, and nothing past the gateway is reachable from the internet.

CLOUDFLARE EDGE THE MAC · 127.0.0.1 ONLY AI appClaude, ChatGPT… Worker gatewayauth · allowlists · caps GitHub sign-inone account only D1 usage logno args, no results Access + Tunnelservice token MCP servernon-admin user Mail (IMAP)read + drafts only Deploy watcherLaunchDaemon Memorygit repo

    03 · Design decisions

    The calls I made, and why.

    It's a single-user server on a laptop in my apartment, so the bar was simple: nothing reachable that doesn't need to be, and nothing that needs me at a terminal to keep running.

    01 · network

    A gateway at the edge, not open ports

    Decision
    The Mac only makes an outbound tunnel connection. The single public entry point is a Cloudflare Worker, and Cloudflare Access admits only the Worker's service token to the tunnel.
    Why
    A home IP never appears anywhere, the router has nothing forwarded, and every request is authenticated twice before it can touch the Mac.
    Instead of
    Port forwarding or a public reverse proxy on the Mac.
    02 · identity

    OAuth for apps, hashed tokens for agents

    Decision
    Claude and ChatGPT sign in with GitHub OAuth, and only my GitHub account is accepted. Coding agents and bots each get their own static token, stored only as a SHA-256 hash and compared in constant time.
    Why
    Each client is named in the logs and can be revoked alone. Adding a client never touches another's secret.
    Instead of
    One shared API key for everything.
    03 · the gate

    Allowlists, not filters

    Decision
    The gateway accepts one JSON-RPC request at a time, on a fixed list of methods and tools, with capped bodies. It hides tools from tools/list that a caller isn't allowed, and a kill switch turns every route off.
    Why
    New tools on the server stay invisible until I add them to the list, so a deploy can't widen access by accident.
    Instead of
    Passing traffic through and trusting the server.
    04 · memory

    Memory is a git repo

    Decision
    The server reads its own clone of my memory repo, kept in sync by a background job. The only write is a new dated note in an inbox folder, committed and pushed. A later session files it.
    Why
    Full history, easy review and rollback, and the same files my laptop agents already use. No database to back up.
    Instead of
    A vector database, or letting apps rewrite memory freely.
    05 · operations

    Deploys by asking in chat

    Decision
    A deploy_request tool only touches a trigger file. A LaunchDaemon sees it, pulls master and ships just what changed. Gateway tests must pass first, and a root-owned helper with a pinned hash swaps the server code.
    Why
    I can merge and deploy from my phone. The worst a caller can do is redeploy what's already on master.
    Instead of
    A timer that pulls constantly, or SSH from the cloud.
    06 · mail

    Mail that can't send

    Decision
    Read-only IMAP tools for Yahoo and iCloud. They search, read and save drafts, and nothing else. Only Claude's own apps get them, decided by the OAuth client's registered redirect, not the name it claims.
    Why
    Email is text from strangers, so it's a prompt-injection channel. Every mail result warns models not to save its content to memory without my OK.
    Instead of
    Full mailbox access for every connected app.
    07 · least privilege

    A locked-down service user

    Decision
    The MCP server runs as a dedicated non-admin user, listens on 127.0.0.1 only, and runs under launchd so it restarts on failure and survives a closed lid.
    Why
    Even a bug in the server can't reach my login account's files or anything beyond its own folder.
    Instead of
    Running it as me, in a terminal tab.
    08 · metrics

    Measure at the one choke point

    Decision
    The gateway writes one row per request to Cloudflare D1: app, method, tool, latency and outcome. It never stores arguments or results, and the write happens after the response, so it can't slow a request.
    Why
    One place sees every call. A private dashboard breaks it down by app and tool; a public endpoint powers the numbers below.
    Instead of
    Separate logs on every server.
    04 · Live numbers

    It's running right now.

    These come straight from the gateway's public stats endpoint. It returns only totals, never app names, tool names or content, and caches them for a minute so this page can't load the Mac.

    05 · Tests and evals

    Proving the models actually use it.

    Unit tests check the plumbing. The harder question is behavioral: when I state a preference in passing, does the model save it, and does the note really land in the repo? So I wrote evals for that.

    69

    gateway tests: auth, allowlists, body caps, mail grants, usage logging and the public stats.

    63

    memory server tests: path safety, inbox format, a full HTTP run that commits and pushes, deploys and mail.

    17

    chat scenarios run through real Claude models against a sandboxed copy of the memory repo with its own git remote.

    case pref-branch-prefix
    user › Quick heads up: from now on I want all my git branches named
            with a pk/ prefix. Anyway, what's 17*23?
    expect a note reaches the remote and contains "pk/"
    
    results  85 / 85 trials passed across two Claude models
    also checked 8 cases that must NOT save: generic questions, a pasted secret, someone else's address
    known gap models often skip the recall step when a message states a preference.
              Recall is reported, not graded, and is the next thing to fix.
    06 · What's next

    More capabilities, same front door.

    The server will host more than memory. The plan keeps one gateway, one sign-in and one tunnel, and gives each capability area its own small server and its own URL path.

    /memory

    Recall, search and inbox notes. Every app.

    /ops

    Deploys now; health, logs and restarts later.

    /mail

    Moves out of the memory server into its own namespace.

    Each path is its own OAuth resource, so a token issued for /memory is rejected at /ops, and each server is its own process, so a bad deploy of a new one can't take memory down. The migration takes four steps, and each one can be reverted on its own:

    1. Add the ops server beside memoryThe existing connector doesn't notice.
    2. Gateway serves several resourcesThe new paths go live while the old URL keeps working.
    3. Switch the apps overOne connector per namespace in each app.
    4. Retire the old URLOnly after the usage log shows two weeks with no traffic on it.
    07 · Stack

    What it's built with.

    TypeScriptNode.jsModel Context ProtocolCloudflare Workers Workers OAuth ProviderCloudflare TunnelCloudflare AccessCloudflare D1Workers KV GitHub OAuthlaunchdgitIMAPVitestnode:testClaude Code

    Back to the home page →