Skip to main content

orbit: a personal CRM as Markdown notes, kept by an MCP server

CI PyPI Python Coverage License Ruff pre-commit

orbit keeps a personal CRM as an Obsidian vault of Markdown notes: the people in your network, their organizations, your interactions with them, the tasks that come out of those, and the topics that keep coming back. You read and edit the notes in Obsidian or any editor; AI agents read and change them through orbit's MCP tools, which know what a person, an organization or an interaction is, and keep the vault trustworthy:

  • One note per person, no duplicates. Agents resolve before they write: resolve_entity finds candidates by email address, name in any order or spelling (Müller, Mueller, "Müller, Hans", H. Müller), nickname, sound-alike surname and company domain, each with its evidence. The agent decides; orbit refuses a "new" person whose address is known, and unsure cases become questions you answer in Obsidian, where a duplicate decided same is merged.
  • No invented fields. Every write is checked against the schema of its note type. Fields you add by hand are kept.
  • Every fact with its source. Observations carry the words that state them and the interaction they come from; field changes are dated, so an older email can't overwrite a newer fact.
  • The notes are the truth. A SQLite index beside the vault makes lookups fast and guarantees uniqueness, but it is derived from the notes alone: delete it, and orbit builds it again. Edits made in Obsidian are indexed within a second, and on 50,000 interactions tools still answer in milliseconds (benchmarks).
  • Upkeep that keeps links intact. Merging two notes, renaming one, or creating the notes that links point to rewrites every link that needs it, in front matter and text, as Obsidian does.

Everything runs locally: no LLM inside, no network access, no cloud.

Install

Requires Python 3.11 or newer. With uv:

uv tool install orbit-crm   # installs the `orbit` command

Or run it without installing, uvx orbit-crm serve ~/crm, or install it with pip into a virtual environment: pip install orbit-crm. The package is called orbit-crm and installs the command twice, as orbit and as orbit-crm.

Create a vault, with your own note, Me.md, and your addresses, so that agents never take you for a contact:

orbit init ~/crm --name "Tobias" --email tobias@example.com

Open the folder as a vault in Obsidian. orbit init also writes three Bases views to 95_Bases/, tables you can sort, filter and edit in place: People (everyone, by organization, not contacted for a year, to confirm), Follow-ups (open tasks, tasks others owe you, people to get back to within two weeks) and Review (questions waiting for your answer). Run orbit init on an existing vault to add what it lacks; it never overwrites a file.

Setting up the MCP server

  1. Start the server, and leave it running:

    orbit serve ~/crm       # prints: orbit MCP server on http://127.0.0.1:8775/mcp
    

    It brings the index up to date, then watches the vault, so edits made in Obsidian count at once. To have it always there, start it at login with your system's scheduler (Task Scheduler, launchd, systemd).

  2. Register it with your agent, once. With Claude Code, for all your projects:

    claude mcp add --scope user --transport http orbit http://127.0.0.1:8775/mcp
    

    Keep --scope user: without it, the server is registered for the current folder only, and sessions started anywhere else don't see it. Other MCP clients connect the same way, to the streamable HTTP endpoint http://127.0.0.1:8775/mcp; a client configured with JSON typically takes {"mcpServers": {"orbit": {"type": "http", "url": "http://127.0.0.1:8775/mcp"}}}.

  3. Start a new agent session: running sessions don't pick up new servers. claude mcp list should show orbit as connected, and an agent asked who is in your CRM answers from it. Connected agents receive orbit's rules in the server's instructions: resolve before writing, write only what a source states, and name the source.

One server serves any number of agents. Alternatively, a client can start orbit itself and speak to it over stdio:

claude mcp add --scope user orbit -- orbit serve --stdio ~/crm

But only one orbit process may open a vault at a time, so with stdio a second session's orbit is refused while the first runs: use HTTP when several sessions work at once. Web connectors (in claude.ai, for example) can't use orbit: they reach servers from the provider's cloud, which can't reach one on your computer.

Updating

uv tool upgrade orbit-crm   # with pip: pip install -U orbit-crm
orbit --version

Then restart orbit serve (Ctrl+C, and start it again; a stdio server starts anew with each session), and start new agent sessions: a client reads the tools and their parameters when a session starts, and keeps that list, so a running session misses new tools and parameters. When a new version changes the index's layout, orbit rebuilds the index from the notes on its first start, by itself. The changelog lists what changed.

The vault

crm/
├── 01_People/          Hans Müller.md, and Me.md for you
├── 02_Organizations/   Acme AG.md
├── 03_Interactions/
│   └── 2026/2026-10/   2026-10-14 Email – AW Offerte Wartungsvertrag 2027.md
├── 04_Tasks/
├── 05_Topics/          projects, deals in progress, recurring themes
├── 90_Review/          questions for you: possible duplicates, job changes
├── 95_Bases/           Obsidian Bases views: People, Follow-ups, Review
└── 99_System/          orbit.toml, nicknames.txt, AGENTS.md

A person, as orbit writes them:

---
type: person
id: per_01M4WNN540CGMAHYXV1R90AA5V
name: Hans Müller
emails:
  - hans.mueller@acme-ag.example
org: "[[Acme AG]]"
job_title: Leiter Einkauf
colleague_of:
  - "[[Anna Keller]]"
last_contact: 2026-10-14
review_status: confirmed
created: 2026-10-14
updated: 2026-10-14
---
## Notes

## Observations
- 2026-10-14 · Decides on the budget for the 2027 maintenance contract · «Das Budget für 2027 ist freigegeben.» · [[2026-10-14 Email – AW Offerte Wartungsvertrag 2027]] · medium

## History
- 2026-10-14 · created: no candidate matched hans.mueller@acme-ag.example · [[2026-10-14 Email – AW Offerte Wartungsvertrag 2027]]

The file name is the note's name and link target, unique across the vault; its identity is the id, which never changes. Front matter is flat (key: value lines and block lists), so YAML parsers, Obsidian Bases and line-based readers all read it the same way. ## Notes is yours: orbit writes to it only to carry it over when two notes are merged, and to keep its links pointing at renamed notes. An example vault with a note of every type is in tests/vaults/example; the format is specified in docs/vault-format.md.

Tools

Tool What it does
resolve_entity Candidates for a person, organization or topic, ranked, with the evidence for each.
get_entity_context A note's fields, observations, last field changes, links in and out, recent interactions, open tasks and pending reviews.
search_entities Full-text and fuzzy search over names, aliases, addresses, domains, job titles and notes.
list_entities Queries over front-matter fields: everyone at an organization, open tasks due before a date, people not contacted since.
log_interaction An email, meeting, call or message, idempotent on its message id; updates last_contact.
upsert_person, upsert_organization, upsert_topic Create a record, or add to one: lists merged, empty fields filled, conflicts reported.
link_entities Set a relation from a fixed vocabulary: org, reports_to, colleague_of (both ways), an interaction's people, …
link_document Record a document outside the vault: a URL, a path or an id.
add_observation A fact with its evidence quote, source and confidence.
set_fields Change fields with a dated history line; a change older than the stored value becomes an observation instead.
create_task, update_task What you promised, what was asked of you, and what others promised you.
propose_review A question for you, answered in Obsidian: a duplicate decided same is merged, one decided different is recorded on both notes.
merge_entities Two notes about the same person, organization or topic become one; links follow, differing values go to review.
rename_entity A new name and file name; every link to the note follows.
create_missing_references Notes for links to notes that don't exist yet: mapped to an alias, or a stub of the type the link's field asks for.
lint_vault Schema conformance, duplicate ids and addresses, clashing file names, broken links, unknown fields; fix repairs the safe cases.

Every write names its source, returns the ids and absolute paths of the notes it changed, and does nothing when called again with the same arguments, where it can. Errors say what to do next. Every parameter is described in the tools' schemas, for the agents that read them.

Commands

Command What it does
orbit init [VAULT] Create a vault, or add what one lacks: its folders, Me.md, 99_System and the Bases views.
orbit serve [VAULT] Serve the vault over MCP (--stdio, --host, --port), watching it for edits made elsewhere.
orbit lint [VAULT] Check the vault (--fix, --json); exits with 1 if errors remain.
orbit reindex [VAULT] Build the index again from the notes.
orbit status [VAULT] Notes per type, pending reviews, clashes and the index, also while a server runs.

