mise-en-space
by spm1001
README.md
# mise-en-space
## Status
**Robustness:** Stable — in daily use, regular releases (currently v0.7.4)
**Works with:** Claude Code, Amp, Gemini CLI (any MCP client)
**Install:** Configure as MCP server (see below)
**Requires:** Python 3.11+, Google OAuth credentials
An MCP sous-chef for Google Workspace that provides a *mise en place* for knowledge work. Peel and pith removed, everything prepped and in its place, ready for Claude to cook with.
## Why another tool for LLMs to use Google Workspace?
[Google's official Workspace MCP](https://github.com/gemini-cli-extensions/workspace)  has 50 tools and requires multiple round-trips for basic tasks — search Gmail, get back a list of IDs, call again for each message, all of it burning context. Because it's essentially a thin wrapper over the Workspace APIs, the tool definitions alone take up ~15k of tokens every session.
Looking around for others, I found plenty of inspiration, but also some snags:
- [taylorwilsdon/google_workspace_mcp](https://github.com/taylorwilsdon/google_workspace_mcp)  covers every Google service, but returns all content inline — a 70-slide deck or 30-message thread lands straight in your context window
- [felores/gdrive-mcp-server](https://github.com/felores/gdrive-mcp-server)  deposits files to disk (Docs→Markdown, Sheets→CSV) the way I wanted, and also used a clever trick to get Drive to do high quality conversions, but only does Google Drive, so its coverage was limited for my needs
- [GongRzhe/Gmail-MCP-Server](https://github.com/GongRzhe/Gmail-MCP-Server)  — pre-built Gmail filter templates. Good ergonomics for a single source, but again, just a single source.
- [aaronsb/google-workspace-mcp](https://github.com/aaronsb/google-workspace-mcp)  — deposits files to disk with per-account folders. The right idea for file handling IMO - don't spam the caller's context window, but I didn't need multi-account support
- [a-bonus/google-docs-mcp](https://github.com/a-bonus/google-docs-mcp)  — tab-aware Docs extraction. Everyone else ignores multi-tab documents.
I wanted something that had the best of all these ideas:
- **Sous-chef philosophy.** Fetch a doc and get the comments too. Fetch an email and get the attachments extracted. Don't make the chef ask for every ingredient separately.
- **Clean extraction.** PDF text comes from poppler's `pdftotext -layout` — measured best on a 636-probe census of real corporate PDFs (97.4% verbatim value survival, 100% on big-table pages) — with Drive server-side OCR as the fallback for scans. Office files convert automatically.
- **Opinionated, LLM-first control surface** 3 tools not 50 - search, fetch, do. ~3k tokens of tool definitions and everything routes through the same three verbs.
- **One call, rich results.** Gmail search returns subjects, senders, snippets, and attachment names — not a bag of IDs requiring N+1 follow-ups.
- **Filesystem-deposits.** Content goes to disk as markdown/CSV, not into the context window. Claude reads (and greps) what it needs.
- **Companion Skill.** I like the pattern where we provide a tool and a companion [Skill](https://docs.anthropic.com/en/docs/claude-code/skills) that acts as the instruction manual on how to use it.
- **[MCP](https://modelcontextprotocol.io) Optional.** Option for CLI based interactions e.g. if you want to use a different agent harness like pi.
## The 3 Verbs
| Verb | Purpose | Deposits files? |
|------|---------|-----------------|
| `search` | Find files and emails across Drive and Gmail (plus activity and calendar) | Yes — results JSON |
| `fetch` | Extract content to `.mise/` as markdown/CSV | Yes — content folder |
| `do` | Act on Workspace — 23 operations: create, copy, move, rename, share, overwrite, prepend, append, replace_text, draft, reply_draft, archive, star, label, comment, comment_reply, suggest, trash, respond, create_event, update_event, freebusy, setup_oauth | Varies |
## CLI
For agents without MCP support — search and fetch in full, plus `create` (the most common `do` operation):
```bash
mise search "quarterly reports"
mise search "from:alice budget" --sources gmail
mise fetch 1abc123def456
mise create "Title" --content "# Markdown content"
```
## Skills
<!-- GENERATED:SKILLS:START -->
1 skill, tabled from `skills/*/SKILL.md` frontmatter by [render-skills.py](https://github.com/spm1001/batterie-de-savoir/blob/main/scripts/render-skills.py) — regenerate from this repo's root with
`uv run --script ../batterie-de-savoir/scripts/render-skills.py .`
| Skill | What it does |
|-------|--------------|
| `/mise` | Orchestrates content fetching via the mise MCP server's search/fetch/do tools |
<!-- GENERATED:SKILLS:END -->
## Supported Content Types
| What's in the larder | What the chef gets |
|--------|-------------|
| Google Docs | Markdown + open comments |
| Google Sheets | CSV + chart PNGs + open comments |
| Google Slides | Markdown + selective thumbnails + open comments |
| Gmail threads | Markdown with signature stripping via [talon](https://github.com/mailgun/talon), attachment extraction |
| PDFs | Layout-aligned text (`pdftotext -layout` → Drive OCR fallback for scans) |
| Office files (DOCX/XLSX/PPTX) | Markdown or CSV via Drive conversion |
| Video/Audio | AI summary + metadata (requires a chrome-debug browser session) |
| Images | Deposited as-is; SVG rendered to PNG |
## Architecture
```
server.py MCP server (thin wrappers around tools; ≤500 lines, enforced)
cli.py CLI interface (same tools, no MCP)
mise_en_space/ Library facade — import mise for in-process use (see below)
tools/ Business logic — routing, orchestration, do() dispatch, remote mode
adapters/ Thin Google API wrappers (one per service)
extractors/ Pure functions — no I/O, no MCP awareness (testable without APIs)
workspace/ File deposit management
resources/ MCP resource text (mise://docs/*)
skills/ Claude skill (auto-discovered by plugin system)
```
**Layer rules:**
- Extractors never import from adapters or tools (no I/O)
- Tools wire adapters → extractors → workspace
- Server and CLI are both thin wrappers around tools
- All of it mechanically enforced by `tests/unit/test_architecture.py` — including the root-level utility files, by discovery
Adding a new content type means: adapter (API call), extractor (parse), tool (wire + deposit). The layers are independent.
## Using mise as a library
Services that run headless (glaneur's nightly harvest, Garni's Cloud Run agents) import mise in-process instead of speaking MCP. The contract is the `mise_en_space` package — one class, the same three verbs:
```python
from mise_en_space import Mise
ws = Mise(ambient=True, base_path=workdir) # Cloud Run SA via ADC; also:
# token_path=..., credentials=..., or Mise()
listing = ws.search(folder_id=CORPUS) # discovery
result = ws.fetch(file_id) # deposit: manifest.json + content.md
doc = ws.do("create", title=..., content=markdown, folder_id=SHARED_DRIVE_FOLDER)
```
The worked example — hydrate a Drive folder, write a Doc back, as a service account — is [`examples/hydrate_and_write_back.py`](examples/hydrate_and_write_back.py); its header covers installing the wheel (you supply jeton yourself — uv source maps don't ride wheel metadata) and the service-account facts that bite (writes land only in Shared Drives). The full credential and deposit contract is the package docstring: `python -c "import mise_en_space; help(mise_en_space)"`.
Three facts a headless PDF pipeline needs. **The bulk-text tool is poppler's `pdftotext -layout`** — the extraction primary since suite 1.67 — and the wheel cannot bundle it: put `poppler-utils` in the consumer image (Dockerfile `apt-get install poppler-utils`) or PDF text silently degrades to markitdown (worse on tables) or Drive conversion (slow, needs write scope), with only a cue warning to say so. `fetch(file_id, thumbnails=False)` skips page/slide PNG rendering — measured 154s → 59s and 77 MB → 0 of deposit weight on a 256-page annual report — so pass it whenever nothing will look at pixels. And **no extraction path guarantees page boundaries**: form-feed survival is per-PDF (markitdown kept a two-page fixture's marker and dropped all 255 of that same annual report's), so a page-citing consumer must gate on the measured fields every PDF deposit carries — `page_markers` (form feeds in content.md) against `pdf_pages` (poppler's count, when available) — and heed the warning cue that fires whenever per-page citations can't be derived. Never infer page fidelity from `extraction_method`.
## Setup
### 1. Clone and install
```bash
git clone https://github.com/spm1001/mise-en-space.git
cd mise-en-space
uv sync # requires uv — https://docs.astral.sh/uv/
```
**System prerequisite: poppler.** PDF text extraction (`pdftotext -layout`) and PDF/deck page thumbnails shell out to [poppler](https://poppler.freedesktop.org/):
```bash
sudo apt-get install poppler-utils # Debian/Ubuntu
brew install poppler # macOS
```
Without it mise still works — PDF text falls back to markitdown (measurably worse on tables) or Drive conversion, and thumbnails are skipped — and both the session-start hook and every affected fetch say so rather than degrading silently.
### 2. Google OAuth
mise-en-space uses [jeton](https://github.com/spm1001/jeton) for OAuth.
**Quick version:** `credentials.json` ships with the repo. Just run `uv run python -m auth --auto`.
```bash
uv run python -m auth --auto # Opens browser + localhost listener (machine with a browser)
uv run python -m auth # Headless — prints the consent URL to paste into any browser
uv run python -m auth --code URL_OR_CODE # Exchange the code from the headless flow
```
With `--auto`, grant permissions in the browser and you're done.
**Scopes requested:** Drive (read+write), Gmail (read+write), Docs/Sheets/Slides (read+write), Drive Activity, Drive Labels, Calendar, Forms, and the staff directory (read). The directory scope is `admin.directory.user.readonly`, which reads more alarming than it is — mise only ever calls the Directory API's `domain_public` view, documented by Google as available to any user on the domain, and never the administrator view. See [`oauth_config.py`](oauth_config.py) for the full list and rationale.
<details>
<summary>Bringing your own GCP project (advanced)</summary>
If you prefer your own OAuth credentials instead of the bundled ones:
1. Create or select a [Google Cloud project](https://console.cloud.google.com)
2. Enable these APIs in [APIs & Services > Library](https://console.cloud.google.com/apis/library):
- Google Drive API, Gmail API, Google Docs API, Google Sheets API
- Google Slides API, Google Calendar API, Drive Activity API, Drive Labels API
3. Configure [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) (External, add your email as test user)
4. Create [OAuth credentials](https://console.cloud.google.com/apis/credentials) (Web Application type)
- Add `http://localhost:3000/oauth/callback` as an authorized redirect URI
5. Download the JSON and replace `credentials.json` in the repo root
6. Run `uv run python -m auth`
</details>
**Troubleshooting:**
| Problem | Fix |
|---------|-----|
| `redirect_uri_mismatch` | Only applies if using your own GCP project — add `http://localhost:3000/oauth/callback` to redirect URIs |
| `access_denied` | Add your email as a test user on the OAuth consent screen |
| `No browser available` | Use `--manual` flag, or SSH with port forwarding (`-L 3000:localhost:3000`) |
### 3. Add to Claude as MCP server
Add to `~/.claude.json`:
```json
{
"mcpServers": {
"mise": {
"type": "stdio",
"command": "uv",
"args": ["--directory", "/path/to/mise-en-space", "run", "python", "server.py"]
}
}
}
```
### 4. Link the skill (recommended)
The `skills/` directory contains a Claude skill that teaches Claude *how* to use mise effectively — Gmail operators, the exploration loop, comment checking patterns. The plugin system auto-discovers skills from `skills/*/SKILL.md`.
```bash
# For pi
ln -s /path/to/mise-en-space/skills/mise ~/.pi/agent/skills/mise
```
Without the skill, Claude can call the tools but won't know the patterns that make them useful (like following `email_context` hints or filtering large results with jq).
### 5. Email attachment extractor (optional)
The `apps-script/` directory contains a Google Apps Script that runs in your Google account, extracting email attachments to dated Drive folders (`Email Attachments/YYYY-MM/`). This enables Drive fullText search to find content inside PDF attachments — the "pre-exfil detection" pattern that makes mise searches across Gmail and Drive seamless.
See [`apps-script/README.md`](apps-script/README.md) for setup instructions.
## What to Expect (Latency)
MCP server startup is ~1.3s (import + first auth). After that, the server stays alive — subsequent calls skip startup.
| Operation | Typical | Range | Notes |
|-----------|---------|-------|-------|
| **Search (single source)** | ~1s | 0.2–1.3s | Drive and Gmail similar |
| **Search (Drive + Gmail)** | ~0.8s | 0.6–1.1s | Parallel — faster than either alone |
| **Fetch: Google Doc** | ~2s | 1.7–3.1s | Single API call |
| **Fetch: Gmail thread** | ~2.4s | 1.8–3.0s | Thread + message batch |
| **Fetch: PDF** | ~2.5s | 2.1–3.0s | pdftotext (since 1.67; extraction itself is ~55× faster than the markitdown benchmarked here — download and thumbnails dominate); scans fall back to Drive OCR (5–15s) |
| **Fetch: Google Sheet** | ~4s | 1.9–5.9s | 2 API calls (metadata + values) |
| **Fetch: Slides (7 slides)** | ~6s | 3.1–9.3s | ~0.5s per thumbnail, sequential |
| **Fetch: XLSX** | ~6s | 6.1–6.7s | Drive upload → convert → export |
| **Fetch: DOCX** | ~9s | 8.3–9.9s | Same pipeline, larger payloads |
*Benchmarked 9 Feb 2026 at [`fd5f9d0`](../../commit/fd5f9d0), 3 runs each, warm server, London → Google APIs.*
**The slow paths:** Office files (DOCX/XLSX) are unavoidably slow — Drive does server-side conversion (upload → convert → export → cleanup). Gmail attachments that are Office files are listed but not auto-extracted for this reason; use `fetch(thread_id, attachment="file.xlsx")` on demand.
Detailed timing data and flow diagrams: [`docs/information-flow.md`](docs/information-flow.md)
## The Kitchen
Mise en Space is part of [Batterie de Savoir](https://spm1001.github.io/batterie-de-savoir/) — a suite of tools for AI-assisted knowledge work. See the [full brigade and design principles](https://spm1001.github.io/batterie-de-savoir/) for how the tools fit together.
TDQS
A4.1/5.0
Scored across 3 tools
Disambiguation5/5
Each tool targets a distinct aspect: 'do' performs actions on Google Workspace, 'fetch' retrieves specific content to a local directory, and 'search' finds items across services. No overlap in primary purpose.
Naming Consistency5/5
All tool names are single, lowercase verbs ('do', 'fetch', 'search'), creating a uniform and predictable pattern. No mixing of styles.
Tool Count4/5
Only 3 tools, but each is highly capable (e.g., 'do' encompasses many sub-operations). The count is slightly low for the broad domain, but the tools are well-scoped.
Completeness4/5
Covers essential operations: create, edit, fetch, search, share, and email. Minor gaps like delete/trash are absent, but the core workflows are supported.
Maintenance
ActivityActive
ResponsivenessNo issues