ebony-enriching
MCP server: an MCP lab notebook.
Status
Tool 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 — 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 (a version may be pinned: see the note below)
# With env vars (custom data dir, restricted scope):
EBONY_ENRICHING_DIR=$HOME/EbonyEnriching \
EBONY_SCOPE=read_only \
uvx ebony-enriching
Releases up to 0.1.12 fail at import under mcp 2.x, which uvx resolves unless told otherwise; for such a release add --with 'mcp<2' (for example uvx --with 'mcp<2' ebony-enriching@0.1.0).
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 in the paths with your username, and in EBONY_INTERNAL_TOKEN with any non-empty value: a non-empty token lifts the read-only cap, and it is not checked on requests):
<?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>
<key>EBONY_INTERNAL_TOKEN</key>
<string>CHANGE-ME</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
Environment=EBONY_INTERNAL_TOKEN=CHANGE-ME
[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 \
-e EBONY_INTERNAL_TOKEN=CHANGE-ME \
-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.
Non-streaming HTTP responses of 256 bytes or more are gzipped when the client sends Accept-Encoding: gzip. MCP responses on /sse are text/event-stream and are not compressed.
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. While EBONY_INTERNAL_TOKEN is unset, the scope is capped at read_only whatever EBONY_SCOPE says (an unconfigured token means read-only), and the server logs a warning naming the cap; set the token to serve read_write.
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)
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 tool requires it.) To serve some callers read-only and others read-write, run two instances on different ports with different EBONY_SCOPE values. Capped at read_only while EBONY_INTERNAL_TOKEN is unset. |
EBONY_INTERNAL_TOKEN |
(unset) | Unset: the server is read-only whatever EBONY_SCOPE says. Set: EBONY_SCOPE applies. A deliberate switch only: it is not checked on incoming requests. |
EBONY_ENABLE_TRANSPORT_SECURITY |
true |
DNS-rebinding protection on the /sse transport: validates the Host (and, if present, Origin) header against the allowlists below so a malicious web page can't drive the tools via a rebound localhost connection. Leave on; set false only if a trusted proxy already validates these. |
EBONY_ALLOWED_HOSTS |
localhost, 127.0.0.1, [::1] (any port) |
Comma-separated Host values to accept (<host>:* matches any port). An unlisted Host is rejected with HTTP 421. Set this if reaching the server by a bound hostname or across containers (e.g. EBONY_ALLOWED_HOSTS=ebony:35834) — binding 0.0.0.0 alone is not enough. |
EBONY_ALLOWED_ORIGINS |
http://localhost[:PORT], http://127.0.0.1[:PORT] |
Comma-separated browser Origin values to accept (also scopes CORS). An absent Origin — i.e. a non-browser MCP client — always passes; a foreign Origin is rejected with HTTP 403. |
Tests
uv run pytest
Fast (~0.3s); exercises the full tool surface in-process.
Releasing
Tag-driven: pushing a v* tag fires the release workflow. The CI gate enforces the
SSOT contract — pyproject.toml [project].version is the single source of truth —
with four checks: the pushed tag must equal that version; the version must carry no
.devN marker (git bump release drops it before tagging); the version must be
strictly greater than the previous v* tag; and the tagged commit must be on
main.
Cut releases from the ebony-enriching-main worktree, promoting develop (see
docs/CONTRIBUTING.md and the handbook's
releases.md):
git pull --ff-only # sync main
git -C ../ebony-enriching-develop pull --ff-only # sync develop too, so the merge below promotes its true tip
git merge --no-ff develop # promote the integrated work
git bump patch # or minor / major / X.Y.Z — edits pyproject, commits the bump
git release # annotated tag vX.Y.Z derived from the version SoT
git push --follow-tags # the tag push fires the release workflow
git back-merge # the last step: brings main's release back to develop through a checked PR
Commit message convention
After the publish jobs, a changelog job generates the new CHANGELOG.md section — an LLM-written "Highlights" paragraph plus a categorized list written by dev-tools' generate-changelog — commits it back to main, and creates the GitHub Release with the same content as its body. Categorization uses Conventional Commits prefixes (the full list is in the ParkviewLab handbook's commits-and-changelogs.md):
| Title | Group in the notes | Notes |
|---|---|---|
any type with ! after it (feat!:), or a breaking-change footer |
Breaking changes | listed there once, whatever its type |
feat: |
Features | user-visible |
fix: |
Bug fixes | user-visible |
perf: |
Performance | user-visible |
refactor: |
Refactor | |
docs: |
Docs | |
test: |
Tests | |
revert: |
Reverts | GitHub's Revert button titles a PR Revert "…", which has no type |
build: / chore: / ci: / style: |
Maintenance | |
| any other title | Other changes | the whole title |
| a commit with no pull request | Direct commits | its subject and short hash |
A title without a recognised type is not dropped: it is listed whole under Other changes. So prefix your PR titles, and correct a title before the merge, since retitling afterwards does not change the commit. The groups appear in the order above, and an empty group is left out.
License
ebony-enriching is licensed under either of, at your option:
- the MIT license (
LICENSE-MIT), or - the Apache License, Version 2.0 (
LICENSE-APACHE).
In SPDX terms: MIT OR Apache-2.0. See LICENSING.md for the
full statement.
Unless you explicitly state otherwise, any contribution you submit for inclusion shall be dual-licensed as above, without any additional terms or conditions.
Metadata
Release files for ebony-enriching 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| ebony_enriching-1.0.0.tar.gz | 129.8 kB | With provenance | Details for ebony_enriching-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| ebony_enriching-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for ebony_enriching-1.0.0-py3-none-any.whl |
Total release size: 178.4 kB
Release files / ebony_enriching-1.0.0.tar.gz
| Download URL | ebony_enriching-1.0.0.tar.gz |
|---|---|
| Size | 129.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
764446785c4a5e972a7e58db7e272bb4ed6ad674b49bf513431c2f7391ed2326
|
|
BLAKE2b-256 checksum How to use checksums |
0be6847ccb6dbd070ab6fdbdff5016f1b7c4ec5f95f4f12ff326436e4efe4a87
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / ebony_enriching-1.0.0-py3-none-any.whl
| Download URL | ebony_enriching-1.0.0-py3-none-any.whl |
|---|---|
| Size | 48.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3b4236a10acb0a5005d764e8c88a074843136d62049727bff14b1af490db4e5a
|
|
BLAKE2b-256 checksum How to use checksums |
0a64539453348de3bd1359408e310e264599d0e6381f3bb35bb8a3ca72b51694
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logSecurity for ebony-enriching 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for ebony_enriching-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.