Skip to content

Architecture

Kritarth-Dandapat edited this page Jul 10, 2026 · 1 revision

Architecture

context-kernel is a straight pipeline: hand-edited Markdown goes in, an authenticated MCP tool call comes out. There is no runtime database, no queue, and no server process other than the Cloudflare Worker itself.

The pipeline

  content/  (gitignored, SACRED, hand-edited)      content.example/  (committed templates)
  profile.md  goals.md  writing-prefs.md
  figure-prefs.md  answer-prefs.md
  resume.md  current-work.md  skills.md  ...
          |
          v
  scripts/generate-artifacts.ts   (compile content/*.md -> KV bulk payload + meta)
          |
          v
  artifacts/kv-bulk.json  ---->  Cloudflare KV
                                      |
                                      v
                               src/worker.ts  (MCP server over HTTP, token-gated)
                                      |
                         +------------+-------------+
                         | read tools               | write tools
                         | get_context / list /     | append_journal / list_journal
                         | get_meta (READ_TOKEN)     | (WRITE_TOKEN)
                         v                           v
                    curated context in KV      journal:* entries in KV
                                                     |
                                                     v
                                    scripts/promote.ts  (owner pulls journal,
                                    curates by hand into content/, commits, rebuilds)

Two things never touch each other except through a human: the journal (agent-writable) and the curated context (agent-readable, human-writable). See Security-Model for why that boundary is enforced at the code level, not just by convention.

Stages, in order

1. content/*.md — the source of truth. Each file is one curated "section" (profile.md, goals.md, current-work.md, and so on). This directory is gitignored and never committed; the repo only ships content.example/, a set of obviously-fake placeholder sections so the build and tests work out of the box on a fresh clone. See Design-Decisions (D5) for why engine and content are split this way.

2. scripts/generate-artifacts.ts — the build step (npm run build). It reads every *.md file in content/ (falling back to content.example/ if content/ doesn't exist yet), concatenates them into one full-context document, and produces:

  • artifacts/kv-bulk.json — an array of {key, value} pairs in the shape wrangler kv bulk put expects (also the shape of Cloudflare's KV bulk-write REST API).
  • artifacts/meta.json{ version, generated_at, source_rev, content_hash }, the same object stored under the meta:json KV key.

source_rev is the current git commit hash of the repo doing the build (best-effort; "unknown" outside a git repo). content_hash is a SHA-256 of the full concatenated markdown, so a client can tell whether the served context has changed since it last read it.

3. Cloudflare KV — the artifacts are uploaded with wrangler kv bulk put. KV is the only persistent store this project uses; there is no separate database.

4. src/worker.ts — the Cloudflare Worker that is the actual MCP server. It exposes one route, /mcp, and speaks the MCP streamable-HTTP transport via the Cloudflare Agents SDK's createMcpHandler (a stateless handler — no Durable Object, since every tool call is a pure KV lookup keyed only by its own arguments). Every request is checked for a valid bearer token before any MCP dispatch or KV read happens; see Security-Model for the two-layer auth check.

5. MCP tools (src/mcp/tools.ts) — the tools a connected Claude session actually calls. Read tools return curated context; the one write tool appends to the journal. See below for the full list.

6. scripts/promote.ts — the manual promotion step. The owner runs it locally, it lists journal entries pulled from KV, and the owner decides by hand what (if anything) becomes a permanent edit to content/*.md. This script does not, and must not, write to content/ automatically — that would defeat the entire point of the curation gate (see Design-Decisions, D2).

KV key scheme

KV key Written by Read by Content
context:full:md build step get_context (no section arg) Full curated context, all sections concatenated in filename-sorted order
section:<name>:md build step get_context(section), list_sections One curated section's markdown
meta:json build step get_meta { version, generated_at, source_rev, content_hash }
journal:<ulid> append_journal list_journal, promote.ts One journal entry: { server, timestamp, tags, note }
journal:index append_journal list_journal, promote.ts Ordered JSON array of journal ULIDs, append-only, for fast listing without a full KV scan

Curated keys (context:full:md, section:*:md, meta:json) are only ever written by the build step running locally and pushed with wrangler kv bulk put. No MCP tool writes to them — that is the concrete enforcement of "no curated-write tool" (see Security-Model).

The MCP tools

Tool names are namespaced context_kernel_* on the wire (so a client showing raw tool names doesn't collide with another server's get_context), but the underlying ToolName used for auth scoping is the bare name.

Read tools (READ_TOKEN):

  • get_context(section?) -> markdown. No section returns the full curated context; an unknown section name is a distinguishable error, not an empty string.
  • list_sections() -> {name, hash}[]. The hash is computed at read time (SHA-256 of the section text), not stored separately, so a client can detect staleness against a previously-seen hash.
  • get_meta() -> {version, generated_at, source_rev, content_hash}.

Write tools (WRITE_TOKEN):

  • append_journal(server, note, tags?) -> {id}. Validates that server and note are non-empty strings before touching KV at all — a validation failure never reaches the KV write, same posture as an auth failure.
  • list_journal(since?, limit?) -> entries. Gated behind the write token, not the read token, because it exposes raw, unpromoted notes — see Security-Model.

Implementation note: stateless server per request

The MCP SDK (>= 1.26) requires a fresh McpServer instance per request when using a stateless handler, to avoid leaking one caller's response into another caller's connection. createServer() in src/mcp/tools.ts is called fresh inside fetch for this reason, closed over that request's token, rather than being built once at module scope.

Clone this wiki locally