{"id":50945488,"url":"https://github.com/igor47/notmuchproxy","last_synced_at":"2026-06-17T19:33:56.226Z","repository":{"id":363924120,"uuid":"1265530177","full_name":"igor47/notmuchproxy","owner":"igor47","description":"Read-only OpenAPI + MCP server over a notmuch email archive, for LLM tool use (Open WebUI, Claude)","archived":false,"fork":false,"pushed_at":"2026-06-10T23:32:21.000Z","size":212,"stargazers_count":0,"open_issues_count":6,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-11T00:20:06.594Z","etag":null,"topics":["email","fastapi","llm-tools","mcp","notmuch","openapi"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/igor47.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-10T21:23:43.000Z","updated_at":"2026-06-10T23:32:25.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/igor47/notmuchproxy","commit_stats":null,"previous_names":["igor47/notmuchproxy"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/igor47/notmuchproxy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igor47%2Fnotmuchproxy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igor47%2Fnotmuchproxy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igor47%2Fnotmuchproxy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igor47%2Fnotmuchproxy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/igor47","download_url":"https://codeload.github.com/igor47/notmuchproxy/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/igor47%2Fnotmuchproxy/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34463553,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-17T02:00:05.408Z","response_time":127,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["email","fastapi","llm-tools","mcp","notmuch","openapi"],"created_at":"2026-06-17T19:33:55.337Z","updated_at":"2026-06-17T19:33:56.214Z","avatar_url":"https://github.com/igor47.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# notmuchproxy\n\n[![ci](https://github.com/igor47/notmuchproxy/actions/workflows/ci.yml/badge.svg)](https://github.com/igor47/notmuchproxy/actions/workflows/ci.yml)\n\nGive an LLM read-only access to your email.\n\nnotmuchproxy is a small API server over a [notmuch](https://notmuchmail.org/)\nemail archive. It exposes the same four tools two ways:\n\n- an **OpenAPI/REST API** (schema at `/openapi.json`), usable as an\n  [Open WebUI tool server](https://docs.openwebui.com/openapi-servers/)\n- an **MCP endpoint** (streamable HTTP at `/mcp/`), usable from Claude Code,\n  claude.ai, and any other MCP client\n\nThere is no UI and no write path: the server only ever reads the archive, so\nthe worst an over-eager LLM can do is search your email too enthusiastically.\n\n## The tools\n\n| Tool | REST endpoint | Description |\n| --- | --- | --- |\n| `search_email` | `GET /search?q=...` | Search threads with notmuch query syntax (`from:`, `to:`, `subject:`, `tag:`, `date:`, free text) |\n| `get_thread` | `GET /threads/{thread_id}` | Every message in a thread, oldest first, bodies as plain text |\n| `get_message` | `GET /messages/{message_id}` | A single message by Message-ID |\n| `list_tags` | `GET /tags` | All tags in the archive |\n\nPlus an unauthenticated `GET /healthz`. Everything else requires\n`Authorization: Bearer $NOTMUCHPROXY_API_KEY` — including the MCP endpoint.\nThe MCP tools are derived from the OpenAPI schema at startup, so the two\nsurfaces can't drift apart.\n\nSearch queries are validated before they reach xapian. Unknown prefixes\n(`status:unread`), capitalized prefixes (`From:alice`), and nonexistent tags\n(`tag:handled`) would otherwise silently match nothing — to xapian they are\njust terms no message contains — so a mistyped query looks exactly like an\nempty mailbox. The proxy instead rejects them with a 400 naming the problem\nand suggesting the fix (`for unread mail use tag:unread`; `Tags in this\narchive: ...`), which is the kind of feedback LLM callers actually act on.\n`docs/email-assistant-knowledge.md` has a system-prompt blurb teaching small\nmodels to use the tools well.\n\n## Configuration\n\nEverything is environment variables:\n\n| Variable | Required | Description |\n| --- | --- | --- |\n| `NOTMUCH_DATABASE` | yes¹ | path to the notmuch database root (the directory containing `.notmuch`); the docker image defaults it to `/mail` |\n| `NOTMUCHPROXY_NOTMUCH_BIN` | no | notmuch executable (default: `notmuch`) |\n| `NOTMUCHPROXY_EXCLUDE_TAGS` | no | comma-separated tags (e.g. `spam,deleted`) whose messages are excluded from *all* results — searches (even explicit `tag:spam` queries), threads, single messages, and the tag list. Useful for noise and for keeping adversarial spam content away from the model. |\n| `NOTMUCHPROXY_CORS_ORIGINS` | no | comma-separated origins allowed for CORS; `*` (the default) allows any origin, empty string disables CORS. Needed when a browser calls the API directly, e.g. tool servers added in Open WebUI's *user* settings. The auth token remains the actual access control. |\n\n### Authentication\n\nPick exactly one mechanism (the server refuses to start with both or neither);\nit applies to both the REST API and the MCP endpoint.\n\n**Static bearer token** — simplest; what Open WebUI's OpenAPI tool servers and\nClaude Code's `--header` flag speak. claude.ai custom connectors can *not* use\nthis mode (they only support OAuth).\n\n| Variable | Description |\n| --- | --- |\n| `NOTMUCHPROXY_API_KEY` | the bearer token clients must present |\n\n**OIDC via an external identity provider** — works with any OIDC IdP\n(authentik, Keycloak, Google, ...). notmuchproxy presents a spec-compliant\nMCP authorization server to clients — including the dynamic client\nregistration claude.ai requires — while acting as an ordinary OIDC client of\nyour IdP upstream (your IdP does not need to support DCR itself). Tokens\nissued through the flow are accepted on both the MCP and REST endpoints.\n\n| Variable | Description |\n| --- | --- |\n| `NOTMUCHPROXY_OIDC_CONFIG_URL` | the IdP's OIDC discovery URL, e.g. `https://auth.example.com/application/o/notmuchproxy/.well-known/openid-configuration` for authentik |\n| `NOTMUCHPROXY_OIDC_CLIENT_ID` | client id of the app registered at the IdP |\n| `NOTMUCHPROXY_OIDC_CLIENT_SECRET` | client secret of that app |\n| `NOTMUCHPROXY_PUBLIC_URL` | public base URL of this server, e.g. `https://notmuch.example.com` — used for OAuth callbacks and discovery metadata; claude.ai requires HTTPS |\n\nIdP setup (authentik example): create an OAuth2/OpenID provider with a\nconfidential client and redirect URI `$NOTMUCHPROXY_PUBLIC_URL/auth/callback`,\nscopes `openid profile email`. Who may authorize is controlled by your IdP's\nown policies (in authentik, bind the application to users/groups).\n\n¹ optional if the host has a notmuch config that already points at the database.\n\n## Running in production\n\nThe server is distributed as a docker image. Mount your maildir — which must\nalready contain the `.notmuch` index — read-only at `/mail`:\n\n```sh\ndocker run -d -p 8000:8000 \\\n  -e NOTMUCHPROXY_API_KEY=some-long-random-string \\\n  -v /path/to/your/mail:/mail:ro \\\n  ghcr.io/igor47/notmuchproxy:latest\n```\n\nIndexing (`notmuch new`) is *not* done by this container — keep running it\nwherever your mail is delivered. The container picks up index updates\nautomatically since xapian readers don't block writers.\n\n### docker compose, as a non-root user\n\nThe image runs as a built-in non-root user (uid 1000) by default. If your\nmaildir is owned by a different user, override `user:` so the container can\nread the mount — no rebuild needed:\n\n```yaml\nservices:\n  notmuchproxy:\n    image: ghcr.io/igor47/notmuchproxy:latest\n    restart: unless-stopped\n    # run as the uid/gid that owns your maildir (`id -u`/`id -g`);\n    # omit entirely if uid 1000 can read your mail\n    user: \"1000:1000\"\n    ports:\n      - \"8000:8000\"\n    environment:\n      NOTMUCHPROXY_API_KEY: ${NOTMUCHPROXY_API_KEY:?set this in .env}\n    volumes:\n      - /path/to/your/mail:/mail:ro\n```\n\nThe app never writes to the archive (and the `:ro` mount enforces that), so\nread permission on the maildir is all it needs.\n\n## Connecting clients\n\n### Open WebUI\n\nIn static mode, add an OpenAPI tool server (Admin Settings → Tools):\n\n- URL: `http://your-host:8000`\n- Auth: Bearer, key = your `NOTMUCHPROXY_API_KEY`\n\nOpen WebUI fetches `/openapi.json` (which is unauthenticated, like `/healthz`)\nto discover the tools, then sends the bearer token on each call.\n\nIn OIDC mode, use Open WebUI's MCP tool server type instead, pointed at\n`https://your-host/mcp` with OAuth 2.1 auth — it performs the same discovery\nand login flow as claude.ai.\n\nTool servers added under **Admin** Settings are called from the Open WebUI\nbackend, but ones added in a user's own Settings → Tools are called directly\nfrom the browser — that path needs CORS, which is enabled for all origins by\ndefault (lock it down with `NOTMUCHPROXY_CORS_ORIGINS=https://your-webui-host`).\n\n### claude.ai (OIDC mode only)\n\nSettings → Connectors → Add custom connector, URL `https://your-host/mcp`.\nClaude discovers the OAuth endpoints, registers itself dynamically, and sends\nyou through your IdP's login/consent in the browser. No client id/secret needs\nto be entered on the claude.ai side.\n\n### Claude Code\n\nStatic mode:\n\n```sh\nclaude mcp add --transport http notmuch http://your-host:8000/mcp \\\n  --header \"Authorization: Bearer $NOTMUCHPROXY_API_KEY\"\n```\n\nOIDC mode — omit the header; Claude Code runs the OAuth flow in your browser:\n\n```sh\nclaude mcp add --transport http notmuch https://your-host/mcp\n```\n\n### Other MCP clients\n\nAny client that speaks streamable HTTP can connect to `http://your-host:8000/mcp`,\nauthenticating with the static bearer token or the OAuth flow depending on\nthe server's configured mode.\n\n## Development\n\nTooling is managed by [mise](https://mise.jdx.dev/); the notmuch CLI must be on\nyour PATH (it's in every distro's repos).\n\n```sh\nmise install        # python + uv\nmise run install    # create venv, sync deps\nmise run test       # run the test suite (builds a throwaway notmuch archive)\nmise run check      # ruff lint + format check + pyright (CI mode)\nmise run check:fix  # same, but auto-fix what's fixable\nmise run dev        # serve on :8000 against generated fixtures (key: dev-key)\n```\n\nOther tasks: `mise run fixtures` (regenerate the local dev archive),\n`mise run docker:build`, `mise run docker:test` (run the suite inside docker),\n`mise run docker:run`. See `mise tasks` for the full list.\n\nCI runs the same mise tasks, then runs the suite again inside the docker image\n(against Debian's notmuch rather than the host's) before pushing to\n`ghcr.io/igor47/notmuchproxy` on pushes to main and `v*` tags.\n\n## Architecture notes\n\n- **notmuch access**: shells out to the `notmuch` CLI using `--format=json`\n  output, via a thin wrapper in `src/notmuchproxy/notmuch.py`. No Python\n  bindings, so there is no libnotmuch version-matching to worry about; the\n  database path is passed via the `NOTMUCH_DATABASE` environment variable.\n- **one definition, two protocols**: the FastAPI routes are the source of\n  truth; [fastmcp](https://gofastmcp.com)'s `FastMCP.from_fastapi()` converts\n  the OpenAPI schema into MCP tools at startup and dispatches tool calls to\n  the routes in-process.\n- **bodies**: `text/plain` parts are preferred; HTML-only messages get a naive\n  tag-stripped rendering. Attachments are listed by filename but not served.\n- **fixtures**: `python -m notmuchproxy.fixtures \u003cdir\u003e` generates a small\n  synthetic maildir + notmuch index, used by the tests and `mise run dev`.\n\n## License\n\n[MIT](LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figor47%2Fnotmuchproxy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Figor47%2Fnotmuchproxy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Figor47%2Fnotmuchproxy/lists"}