Skip to main content
Glama
ownego

selfhog-mcp

by ownego

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.

Related MCP server: posthog-mcp

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers