The campaign codex you and your agent keep together, with recall that works by meaning, computed entirely inside your browser tab.
Lorekeeper is our entry for the OpenAI WebMCP Challenge: a web app built to show what people and their agents can do together when the page itself offers the tools. Live at lorekeeper.extenddb.org.
Here is the loop that matters, with no key and no setup: an agent asks the codex a question through WebMCP, and the page answers in front of you.
Lorekeeper is a living world codex for tabletop campaigns and long-form fiction. Your agent chronicles the campaign as you play: NPCs, places, plot threads, promises the party made, lies an NPC told. Weeks later, you or the agent ask a question in plain language and the codex answers by meaning, not keyword: every entry is embedded in the page by a bundled MiniLM model, and every query runs through ranked vector search executed by a database engine running inside the tab. Recall never touches a server, and relevance shows as a plain percentage on every card.
It is a static web page whose five tools (record_lore, recall_lore,
list_lore, revise_lore, forget_lore) are driven three ways, all against
the same store:
- WebMCP, for agent-native browsers: the page registers the five tools so
a browser-based agent can read and write the very same entries you see on
screen. The read tools are annotated read-only,
forget_loreis marked destructive, and the write tools' descriptions carry untrusted-content guidance for the model. When the page detects this surface it gives the whole layout to the codex, because your browser already carries the agent. - The scribe panel in the page, for every other browser: bring your own OpenAI-compatible endpoint, model and API key, and the whole loop runs with no flag and no special browser. Type what happened at the table and the model records entries as it reads; ask a question and it recalls and answers from the codex. In an agent-native browser this panel waits behind the "use your own model" link in the status bar.
- You: a composer adds entries by hand, every card has inline editing, and the suggested queries beside the search box run semantic recall in one click, no key required.
The fastest way to see the point is to let an agent discover the page:
-
ChatGPT desktop app: open the built-in browser (Cmd+Shift+B) in Work or Codex mode with a GPT-5.6 model that supports site tools, and navigate to the app. The tools register on load; the site-tools arrow in the address bar lists all five. Then ask ChatGPT:
Chronicle this: the party spared the bandit chief, and she owes them a debt.
ChatGPT calls
record_loreagainst the page and the card lands in the codex in front of you. Ask it a question ("why is everyone falling sick?") and it answers from the codex throughrecall_lore, ranked by meaning. -
Chrome with WebMCP: launch with
--enable-features=WebMCP(the API is in origin trial from Chrome 149) and the same registration happens. -
Any other browser: the page works as a plain codex, and the scribe panel takes any OpenAI-compatible endpoint so an agent can sit in the page itself.
A first visit lands on a sample campaign, its entry dates spread over recent weeks, so there is something to recall immediately; a notice above the cards clears the sample when you want to start your own world, and a returning visitor with an empty codex gets a button to load it again.
The store behind all three is ExtendDB, an open-source DynamoDB-compatible database engine, compiled to WebAssembly and running inside the tab over SQLite, with a 384-dimension cosine vector index for semantic recall. Your codex never leaves the page: entries, embeddings and search all run in the tab, with no server and no network calls. The one thing that does use the network is the scribe panel, and only when you configure it; see below for exactly what it sends.
Your world is a file, too: an export control in the codex header downloads
the whole codex as a single .codex file (the engine's own bytes), and
import loads one back, replacing the current world after a confirmation.
Copy it, share it with your table, fork a campaign at a decision point and
play both branches.
One campaign travels on its own, too. The Export campaign control in the
header downloads the open campaign as portable JSON: a manifest naming the
campaign, the entry count, and the embedding model with its dimensions, plus
every entry as the plain DynamoDB Item document the page itself stored,
vectors included. Import campaign merges such a file into the open campaign
through ordinary PutItem calls, preserving ids, timestamps and tags. The
shipped vectors are reused as they are, so nothing is re-embedded and recall
ranks identically on the destination machine; an entry without a vector, or
a file from a different embedding model, is re-embedded from its text. A
file exported from a different campaign asks before it merges.
The same click also downloads promote.sh: a generated aws CLI script that
creates the codex table on managed Amazon DynamoDB (create-table) and
writes the exported entries into it with batch-write-item, carrying the
same Item JSON and refusing to report success when a batch comes back with
unprocessed items. The script drops the vec attribute: vectors are derived
data, and whichever application later reads the cloud table can re-derive
them from the entry text. Lorekeeper never runs the script or calls aws; it
is a text artifact for you to read and run, and it is the portability claim
below made demonstrable in one click. Since one click saves two files, the
browser may ask once for permission to download multiple files; allow it and
promote.sh arrives beside the JSON.
Campaign knowledge is keyed by meaning, and meaning is the one thing keyword
search does not do. Nobody remembers whether the note said "ferryman" or
"toll gate"; they remember that the party promised somebody something. Chat
logs scroll away, and localStorage is a sticky note, not a codex.
Lorekeeper gives the agent durable, structured, semantically searchable memory of the campaign with real database semantics: conditional writes, consistent reads, ranked vector search. Because the engine speaks the DynamoDB wire protocol, the same requests and the same data model deploy unchanged to a managed DynamoDB table when a campaign outgrows the tab.
web/tools.mjsis the whole tool layer: five tools mapped onto plain DynamoDB JSON requests against a table with a cosine vector index. It exports both the handlers and the tool definitions; WebMCP registration and the scribe panel's function-calling schemas are derived from the same definitions, so there is no second copy to drift. Each registered descriptor also carries annotations (read-only on the read tools, destructive onforget_lore) and a description that tells an agent when to reach for the tool, not only what it does.web/agent.mjsis the in-page scribe: an OpenAI-compatible chat completions loop, one plain JSON request per model round, that keeps calling tools and feeding results back, rendered as a conversation next to the codex.web/app.mjswires the human surface: entry cards with relevance percentages, the campaign picker in the header, the composer, inline editing (including a threads field that retags an entry throughrevise_lore), a search box that runs the exact samerecall_lorepath the agents use, the suggested-query chips, export and import, and a wire view showing every raw request and response. The codex re-renders once per completed scribe turn, so a burst of agent writes lands as one refresh; human actions refresh immediately.web/export.mjsbuilds the portable campaign export: the manifest plus plain-item JSON document, and thepromote.shpromotion script generated from the same items.web/seed.mjsholds the starter campaign and the suggested queries; a first visit seeds the campaign automatically, with entry dates spread over recent weeks, and the sample notice clears it again.web/webmcp-shim.mjsadapts to the WebMCP registration surface exposed by the host browser.web/vendor/holds the engine and model assets, all served same-origin. Seeweb/vendor/VENDOR.mdfor exactly what goes there.
Entries persist across reloads: the engine runs on an IndexedDB-backed store
with relaxed durability, plus a snapshot flush when the page is hidden, and
the same flush runs after an import so a reload keeps the imported world.
revise_lore uses a conditional write, so a human edit and an agent edit
cannot silently clobber each other; the stale writer gets a conflict,
re-reads, and retries. The inline card editor saves through the same path and
shows a conflict view when it loses.
Codices are per campaign. The picker in the header switches between them,
each campaign is its own table partition, and recall searches only the
active campaign. The campaign list and the open campaign survive a reload
in the engine's own app_meta table. A Web Lock guards the single-writer
engine: a second tab on the same origin stays out of the persistent store
and offers a Take over button instead of risking the codex.
The panel takes any OpenAI-compatible chat completions endpoint. To set it up, open the setup block in the panel and fill in three fields:
- Endpoint: defaults to
https://api.openai.com/v1/chat/completions; any compatible server works, including a local one. - Model: defaults to
gpt-4o-mini. - API key: sent only to the endpoint you entered.
Apply the setup and the block collapses; reopen it any time to change
endpoints. The key is held in memory for the session. If you tick "remember
key" it is kept in sessionStorage, which a closed tab forgets; it is never
written to localStorage, a cookie, or a URL.
What stays local versus what goes to your endpoint: when you use the scribe panel, your messages, the model's replies, and the tool calls and results inside that conversation go to the model endpoint you configured; the codex itself, its embeddings, and every search stay in the tab. If you never configure the panel, the page makes no network requests at all after loading. The badge in the status bar tracks the codex, and the wire view plus your browser's network panel let you verify both halves of that claim.
The vendored runtime (web/vendor/: the ExtendDB WebAssembly build, the
transformers.js runtime, and the MiniLM model files) is committed, so the
page runs from a checkout with no build step.
-
Serve the
web/directory over http on localhost:npm run serve # http://127.0.0.1:4173/ (tests/serve.mjs)or any static file server, for example:
python3 -m http.server 8080 --bind 127.0.0.1 --directory web
-
Open the page. The status bar reports the engine, the embedding model, and the codex badge once everything is loaded, and a first visit arrives with the sample campaign already seeded.
-
Optional, for the in-page scribe: configure the panel as described above. This works in any browser.
-
Optional, for WebMCP: open the page in a WebMCP-capable browser (the ChatGPT desktop app's built-in browser, or Chrome with
--enable-features=WebMCP) and the tools register automatically on load. The page then hands the full width to the codex; the scribe stays one click away in the status bar. Without WebMCP the page shows a short hint and everything else still works.
Playwright drives the real page (vendored engine, vendored model, WebMCP
tools) in Chromium launched with --enable-features=WebMCP:
npm install
npx playwright testThe test server binds port 4173 by default. Set LOREKEEPER_TEST_PORT to run
the suite on another port, for example when a preview server already holds
4173:
LOREKEEPER_TEST_PORT=4599 npx playwright testThe suite covers boot (engine + model + zero cross-origin requests before the
panel is configured), all five tools against the live store, the
revise_lore conflict shape, the composer and inline editing including the
conflict view, the sample campaign seeding, the suggested queries, the
relevance display, export and import (both the whole-world .codex file and
the portable campaign JSON with vector reuse, the re-embed fallback, and the
generated promote.sh), the campaign picker with per-campaign
recall isolation, the multi-tab guard, a WebMCP end-to-end round trip through
the browser's own tool-execution surface, a registration spec pinning the
annotations and tool descriptions, and a scribe-panel round trip against a
stubbed endpoint. No test calls a real model.
New to the project? docs/HANDOVER.md carries the architecture, how to run the agent locally, the ground rules for changes, and the known issues ranked by value.
The engine in web/vendor/extenddb/ is a prebuilt WebAssembly binary. It is
not a black box: docs/BUILDING-THE-ENGINE.md
documents the pinned source commit, the persistence patch carried in
patches/, the containerized build command, and the sha256 of the
shipped binary, so you can rebuild and verify it yourself.
Experimental. This app tracks an experimental WebAssembly build of ExtendDB; interfaces may change as that work stabilizes.
- ExtendDB: the DynamoDB-compatible engine powering the store. Apache-2.0.
- transformers.js: in-browser inference runtime for the embedding model. Apache-2.0.
- ONNX Runtime Web: the WASM backend transformers.js runs on. MIT.
- Xenova/all-MiniLM-L6-v2: the sentence embedding model (ONNX conversion of sentence-transformers/all-MiniLM-L6-v2), quantized q8, 384 dimensions. Apache-2.0.
This repository itself is licensed under Apache-2.0 (see LICENSE).