VAULT defaults to the current folder. Results go to stdout; everything else orbit says goes to stderr.

Configuration

99_System/orbit.toml, written by orbit init:

Setting Default
me the id of Me.md Your own note, which your addresses resolve to.
server.host, server.port 127.0.0.1, 8775 Where orbit serve listens. It warns when told to listen beyond this machine.
identity.free_mail_domains gmail.com, outlook.com, gmx.ch, bluewin.ch, … Domains that say nothing about an employer.
identity.role_addresses info, noreply, team, sales, support, … Mailboxes that never belong to a person.

99_System/nicknames.txt lists first names that stand for one another, one group per line: Hans = Hansi = Hansruedi. The one environment variable, ORBIT_HOME (default ~/.orbit/), says where the indexes live: outside the vault, one database per vault, so sync tools never see a live database.

How it works

  • Reading and writing. Anything may read the vault. Agents write through orbit's tools, which check the index, read the note again (an edit made by hand may not be indexed yet), write it atomically (a temporary file, then a rename), and update the index in the same call. Tools run one at a time, and one orbit process at a time may open a vault, so nothing writes concurrently.
  • The index holds ids, paths and front matter; addresses, domains, name keys and sound-alike codes; links both ways; field changes from the History sections; and full-text search. Its unique indexes on ids, file names, people's addresses and message ids make duplicates impossible through orbit; a hand edit that breaks one is recorded as a clash, which lint_vault reports, instead of stopping the index.
  • Duplicates. Names are compared without case, accents or word order (ä as ae, titles dropped), with initials, nicknames and Kölner Phonetik; addresses exactly; organizations by domain, Swiss UID and name without legal form. orbit ranks the candidates; the agent decides, or asks you through a review.

Scope

In scope: one owner's network, in Markdown files on their machine; any number of local agents; Linux, macOS and Windows. Out of scope, by design: an LLM inside orbit, network access and cloud services, reading mailboxes or the web (agents that do so use orbit's tools), authentication, and legal or compliance features.

Versioning

orbit is in early development (0.x): the vault format, the index, the tools and the Bases views work and are tested, and minor versions may still change the tools and the format. From 1.0, it follows semantic versioning: within a major version, the vault format, the MCP tools and their parameters, the commands and their JSON output, and orbit.toml stay compatible. The index is derived and rebuilt when its layout changes, so it never needs migrating. The changelog lists every change.

Development

git clone https://github.com/tobiaswuerth/orbit
cd orbit
uv sync                     # the virtual environment, with the development tools
uv run pre-commit install   # lint, format and config checks on every commit
uv run pytest --cov         # 100% line and branch coverage is required

The tests use temporary vaults and a temporary ORBIT_HOME, and run the MCP server for real, in memory. See CONTRIBUTING.md for the code layout and how changes are made, and SECURITY.md for reporting vulnerabilities.

License

MIT

Metadata

Release files for orbit-crm 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for orbit-crm 0.1.1
File Size Uploaded Provenance
orbit_crm-0.1.1.tar.gz 70.7 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for orbit-crm 0.1.1
File Interpreter ABI Platform
orbit_crm-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 150.9 kB

Release files / orbit_crm-0.1.1.tar.gz

Download URL orbit_crm-0.1.1.tar.gz
Size 70.7 kB
Tags Source
SHA-256 checksum
How to use checksums
c9c0a194179872a180d6cdfb7be3ecd5613f811b39c3ab4431d6b8bcd34e3332
BLAKE2b-256 checksum
How to use checksums
ebb4a7dba72f1f9e26232b74f56c8b42dbb059d34aeacfdfe358c0c2e01358a4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 22, 2026.

Transparency log

Release files / orbit_crm-0.1.1-py3-none-any.whl

Download URL orbit_crm-0.1.1-py3-none-any.whl
Size 80.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2a1229b433f5d5e6342ec0cdf69d7c8fa1d24a40e168262b64286126991da413
BLAKE2b-256 checksum
How to use checksums
d4e4c61f1229db23400fc6fae194939f0e7fda1bf4dd2ad74b6bac46463d70af
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

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 22, 2026.

Transparency log

Security for orbit-crm 0.1.1

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.

Select from 2 files

Attestation for orbit_crm-0.1.1.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 22, 2026.

Transparency log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page