Skip to content

About

Run PostHog's MCP server against a self-hosted PostHog instance. Unofficial.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

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, used unmodified under the MIT license — see NOTICE. This project adds a container entrypoint and an installer, nothing more.

Not to be confused with @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

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:

{
  "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:

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. NOTICE carries the bundled PostHog server's own MIT grant and attribution.

About

Run PostHog's MCP server against a self-hosted PostHog instance. Unofficial.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages