Graphiti

Temporal Knowledge Graphs  ·  MCP Protocol  ·  Provenance-First

Zep's open-source framework for building temporal context graphs — knowledge graphs where facts have validity windows, entities evolve over time, and everything traces back to provenance. Deployed as an MCP server on home lab infrastructure, integrated with both Hermes and Claude Code agents as the primary long-term memory for F5 BIG-IP administration.

What Graphiti Does Differently (If you can get it to work)

Static wikis and RAG pipelines give you point-in-time snapshots. When information changes, you either overwrite the old fact (losing history) or you have conflicting facts with no way to know which one is current. Graphiti solves this with bi-temporal fact management: every fact has a validity window, and when information changes, old facts are invalidated (not deleted), so you can query what's true now or what was true at any point in the past.

Every entity and relationship traces back to its source episode — the raw data that produced it. This is provenance you can audit, not just citations you hope are accurate. And retrieval is hybrid: semantic embeddings + keyword (BM25) + graph traversal, typically sub-second latency.


The state of this project as of this writing (07/17/2026) is not good. The GitHub Version is v0.29.2, the DockerHub Version is v0.22.0 (last updated 9 months ago) and there are over 2,900 Forks (and mine is one of them). It took quite some time of modifying code and bind mounting them into the Docker Compose to override all kinds of this just to get it to work (and I mean work I mean making the application honor the seeings in the .env file).


Once you DO get it working, its well worth the effort. If they don't start maintaining it better then I'll turn my Fork into my own project and support it myself...and I'm not saying this to slight anyone. Whoever is involved is supporting this for FREE and in their own time. So MAD Respect and hats off to you! Thank you!

Architecture

Graph Engine

  • FalkorDB (Redis-based) — default, lightweight, bundled in Docker
  • Neo4j — supported for production deployments
  • Entities, relationships, and episodes with temporal validity
  • Bi-temporal fact management — invalidate, don't overwrite
  • Provenance: every fact traces to its source episode

Retrieval

  • Hybrid search — semantic embeddings + BM25 keyword + graph traversal
  • search_memory_facts — find facts and relationships
  • search_nodes — find entity summaries and information
  • get_episodes — pull recent raw episodes for provenance
  • Typical query latency: sub-second

LLM Providers

  • OpenAI (Currently HARD CODED)
  • Ollama.com (Because I Fixed It)
  • Local Models (Because I Fixed It)
  • Anthropic, Google Gemini, Groq (I highly doubt it)
  • OpenRouter / DeepSeek / Together (I highly doubt it)
  • Embeddings: OpenAI, sentence-transformers (local), Voyage, Gemini

MCP Tools

Tool Description
add_episode Store episodes (text, messages, or JSON) in the knowledge graph. Automatic entity/relationship extraction.
search_memory_facts Find relevant facts and relationships using hybrid search (semantic + keyword + graph traversal).
search_nodes Search for entity summaries and information. Use before asking the user to repeat context.
get_episodes Retrieve recent raw episodes for provenance context.
add_triplet Add a specific source → relationship → target fact. Skips extraction when you know the exact structure.
delete_episode Remove an episode and cascade-delete facts it solely produced.
get_entity_edge Fetch a specific fact by UUID for detailed inspection.
get_episode_entities Trace which entities and facts came from a specific episode.
summarize_saga Refresh the running summary for a tracked multi-episode thread.
build_communities Detect entity clusters across the graph. Occasional maintenance operation.
clear_graph Wipe all data for a group. Destructive — only on explicit user request.
get_status Check the server/database connection health.

Home Lab Deployment

Graphiti is deployed on home lab infrastructure behind Traefik with TLS termination, co-located with the BIG-IP Administrator LLM Wiki MCP server (but Graphiti is replacing the LLM Wiki and it will be retired).

Endpoint https://traefik.fronted.internal.dns.url/mcp/ — Streamable HTTP with no auth (internal network)
Database FalkorDB (Redis-based) — bundled with MCP server in Docker Compose
Group ID f5-admin — isolates F5 knowledge from other agents' graphs
LLM Ollama Cloud (GLM-5.x) for entity extraction; local sentence-transformers for embeddings
Reverse Proxy Traefik with TLS termination — same pattern as all MCP servers on the infrastructure

Graphiti vs. LLM Wiki: Complementary, Not Competing

Both the LLM Wiki (via Generic LLM Wiki MCP) and Graphiti serve knowledge to agents, but they solve different problems:

Aspect LLM Wiki (MCP) Graphiti
Data model Curated Markdown pages with frontmatter Temporal knowledge graph with entities, edges, and episodes
Knowledge type Authoritative reference — configuration syntax, best practices, troubleshooting guides Environment-specific facts — device inventories, VIP configs, past decisions, troubleshooting history
Temporal awareness None — pages are point-in-time snapshots Bi-temporal — facts have validity windows, old facts are invalidated not deleted
Provenance File-based — sources: frontmatter links to raw documents Episode-based — every entity and fact traces to the raw data that produced it
Contradiction handling Manual — agent flags contradictions for human review Automatic — old facts invalidated, history preserved
Retrieval Full-text search across titles, tags, and bodies Hybrid — semantic + BM25 keyword + graph traversal
Curation model Human curates sources, agent writes and maintains pages Agent ingests episodes, extracts entities and relationships automatically
Best for "What is the correct TMSH syntax for creating a pool?" "What VIPs did we configure on bigip-01 last month and why?"

Lessons Learned

  • Small models struggle with structured extraction. Graphiti depends on structured JSON output for entity/edge extraction and deduplication. Models under ~7B parameters frequently emit non-conforming JSON, causing ingestion failures. Use the largest model you can run, and switch structured_output_mode from json_schema to json_object if you see extraction failures.
  • Ollama requires OpenAIGenericClient, not OpenAIClient. Ollama doesn't support the /v1/responses endpoint. Using the wrong client causes cryptic errors. Always use OpenAIGenericClient for Ollama and other OpenAI-compatible providers.
  • FalkorDB has no password by default. The combined Docker container ships with no authentication on the Redis port. Set a password for any deployment beyond local testing.
  • Trailing slash on the MCP URL matters. The Graphiti MCP URL must include the trailing slash (/mcp/, not /mcp). Some servers redirect; others 404 without it.
  • The MCP server is labeled "experimental." The API may change between releases. Pin your Docker image version if stability matters.
  • Episode processing is async. The MCP server processes episodes with configurable concurrency (SEMAPHORE_LIMIT, default 10). Tune based on your LLM provider's rate limits.
  • Graphiti and the LLM Wiki are complementary, not redundant. Trying to use one for the other's job leads to frustration. Use the wiki for curated reference material; use the graph for evolving environment-specific facts.

Details

Status Deployed · Active — serving as primary long-term memory for F5 Administrator agents
GitHub Project getzep/graphiti — Apache-2.0, 28k+ GitHub stars
DockerHub Project getzep/graphiti — Last updated 9+ Months ago
Language Python (core library) · TypeScript (MCP server)
Database FalkorDB (Redis-based, default) · Neo4j (production alternative)
Protocol MCP (Model Context Protocol) · Streamable HTTP · Stateless
Deployment Docker Compose on home lab infrastructure · Traefik reverse proxy with TLS
LLM Provider Ollama Cloud (GLM-5.x) for extraction · sentence-transformers (local) for embeddings
Integrations Generic LLM Wiki (MCP) (complementary knowledge) · TMSH LLM Wiki Brain (syntax cross-reference)
Hermes Skill f5-graphiti — search-first protocol, entity types, tool reference, pitfalls

Example compose.yml, .env, and README.md for a Working Instance

Below is a known-working Docker Compose and environment configuration for running the full Graphiti stack (MCP server + REST API, each with its own graph database). The compose file combines both halves of the upstream project into one stack. The env file documents every variable with inline comments explaining what each one does and why certain values are set the way they are.

This is a LOT, but since I have a Fork of the Project I think this is the best place for me to put this information for others to reference.

compose.yml

