ebony-enriching
Enables managing research-in-flight as a lab notebook with proposals, experiments, and gap signals, using filesystem storage and append-only semantics.
README
ebony-enriching
MCP server: an MCP lab notebook.
Status
v0.1 surface complete. 13 tools across 2 permission tiers, covering the full proposal / experiment / gap lifecycle.
- READ_ONLY (6):
status,read_proposal,list_proposals,read_experiment,list_experiments,list_gaps - READ_WRITE (7):
bootstrap,write_proposal,update_proposal_status,supersede_proposal,write_experiment,add_gap,remove_gap
No REMOVE_DESTRUCTIVE tier in v0 — lab-notebook semantics are append-only with status transitions (don't delete proposals, transition to rejected; don't delete experiments, they're the historical record). Gaps are the one exception: remove_gap exists because a gap is a transient signal that gets resolved when the answering work lands.
Lab notebook
ebony-enriching records; it doesn't decide. Lifecycle policy (when to mark a proposal rejected, when to auto-test vs. defer to user review, what counts as falsifiability) lives in the agents using this server, guided by the substrate's POLICY.md. The MCP tools enforce storage correctness (path safety, atomicity, schema validation) and nothing else.
Run
Five operating modes. Pick whichever fits.
| Mode | When to use |
|---|---|
1. uvx (one-off) |
Try it once, no install. |
2. uv tool install (pinned daemon) |
Run it occasionally, want it on $PATH. |
| 3. macOS LaunchAgent | Persistent daemon on a Mac. |
| 4. Linux systemd user unit | Persistent daemon on Linux. |
| 5. Docker / docker compose | Container deployment. |
Prereqs
-
uv-based modes (1–4) need
uvandgit. uv ships a portable Python 3.13, so no system Python install required.curl -LsSf https://astral.sh/uv/install.sh | sh -
Docker mode (5) needs
docker(or compatible). The image bundles Python 3.13; nothing else on the host.
In every mode the server listens on PORT (default 35834). Sanity-check it's up:
curl http://127.0.0.1:35834/health
1. One-off — uvx
uvx resolves the package into a temporary venv and runs it once. Nothing persists between runs.
uvx ebony-enriching # latest release
uvx ebony-enriching@0.1.0 # pin a specific version
# With env vars (custom data dir, restricted scope):
EBONY_ENRICHING_DIR=$HOME/EbonyEnriching \
EBONY_SCOPE=read_only \
uvx ebony-enriching
Good for kicking the tires or running on a CI box where you don't want to leave anything on disk.
2. Pinned daemon — uv tool install
Installs the ebony-enriching command on your $PATH, isolated in its own venv that uv manages. Faster startup than uvx (no resolve on each run).
uv tool install ebony-enriching
ebony-enriching # foreground server
To upgrade: uv tool upgrade ebony-enriching. To remove: uv tool uninstall ebony-enriching.
For a real "always running" setup, see the launchd / systemd recipes below.
3. macOS persistent daemon (launchd)
After uv tool install ebony-enriching, register a LaunchAgent so the daemon starts at login and restarts if it crashes.
Save this as ~/Library/LaunchAgents/com.garycoding.ebony-enriching.plist (replace CHANGE-ME with your username):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.garycoding.ebony-enriching</string>
<key>ProgramArguments</key>
<array>
<string>/Users/CHANGE-ME/.local/bin/ebony-enriching</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>EBONY_ENRICHING_DIR</key>
<string>/Users/CHANGE-ME/Documents/EbonyEnriching</string>
<key>EBONY_SCOPE</key>
<string>read_write</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key>
<string>/Users/CHANGE-ME/Library/Logs/ebony-enriching.out.log</string>
<key>StandardErrorPath</key>
<string>/Users/CHANGE-ME/Library/Logs/ebony-enriching.err.log</string>
</dict>
</plist>
Load and start it:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.garycoding.ebony-enriching.plist
launchctl kickstart -k gui/$(id -u)/com.garycoding.ebony-enriching
# Check status:
launchctl print gui/$(id -u)/com.garycoding.ebony-enriching | head -30
# Tail logs:
tail -f ~/Library/Logs/ebony-enriching.{out,err}.log
# Stop / unload:
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.garycoding.ebony-enriching.plist
4. Linux persistent daemon (systemd)
After uv tool install ebony-enriching, register a user-scope systemd unit so no root is required.
Save this as ~/.config/systemd/user/ebony-enriching.service:
[Unit]
Description=ebony-enriching MCP server (lab notebook substrate)
After=network-online.target
[Service]
Type=simple
ExecStart=%h/.local/bin/ebony-enriching
Restart=on-failure
RestartSec=5
Environment=EBONY_ENRICHING_DIR=%h/Documents/EbonyEnriching
Environment=EBONY_SCOPE=read_write
[Install]
WantedBy=default.target
Enable and start:
systemctl --user daemon-reload
systemctl --user enable --now ebony-enriching
# Check status:
systemctl --user status ebony-enriching
# Tail logs:
journalctl --user -u ebony-enriching -f
# Stop:
systemctl --user disable --now ebony-enriching
To keep the daemon running when the user is logged out, enable lingering:
loginctl enable-linger "$USER"
5. Docker / GHCR
Pull the published multi-arch image (linux/amd64 + linux/arm64) and run it directly:
docker pull ghcr.io/parkviewlab/ebony-enriching:latest
docker run --rm \
-p 35834:35834 \
-e EBONY_SCOPE=read_write \
-v ebony-data:/data \
ghcr.io/parkviewlab/ebony-enriching:latest
Pin a specific version with a tag — :0.1.0, :0.1, or :latest. See the container registry for available tags.
For a real deployment, copy docker-compose.yml, edit env vars if needed, then:
docker compose up -d # start in background
docker compose logs -f # tail logs
docker compose pull && docker compose up -d # upgrade
docker compose down # stop, keep volume
docker compose down -v # stop and drop the volume
From source (for development)
git clone https://github.com/ParkviewLab/ebony-enriching.git
cd ebony-enriching
uv sync
EBONY_ENRICHING_DIR=~/Documents/EbonyEnriching uv run python -m ebony_enriching
Endpoints
POST /sse— MCP Streamable HTTP transport. Tools.GET /health— liveness probe ({ok, version, uptime_seconds}).GET /admin/version— server identity + scope + configured EbonyEnriching path.GET /docs— OpenAPI / Swagger UI for the HTTP routes.
HTTP responses are gzipped when the client sends Accept-Encoding: gzip.
MCP tools
Two permission tiers controlled by EBONY_SCOPE. A caller at tier N sees and may call any tool whose required scope is ≤ N.
read_only (6 tools):
status— EbonyEnriching path, existence, single-writer mutex state. Always safe to call.read_proposal— read a single proposal by id. Returns full frontmatter + body.list_proposals— list proposals, optionally filtered bysystem(subdir),status(lifecycle state), orkind(proposal_kind). Malformed proposals appear withvalid: falserather than being silently dropped.read_experiment— read one experiment record by(proposal_id, run_timestamp). Returns full input + result. The returnedrun_timestampis the canonical form (filename-derived) and matches whatwrite_experimentandlist_experimentsreturn for the same experiment.list_experiments— list experiments. Withproposal_id, only that proposal's runs; without, all experiments. Returns summary metadata.list_gaps— parsegaps.mdand return all gap entries (id, query, created_at, optional why / source).
read_write (+7 tools):
bootstrap— initialize the canonical directory layout atEBONY_ENRICHING_DIR; drop ingaps.md/schema/SCHEMA.md/schema/POLICY.md/config.tomlplaceholders. Idempotent — reports only what was newly created.write_proposal— write a proposal toproposals/<subdir>/<id>.md. Schema-related kinds (schema_addition/schema_drift/schema_removal) route toproposals/schema/; others toproposals/<proposed_by>/. Atomic write.mode='create'(default) rejects overwrites withalready_exists;mode='update'requires the file to exist. Rejects withid_conflictif the same id is present in a different subdir (ids are unique across all subdirs). The validated model (with schema defaults applied) is what lands on disk.update_proposal_status— update a proposal's lifecycle fields (status, optionaltest_status,test_cost) in-place. RMW under the single-writer mutex. Validates values against their StrEnum but does NOT enforce transition rules — that policy lives in the agents using this server.supersede_proposal— link two proposals: setssuperseded_by: new_idonold_idandsupersedes: old_idonnew_id. Both must already exist; does not transition statuses.write_experiment— record one run of a proposal's prediction test atexperiments/<proposal_id>/<run-timestamp-with-microseconds>.md.run_timestampdefaults to now (UTC) and is recorded at microsecond precision so simultaneous writes don't collide. Doesn't check that the referenced proposal exists.add_gap— record an unanswered query ingaps.md.gap_idis derived from the query (SHA-256 hex, truncated to 8 chars; lowercase + collapsed whitespace), so adding the same query twice is idempotent (returnsalready_present: true).remove_gap— drop a gap bullet by id. Idempotent — unknown id returnsremoved: 0.
On-disk layout
$EBONY_ENRICHING_DIR/
├── proposals/
│ ├── schema/ # schema_addition / schema_drift / schema_removal kinds (any proposer)
│ ├── cogitate/ # subdir = `proposed_by` value
│ ├── curate/
│ ├── research/
│ ├── toolsmith/
│ └── converse/
├── experiments/
│ └── <proposal-id>/
│ └── <run-timestamp-with-microseconds>.md # e.g. 2026-05-17T12-30-45-123000Z.md
├── gaps.md # one bullet per open gap (managed by add_gap / remove_gap)
├── schema/
│ ├── SCHEMA.md # human-readable narrative of proposal / experiment / gap shape
│ └── POLICY.md # human-readable falsifiability + cost-tier policy
└── config.toml # reserved (empty in v0)
bootstrap materializes this layout. Proposal subdirs route by proposal_kind (schema-related kinds land in proposals/schema/; everything else lands in proposals/<proposed_by>/).
Configuration
| Env var | Default | Purpose |
|---|---|---|
PORT |
35834 |
HTTP listen port. |
HOST |
0.0.0.0 |
HTTP bind address. |
EBONY_ENRICHING_DIR |
~/Documents/EbonyEnriching |
Path to the lab notebook this server wraps. Call bootstrap once to materialize the canonical layout. EBONY_DIR is accepted as a shorter alias. |
EBONY_SCOPE |
read_write |
read_only, read_write, or remove_destructive. Server-wide (single tier per process); tiered so a caller at tier N sees every tool whose required scope is ≤ N. (remove_destructive is reserved — no v0 tool requires it.) To serve some callers read-only and others read-write, run two instances on different ports with different EBONY_SCOPE values. |
Tests
uv run pytest
Fast (~0.3s); exercises the full v0.1 tool surface in-process.
Releasing
Tag-driven via the release workflow on push of a v* tag. The CI gate enforces an SSOT contract: pyproject.toml is the only place the version lives, and CI verifies the pushed tag matches before publishing.
To release a new version:
# 1. Bump pyproject.toml version manually (e.g. 0.1.0 → 0.1.1) and commit:
$EDITOR pyproject.toml
git commit -am "release v0.1.1"
# 2. Tag and push:
git tag -a v0.1.1 -m "release v0.1.1"
git push --follow-tags # CI fires
License
MIT. See LICENSE.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。