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. The canonical label lives in the DB (schema v7+, on pending_handoffs, activity_log, context_sections, system_snapshots). Markdown exports include a visible stored-data warning and advisory per-block copies of the DB labels so file consumers do not lose the instruction boundary. Because the fallback is editable, those serialized labels are never proof of operator authorship and are ignored on import.
- Writers set it via an optional
source_trustparam; the conservative default isagent.create_handoffandupdate_sectionrequire exact channel-bound owners even when the global auth rollout mode isoff, and MCP requests foroperatortrust are clamped toagent. Every accepted section content change receives fresh provenance; it never inherits operator trust from an older version. - The gate lives at the one dangerous transition —
pick_up_handoffmoving a handoffpending → active:operator-trust → picks up in one call (ccandcodex).- either client + non-
operator→ refused. The consuming MCP principal cannot confirm its own input. Review and promote the exact pending row first withuv run python -m bridge_db --promote-handoff <id>in an interactive terminal.
- 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/refused) is written to the audit log.
MCP clients cannot mint operator provenance. An operator-directed handoff is created as
agent, reviewed in an interactive terminal, and promoted withuv run python -m bridge_db --promote-handoff <id>. The promotion rechecks the exact reviewed row under a write lock and refuses changed or non-pending handoffs.
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. Every
changed or new file section is labeled ingested regardless of auth rollout
mode; unchanged content preserves its existing label. Promote reviewed content
with --promote-section <section>: the TTY ceremony displays and confirms the
exact version and digest, then rechecks it under a write lock before granting
operator trust.
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 --promote-handoff 42 # review/promote one pending handoff (TTY only)
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 separates storage integrity from operating freshness:
-
overallandstorage_health:healthyordegraded, based only on DB, schema, fallback-file, and FTS integrity.overallremains the compatibility alias for existing consumers. -
operating_state:fresh,attention,stale, orunknown, derived from thefreshnessblock without changing command success semantics. -
freshness: the detailed operating-truth block described below. -
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,superseding_activity_id, 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; superseded means newer owner activity exists after the latest
snapshot; stale means an aged 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:
Storage health: <healthy|degraded>
Operating state: <fresh|attention|stale|unknown>
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.
Each shipped event also carries a delivery_state object. It reports only
receipt-backed bridge facts (downstream_sync_pending, downstream_synced, or
policy_dispositioned); Git, merge, default-branch, deploy, and production
readback dimensions remain unknown unless another authority proves them.
A synced disposition is source-owned terminal proof: the MCP connection must
be bound to the same principal stored in the activity row's source. A
different principal cannot finalize that event, even if it verified a
downstream object. Cross-source verification requires a future explicit
delegation contract; it must not be represented by borrowing the event owner's
caller value. The same ownership rule applies to policy dispositions because
they terminally waive the source's downstream obligation. Cross-source policy
adjudication also requires an explicit delegation contract.
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