---
# Full Graphiti stack: both upstream consumer interfaces (MCP server + REST
# API), each with its own graph database, mirroring what you get by running
# getzep/graphiti's two docker-compose.yml files (root + mcp_server/docker/)
# together. See README.md for why these are two separate graphs rather than
# one shared one.
services:
  # ---- Graph DB #1: FalkorDB, backs graphiti-mcp ----
  falkordb:
    image: falkordb/falkordb:v4.20.1
    container_name: graphiti-falkordb
    environment:
      - BROWSER=${BROWSER:-1} # FalkorDB web UI on :3000
      # REDIS_ARGS (not FALKORDB_PASSWORD -- that var isn't read by this
      # image at all) is how the falkordb/falkordb image actually enables
      # Redis auth: https://docs.falkordb.com/operations/docker.html
      - REDIS_ARGS=--requirepass ${FALKORDB_PASSWORD}
    env_file: .env
    volumes:
      - ./falkordb_data:/data
    ports:
      - "6379:6379" # Redis / FalkorDB
      - "3000:3000" # FalkorDB Web UI
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${FALKORDB_PASSWORD}", "-p", "6379", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped
    networks:
      - graphiti

  # ---- MCP server (first-party, for MCP clients: Hermes, Cursor, Claude, etc.) ----
  graphiti-mcp:
    # Our own build from github.com/itlostandfound/graphiti-fixed (branch
    # mcp-fixed, tag mcp-v1.0.2-fixed) -- the getzep/graphiti mcp-v1.0.2
    # source with the reranker, DNS rebinding, and LLM base_url fixes baked
    # in directly, instead of bind-mounting patches over the stock image.
    # See README.md's "Our own fixed images" section.
    image: itlostandfound/graphiti-mcp:1.0.2-fixed
    container_name: graphiti-mcp
    env_file: .env
    depends_on:
      falkordb:
        condition: service_healthy
    volumes:
      - ./config/config.yaml:/app/mcp/config/config.yaml:ro
      - ./mcp_logs:/var/log/graphiti
      - ./hf_cache:/root/.cache/huggingface # persist the ~2GB reranker model download across restarts
    ports:
      - "8000:8000" # http://localhost:8000/mcp/  (health: /health)
    restart: unless-stopped
    networks:
      - graphiti

  # ---- Graph DB #2: Neo4j, backs graphiti-api ----
  neo4j:
    image: neo4j:5.26.2
    container_name: graphiti-neo4j
    environment:
      - NEO4J_AUTH=${NEO4J_USER:-neo4j}/${NEO4J_PASSWORD:-password}
    volumes:
      - ./neo4j_data:/data
      - ./neo4j_logs:/logs
    ports:
      - "7474:7474" # Neo4j Browser (HTTP)
      - "7687:7687" # Bolt
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:7474 || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    restart: unless-stopped
    networks:
      - graphiti

  # ---- REST/FastAPI server (general-purpose API: /messages, /search, /docs, ...) ----
  graphiti-api:
    # Our own build from github.com/itlostandfound/graphiti-fixed (branch
    # api-fixed, tag v0.22.0-fixed) -- the getzep/graphiti v0.22.0 source
    # with the embedder-model-name fix baked in directly. See README.md's
    # "Our own fixed images" section.
    image: itlostandfound/graphiti-api:0.22.0-fixed
    container_name: graphiti-api
    env_file: .env
    environment:
      - NEO4J_URI=bolt://neo4j:7687
      - NEO4J_USER=${NEO4J_USER:-neo4j}
      - NEO4J_PASSWORD=${NEO4J_PASSWORD:-password}
      - db_backend=neo4j
      - PORT=8000
      # graph_service/config.py hardcodes these exact var names -- can't
      # rename OLLAMA_API_KEY/OLLAMA_BASE_URL for it, so map them here instead.
      - OPENAI_API_KEY=${OLLAMA_API_KEY}
      - OPENAI_BASE_URL=${OLLAMA_BASE_URL:-https://ollama.com/v1}
    depends_on:
      neo4j:
        condition: service_healthy
    ports:
      - "8001:8000" # http://localhost:8001/docs (Swagger), /redoc, /healthcheck
    healthcheck:
      test:
        [
          "CMD",
          "python",
          "-c",
          "import urllib.request; urllib.request.urlopen('http://localhost:8000/healthcheck')",
        ]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: unless-stopped
    networks:
      - graphiti

  # ---- Optional: build graphiti-api with FalkorDB support instead of Neo4j ----
  # The published zepai/graphiti:latest image is built WITHOUT the falkordb
  # extra (see getzep/graphiti's root Dockerfile: INSTALL_FALKORDB defaults to
  # false), so it can't talk to FalkorDB out of the box -- that's why
  # graphiti-api uses Neo4j above. If you'd rather have graphiti-api and
  # graphiti-mcp share the SAME FalkorDB graph, build locally instead:
  # comment out the "graphiti-api" and "neo4j" services above, then uncomment
  # this block (requires internet access + BuildKit git-context support on
  # the Docker host).
  #
  # graphiti-api-falkordb:
  #   container_name: graphiti-api
  #   build:
  #     context: https://github.com/getzep/graphiti.git#v0.29.2
  #     args:
  #       INSTALL_FALKORDB: "true"
  #   env_file: .env
  #   environment:
  #     - FALKORDB_HOST=falkordb
  #     - FALKORDB_PORT=6379
  #     - FALKORDB_DATABASE=${FALKORDB_DATABASE:-default_db}
  #     - db_backend=falkordb
  #     - PORT=8000
  #     - OPENAI_API_KEY=${OLLAMA_API_KEY}
  #     - OPENAI_BASE_URL=${OLLAMA_BASE_URL:-https://ollama.com/v1}
  #   depends_on:
  #     falkordb:
  #       condition: service_healthy
  #   ports:
  #     - "8001:8000"
  #   restart: unless-stopped
  #   networks:
  #     - graphiti

networks:
  graphiti:
    driver: bridge

env.example

# ============================================================
# graphiti — Environment Configuration Reference
# ============================================================
# Copy this file to .env and fill in the values for your environment.
# This stack combines BOTH halves of the upstream project:
#   - mcp_server/  -> graphiti-mcp  (zepai/knowledge-graph-mcp:standalone) + FalkorDB
#   - server/      -> graphiti-api  (zepai/graphiti:latest, the REST/FastAPI service) + Neo4j
# They are two independent graph databases (see README.md for why) -- each
# app's env vars are prefixed/named per its own upstream schema, so some
# values (Ollama endpoint, model name) are intentionally duplicated under
# different var names.
# ============================================================

# ---- LLM (chat/extraction) -- Ollama Cloud ----
# ollamarelay (local container) has been decommissioned; LLM calls now go to
# Ollama Cloud's OpenAI-compatible endpoint. Get a key from https://ollama.com
#
# Left genuinely blank on purpose -- unlike the passwords below, this is a
# real account credential tied to your Ollama Cloud billing. There's no
# value that could be generated here that would actually authenticate;
# filling it with anything else would just fail differently. This is the
# one thing in this file that has to come from you.
MODEL_NAME=glm-5.1:cloud
OLLAMA_API_KEY=
OLLAMA_BASE_URL=https://ollama.com/v1

# Consumed directly by graphiti-api (server/graph_service/config.py reads the
# literal names OPENAI_API_KEY / OPENAI_BASE_URL -- can't be renamed upstream)
# so they're just mapped from the Ollama Cloud values above in compose.yml /
# compose.traefik.yml's "environment:" block for that service. Nothing to
# fill in here for graphiti-api specifically.

# ---- Reranker (graphiti-mcp) -- fully local, no API key ----
# graphiti_mcp_server.py upstream never passes a cross_encoder= to Graphiti(),
# so it silently falls back to graphiti_core's bare OpenAIRerankerClient()
# default, which requires a real OpenAI key and hardcodes model "gpt-4.1-nano"
# plus a logit_bias keyed to OpenAI-tokenizer-specific token IDs (meaningless
# against any other model, so no Ollama Cloud/LM Studio substitute works
# either). Fixed at the source in our own image (itlostandfound/graphiti-mcp)
# instead of paying for that or bind-mounting a patch -- passes graphiti_core's
# local BGERerankerClient (BAAI/bge-reranker-v2-m3 via sentence-transformers)
# instead, falling back to a dependency-free lexical reranker if that's ever
# unavailable. Fully local, free, no key needed here at all -- the model
# downloads once (~2GB) from HuggingFace Hub on first startup and is cached
# in the ./hf_cache volume after that. See README.md's "Our own fixed images".

# ---- Embedder -- LOCAL (LM Studio), NOT Ollama Cloud ----
# Ollama Cloud's catalog is large chat/completion models; embeddings are kept
# local (do NOT reuse a general chat model like qwen3-8b here -- see
# README.md's "Embedder: local, not cloud" section for why).
#
# graphiti-api's embedder model name used to be hardcoded upstream to
# "text-embedding-3-small" with no override -- fixed at the source in our own
# image (itlostandfound/graphiti-api) now, so EMBEDDER_MODEL below is a REAL
# model name, not an alias. No more loading anything under a fake identifier
# to satisfy a hardcoded string -- both graphiti-mcp and graphiti-api read
# this the same way graphiti-mcp always could.
#
# Because it's a real name now, normal LM Studio JIT load/unload is safe to
# use again: leave it unloaded, let the first request load it, let it idle
# out after the TTL, no need to keep it pinned in memory or use the CLI
# --identifier workaround from before. (That workaround was specifically
# needed because custom/aliased identifiers have documented JIT bugs in LM
# Studio's tracker -- a real catalog model name doesn't hit that path.)
# `lms ls` will show you the exact key if this doesn't match your setup.
#
# localhost is correct FROM the LM Studio host itself, but graphiti-mcp and
# graphiti-api reach this from inside their own containers -- localhost
# there means the container, not your LM Studio machine. Swap this to
# host.docker.internal:1234 (Docker Desktop, same machine as the Docker host)
# or your LM Studio machine's LAN IP (separate machine, e.g. deploy server is
# a VM and LM Studio runs on your workstation) before deploying; left as
# localhost here deliberately so it's obvious this needs to be corrected
# per-environment rather than silently wrong.
#
# Getting the IP right isn't enough on its own: LM Studio's server defaults
# to binding localhost-only. A correct LAN IP will just hang/time out (not
# fail fast) until you also enable "Serve on Local Network" (or equivalent
# bind-to-all-interfaces setting) in LM Studio's Developer/server settings --
# cost about an hour to track down once, don't repeat that.
EMBEDDER_API_URL=http://localhost:1234/v1

# LM Studio doesn't check this, but the OpenAI client library used
# upstream still requires a non-empty string to be present -- any value works.
EMBEDDER_API_KEY=lm-studio-no-key-required
EMBEDDER_MODEL=text-embedding-nomic-embed-text-v1.5@q8_0

# nomic-embed-text-v1.5's native output is 768-dim.
EMBEDDER_DIMENSIONS=768

# ---- FalkorDB (graph DB for graphiti-mcp) ----
# Auth is ON by default here (not upstream's default -- FalkorDB ships with
# no password unless told otherwise). This one value does double duty: the
# falkordb service's REDIS_ARGS=--requirepass ${FALKORDB_PASSWORD} (see
# compose.yml/compose.traefik.yml) makes the server actually enforce it, and
# graphiti-mcp reads this same var as the client-side password. Both sides
# read the exact same var, so they can never drift out of sync with each
# other. Randomly generated -- change it, don't remove it (an empty value
# here is NOT the same as no auth; see below).
FALKORDB_PASSWORD=NEEDS.PASSWORD
FALKORDB_DATABASE=default_db

# Enable/disable the FalkorDB web UI on port 3000 (1 = enabled, 0 = disabled)
BROWSER=1

# ---- Neo4j (graph DB for graphiti-api) ----
# Randomly generated, replacing upstream's own throwaway "password" default.
NEO4J_USER=neo4j
NEO4J_PASSWORD=NEEDS.PASSWORD

# ---- Graphiti application settings ----
GRAPHITI_GROUP_ID=main
# Episode processing concurrency. Local/Ollama models: keep this low (3-5).
SEMAPHORE_LIMIT=5
# Telemetry is opt-out (PostHog, system info only -- not your data)
GRAPHITI_TELEMETRY_ENABLED=false

# ---- Traefik routing (compose.traefik.yml only) ----
# Public hostnames (bare hostname, no scheme, no path)
MCP_DOMAIN=graphiti-mcp.whateveryouwant.com
API_DOMAIN=graphiti-api.whateveryouwant.com
FALKORDB_UI_DOMAIN=graphiti-falkordb.whateveryouwant.com
NEO4J_UI_DOMAIN=graphiti-neo4j.whateveryouwant.com

README.md

Full Graphiti stack — both halves of the upstream project, each running against its own graph database. Both application images are our own build, not upstream's — see "Our own fixed images" below for why and what's different.

Component Upstream location Image Graph DB
MCP server (for MCP clients: Hermes, Cursor, Claude, ...) mcp_server/docker/ itlostandfound/graphiti-mcp:1.0.2-fixed FalkorDB
REST/FastAPI server (/messages, /search, /docs, ...) server/ (root docker-compose.yml) itlostandfound/graphiti-api:0.22.0-fixed Neo4j

All images are pinned to specific tags rather than :latest-style moving targets, so a docker compose pull won't silently change what's running: falkordb/falkordb:v4.20.1, itlostandfound/graphiti-mcp:1.0.2-fixed, neo4j:5.26.2, itlostandfound/graphiti-api:0.22.0-fixed. The version numbers in our tags match the exact upstream getzep/graphiti tags our fork's fixes are built on (mcp-v1.0.2 and v0.22.0 respectively) — chosen because those are what the last officially-published zepai/* images on Docker Hub actually were, confirmed by checking the registry directly rather than inferring from the publish workflow's logic (which turned out not to be reliable — see git history / commit messages on our fork for how that was discovered).

See Graphiti-Investigation-Report.md for the original background research. Two things in that report don't match the current upstream source (verified against getzep/graphiti directly): sentence_transformers isn't an embedder option the MCP server's EmbedderFactory actually supports, and the config key is providers.openai.api_url, not api_base. Both are handled correctly in config/config.yaml here.

Why two separate graph databases

The v0.22.0 source our graphiti-api build is based on has no FalkorDB support at all — not just "extra not installed": at that version (Oct 2025), server/graph_service/config.py doesn't even have a db_backend/falkordb_* field yet, and zep_graphiti.py constructs ZepGraphiti(uri=..., user=..., password=...) directly with no branching. FalkorDB support for the REST API was added later in getzep/graphiti's history (visible at git tag v0.29.2), but we built our fix on v0.22.0 specifically because that's what the last officially-published zepai/graphiti Docker Hub tag actually was — keeping our fork's baseline aligned with a known, previously-working reference point rather than jumping to newer upstream code we haven't run. Nothing stops you from rebasing the api-fixed branch on v0.29.2 later and adding real FalkorDB support to graphiti-api — it's your fork now — just hasn't been done here.

If you want them sharing a single FalkorDB graph, compose.yml has a commented-out graphiti-api-falkordb service that builds the REST API from a git context with INSTALL_FALKORDB=true — currently still pointed at upstream getzep/graphiti#v0.29.2, you'd want to repoint it at your own fork if you go this route.

Setup

  1. Copy env.example.txt to .env. LLM/extraction calls go to Ollama Cloud (OLLAMA_BASE_URL=https://ollama.com/v1, needs a real OLLAMA_API_KEY from ollama.com — there's no local relay to fall back on anymore). Embeddings are kept local (LM Studio or similar) — fill in EMBEDDER_API_URL / EMBEDDER_MODEL once you've got a dedicated embedding model loaded and reachable from the Docker host. See "Embedder: local, not cloud" below for why and what to load.
  2. Deploy behind Traefik:
    docker network create graphitinet # one-time, since compose.traefik.yml expects it external
    docker compose -f compose.traefik.yml up -d
  3. Point your MCP clients at it — see "Connecting MCP clients" below.

Local testing (no Traefik)

docker compose up -d

Connecting MCP clients

Only graphiti-mcp is relevant here — graphiti-api is a plain REST API, not an MCP server. The MCP endpoint is https://${MCP_DOMAIN}/mcp/ behind Traefik, or http://localhost:8000/mcp/ for local testing via compose.yml. All three clients below support HTTP-transport MCP servers, just configured differently.

Hermes — add under mcp_servers in the profile's config.yaml:

mcp_servers:
  graphiti:
    url: "https://graphiti-mcp.whateveryouwant.com/mcp/"

Claude Code — either claude mcp add --transport http graphiti https://graphiti-mcp.whateveryouwant.com/mcp/, or add directly to .mcp.json in a project:

{
  "mcpServers": {
    "graphiti": {
      "type": "http",
      "url": "https://graphiti-mcp.whateveryouwant.com/mcp/"
    }
  }
}

Claude Desktop — check Settings for a "Custom Connector" / remote MCP server option first (recent versions support adding a URL directly, no bridge needed). If your version only supports local stdio servers, bridge through mcp-remote in claude_desktop_config.json instead:

{
  "mcpServers": {
    "graphiti": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://graphiti-mcp.whateveryouwant.com/mcp/"]
    }
  }
}

After connecting, verify with a round trip: call add_memory with some test text, then search_memory_facts or search_nodes and confirm it returns something — that exercises the full path (LLM extraction via Ollama Cloud → embedding via LM Studio → FalkorDB storage → retrieval) in one shot.

Endpoints exposed

Endpoint Local (compose.yml) Traefik (compose.traefik.yml)
MCP server (/mcp/, /health) localhost:8000 https://${MCP_DOMAIN}/
REST API (/docs, /redoc, /messages, /search, /entity-node, /entity-edge/{uuid}, /group/{group_id}, /episode/{uuid}, /clear, /episodes/{group_id}, /get-memory, /healthcheck) localhost:8001 https://${API_DOMAIN}/
FalkorDB Web UI localhost:3000 https://${FALKORDB_UI_DOMAIN}/
Neo4j Browser localhost:7474 https://${NEO4J_UI_DOMAIN}/
FalkorDB / Redis (raw wire protocol) localhost:6379 localhost:6379 (direct-published)
Neo4j Bolt (raw wire protocol) localhost:7687 localhost:7687 (direct-published)

Redis and Bolt aren't HTTP, so Traefik can't Host()-route them the way it does the four web UIs above — they're published directly on both compose files. Each has a commented-out Traefik TCP router block showing how to front it properly if you add a dedicated static entryPoint to ../1.traefik/data/traefik.yml (out of scope here since that's shared infra).

