Skip to main content

majordomo

Code Quality Checks Python 3.11+ License: GPL v3

A command-line tool that reads Google Chat, reports task activity, and sends messages. The command line is the primary interface; an MCP server is a secondary interface for AI agents. Both are thin front doors over one shared core.

What it does

majordomo reconstructs the tasks made through Google Chat's "Create a task for @Person (via Tasks)" from the chat messages, and reports who holds which tasks, and who assigned them, across spaces over a date range (why messages).

  • Tasks by assignee, space, and date; "assigned to me" and "assigned by me".
  • Spaces, people (participants with message and task counts), and raw messages by space or thread.
  • Domain or consumer, and who owns it. spaces reports whether each space belongs to a Google Workspace domain or a consumer/personal account, straight from the Chat API; --owner also resolves the space's Owner.
  • Three modes, one shape. The fast path reads an existing server-side cache of Chat (the data model). --live is up-to-dateness: it serves the cache and tops it up from the Chat API with anything newer. --nocache reads the Chat API directly and decodes tasks itself, so the tool also works without the cache. Every result is tagged with its source; an unforced read uses the cache and falls back to the direct API automatically when the cache is unreachable.
  • Send a message to a space, or a reply into a thread, as the logged-in account with optional file attachments, or as a Chat app (majordomo send, --as).
  • A privacy sieve in the core drops blocked spaces (and assignees) before any caller (CLI or MCP) can see them; a blocked space named explicitly, to read or to send into, answers "not found" exactly as an absent one does.
  • Output as a rich console table, --json, or --csv.

Install

The simplest cross-platform install is from PyPI:

pip install majordomo                  # reads and sends through the Chat API
pip install "majordomo[bi,mcp]"        # plus the cache fast path and the MCP server

A plain install talks to Google directly and is complete on its own. The bi option adds the driver for reading an existing server-side cache of Chat, which is an accelerator for sites that already run that mirror.

With uv, uvx majordomo ... runs it without installing and uv tool install majordomo installs it permanently.

Homebrew (macOS or Linux):

brew tap overseers-desk/od
brew install majordomo

Python 3.11+.

Run directly, no virtualenv, with Ubuntu/Debian packages:

sudo apt-get install python3-typer python3-rich \
  python3-googleapi python3-google-auth python3-google-auth-oauthlib
# only if you read the server-side cache (--cache, --live) as well:
sudo apt-get install python3-pymysql

Then run from the repo without installing the package:

PYTHONPATH=src python3 -m majordomo spaces

Or install the package (puts majordomo on your PATH):

python3 -m venv .venv && . .venv/bin/activate
pip install -e ".[bi,mcp]"        # drop the options you don't need
majordomo --help

The MCP server (majordomo mcp) needs the mcp PyPI package, which is not in apt; install it through the virtualenv above (the mcp option).

Configuration

majordomo reads two files from ~/.config/majordomo/:

config.toml (hand-edited):

[me]
user_id = "users/1234567890"      # your Chat id, for --to-me / --by-me
                                   # (find it with `majordomo people`)

[sieve]
block_spaces = ["spaces/AAAA"]     # never shown through any front door
block_assignees = ["users/9999"]   # drop these assignees from every report

[api]                              # optional; defaults shown (OAuth for the API path)
token_file = "~/.config/majordomo/token.json"
client_file = "~/.config/majordomo/client_secret.json"

[identity.bot]                     # optional; a Chat app to act as, with --as bot
service_account_file = "~/.config/majordomo/bot-key.json"

.env (the cache database connection, when using the cache path):

MYSQL_HOST=…
MYSQL_PORT=3306
MYSQL_USER=…
MYSQL_PASSWORD=…
MYSQL_DATABASE=…

The same keys may sit in a [db] table in config.toml instead; a key set in both takes [db]'s value.

Authenticating

majordomo signs in as you, through Google's own browser consent screen. majordomo login opens that flow, the account you pick grants access to its own Chat, and the resulting refresh token is stored for that account. Two things follow:

  • Any account works, consumer @gmail.com or Google Workspace, as long as it can grant OAuth consent. No Workspace administrator has to enable anything first.
  • You see only what that account sees. The tool reads the signed-in user's own spaces and nothing beyond them.

Domain-wide delegation, where a service account impersonates users, is not used: only a Google Workspace administrator may authorize it, and only for accounts inside that Workspace domain, which excludes consumer Gmail and any account you do not administer. Per-account OAuth costs one login per user and, in exchange, works for everyone.

majordomo login writes the token file used by --live (for the top-up), --nocache, send, attachments, spaces --owner, and naming people. It needs a Desktop OAuth client with the Google Chat API, the People API and the Google Tasks API enabled, saved as client_secret.json in the config directory. The permissions it asks for:

Permission For
chat.spaces.readonly, chat.messages.readonly Reading spaces, messages and files over the Chat API
chat.messages.create send, with its attachments
chat.memberships.readonly spaces --owner: finding each space's Owner
contacts.readonly, contacts.other.readonly, directory.readonly, userinfo.profile Naming people through the People API, from your saved contacts, your "other contacts", your Workspace directory, and your own profile
tasks.readonly tasks --to-me: notes, due dates and titles from your own Google Tasks list, for the tasks assigned to you

Upgrading can add permissions. A new version may need a permission your saved login does not grant; naming people through the People API is one. Before it does anything else, the command works out every permission its own shape needs (reading the API directly, --owner, sending, showing people) and opens Google's consent page once for whatever is missing. That happens up front, so a decline or an unanswered page costs nothing already spent reading. The consent adds to what you granted before. When nobody is there to answer the page (a cron job, CI, an SSH session without a terminal), the command still answers: people show as users/<id>, and a line says to run majordomo login, which grants every permission at once.

majordomo login

Acting as a Chat app

A Chat app's service-account key, named as [identity.NAME] in config.toml, lets majordomo act as that app with no login: --as NAME before the command, or identity on the MCP send, spaces, tasks and messages tools.

  • send posts as the app: text only, into spaces it is a member of, and --to reaches the app's own DM with that person.
  • spaces, tasks and messages read over the Chat API as the app, under the sieve: only the spaces it is a member of and their public messages, --person alone reading the app's DM with that person. The cache mirrors a person's view of Chat, not the app's, so --as reads the API without a source flag, and --cache and --live refuse under it.
  • Senders show as users/<id> unless a run on your own login has already named them, while task assignees keep the @-spelling of the message that assigned them; tasks --to-me comes without notes or due dates. The app has no People API and no Google Tasks list.
  • Reading messages as the app needs the chat.app.messages.readonly scope, which a Google Workspace administrator approves for the app once, as Google's Chat app authentication page describes. Until then, and in a space the app is not a member of, the read fails saying so. Sending needs no approval.
  • people, spaces --owner, attachments and login refuse under --as.

Commands

majordomo spaces
majordomo spaces --owner              # who owns each space too, by name
majordomo people --window year
majordomo people --person Alice       # one person: every spelling, email, DM space
majordomo tasks --to-me --window month
majordomo tasks --assignee Alice --since 2026-01-01
majordomo messages --space "Back Office" --window 7d
majordomo messages --person Alice --window 7d        # your DM with Alice, both sides
majordomo messages --thread spaces/AAAA/messages/BBBB
majordomo attachments --space spaces/AAAA --window 30d       # what files were posted
majordomo attachments --person Alice --window all            # the files in your DM
majordomo attachments --message spaces/AAAA/messages/BBBB --download ~/Downloads
majordomo send --space spaces/AAAA "On my way."
majordomo send --thread spaces/AAAA/messages/BBBB "Done, see the doc."
majordomo send --to alice@example.com "Lunch?"       # a person's existing 1:1 DM
majordomo send --space spaces/AAAA "Here it is." --attach ./report.pdf  # repeat --attach for several; text optional
majordomo --as bot send --space spaces/AAAA "Build finished."           # as the Chat app in [identity.bot]
majordomo mcp                       # run the MCP server (stdio)
  • Source: default cache with an automatic direct-API fallback. --cache forces the cache; --live adds a freshness top-up from the API; --nocache reads the API directly. attachments is the exception: files are read over the API always, the cache mirroring message text and not the files themselves. spaces's domain and Owner fields are the same exception: read over the API directly, so they populate on --nocache/--owner and render blank on a plain cache read, the mirror storing neither.
  • Every report that shows a person names them through the Google People API: task assignees, message and file senders, space owners, and people. A person the API cannot name shows as users/<id>. What majordomo fetched is cached in people.json and spaces.json under $XDG_CACHE_HOME/majordomo/ (~/.cache/majordomo/ when unset), and deleting them costs only a refetch.
  • A person (--person, --assignee, --to) is users/<id>, an email, or a name. A name matches People API names, and on the cache path also the spellings seen in task assignments and @-mentions. It must match one person; an ambiguous name lists the candidates with their ids. A space (--space) is spaces/<id> or its display name.
  • Window: 7d | 30d | month | year | all, or --since / --until (ISO dates).
  • Output: default console, --json, or --csv.
  • attachments lists what was posted; add --download DIR to save each file into an existing directory under the name it was posted with, and the path written appears in the output. A file already at that name is left alone and the download saved beside it as name-2, name-3, so a download never clobbers.
  • spaces also reports each space's domain (a Google Workspace domain space, or a consumer/personal one) straight from the Chat API; --owner resolves who owns it too, one extra call per space that is not a direct message.

Library use

Imported as a library, majordomo reads only what it is handed: a config, credentials, a directory for its name cache, and a replay bound. It reads no .env, no environment variable, and no file under the home directory but the config.toml that load_config() reads when handed nothing; the command and the MCP server read those and pass them in. The import itself reads nothing.

from datetime import datetime, timezone
import majordomo

# Reading as a Chat app: the key sits under [identity.NAME] and identity= names it.
# A personal login instead names the token `majordomo login` wrote,
# "api": {"token_file": "/path/to/token.json"}, and passes no identity.
cfg = majordomo.load_config(
    {
        "me": {"user_id": "users/1234567890"},
        "sieve": {"block_spaces": ["spaces/AAAA"]},
        "identity": {"bot": {"service_account_file": "/srv/app/chat-app-key.json"}},
    },
    identity="bot",
    world_as_of=None,                      # or "2026-07-12T17:07:00+10:00"
)
data_dir = "/srv/app/majordomo"            # cache_dir: the name cache's directory
rows = majordomo.tasks(cfg, start=datetime(2026, 7, 1, tzinfo=timezone.utc),
                       cache_dir=data_dir)
msgs = majordomo.messages(cfg, "Back Office", cache_dir=data_dir)
for note in majordomo.drain_notes():
    print(note)

A google-auth credentials object of any kind can be passed as credentials= to tasks, messages or make_reader in place of the file the config names.

  • load_config takes a dict in config.toml's shape, copied and taken as written (it names its own paths); a path to a TOML file; or nothing, which reads your own config.toml. identity="NAME" acts as the Chat app in [identity.NAME], which reads over the Chat API ("cache" and "live" refuse). A program hands the replay bound to world_as_of; a WORLD_AS_OF in its host's environment bounds nothing.
  • tasks and messages return the rows --json carries, as dicts whose datetimes are naive UTC; an aware start or end is converted and a naive one taken as UTC. source is "cache", "live", "nocache", or None for the cache with the Chat API as its fallback; the cache's connection is the config's [db] table. credentials, a google-auth credentials object, serves every Google call in place of the token file [api] names. cache_dir holds the name cache's people.json and spaces.json; without it nothing is kept on disk and each call fetches the names it shows. Both call make_reader(cfg, source, credentials=, cache_dir=), whose reader's source names the one that served.
  • send(cfg, space=…, text=…) posts as majordomo send does (thread or to in place of space, attachments beside or instead of text), signing in with the token file [api] names or as the app under identity, and refusing a space the config's [sieve] blocks as "not found".
  • A refusal raises MajordomoError: NotFound for a space, person or thread that matches nothing, a blocked space answering the same; ScopeError for credentials short of a Google scope, named in .scopes; and a plain MajordomoError otherwise, a name that matches several listing them.
  • The library prints nothing. What it has to say (people shown as ids for want of a permission, a bound the store does not reach) waits in drain_notes().

Replay bounds: WORLD_AS_OF

WORLD_AS_OF is the office-wide replay bound (design: WORLD_AS_OF.design.md): an ISO-8601 timestamp with a timezone offset, exported into the environment by a replay harness so a run sees the world as it stood at that instant.

WORLD_AS_OF='2026-07-12T17:07:00+10:00' majordomo tasks --window 7d
  • Unset: normal operation, at no cost.
  • Set: nothing dated after the bound is reported, on every source (cache, --live, --nocache) and through both front doors (CLI and MCP). Relative windows anchor to the bound, not to now: 7d is the seven days before it, month the calendar month before the one containing it. A --until later than the bound is clamped down with a stderr note. Under a past bound --live degrades to the cache read (a top-up would fetch only what the bound excludes). The JSON/MCP envelope carries world_as_of, so a log proves the answer was bounded. send is refused while the bound is set: a bounded run is a replay, and a send would act in the real present.
  • Set but unparseable, or missing its timezone offset: a hard error on every command, including ones that fetch no dates, because a silently ignored bound would produce a contaminated run that looks valid.

The bound is honest about what it cannot rewind. Space and user display names, People API names included, are current-state (neither the mirror nor the API keeps prior names) and the output says so. A message edited after the bound carries its post-edit text, marked edited_after_bound on the API path where the edit is observable. A bound older than the oldest cached message earns a warning that the store does not reach the as-of instant. For replaying the past the cache is the higher-fidelity source: the mirror retains messages the API has since dropped through deletion.

Not yet (deferred)

  • Completion-rate stats. Per-person and per-space completion rates over a window.

License

See LICENSE.

Metadata

Release files for majordomo 0.4.0

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

Source distribution (sdist)

Source distribution for majordomo 0.4.0
File Size Uploaded Provenance
majordomo-0.4.0.tar.gz 129.2 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for majordomo 0.4.0
File Interpreter ABI Platform
majordomo-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 213.7 kB

Release files / majordomo-0.4.0.tar.gz

Download URL majordomo-0.4.0.tar.gz
Size 129.2 kB
Tags Source
SHA-256 checksum
How to use checksums
4154d1064420da1bb7638611e242afd419398e0db10c733923cd73ebd70e8635
BLAKE2b-256 checksum
How to use checksums
483836e56c26c2f1b0a5795c8b352ed2a3ebe7a23465b8e4caa41ef28599b4d9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.12

Release files / majordomo-0.4.0-py3-none-any.whl

Download URL majordomo-0.4.0-py3-none-any.whl
Size 84.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
970da9b740d381b9539d5fe4ed236d63de6b24a1c6b55f219c751c0a15046b9b
BLAKE2b-256 checksum
How to use checksums
3f91043be81a7f46a7c6ff6946d597bff44bae27061b6f988de491b02c62cc49
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.12

Security for majordomo 0.4.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.

No provenance found

None of the files in this release were uploaded with provenance metadata.

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.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

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