Back to notes

saastro-relay: persistent memory for working with AI on real projects

Every AI session starts from zero. saastro-relay is the MCP server I built to fix that: it syncs context and code snapshots between Claude.ai and Claude Code with a dual local + cloud architecture.

AI MCP tooling architecture

The problem nobody solves well

I work with AI every day. Not as an experiment — as a real part of how I design, build and maintain projects. Claude Code in the terminal, Claude.ai in the browser, both in parallel depending on the task.

And there’s a problem that keeps coming up: every session starts from zero.

You can have a project with months of technical decisions, well-thought-out architecture, established conventions. You open a new session and you have to re-explain everything. Or you copy and paste context manually. Or you work with an AI that knows nothing about what you’ve built.

That doesn’t scale.

saastro-relay is the tool I built to fix it.


What is saastro-relay

An MCP (Model Context Protocol) server that acts as a shared context bridge between Claude.ai and Claude Code.

The core idea is simple: instead of relying on each session remembering something, the context lives in structured .md files inside the repository itself. The server exposes that context as MCP tools. Any instance of Claude — whether in the browser or the terminal — can read and write that context.

Estructura de conocimiento por proyecto

/knowledge

├─

architecture.md

Contexto narrativo — el porqué, decisiones, arquitectura. Lo escribe Claude.

├─

architecture.snap.md

Snapshot automático — tipos, interfaces, firmas. Lo genera el extractor.

├─

database.md + database.snap.md

Una sección por dominio del proyecto.

└─

INDEX.md

Estado general del proyecto. Claude Code puede leerlo sin llamar al MCP.

Narrativa

Cambia despacio. Editada por Claude o manualmente. Versionable con git.

Snapshot

Cambia con cada commit. Generado automáticamente. Nunca se edita a mano.

Each project has a /knowledge directory with sections. Each section has two files:

  • {section}.md — narrative context: the why, the decisions, the architecture
  • {section}.snap.md — automatic snapshot: types, interfaces and signatures extracted from real code

The separation is deliberate. The narrative is written by Claude (or me). The snapshot is generated by a tool automatically every time the code changes. They never mix because they change at different rates and for different reasons.


Dual architecture: local and cloud

This is where it gets interesting.

Claude Code runs locally — it has direct access to the filesystem and the SQLite database. Claude.ai is a cloud service — it needs to connect via HTTP to a server accessible from the internet.

Solution: two execution modes with the same 12-tool MCP API.

Arquitectura dual: local + cloud

Modo local — Bun

Runtime: Bun + TypeScript

Storage: bun:sqlite + filesystem

Watcher: Chokidar → snapshots

Git hook: post-commit.sh

Auth: sin auth en localhost

Expuesto via: Cloudflare Tunnel

Puerto: localhost:3456

Túnel: mcp.jcenlo.com

Modo cloud — CF Workers

Runtime: Workers V8 (serverless)

Storage: D1 + R2

Sin watcher (stateless)

Recibe snapshots via POST

Auth: OAuth 2.1 en D1

Expuesto via: Cloudflare edge

12 MCP tools — misma API

Por qué dos entry points y no uno

Workers V8 no soporta bun:sqlite, fs ni procesos persistentes. El SDK de MCP necesita estado de sesión que no sobrevive entre invocaciones de un Worker. Duplicar los handlers con backends distintos es más honesto que una abstracción forzada en v1.

Local mode (Bun)

  • Runtime: Bun with TypeScript directly, no compilation
  • Storage: bun:sqlite for metadata, filesystem for the .md files
  • Watcher: Chokidar watches source files and regenerates snapshots automatically
  • Git hook: post-commit.sh triggers a snapshot after each commit
  • Auth: no auth on localhost (ADR-006), BRIDGE_SECRET as consent gate for the tunnel
  • Tunnel: Cloudflare Tunnel exposes mcp.jcenlo.comlocalhost:3456

Cloud mode (Cloudflare Workers)

  • Runtime: Workers V8 — serverless, no persistent processes
  • Storage: D1 (SQLite compatible) for metadata, R2 for the .md files
  • Auth: full OAuth 2.1 persisted in D1, because Claude.ai requires it
  • No watcher or extractor — stateless by definition

The reason for not abstracting both into one is pragmatic: Workers V8 doesn’t support bun:sqlite, fs or persistent processes. The MCP SDK needs session state between requests that doesn’t survive between Worker invocations. Duplicating the handlers with different backends is more honest than a forced abstraction for v1.


The data flow

Flujo de datos

1

Watcher local

Detecta cambios en archivos fuente (Chokidar)

Corre en segundo plano con el entorno de dev

2

Extractor

ctags → ast-grep → regex. Extrae tipos, interfaces, firmas exportadas

Solo la superficie pública — nunca internals

3

Filesystem

Escribe {section}.snap.md en /knowledge del repo

Versionable con git, legible sin el servidor

4

Worker (opcional)

POST /snapshot sincroniza el snapshot al cloud

Permite que Claude.ai acceda al código actualizado

5

Claude Code

Conecta a localhost:3456, llama a session_init

Lee contexto narrativo + snapshot fresco

6

Claude.ai

Conecta a mcp.jcenlo.com via OAuth, llama a session_init

Mismas herramientas, mismo contexto desde el cloud

When I work on a project:

  1. The local watcher detects changes in source files
  2. Extracts types, interfaces and signatures with a fallback chain: ctags → ast-grep → regex
  3. Writes the .snap.md to the local filesystem
  4. Optionally pushes the snapshot to the Worker via POST /snapshot
  5. Claude Code connects to the local server on localhost:3456 and reads fresh context
  6. Claude.ai connects via mcp.jcenlo.com and accesses the same context from the Worker

The result: I open a new session in Claude.ai, call session_init with the project and topic, and have all context available in seconds. No copy-pasting. No re-explaining.


The design decisions that matter most

Building this involved making some non-obvious decisions.

The .md files live in the repo, not in the database

SQLite stores only metadata: status, timestamps, sync log. The actual content lives in the filesystem. This means context is versionable with git, readable directly without the server, and fully portable. No vendor lock-in.

Two separate knowledge layers

Narrative and snapshot in different files because they change for different reasons. If you mix them, the automatic extractor overwrites Claude’s work. If you separate them, each can evolve at its own pace.

Optimistic locking on save_context

Claude.ai and Claude Code can write to the same section. Last-write-wins would silently destroy valid work. save_context accepts a last_read_at and rejects if there were changes in between — it returns both versions and proposes a merge.

Namespace per project everywhere

A single server can manage multiple projects. Without namespacing, database in one project and database in another would be indistinguishable. The .saastro-relay file at the repo root acts as a namespace token.


What I learned building it

Working with AI effectively isn’t just about choosing the right model or writing good prompts. It’s about designing the information system around the model.

The difference between a session where AI delivers real value and one where you’re explaining the same thing for the fifth time is, almost always, context. saastro-relay is a bet on externalising that context into a persistent, structured layer.

I also learned something about building for yourself: the best tools I’ve built come from a friction I feel in my own workflow. Not from market analysis. This is one of them.

The code is in active development. The server runs in production at mcp.jcenlo.com.