Embedder: local, not cloud

The LLM (chat/extraction) and embedder are configured as two separate OpenAI-compatible endpoints on purpose — llm points at Ollama Cloud, embedder points at a local server. Ollama Cloud's catalog is large chat/completion models (gpt-oss:120b, qwen3:235b, etc.); it's not really the place to host a small dedicated embedding model, and there's no cost/latency reason to send embedding calls over the network anyway.

Load a dedicated embedding model for EMBEDDER_MODEL — not a general chat model. A chat LLM (e.g. qwen3-8b) isn't trained for embeddings the way nomic-embed-text-v1.5 / bge-small-en-v1.5 / gte-small are (contrastive objective vs. next-token prediction) — even if a server like LM Studio can pool its hidden states into a vector on request, retrieval quality is typically worse than a purpose-built ~100-400M-param embedding model, despite being far heavier to run. Reserve a general chat model for the llm role if you ever want a local fallback to Ollama Cloud, not for embedder.

This deployment uses nomic-embed-text-v1.5 in LM Studio, under its real model identifier (EMBEDDER_MODEL=text-embedding-nomic-embed-text-v1.5@q8_0 by default — run lms ls if yours differs). No fake alias needed — see "Our own fixed images" below for why that used to be necessary and isn't anymore. Because it's a real catalog name now, ordinary LM Studio JIT load/unload is safe: leave it unloaded, let the first request load it, let it idle out after the TTL. You don't need to keep it pinned in memory or use lms load --identifier — that workaround was specifically needed because LM Studio's JIT loader has documented bugs around custom/aliased identifiers; a real catalog name doesn't hit that code path.

