selfhog-mcp
by ownego
README.md
# selfhog-mcp
Run PostHog's MCP server against a **self-hosted** PostHog instance.
> **Unofficial.** Not affiliated with PostHog Inc. The server itself is
> PostHog's, from [`services/mcp`](https://github.com/PostHog/posthog/tree/master/services/mcp),
> used unmodified under the MIT license — see [NOTICE](NOTICE). This project
> adds a container entrypoint and an installer, nothing more.
>
> Not to be confused with [`@posthog/mcp`](https://www.npmjs.com/package/@posthog/mcp)
> on npm, which is a tracking SDK rather than a server.
PostHog's hosted MCP server at `mcp.posthog.com` routes only to PostHog Cloud,
so self-hosted instances cannot use it.
## Install
```bash
npx @ownego/selfhog-mcp init \
--host https://posthog.example.com \
--key phx_your_personal_api_key
```
That checks the instance and the key, pulls the image, starts a container that
survives reboots, waits for it to be ready, and registers it with Claude Code
at **user scope** — once, for every directory, not just the one you ran `npx`
from. Restart Claude Code afterwards.
Before doing anything else, `init` calls the instance's current-user API with
the key you gave it. A container's `/readyz` only checks its own embedded
Redis, not whether `--host` actually points at a reachable PostHog instance —
without this check, a typo'd host or a stale key would only surface later, as
an opaque `500` on your first tool call. A bad host or key now fails `init`
immediately, before anything is pulled or registered, with a message that says
which it was.
Create the key at `https://<your-instance>/settings/user-api-keys?preset=mcp_server`.
Requires Docker.
### Flags
| Flag | Effect |
|---|---|
| `--no-pull` | Skip `docker pull` and let `docker run`/`docker start` use whatever image is already on this machine — if the image isn't present locally either, `docker` itself will still reach the network and fail with its own message. For local image development, or a machine with no network access that has already pulled the image once. |
| `--skip-verify` | Skip the instance/key check. For setting up before the instance is reachable. |
| `--replace` | Take over a name another MCP server already holds. Without it, `init` refuses and leaves the existing one alone. |
| `--server-name <name>` | Register under a different name. Accepted by `uninstall` too, so the pair stays symmetric. |
### Upgrading
Re-run the same `init`. It always re-pulls the image (unless `--no-pull` is
given), and recreates the container if anything it depends on has moved: a
different `--host`, a different `SELFHOG_PORT`, a different
`SELFHOG_IMAGE`, or a newer build of the tag it is already on. An
up-to-date container is left running untouched, so re-running `init` is cheap
and safe.
If the pull itself fails but the image is already present locally — offline,
or a locally built image that was never pushed — `init` warns that it is
proceeding with the local copy (which may be stale) instead of failing
outright. It only fails the pull outright when no usable image exists locally
either.
## What it runs
One long-lived container holding the PostHog MCP server and a small Redis that
the server requires. Clients talk to it over HTTP on `localhost:8787`, so there
is no extra process per editor session.
Your API key is **not** stored in the container. It lives in your MCP client's
config and travels per request, so one container can serve several people, each
with their own key.
## Commands
| Command | Effect |
|---|---|
| `init --host <url> --key <phx_...>` | Verify, pull, start, and register. Safe to re-run. |
| `status` | Show whether the container is running, its port, and which instance it points at. |
| `stop` | Remove the container. |
| `uninstall` | Remove the container and the user-scope client registration. |
### Environment variables
| Variable | Effect |
|---|---|
| `POSTHOG_API_BASE_URL` | Fallback for `--host`. |
| `POSTHOG_PERSONAL_API_KEY` | Fallback for `--key`. |
| `SELFHOG_PORT` | Host port to publish on (default `8787`). |
| `SELFHOG_IMAGE` | Image to run (default `ghcr.io/ownego/selfhog-mcp:latest`). |
| `SELFHOG_CONTAINER` | Container name (default `selfhog-mcp`). Change it, along with `SELFHOG_PORT` and `--server-name`, to run two instances side by side. |
Re-running `init` under a name it already owns re-registers rather than skipping.
That is what makes a rotated API key take effect: the stored header is rewritten
with the current key. It works by removing the entry and adding it back, because
`claude mcp add` refuses a name that is already taken.
That removal is also why `init` checks who holds the name before it starts. Left
unchecked, re-registering would delete a server this tool never created. So if
the name belongs to something else, `init` stops before pulling anything, tells
you where that server points, and leaves it alone. Pass `--replace` to take the
name over, or `--server-name` to register under a different one.
## Other MCP clients
`init` only knows how to drive Claude Code's CLI. If `claude mcp add` is not
available, or you use a different client, `init` prints a JSON block to paste
instead. It is an ordinary HTTP MCP server entry:
```json
{
"mcpServers": {
"posthog": {
"type": "http",
"url": "http://localhost:8787/mcp",
"headers": { "Authorization": "Bearer phx_your_personal_api_key" }
}
}
}
```
Merge it into your client's user-wide config: `~/.claude.json` for Claude Code
(the same scope `claude mcp add --scope user` writes to), `~/.cursor/mcp.json`
for Cursor, `claude_desktop_config.json` for Claude Desktop. The container
itself is already running by that point — only the client registration is
manual.
## Memory
One container, measured on the published image:
| Constraint | Behaviour |
|---|---|
| unconstrained | ~780 MiB for its first minute, settling near 220 MiB once V8 collects |
| `-m 512m` | comfortable — 323 MiB used |
| `-m 256m` | runs, but at 253/256 MiB with no headroom |
The first-minute figure is not a requirement. Node sizes its heap to the memory
it is given, so the container adapts to a cap rather than needing the peak. If
you are placing it somewhere with a limit, 512 MB is a sensible floor.
## Known rough edges
**Some tools will fail on smaller instances.** The tool catalogue is generated
from all of PostHog's products, and a self-hosted deployment does not implement
every endpoint behind them. Narrow the catalogue with the upstream query
parameters, for example `?features=flags,sql,insights` or
`?tools=execute-sql,dashboard-get` appended to the MCP URL.
**There is no version to pin to.** PostHog's Docker Hub release tags have not
moved since 2023-05-17 and hobby deploys track `master`, so images here are
tagged `sha-<posthog-commit>` with a rolling `latest`. If you need to match your
instance exactly, pin the SHA.
## Advanced
`MCP_MODE=stdio` runs the image as a stdio MCP server instead, for clients that
spawn the process themselves:
```bash
claude mcp add --scope user posthog -- docker run -i --rm \
-e MCP_MODE=stdio \
-e POSTHOG_API_BASE_URL=https://posthog.example.com \
-e POSTHOG_AUTH_HEADER="Bearer phx_..." \
ghcr.io/ownego/selfhog-mcp
```
`-e MCP_MODE=stdio` is required: the image defaults to `MCP_MODE=http`, and
without the flag this command starts the HTTP daemon, never speaks JSON-RPC,
and hangs until the client gives up.
This costs a container per editor session and puts the key in the container's
environment, so `init` is the better default.
## Releasing
Tag a version and publish a GitHub release; `publish-npm.yml` does the rest.
It runs the tests, checks the release tag matches `package.json`, installs the
packed tarball and runs the binary, then publishes.
Publishing uses npm trusted publishing over OIDC — there is no npm token in
this repository. The package's trusted publisher on npmjs.com names this
repository and `publish-npm.yml`; a release from anywhere else cannot publish
it. Provenance is attested to the commit the release was built from.
## License
MIT — see [LICENSE](LICENSE). [NOTICE](NOTICE) carries the bundled PostHog
server's own MIT grant and attribution.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues