DEV Community

Cover image for Building a Context API for AI Agents: Goodbye GitHub Raw URLs
Mario Semper
Mario Semper

Posted on

Building a Context API for AI Agents: Goodbye GitHub Raw URLs

How I solved Claude's "amnesia problem" with a simple microservice


Every conversation with an AI assistant starts the same way: from scratch.

If you're using Claude, ChatGPT, or any LLM for serious work – coding, product development, writing – you know the pain. You have project briefs, architecture docs, PRDs, and user stories. And every single chat, you're either:

  1. Copy-pasting walls of text
  2. Re-explaining context you've explained 100 times
  3. Watching the AI confidently hallucinate details it should know

I got tired of this. So I built a Context API that gives Claude persistent, structured access to my project documentation. Here's how – and why you might want something similar.


The Problem: GitHub Raw URLs Suck for AI Context

My first attempt was simple: store all project docs in a GitHub repo and have Claude fetch them via raw URLs.

https://raw.githubusercontent.com/myorg/context/master/projects/chainsights/prd.md
Enter fullscreen mode Exit fullscreen mode

Elegant, right? Version controlled, easy to update, free hosting.

Except it doesn't work.

GitHub's CDN caches raw files aggressively. Even with ?nocache=1 appended, I'd often get stale content. Claude would reference outdated requirements. Bugs would slip through because the AI was working with last week's architecture doc.

Other problems:

  • No structured queries – you get raw markdown, nothing more
  • No delta updates – fetch everything or nothing
  • No write-back – Claude can't persist insights or tasks
  • Two-repo sync hell – docs live in project repos, but Claude needs them centralized

I needed something better.


The Solution: A Dedicated Context Service

I built a simple API service that:

  1. Receives docs from GitHub Actions – when I push to _bmad-output/ in any project, a webhook syncs to the API
  2. Serves structured JSON to Claude – not raw markdown, but typed documents with metadata
  3. Supports delta queries – "give me only what changed since yesterday"
  4. Keeps version history – every update is preserved
  5. Detects orphans – renamed or deleted docs don't become zombies

The architecture looks like this:

┌─────────────────┐     GitHub Action      ┌─────────────────┐
│  Project Repo   │ ───────────────────▶  │  Context API    │
│  (chainsights)  │   POST /sync           │  (api.masem.at) │
└─────────────────┘                        └────────┬────────┘
                                                    │
┌─────────────────┐                                 │
│  Project Repo   │ ───────────────────────────────▶│
│  (tellingcube)  │                                 │
└─────────────────┘                                 │
                                                    ▼
                                           ┌─────────────────┐
                                           │    Claude       │
                                           │  GET /context   │
                                           └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

The Data Model

Three tables. That's it.

Projects

CREATE TABLE ctx_projects (
  id UUID PRIMARY KEY,
  slug VARCHAR(50) UNIQUE NOT NULL,  -- "chainsights"
  name VARCHAR(100) NOT NULL,        -- "ChainSights"  
  github_repo VARCHAR(200),          -- "myorg/chainsights"
  is_active BOOLEAN DEFAULT true
);
Enter fullscreen mode Exit fullscreen mode

Documents

CREATE TABLE ctx_documents (
  id UUID PRIMARY KEY,
  project_id UUID REFERENCES ctx_projects(id),
  doc_type VARCHAR(50) NOT NULL,     -- "brief", "prd", "epic", "story"
  doc_key VARCHAR(100) NOT NULL,     -- "prd", "epic-auth-flow"
  title VARCHAR(200),
  content TEXT NOT NULL,
  file_sha VARCHAR(40),              -- Git SHA for change detection
  version INTEGER DEFAULT 1,
  updated_at TIMESTAMPTZ,

  UNIQUE(project_id, doc_type, doc_key)
);
Enter fullscreen mode Exit fullscreen mode

Document History

CREATE TABLE ctx_document_history (
  id UUID PRIMARY KEY,
  document_id UUID REFERENCES ctx_documents(id),
  content TEXT NOT NULL,
  file_sha VARCHAR(40),
  version INTEGER NOT NULL,
  created_at TIMESTAMPTZ
);
Enter fullscreen mode Exit fullscreen mode

The file_sha is key – when the GitHub Action syncs, we compare SHAs. Same SHA? Skip the update. Different SHA? Archive the old version, update the current.

Idempotent syncs. No duplicate writes. Clean history.


The API

Reading (for Claude)

# Get all docs for a project
GET /v1/context/chainsights

# Get only briefs and PRDs
GET /v1/context/chainsights?types=brief,prd

# Get changes since a timestamp
GET /v1/context/chainsights?since=2024-02-20T00:00:00Z

# Get a specific document
GET /v1/context/chainsights/epic/auth-flow
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "project": {
    "slug": "chainsights",
    "name": "ChainSights"
  },
  "documents": [
    {
      "doc_type": "prd",
      "doc_key": "prd",
      "title": "Product Requirements Document",
      "content": "# ChainSights PRD\n\n## Overview...",
      "version": 7,
      "updated_at": "2024-02-21T09:15:00Z"
    }
  ],
  "updated_at": "2024-02-21T09:15:00Z"
}
Enter fullscreen mode Exit fullscreen mode

Writing (from GitHub Actions)

