SQLite-backed MCP server for shared state across Claude.ai, Claude Code, Codex, and related local ops tools.
bridge-db replaces ad hoc edits to a shared markdown file with a structured SQLite store and a focused MCP tool surface: cross-system state, FTS5 lexical recall, shipped-event sync receipts, shipped-event dispositions, and observability over the audit and recall logs. The markdown bridge file is regenerated from the DB via export_bridge_markdown and remains available as a fallback for file-based clients.
- Python 3.12+
- uv — fast Python package manager
git clone https://github.com/saagpatel/bridge-db.git
cd bridge-db
uv sync # install all deps into .venv
uv run pytest # verify the install- Steady maintenance. Scope is cross-system state coordination, lexical
recall, and observability — not a general knowledge store. - Schema v10: context sections carry monotonic
versiontokens; stale writes and raced handoff claims produce durablewrite_conflictsreceipts. - Schema v12: adds the
session_classificationsidecar for heuristic cost-routing attribution while keepingsession_costsas pure actuals. Schema v11 backfills activitytagsintocontent_indexso lifecycle tags (SHIPPED, DECISION, ...) are recall-able on existing DBs. - Schema v13: adds
claimed_bytopending_handoffs(the INV-13 claimant gate forclear_handoff). Riding the same migration train, activity retention now exempts rows taggedSHIPPEDorLEDGER(case-insensitive) from the 50-per-source prune — BD-INV-1: retention never deletes a protected row, its receipt, or its disposition. - Schema v14: collapses the shipped-sync trio (
shipped_sync_receipts+shipped_event_dispositions) intoactivity_logsync_*disposition columns and drops the two child tables. A shipped event's terminal sync state (asynceddownstream receipt or a policy disposition) now lives on the activity row itself, written by the singlerecord_dispositionverb. Because the state is the row, BD-INV-1's guarantee is structural — no FK-CASCADE can orphan a receipt. - FTS5
content_indexmirrors all content tables;healthandstatusverify source-row / FTS-row alignment. statusincludes a native freshness block for owner-specific snapshot, activity, handoff, and shipped-event attention. Freshness attention is advisory: top-levelok/overallremain tied to DB, schema, fallback-file, and FTS health.- 26 MCP tools across 10 modules (activity, handoffs, context, snapshots, cost, export, health, recall, audit, conflicts).
Claude.ai ──────────────────────────────────────────────┐
(direct MCP via Claude Desktop) │
(fallback: markdown file via Filesystem MCP) │
▼
CC skills ──► MCP stdio ──► bridge-db process ──► SQLite (WAL)
Codex ──► MCP stdio ──► bridge-db process ──► ~/.local/share/bridge-db/bridge.db
│
export_bridge_markdown
│
▼
~/.claude/projects/<encoded-home>/
memory/claude_ai_context.md
No shared daemon. Each MCP client spawns its own bridge-db process via stdio. WAL mode + PRAGMA busy_timeout=15000 handles concurrent writer waiting; logical stale-write protection comes from CAS on mutable context sections.
Verify the current tool count from source with
rg '@mcp\.tool' src/bridge_db -c. As of the 2026-07-12 v14 collapse, the
surface is 24 tools across these 10 modules:
| Module | Tools |
|---|---|
| activity | log_activity, get_recent_activity, get_activity_signal, get_shipped_events, record_disposition |
| handoffs | create_handoff, get_pending_handoffs, pick_up_handoff, clear_handoff |
| context | update_section, get_section, get_all_sections, sync_from_file |
| snapshots | save_snapshot, get_latest_snapshot |
| cost | record_cost, get_cost_history |
| export | export_bridge_markdown |
| health | health, status |
| recall | recall, recall_stats |
| audit | audit_tail |
| conflicts | get_write_conflicts |
Write tools enforce caller ownership, so systems can only write the slices of state they own. Recent hardening also added notion_os and personal_ops as first-class activity and cost writers.
Instruction-bearing rows carry a source_trust label — operator, agent, or ingested — recording who authored the content. It lives only in the DB (schema v7+, on pending_handoffs, activity_log, context_sections, system_snapshots) and is never serialized into the markdown export, which would otherwise launder provenance.
- Writers set it via an optional
source_trustparam; the conservative default isagent(a Claude-dispatched write is agent-authored unless the operator asserts otherwise).update_sectionpreserves an existing section's label on a content-only re-sync. - The gate lives at the one dangerous transition —
pick_up_handoffmoving a handoffpending → active:operator-trust → picks up in one call (ccandcodex).cc+ non-operator→ returnsrequires_confirmationand does not transition; re-invoke withconfirm=Trueto proceed.codex+ non-operator→ refused (Codex runs withdanger-full-access;confirmcannot bypass it). Promote the handoff tooperatortrust first.
- Visibility:
get_pending_handoffs,get_section,get_all_sections,get_recent_activity,get_activity_signal,get_shipped_events,get_latest_snapshot, andrecallhits carrysource_trustplusinstruction_boundarymetadata that tells consumers returned content is stored data, not instructions. Lifecycle aggregates use a trust summary andsource_trust="mixed"when rows differ.statusreportspending_handoffs_by_trustandhealtha full per-tablesource_trust_breakdown. Each gate decision (allowed/confirmation_required/refused) is written to the audit log.
Consumers authoring an operator-directed handoff (e.g. the
vibe-code-handoffskill) should passsource_trust="operator"oncreate_handoffso it picks up without confirmation.
Context sections are the mutable bridge surface, so they carry a monotonic
version token. Consumers should read with get_section, edit locally, then
call update_section(..., if_match_version=<version>). A stale token returns
ok=false, conflict=true, and a receipt_id instead of clobbering a newer
row. if_match_updated_at remains as a compatibility guard, but version is
the preferred token because timestamps have one-second resolution.
Existing-row blind writes are rejected unconditionally with a durable
missing_cas receipt — if_match_version (or if_match_updated_at) is
required for any write to a section that already exists. New section
inserts without CAS remain allowed, since there is nothing to CAS against.
export_bridge_markdown records the exported version/hash for each rendered
Claude.ai-owned context section. Later sync_from_file imports a changed
fallback-file section only if the DB still matches that exported base. If the DB
has advanced, the import is rejected and recorded in write_conflicts.
Exports to the real Claude.ai fallback path are also guarded against empty
fixture-like output: bridge-db refuses to overwrite that file when all four core
Claude.ai-owned sections would render as _Not yet populated._. Set
BRIDGE_DB_ALLOW_EMPTY_BRIDGE_EXPORT=1 only for an intentional empty bootstrap.
Use get_write_conflicts(status="open") to inspect stale section writes,
stale markdown imports, and raced handoff claims.
uv run pytest # run all tests
uv run pyright # type check (strict mode)
uv run ruff check # lint
uv run python -m bridge_db --doctor # local environment diagnostics
uv run python -m bridge_db --status # compact operator summary
uv run python -m bridge_db --dogfood # read-only observability dogfood pass
uv run python -m bridge_db --rebuild-content-index # repair FTS recall index drift
uv run python -m bridge_db --reconcile-canonical-keys # backfill GHRA repo_full_name keys
uv run python -m bridge_db --log-session-boundary bridge-db # FTS-safe CC hook logging
uv run python -m bridge_db # start MCP server (stdio)
uv run python -m bridge_db.migration # migrate from bridge markdownReplace /path/to/bridge-db below with the absolute path to your clone of this repo
(e.g. $(pwd) if you are already inside it, or ~/Projects/bridge-db as a common convention).
Claude Code (user-scoped):
claude mcp add --scope user bridge-db -- uv run --directory /path/to/bridge-db python -m bridge_dbCodex (~/.codex/config.toml):
[mcp_servers.bridge-db]
command = "uv"
args = ["run", "--directory", "/path/to/bridge-db", "python", "-m", "bridge_db"]- DB:
~/.local/share/bridge-db/bridge.db - Bridge file:
~/.claude/projects/<encoded-home>/memory/claude_ai_context.md(Claude Code encodes your home dir path by replacing/with-; the default is derived automatically at runtime — override viaBRIDGE_FILE_PATHif needed) - Retention: unprotected activity entries keep the newest 50 per source; rows
tagged
SHIPPEDorLEDGER(case-insensitive) are permanently retained (BD-INV-1); 10 snapshots per system family (Codex operating and consulted-node snapshots are retained independently) - Health check:
healthMCP tool oruv run python -m bridge_db --doctor - Operator summary:
uv run python -m bridge_db --status - Dogfood pass:
uv run python -m bridge_db --dogfoodbundles the status, FTS index, WAL, recall, and shipped-sync audit checks used after bridge-sync runs - FTS repair:
uv run python -m bridge_db --rebuild-content-indexrebuilds the localcontent_indexfrom source tables when health reports recall-index drift - Canonical-key reconcile:
uv run python -m bridge_db --reconcile-canonical-keysrewrites storedactivity_logandpending_handoffscanonical_keyvalues through GithubRepoAuditor's registry, storing GHRArepo_full_namefor repo-backed projects and leaving unresolvable rowsNULL. - Session boundary logging: Claude Code's SessionEnd hook should call
uv run --directory /path/to/bridge-db python -m bridge_db --log-session-boundary <project>rather than writing SQLite directly; this path adds the FTS row and does not run activity retention pruning - Migration:
uv run python -m bridge_db.migration(idempotent — safe to re-run)
The MCP status result enriches the compact summary with freshness:
thresholds_hours:snapshot_stale_after=48.0,activity_quiet_after=72.0,pending_handoff_stale_after=168.0, andactive_handoff_stale_after=72.0.snapshots: per-ownerccandcodexentries withstate,owner,latest_snapshot_date,latest_created_at,age_hours, andnext_action. Snapshot refresh actions stay owner-specific:cc_refresh_snapshotbelongs tocc;codex_refresh_snapshotbelongs tocodex.activity_sources:cc,codex,claude_ai,notion_os, andpersonal_opsentries withstate,latest, andage_hours.handoffs: pending/active counts, stale counts, oldest ages, and unknown-age counts for pending and active handoffs.shipped_events: raw unprocessed, actionable unprocessed, dispositioned/non-actionable unprocessed, processed-without-receipt count, and the shipped-eventnext_action.overall:fresh,attention,stale, orunknown.next_actions: up to five deterministic{action, owner, reason}entries.
Freshness states use a narrow vocabulary: fresh means recent enough for that
surface; quiet means an activity source has no recent rows and is not a
health failure; stale means a snapshot or handoff needs refresh/review;
missing means the expected owner row is absent; and unknown means a missing
or unparsable timestamp prevents age calculation.
The CLI status command prints freshness as compact hints:
Freshness: <overall>
Next actions: <action> (<owner>), ...
These lines do not change the command's success semantics. uv run python -m bridge_db --status still exits from bridge health, so freshness attention by
itself does not fail the command. bridge-db remains MCP-first and
SQLite-native; the markdown export is a fallback/mirror, and freshness adds no
service, table, migration, tool, or CLI flag.
activity_log retention is two-tier: unprotected rows remain recent context
capped at 50 per source, while rows tagged SHIPPED or LEDGER are
permanently retained (BD-INV-1). The durable ledger and each shipped event's
sync disposition (its downstream receipt or its policy decision) both live on
the activity row itself, in the sync_* columns added at schema v14 — a
protected row's receipt or disposition can no longer cascade-die with it,
because the state IS the row and the parent row never prunes. Treat
processed_shipped_without_receipt=0
and fts_missing=0 as primary clean signals, and use
actionable_unprocessed_shipped=0 with
dispositioned_unprocessed_shipped>0 when policy dispositions explain why raw
unprocessed_shipped remains nonzero.
get_recent_activity is the raw compatibility feed: it returns individual
activity rows exactly as stored, including high-volume lifecycle telemetry such
as Claude Code SessionEnd rows tagged session-boundary. Operator-facing
consumers should use get_activity_signal instead. It keeps substantive rows
visible while compressing repeated lifecycle rows into aggregates keyed by
source, project, summary family, and hour/day time bucket. Raw rows and audit
events remain available for debugging and forensic review.
Activity rows preserve two time concepts:
timestampis the caller-supplied logical activity date or timestamp. When omitted bylog_activity, it defaults to the operator-local calendar date.created_atis the UTC insertion timestamp assigned by SQLite.
For activity discovery APIs with since (get_recent_activity,
get_activity_signal, and get_shipped_events), a row is visible when either
timestamp >= since or created_at >= since (with date-only values interpreted
as UTC midnight for created_at). This keeps closeouts created just after UTC
midnight discoverable even when their logical activity date is the prior local
day.
For Notion reconciliation, treat each shipped event's notion_sync object as the
machine-readable gate:
ready: fetch the explicitnotion_page_id, update only that row, fetch it again, then callrecord_disposition(disposition='synced', ...)with the readback proof.meta_no_target: do not update Notion. Record the event withdisposition='synced',downstream_system=policy, anddownstream_refpointing to the configured policy file after verifying the policy applies.unmatched,no_notion_target, orregistry_unavailable: leave the event unprocessed and repair the project registry or mapping source first.
For non-receipt handling, use record_disposition with a policy disposition
(unsynced_by_policy / no_durable_target / superseded_without_receipt /
declined_mapping) and a reason when an operator policy says the row should
remain auditable but should not proceed to a downstream receipt. The disposition
appears as policy_disposition on get_shipped_events; it does not add
PROCESSED and does not claim sync. record_disposition is SHIPPED-only — the
former mark_shipped_processed path for non-shipped operational events
(TASK_DONE, APPROVAL_SENT, PLANNING_APPLIED, REVIEW_CLOSED) is retired.
Claude.ai may still write its owned sections directly to the bridge markdown file. To keep those edits from being overwritten on the next export, sync_from_file imports the four Claude.ai-owned sections (career, speaking, research, capabilities) from BRIDGE_FILE_PATH into context_sections before bridge consumers read from SQLite.
Claude Code's /start workflow now runs mcp__bridge_db__sync_from_file() before calling bridge read tools, so file edits are pulled into the DB at session start instead of waiting for a later export cycle.
The current operating model is:
- MCP is the primary coordination path.
sync_from_fileis the compatibility safety net for Claude.ai-owned file edits.export_bridge_markdownkeeps the fallback markdown artifact in sync for file-based consumers.
integration-spec.md— Claude.ai direct MCP and fallback-file integrationROADMAP.md— Execution roadmap for the next integration phasesdocs/EXTERNAL-WRITER-AUDIT.md— Direct bridge-db writer audit outside the repodocs/BRANCH-RETIREMENT.md— Branch cleanup and stale-ref disposition policy
docs/internal/OPERATOR-CHECKLIST.md— Local verification checklistdocs/internal/POST-SYNC-REVIEW.md— Bridge-sync post-run evidence checklistdocs/internal/PHASE-3-DECISION.md— Architectural decision: watcher vs startup syncdocs/internal/codex-migration.md— Per-consumer migration instructions