-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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.
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 shapewrangler kv bulk putexpects (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 themeta:jsonKV 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 | 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).
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. Nosectionreturns 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 thatserverandnoteare 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.
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.