Our own fixed images

Four separate upstream bugs across three files in getzep/graphiti — none of them had any config-driven workaround, all of them were found by actually running this stack, not by reading the source in advance. Originally worked around by bind-mounting corrected copies of the affected files over the stock zepai/* images at container start; now fixed at the source instead, in a real fork: github.com/itlostandfound/graphiti-fixed, two branches (mcp-fixed off tag mcp-v1.0.2, api-fixed off tag v0.22.0 — matching the exact upstream versions the last officially-published Docker Hub images were built from), built into itlostandfound/graphiti-mcp:1.0.2-fixed and itlostandfound/graphiti-api:0.22.0-fixed and pushed to Docker Hub. Deploy server pulls pre-built images same as before — no code, no build step, no patches/ directory on it. If getzep/graphiti ever fixes these upstream, our fork's commits show exactly what to diff against to confirm, and the compose files can just point back at zepai/*.

1. MCP reranker → local, no API key. mcp_server/src/graphiti_mcp_server.py constructs Graphiti(...) twice (FalkorDB branch, Neo4j branch) and never passes cross_encoder= either time, so graphiti_core silently defaults to OpenAIRerankerClient() — hardcoded model gpt-4.1-nano, plus a logit_bias keyed to specific OpenAI-tokenizer token IDs that are meaningless against any other model's vocabulary (so unlike the embedder issue below, no Ollama Cloud/LM Studio substitute could ever work here even in principle — it would silently score relevance wrong, not just fail to connect).

Fix (fork commit on mcp-fixed): passes a local cross_encoder= to both Graphiti(...) calls instead of letting it default to OpenAI's. It tries graphiti_core's own BGERerankerClient (BAAI/bge-reranker-v2-m3 via sentence-transformers) first, since that's a real cross-encoder model, not a heuristic — the mcp_server Dockerfile's providers extra lists sentence-transformers as a dependency, so this should be available. It wasn't, in the actual published 1.0.2-standalone image (confirmed by the import failing at runtime — what a Dockerfile says it installs and what's actually in a published tag turned out not to be reliably the same thing twice in this project). So the fix falls back to a small dependency-free lexical-overlap reranker instead of crash-looping when that import fails — weaker than a real cross-encoder, but retrieval already went through semantic + BM25 + graph traversal before reranking runs, so this is a refinement pass, not the primary relevance signal. If sentence-transformers is ever actually present, BGERerankerClient is used automatically with no changes needed — the ./hf_cache volume is still mounted for that case.

2. DNS rebinding → disabled for proxied deployments. Confirmed as an actual upstream bug: getzep/graphiti#1205. FastMCP() is constructed at module level with no host= argument, so it defaults to "127.0.0.1" — and the SDK auto-enables DNS rebinding protection (Host header allowlist locked to 127.0.0.1/localhost/::1) whenever host is one of those at construction time. config.yaml's server.host: "0.0.0.0" is only applied after construction (mcp.settings.host = ..., further down the file), which doesn't retroactively fix the allowlist. Result: every request that arrives with a real Host header — i.e. anything routed through Traefik — gets rejected with 421 Invalid Host header, no matter what config.yaml says.

Fix (same fork commit as the reranker fix above, mcp-fixed branch): explicitly passes transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False) to the FastMCP(...) constructor. DNS rebinding protection defends a localhost-bound service against malicious-webpage-via-browser attacks — not the threat model for a server intentionally exposed through Traefik behind TLS, so disabling it outright is correct here rather than fighting the config-ordering bug.

3. LLM client → uses OLLAMA_BASE_URL, right API shape. mcp_server/src/services/factories.py's LLMClientFactory.create() has two separate bugs in its 'openai' case:

  1. base_url is never read at all. EmbedderFactory.create() a few dozen lines below correctly does base_url=config.providers.openai.api_url — the LLM path is just missing the equivalent line entirely. Every LLM request silently went to real api.openai.com regardless of what OLLAMA_BASE_URL/config.yaml's api_url were set to, so it failed with 401 Incorrect API key provided — an Ollama Cloud key obviously isn't a valid OpenAI one, but you wouldn't know that from the error alone without checking which host the request actually went to.
  2. Always uses OpenAIClient, which only works against real OpenAI. That class calls self.client.responses.parse(...) — OpenAI's newer Responses API (POST /v1/responses), which only OpenAI itself implements. OpenAIGenericClient is the class actually meant for this ("targets any OpenAI-compatible /chat/completions endpoint" per its own docstring) — even with base_url fixed, OpenAIClient would still fail against Ollama Cloud once the request reached the right host, just with a different error.

Fix (fork commit on mcp-fixed, mcp_server/src/services/factories.py): passes base_url through, and picks OpenAIClient only when api_url is unset or actually points at api.openai.com (preserving the reasoning/verbosity parameter support that class has for the gpt-5/o1/o3 family) — OpenAIGenericClient otherwise.

4. API embedder → wired up to real model name. server/graph_service/config.py defines an embedding_model_name field and server/graph_service/zep_graphiti.py's get_graphiti() applies the equivalent model_name field to the LLM client — but never applies embedding_model_name to the embedder at all. It's left on graphiti_core's bare OpenAIEmbedder() default: hardcoded model "text-embedding-3-small", connection falling through to the openai-python SDK's own OPENAI_API_KEY/OPENAI_BASE_URL env var fallback (which is why it happened to share the LLM's Ollama Cloud connection by accident before this was fixed, rather than reaching LM Studio at all).

Fix (fork commit on api-fixed, built on tag v0.22.0 specifically — this file's structure differs meaningfully from main/v0.29.2, no _create_graphiti_client() helper or FalkorDB branch exists yet at this version), two files:

  • server/graph_service/config.py — adds embedder_api_url / embedder_api_key fields (separate from the LLM's openai_base_url/openai_api_key), and gives embedding_model_name a validation_alias='EMBEDDER_MODEL' so it reads the same env var graphiti-mcp already uses, instead of needing a second differently-named var for the same value.
  • server/graph_service/zep_graphiti.py — applies those three fields to client.embedder.config in get_graphiti(), mirroring exactly what upstream already does for client.llm_client.config three lines above it.

No new env vars needed beyond what was already in .env for graphiti-mcp — both services now read EMBEDDER_API_URL/EMBEDDER_API_KEY/EMBEDDER_MODEL the same way, via the same shared .env.

Switching provider or backend

config/config.yaml (mounted over the MCP server's default) keeps the full upstream provider matrix — switch llm.provider / embedder.provider and set the matching env vars in .env to move off Ollama Cloud / your local embedder. The REST API's provider is controlled entirely through .env (OPENAI_API_KEY/OPENAI_BASE_URL/MODEL_NAME, mapped from the OLLAMA_* vars in the compose files' environment: blocks) since it has no mounted config file.

MCP server tools

add_memory, add_triplet, search_nodes, search_memory_facts, summarize_saga, build_communities, get_episode_entities, delete_entity_edge, delete_episode, get_entity_edge, get_episodes, clear_graph, get_status

Warnings

  • Small/local LLMs frequently emit non-conforming JSON for entity/edge extraction — use the largest model your Ollama Cloud plan gives you access to.
  • The REST API's embedder model name is hardcoded upstream to text-embedding-3-small with no override — fixed at the source in our fork rather than worked around, so EMBEDDER_MODEL is a real model name for both services now. See "Our own fixed images" above.
  • FALKORDB_PASSWORD and NEO4J_PASSWORD ship with real, randomly-generated values in env.example.txt (auth is ON by default here, unlike upstream's own no-password default) — rotate them if this ever moves beyond a closed LAN, but don't blank FALKORDB_PASSWORD out without understanding the two-sided wiring: falkordb's REDIS_ARGS=--requirepass ${FALKORDB_PASSWORD} (in both compose files) is what makes the server enforce it, and graphiti-mcp reads the exact same var as the client password — same env var, both sides, by design, so they can't drift out of sync. A present-but-empty value (as opposed to the var being genuinely absent) is a real footgun either way: graphiti_mcp_server.py reads FALKORDB_PASSWORD via a raw os.environ.get(...) with no empty-string normalization (unlike config.yaml's ${FALKORDB_PASSWORD:} expansion, which does normalize), so an empty-but-present value makes the client send an empty-password AUTH — if the server isn't also configured to require one, that gets rejected outright and graphiti-mcp crash-loops with AUTH <password> called without any password configured for the default user.
  • GRAPHITI_TELEMETRY_ENABLED=false is set by default in env.example.txt (opt-out PostHog telemetry on the MCP server, no data collection beyond system info). The REST API image doesn't expose an equivalent toggle.
  • graphiti-mcp's search-result reranker is unconditionally OpenAI's upstream, with no config override point at all — fixed at the source in our fork to use graphiti_core's local BGERerankerClient. See "Our own fixed images" above. First startup downloads the ~2GB reranker model from HuggingFace Hub — expect a delay before the server reports ready the first time.
  • Data does not migrate between FalkorDB and Neo4j — each is its own independent graph here, not a replica of the other.