Claude Corner

Claude Keeps Guessing Why Your Code Looks That Way? Give It a Repo Graph That Rebuilds on Every Merge.

By · 27 Oct 2026

Claude Corner illustration for “Claude Keeps Guessing Why Your Code Looks That Way? Give It a Repo Graph That Rebuilds on Every Merge.”

At a glance

  • A short Python builder turns repo files, merged PRs and open issues into a graph Claude Code reads through the official MCP memory server.
  • One claude mcp add line connects it, and Claude queries it with search_nodes and open_nodes.
  • A small GitHub Actions job rebuilds it on every push to main and weekly, then uploads an artifact instead of pushing.

You ask Claude why a config file looks the way it does. It reads the file, guesses and moves on. The real answer sits in a PR from last spring and the issue it closed, and Claude has no map of how code, docs, PRs and issues connect. A knowledge graph is that map.

Why it matters

The official MCP memory server stores a graph as entities, relations and observations in one JSONL file, and its tools let Claude search it and open nodes with their relations. Our read: a graph answers which merged PRs changed this file and which issues they closed in one lookup instead of a chain of searches. The hard part is keeping it true, so rebuild it on a schedule and give it an owner.

How to build a repo graph and keep it fresh

We ran the builder and memory server on a public repo on Oct 27. The claude mcp add line and workflow were checked against the docs, not run. The path is absolute because the server resolves a relative MEMORY_FILE_PATH against its own package folder.

# We have not run this script end to end on every platform. Run it from your repo root.
# Needs git, gh, python3, Claude Code. Optional install, pinned 5:03 PM ET Oct 27, Node 24 or later:
# npm install -g @modelcontextprotocol/server-memory@2.0.0
set -euo pipefail
mkdir -p scripts .github/workflows
cat > scripts/build_graph.py <<'EOF'
import json, os, subprocess as sp
def sh(*a): return sp.run(a, check=True, capture_output=True, text=True).stdout
def gh(*a): return json.loads(sh("gh", *a))
nodes, rels = {}, []
def node(n, kind, *obs): nodes.setdefault(n, {"type": "entity", "name": n, "entityType": kind, "observations": []})["observations"].extend(obs)
for f in sh("git", "ls-files").splitlines():
    if f.endswith((".md", ".py", ".ts", ".go", ".yml")): node(f, "doc" if f.endswith(".md") else "code")
for pr in gh("pr", "list", "--state", "merged", "--limit", "50", "--json", "number,title,mergedAt,files,closingIssuesReferences"):
    p = f"PR #{pr['number']}"; node(p, "pull_request", pr["title"], "merged " + pr["mergedAt"][:10])
    rels += [{"type": "relation", "from": p, "to": f["path"], "relationType": "changes"} for f in pr["files"] if f["path"] in nodes]
    for i in pr["closingIssuesReferences"]:
        n = f"Issue #{i['number']}"; node(n, "issue", "closed by " + p); rels.append({"type": "relation", "from": p, "to": n, "relationType": "closes"})
for i in gh("issue", "list", "--state", "open", "--limit", "100", "--json", "number,title"): node(f"Issue #{i['number']}", "issue", i["title"], "open")
os.makedirs(".graph", exist_ok=True)
open(".graph/repo-graph.jsonl", "w").write("".join(json.dumps(r) + "\n" for r in [*nodes.values(), *rels]))
print(len(nodes), "nodes,", len(rels), "relations")
EOF
cat > .github/workflows/repo-graph.yml <<'EOF'
name: repo-graph
on:
  push: {branches: [main]}
  schedule: [{cron: "0 13 * * 1"}]  # Monday 13:00 UTC, 9 AM EDT. Use 14 after Nov 1.
permissions: {contents: read, pull-requests: read, issues: read}
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - run: python3 scripts/build_graph.py
        env: {GH_TOKEN: "${{ github.token }}"}
      - uses: actions/upload-artifact@v7
        with: {name: repo-graph, path: .graph/repo-graph.jsonl}
EOF
python3 scripts/build_graph.py
claude mcp add repo-graph --env MEMORY_FILE_PATH="$PWD/.graph/repo-graph.jsonl" -- npx -y @modelcontextprotocol/server-memory@2.0.0
# echo "/scripts/build_graph.py @your-org/graph-owners" >> .github/CODEOWNERS

The workflow runs on every push to main plus a weekly Monday cron. Actions cron runs in UTC, so 9 AM ET is 13:00 UTC during daylight time and 14:00 UTC after Nov 1. The job never pushes. It uploads an artifact you pull with gh run download -n repo-graph -D .graph. To commit the graph, have the job open a small PR instead, which needs contents: write, pull-requests: write and the repo setting Allow GitHub Actions to create and approve pull requests.

The commented last line adds a CODEOWNERS owner for the builder. Swap in your own team, then uncomment it so changes to the builder request their review. Each rebuild starts empty, so deleted files and old closed issues drop out. Our recommendation: keep a 50 PR window, and once a month spot check ten nodes and add an entity type only when the graph missed a real question.

Gotchas

  • Claude's own notes do not survive. The next rebuild replaces the file, so put lasting facts in docs or PR descriptions where the builder finds them.
  • Local scope by default. Add --scope project to share the server through .mcp.json. Teammates approve it on first run.

Sources:

Recommended AI prompt

Copy this paragraph into ChatGPT, Claude, Gemini, Grok, or whatever AI you use.

Prompt

I want Claude Code to see how my repo's code, docs, pull requests and issues connect through the official MCP memory server. Ask me which folders matter, how many merged PRs of history I want, and who should own the graph. Then adapt a short builder that writes the memory server's JSONL format, give me the exact claude mcp add command with an absolute file path, and a GitHub Actions workflow that rebuilds weekly and on push to main and uploads an artifact or opens a pull request, never pushing to main. Use only commands from the Claude Code, MCP servers and GitHub docs, and link each one.