POST /v1/context/chainsights/sync
Enter fullscreen mode Exit fullscreen mode
{
  "documents": [
    {
      "doc_type": "prd",
      "doc_key": "prd",
      "title": "Product Requirements Document",
      "content": "...",
      "file_path": "_bmad-output/prd.md",
      "file_sha": "a1b2c3d4..."
    }
  ],
  "known_paths": [
    "_bmad-output/prd.md",
    "_bmad-output/epics/auth-flow.md"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Response:

{
  "synced": 2,
  "created": 0,
  "updated": 1,
  "unchanged": 1,
  "orphaned": [
    {
      "doc_type": "epic",
      "doc_key": "old-feature",
      "file_path": "_bmad-output/epics/old-feature.md"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

The known_paths array is clever – it tells the API "these are all the docs that currently exist." Anything in the database but not in this list? That's an orphan. Maybe it was renamed, maybe deleted. The API flags it so I can clean up.


The GitHub Action

name: Sync BMAD Output to Context API

on:
  push:
    paths:
      - '_bmad-output/**'

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Sync to Context Service
        env:
          MMS_API_KEY: ${{ secrets.MMS_CONTEXT_API_KEY }}
        run: |
          # Collect all docs, build JSON payload
          # POST to api.masem.at/v1/context/chainsights/sync
Enter fullscreen mode Exit fullscreen mode

Configuration per project (.github/mms-context.yml):

project: chainsights
documents:
  - path: _bmad-output/brief.md
    type: brief
    key: brief
  - path: _bmad-output/prd.md
    type: prd
    key: prd
  - path: _bmad-output/epics/*.md
    type: epic
    # key derived from filename
Enter fullscreen mode Exit fullscreen mode

Every push to _bmad-output/ triggers a sync. Docs flow from my repo to the API automatically.


Integrating with Claude

Here's the magic part. In Claude's system prompt (or custom instructions), I add:

You have access to the MMS Context Service for project documentation.

API-Key: mms_context_claude_xxxxx
Base-URL: https://api.masem.at

Endpoints:
- GET /v1/context/{project}?types=brief,prd&format=summary
- GET /v1/context/{project}/{doc_type}/{doc_key}

When the user mentions "Context: {project}", fetch the relevant documents.
Load summaries first, then fetch details as needed.
Enter fullscreen mode Exit fullscreen mode

Now when I start a chat with "Context: chainsights", Claude:

  1. Fetches the project summary (titles + first 500 chars)
  2. Understands what docs are available
  3. Loads specific docs when needed for the task

No more copy-pasting. No more "as I mentioned in our last conversation." Claude just knows.


Caching Done Right

Since we're solving a caching problem, we better cache properly:

Server-side (Redis):

context:chainsights:full           → 5 min TTL
context:chainsights:prd            → 5 min TTL
context:chainsights:epic:auth-flow → 5 min TTL
Enter fullscreen mode Exit fullscreen mode

When a sync happens, we invalidate only the affected keys. Granular, not nuclear.

Client-side (ETag):

Response: ETag: "sha256-of-content"
Request:  If-None-Match: "sha256-of-content"
Response: 304 Not Modified
Enter fullscreen mode Exit fullscreen mode

Claude's requests include the ETag from the last fetch. If nothing changed, we return 304 – fast and bandwidth-friendly.

Delta updates:

GET /v1/context/chainsights?since=2024-02-20T00:00:00Z
Enter fullscreen mode Exit fullscreen mode

Claude can ask "what changed since my last sync?" and only get the deltas. Perfect for long-running conversations.


Results

After two weeks of using this:

  • Zero stale context issues – Claude always has the latest docs
  • Faster conversations – no preamble, no re-explaining
  • Better AI output – consistent terminology, accurate references
  • Cleaner repos – no more masemit-context sync repo

The service handles 7 projects, ~50 documents, and costs me exactly $0/month extra (it runs on existing infrastructure).


Should You Build This?

Yes, if:

  • You use AI assistants for ongoing project work (not just one-off questions)
  • You have structured documentation (PRDs, specs, architecture docs)
  • You're frustrated by context limits and repetition
  • You already have some API infrastructure

Maybe not, if:

  • You're doing ad-hoc AI tasks with no recurring context
  • Your docs change rarely (just upload to Claude Projects)
  • You don't want to maintain another service

What's Next

I'm considering open-sourcing this as a standalone service. If there's interest, I'll clean up the code and publish it.

Features I'm still thinking about:

  • Full-text search across all documents
  • Task write-back – Claude can create tasks that sync back to GitHub Issues
  • Webhooks – notify Slack when syncs fail
  • Multi-tenant – let other teams use it

TL;DR

  1. GitHub raw URLs have caching problems that break AI context
  2. A simple Context API solves this: sync via GitHub Actions, serve via REST
  3. SHA-based idempotency prevents duplicate writes
  4. Delta queries and ETags keep things fast
  5. Claude gets reliable, structured, up-to-date project context

If you're building something similar or want to discuss the approach, drop a comment or find me on LinkedIn, Farcaster or X.


Mario Semper is the founder of masemIT, building developer tools and Web3 analytics. He's currently shipping ChainSights (DAO governance analytics), tellingCube (synthetic business data), PayWatcher (stablecoin payment verification), and occasionally yelling at GitHub's CDN.

Top comments (0)