{
  "markdown": "# Google Multi-Account Connector\n\nA remote [MCP](https://modelcontextprotocol.io) server that gives Claude access to\n**several Google accounts at once** — Gmail, Calendar, Drive, Contacts and Tasks.\nClaude's built-in Google connectors each hold exactly one account; connecting a\nsecond replaces the first. This holds as many as you link, and every search tool\nfans out across all of them in a single call.\n\nYou run it yourself. It holds your Google tokens in your own database, encrypted,\nand talks to Google directly — nothing passes through a third party.\n\nNode and Postgres, four tables, and no dependencies beyond Express, `pg` and\n`jsonwebtoken`. Document text extraction — PDF, Word, Excel, PowerPoint,\nOpenDocument — uses only Node's standard library.\n\n**Status:** running in production against real mailboxes, with 435 automated\nchecks. Built for one person's needs and published because the one-account limit\nis not unique to them. Treat it as working software with a small user base, not\nas a hardened product.\n\n## Quick start\n\n```bash\ngit clone https://github.com/ItsBreeze/google-multi-account-mcp.git\ncd google-multi-account-mcp\nnpm install\ncp .env.example .env      # fill it in — see Setup below\nnpm run migrate           # creates the four tables\nnpm start\n```\n\nThen deploy it somewhere with a public HTTPS URL (Railway, Fly, Render, a VPS —\nanything that runs Node and reaches Postgres) and follow **Setup**.\n\n## Endpoints\n\n| Method | Path | Description |\n|--------|------|-------------|\n| POST | `/mcp` | MCP Streamable HTTP endpoint (Bearer auth) |\n| GET | `/mcp/oauth/authorize` | Consent screen — operator password |\n| POST | `/mcp/oauth/token` | Token + refresh grants |\n| POST | `/mcp/oauth/register` | RFC 7591 dynamic client registration |\n| GET | `/.well-known/oauth-protected-resource` | RFC 9728 discovery |\n| GET | `/.well-known/oauth-authorization-server` | RFC 8414 discovery |\n| GET | `/gmail/connect` | Link an account (repeat per account) |\n| POST | `/gmail/unlink` | Unlink one, revoking the grant at Google |\n\n## Tools (54)\n\nEvery tool takes an optional `account`. On a search, **omitting it fans the call\nout across every linked account** and merges the results — the thing no\nsingle-account connector can do. On a write it names the one account to act on,\nso \"create this event\" is never ambiguous about whose calendar it lands in.\n\n### Gmail (23)\n\n| Area | Tools | Notes |\n|------|-------|-------|\n| Accounts | `list_accounts` | Which accounts are linked, with token health |\n| Search | `search_messages`, `search_threads` | Gmail query syntax, merged and date-sorted across accounts. `search_threads` returns one row per conversation — subject, every participant, message and unread counts, last activity — for \"where does my thread with X stand\" without pulling bodies |\n| Read | `get_message`, `get_thread`, `get_attachment` | Bodies flattened to text and capped at 60 KB; attachment metadata included. **`get_attachment` extracts text from PDF, Word, Excel, PowerPoint and OpenDocument attachments**, so an emailed contract or invoice is readable directly; anything else comes back base64 (2 MB cap) |\n| Send | `send_message`, `reply_to_message`, `forward_message` | Replies thread via `In-Reply-To`/`References`; forwards carry attachments (10 MB cap, skipped ones named) |\n| Drafts | `create_draft`, `list_drafts`, `get_draft`, `update_draft`, `send_draft`, `delete_draft` | `create_draft` with `reply_to_message_id` drafts an in-thread reply for review — the safe path for AI-written mail |\n| Labels | `modify_labels`, `list_labels`, `create_label`, `update_label`, `delete_label` | `modify_labels` takes `message_id` or `thread_id`; removing `INBOX` archives |\n| Trash & spam | `trash_message`, `untrash_message`, `mark_spam` | All take `message_id` or `thread_id`; trash is recoverable for 30 days; `mark_spam` with `unmark: true` restores |\n\n### Calendar (9)\n\n| Area | Tools | Notes |\n|------|-------|-------|\n| Read | `list_calendars`, `list_events`, `search_events`, `get_event` | `list_events` merges every linked calendar into one timeline, defaulting to the next 7 days; recurring series are expanded into actual occurrences, so a weekly standup appears on each day it happens |\n| Write | `create_event`, `update_event`, `delete_event` | Times are ISO 8601; a bare `YYYY-MM-DD` means all-day. `update_event` patches — unmentioned fields keep their value. Attendees are **not** emailed unless `send_updates` says so |\n| Repeats | `create_event`, `update_event`, `delete_event` | `repeat: \"weekly\"` (with `repeat_count` or `repeat_until`) covers the ordinary cases; `recurrence` takes full RFC 5545 rules. On a repeating event, `scope` chooses one occurrence or the whole series — see below |\n| RSVP | `respond_to_event` | accepted / declined / tentative, as the account that was invited; notifies the organiser by default |\n| Scheduling | `suggest_time` | Free slots across **every** linked calendar at once — busy anywhere means busy. Returns whole gaps rather than chopping a 3-hour opening into six half-hour slots |\n\n### Drive (15)\n\n| Area | Tools | Notes |\n|------|-------|-------|\n| Find | `search_files`, `list_recent_files`, `get_file_metadata`, `list_shared_drives` | Text search over names and contents, with optional raw Drive query syntax in `filter`. **Covers My Drive and every shared drive at once**; `drive_id` narrows to one. Trashed files excluded unless you ask for them |\n| Read | `read_file_content`, `download_file_content` | Docs, Sheets and Slides exported (Sheets as CSV); **PDF, Word, Excel, PowerPoint and OpenDocument extracted to text**; `ocr: true` routes scans and images through Google's own conversion; `include_comments` returns the comment threads. `download_file_content` takes `export_as` — turn a Doc into a PDF or docx, a Sheet into xlsx. Text caps at 60 KB, binaries at 2 MB base64 |\n| Write | `create_file`, `update_file`, `copy_file` | Text via `content`, binary via `content_base64`. `convert_to` makes Drive convert the upload into an editable Doc, Sheet or Slides — or a folder. `update_file` renames, moves and describes; overwriting contents additionally needs `replace_content: true`. **On a file you do not own it edits a private copy** — see below |\n| Comment | `comment_on_file` | Leave a comment on a draft, or reply to a thread — the review path that changes nothing in the document |\n| Sharing | `get_file_permissions`, `share_file`, `unshare_file` | Shares with one named person at reader/commenter/writer. Domain-wide and public-link sharing are off unless `DRIVE_ALLOW_PUBLIC_SHARING=true`, and then still need `confirm_public`. **`unshare_file` withdraws access** — by person, by domain, or by removing the public link |\n| Remove | `trash_file`, `untrash_file` | Trash and restore, within the 30-day window — see the scope note below |\n\n### Contacts (2) and Tasks (5)\n\n| Area | Tools | Notes |\n|------|-------|-------|\n| Contacts | `search_contacts`, `list_contacts` | Read-only. Searches saved contacts **and** people the account has corresponded with, so \"email Ann\" resolves to an address instead of a guess |\n| Tasks | `list_task_lists`, `list_tasks`, `create_task`, `update_task`, `delete_task` | `list_id` defaults to the account's first list. `update_task` with `completed: true` ticks a task off; `false` reopens it. Google Tasks has no trash, so `delete_task` is permanent |\n\n## Document text extraction\n\n`read_file_content` and `get_attachment` both return text for PDF, Word, Excel,\nPowerPoint and OpenDocument files, using only Node's standard library — no\nparsing dependency to vendor, audit or keep current.\n\n| Format | How | Notes |\n|--------|-----|-------|\n| .docx / .odt | ZIP + XML | Paragraphs, tabs and line breaks preserved; styles and revision marks dropped |\n| .xlsx / .ods | ZIP + XML | One CSV block per sheet, named from the workbook relationships. Cells are placed by column letter, so a gap stays a gap |\n| .pptx / .odp | ZIP + XML | One block per slide, in presentation order |\n| .pdf | Content-stream parsing | Flate, ASCII85 and ASCIIHex filter chains; literal, hex and octal strings; positioning operators become line breaks |\n\n**Where it stops, and what happens then.** Two kinds of PDF have no text to\nextract: a scan, which is a picture of a page, and one whose fonts are CID-keyed\nor subset, where the bytes in the content stream are glyph numbers that need the\nfont's own tables to become letters. Both would decode into confident-looking\nnonsense, so the result is scored for readability and **refused** rather than\nreturned — the refusal names the cause and points at the fix.\n\nThat fix is `ocr: true` on `read_file_content`, which copies the file as a Google\nDoc (Drive runs OCR during that conversion), exports the text, and deletes the\ncopy in a `finally` block. It reads scans, photographs and images. Two things\nworth knowing: it is the one place this server writes to Drive during a read, and\nthat delete is the only permanent delete in the whole server — its target is a\nfile created seconds earlier by that same call, never anything the user put\nthere. If the cleanup itself fails, the result says so and names the file.\n\nExtraction is capped at 60 KB like every other body, with the cut flagged.\n\n## Shared drives\n\nA shared drive is owned by an organisation rather than by a person, which is\nwhere a company's actual documents live. Drive's own default hides them: a\n`files.list` without `corpora=allDrives` searches only My Drive and files shared\ndirectly with the account, and a request naming a file inside a shared drive\ncomes back `404 File not found` unless it carries `supportsAllDrives` — an error\nthat reads as a wrong id rather than as a missing capability.\n\nEvery Drive request that accepts that parameter now carries it, applied in one\nplace rather than at each of the eighteen call sites, so the next endpoint added\ncannot quietly reintroduce the gap. Which methods accept it is not a guess: it\nis taken from the v3 discovery document, and `files.export`, `comments` and\n`replies` are deliberately left out because they do not take it.\n\nSearches therefore span My Drive and every shared drive by default, and results\nname the drive a file came from rather than just its id. Google recommends\nnarrowing where you can, so `list_shared_drives` gives the ids and `drive_id`\nconfines a search to one.\n\nThis widens what writes can reach as well as reads: a file in a shared drive was\npreviously unreachable by `update_file`, `share_file` or `trash_file`, and now is.\nSince a shared drive holds an organisation's documents rather than one person's,\nthat is exactly the case the ownership rule below exists for.\n\nA drive is named where it can be, and says so where it cannot. `drives.list`\ncovers the drives this account is a **member** of, paged rather than cut off at\nDrive's hundred-per-call — an account in more drives than that would otherwise\nhave the overflow look like non-membership.\n\nA file can also reach you from a drive you are *not* in, through a folder shared\ndirectly. Such a drive cannot be named at all. Its id is also its root folder's\nid, but reaching a file inside a drive grants nothing on the root folder above\nit, so asking answers `File not found` — verified against a real Workspace\naccount, where two of three drives holding recent files were reached this way.\nThose carry `shared_drive_member: false` rather than a bare id, so \"no name\navailable\" is distinguishable from \"lookup failed\".\n\n## Writes land on files you own\n\nA file owned by someone else — or by an organisation, which is **every file in a\nshared drive, including ones you created there** — is not this connector's to\nchange on a model's judgement.\n\n| Situation | What happens |\n|---|---|\n| You own it | The write happens |\n| You do not | A **private copy** is made in your My Drive and edited there |\n| You want the original | `edit_original: true` returns a **draft** and writes nothing |\n| The user approves | `confirm_edit: true` applies it |\n\nThe draft is the point. It states each field as `from → to`, and for a content\nreplacement it reads what is there now and shows **the lines that actually\nchange** — not just \"this will overwrite 40 KB\". Binary content says a preview is\nimpossible rather than faking one. Nothing is written until a second call\narrives, so a person sees the specific change before it lands rather than being\ntold afterwards which of their colleague's documents moved.\n\nThe private copy names `parents: ['root']` deliberately: `files.copy` with no\nparent puts the copy beside the source, which for a shared-drive file would leave\nit in that same shared drive — still not private.\n\n`share_file` and `trash_file` draft too, but have **no copy path**: copying a\ncolleague's document and sharing that spreads their content further, not less.\n\n**Revoking and restoring are never gated.** `unshare_file` and `untrash_file`\nwork without confirmation on any file, yours or not. Widening access needs\napproval; narrowing never does. A brake that needs permission is not a brake.\n\n## Repeating events\n\n`list_events` expands a series into its occurrences — \"what's on Tuesday\" means\nthe standup that Tuesday, not the rule that generates it. The consequence is that\nthe id in hand is almost always one occurrence, and Google offers no flag saying\nwhether an edit was meant for that occurrence or for all of them.\n\nSo `update_event` and `delete_event` take a `scope`:\n\n| scope | Effect |\n|---|---|\n| `this_event` (default) | Changes only the occurrence named by `event_id` — \"move tomorrow's standup\" |\n| `series` | Resolves the occurrence back to its series and changes every one — \"make it 10am from now on\" |\n\nReads carry `recurring_event_id`, which is what makes that resolution possible at\nall, and the result of every write says which it actually did. That matters in\none direction especially: an id naming the series itself changes every occurrence\neven under `this_event`, because that is what patching a series does — so the\nresult says `applies_to: \"series\"` rather than letting it pass silently.\n\nCreating a repeating event takes `repeat: \"daily\" | \"weekly\" | \"monthly\" |\n\"yearly\"`, with `repeat_count` or `repeat_until` to end it. Anything those cannot\nsay — every second Tuesday, weekdays only — goes in `recurrence` as RFC 5545\nlines. Those are checked here rather than at Google: a rule with no `FREQ`, an\nunknown frequency, or a `DTSTART` line all come back naming the problem, where\nGoogle answers a generic `400`. A repeat rule belongs to a series and is silently\ndropped if written to one occurrence, so that is refused too.\n\n## Setup\n\n1. **Google Cloud** — create a project; enable the **Gmail, Calendar, Drive, People\n   and Tasks** APIs; configure the consent screen as **External**, and add every\n   Google address you plan to link as a **test user**. Create an OAuth client of type\n   **Web application** with the authorized redirect URI set to exactly\n   `<PUBLIC_BASE_URL>/gmail/oauth/callback`.\n2. **Env** — set `DATABASE_URL`, `PUBLIC_BASE_URL`, `GOOGLE_CLIENT_ID`,\n   `GOOGLE_CLIENT_SECRET`, `TOKEN_ENC_KEY` and `JWT_SECRET`\n   (see `.env.example`), then deploy. The schema applies itself on boot.\n3. **Link accounts** — visit `<PUBLIC_BASE_URL>/gmail/connect`, sign in with\n   Google, then link each account once. The account you sign in with and the\n   accounts you link are independent; sign in as yourself and link whichever\n   mailboxes you like beneath that identity.\n4. **Add to Claude** — Settings → Connectors → Add custom connector →\n   `<PUBLIC_BASE_URL>/mcp`. Claude registers itself and sends you to the consent\n   screen, where you sign in with Google once.\n\n### Who sees what\n\nEvery mailbox is stored against the identity that linked it, and every MCP token\ncarries that identity as its subject — through refreshes as well as the first\ngrant. Two people using the same deployment reach only their own accounts, and\nnaming somebody else's address in a tool call is refused rather than served.\n`npm run test:isolation` asserts exactly that.\n\nUpgrading from the single-operator build, where every token carried the same\nsubject: set `LEGACY_OWNER_EMAIL` to the address those mailboxes belong to, and\nthe first sign-in from that address adopts them. Nothing needs re-linking.\nUnset it afterwards.\n\n> **If you linked accounts before adding a product:** the granted scopes are fixed at link\n> time, so an account linked before Calendar, Drive, Contacts and Tasks existed\n> holds a Gmail-only grant. Google will not extend it retroactively. Visit\n> `/gmail/connect` again for each account — that adds the missing access and\n> changes nothing else. Until then the new tools name the account and say to\n> re-link it, rather than failing with an opaque 403.\n\n## Deploying\n\nAny host that runs Node and reaches Postgres works. There is nothing to build,\nno container to define, and the schema applies itself on boot — a fresh\ndeployment needs only environment variables.\n\n### Environment\n\n| Variable | Notes |\n|---|---|\n| `DATABASE_URL` | Postgres. The four tables create themselves on first boot |\n| `PUBLIC_BASE_URL` | The public origin, scheme and host only, no trailing slash. Becomes the OAuth issuer and the Google redirect URI, so it must match what Google has registered |\n| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | From the Web application OAuth client |\n| `TOKEN_ENC_KEY` | 32 bytes, base64. Encrypts Google tokens at rest. **Losing or changing it makes every stored token undecryptable** |\n| `JWT_SECRET` | Any long random string; signs MCP tokens and browser sessions via two separately derived keys. Changing it only forces everyone to authenticate again |\n| `LEGACY_OWNER_EMAIL` | Optional, one-time. Adopts mailboxes linked before per-user sign-in existed, on that address's first sign-in |\n| `PORT` | Optional, defaults to 3000. Most platforms set this for you |\n\n`/gmail/check` diagnoses a bad Google client without running the whole consent\nround-trip, and reports which of the two variables Google rejected. Values are\nnever echoed back.\n\n### On Railway\n\nCreate a project from this repository and add a Postgres service, which\nprovides `DATABASE_URL` for the app service to reference. Set the rest above, take the generated domain as\n`PUBLIC_BASE_URL`, and register `<PUBLIC_BASE_URL>/gmail/oauth/callback` as an\nauthorized redirect URI on the OAuth client. Pushes to the default branch\nredeploy automatically.\n\n### Moving an existing deployment\n\nTwo facts make this a cutover rather than a migration, with no downtime and no\nre-linking:\n\n- **Point the new deployment at the same database.** The schema is\n  `CREATE TABLE IF NOT EXISTS`, so it is a no-op against tables that already\n  exist, and every linked account carries over untouched.\n- **Carry `TOKEN_ENC_KEY` across unchanged.** Tokens are AES-256-GCM, which is\n  authenticated: a different key fails the tag check and throws rather than\n  returning nonsense, so the failure is loud — but every account is unusable\n  until the original key is restored.\n\nAn OAuth client accepts several redirect URIs, so **add** the new deployment's\ncallback rather than replacing the old one. Both deployments can then run side\nby side against the same database — nothing else writes those four tables —\nwhich leaves room to verify the new one before retiring the old.\n\nExpect Claude to authenticate once more afterwards if `JWT_SECRET` changed.\nLinked Google accounts survive it: they are keyed to a constant subject, not to\nany particular token.\n\n## Security notes\n\n- Google refresh and access tokens are AES-256-GCM encrypted at rest under\n  `TOKEN_ENC_KEY`. The database never holds a usable token.\n- **Mail cannot be permanently deleted.** The Gmail scope is `gmail.modify`:\n  read, send, label, archive, trash. It deliberately excludes `mail.google.com`,\n  so `trash_message` is recoverable for 30 days and nothing here can destroy mail.\n- **Drive is different, and the difference is worth knowing.** Drive has no\n  equivalent middle scope: `drive.file` only sees files this app itself created,\n  which cannot answer \"find my lease\", so searching and editing existing files\n  needs full `drive` — which *does* permit permanent deletion. There the limit is\n  enforced by the tool surface instead: `trash_file` trashes, and no tool reaches\n  Drive's permanent-delete endpoint. That is a weaker guarantee than Gmail's,\n  because it is a matter of what is exposed rather than what is possible.\n- **Every outward action has an undo, which is the condition for offering it.**\n  `unshare_file` withdraws any grant `share_file` can make, and `untrash_file`\n  restores what `trash_file` removed. The first-party connector has neither,\n  so a permission it grants cannot be taken back through the same interface.\n  This is the one place the wider surface here is also the safer one.\n- **Contacts are read-only** by scope, not just by omission.\n- **Drive writes are guarded where the first-party connector is simply narrower.**\n  Claude's Drive connector shares with one email address and a role — it has no\n  way to publish a file — and its `update_file` changes only the title and parent.\n  Two guards match that default, because both failures are one-way and neither\n  looks alarming in a tool result:\n  - `share_file` takes a named person only. Domain-wide and public-link sharing\n    need `DRIVE_ALLOW_PUBLIC_SHARING=true`, and are refused outright otherwise\n    rather than quietly narrowed to something safer. Even enabled, they need\n    `confirm_public: true` as a second signal, and the refusal spells out what\n    the grant would actually mean before it is repeated.\n  - Overwriting a file's contents needs `replace_content: true` alongside\n    `content`, so a rename can never destroy a document as a side effect.\n- Authorization codes are single-use and hashed; PKCE S256 is required; refresh\n  tokens rotate on every use; `redirect_uri` must match the registration exactly.\n- While the Google app stays in **Testing**, refresh tokens expire after 7 days\n  and accounts must be re-linked. Publishing the app stops that, but Gmail, Drive\n  and Contacts are all restricted scopes, so Google verification applies.\n\n## Architecture\n\n```\nsrc/services/google_http.js     shared transport: URL building, bearer auth,\n                                error unwrapping, bounded-concurrency mapLimit\nsrc/services/gmail_api.js       one thin client per Google API, each returning\nsrc/services/calendar_api.js    shaped results rather than raw payloads\nsrc/services/drive_api.js\nsrc/services/people_api.js\nsrc/services/tasks_api.js\nsrc/services/office_text.js     .docx/.xlsx/.pptx/.odt text, via a ZIP reader\nsrc/services/pdf_text.js        PDF content-stream text, with a readability guard\nsrc/services/text_extract.js    one entry point both Drive and Gmail read through\nsrc/services/gmail_accounts.js  linked accounts, token refresh, scope gating\nsrc/mcp/shared.js               resolveAccount / tokenFor / fanOut / mergeSearch\nsrc/mcp/tools/                  one module per product, assembled by index.js\n```\n\nRate limits are respected rather than discovered: Google meters quota per user\nper second, so per-item detail fetches run five at a time, and a fetch that fails\nanyway is **counted** in `unavailable_*` rather than silently dropped from the\nresults.\n\n## Tests\n\nEleven suites, all plain Node — no framework, no new dependencies. 435 checks.\n\n```bash\n# No database, no network:\nnpm run test:tokens     # MCP and user tokens cannot be swapped, in either direction\nnpm run test:query      # Google query-string construction (repeated array params)\nnpm run test:mime       # entity decoding, body truncation, MIME construction\nnpm run test:threads    # address parsing, thread summaries, cross-account fan-out\nnpm run test:products   # free-slot arithmetic, Drive query quoting, multipart\n                        # upload framing, task dates, scope gating, tool-surface shape\nnpm run test:drive-safety   # the sharing and overwrite guards, both env states\nnpm run test:extract    # PDF and Office text extraction, against real fixtures\nnpm run test:drive-parity   # upload framing, conversion, export and comments, on the wire\nnpm run test:reach      # shared-drive request parameters, recurrence rules, and which\n                        # event a scoped write actually lands on\nnpm run test:ownership  # which file id a write reaches, that a draft writes nothing,\n                        # and that revoking is never gated\n\n# Needs a database:\nDATABASE_URL=postgres://…  npm run test:accounts   # resolution + encryption at rest\n\n# Needs the API running:\nnpm start &\nTEST_BASE_URL=http://127.0.0.1:3000  npm run test:mcp   # OAuth + MCP protocol\n```\n\n## Mounting it inside an existing app\n\n`src/app.js` is the whole standalone server and contains nothing but the\nconnector, so embedding it is a copy of that file's body: require the two\nrouters (`routes/mcp`, `routes/gmail_link`), keep the rate limits and the four\n`.well-known` discovery routes, and run the migration alongside your own.\n\nOne thing not to skip: MCP tokens are signed with a key **derived** from\n`JWT_SECRET` by HMAC rather than with `JWT_SECRET` itself. If your app signs its\nown user tokens with that secret and verifies them without an audience check,\nsharing the key would make every connector token a valid user token for your\napp. `npm run test:tokens` asserts the separation in both directions.\n\n## Contributing\n\nIssues and pull requests are welcome. Two conventions worth knowing first:\n\n- **Suites are plain Node** — no framework, no new dependencies. Each is a file\n  under `test/` with an `npm run test:<name>` script, one line per check, and a\n  non-zero exit on failure.\n- **Tests assert on what goes over the wire.** They stub `global.fetch` and\n  check the query parameters, the request path, the multipart framing. The\n  failures worth catching here are calls that succeed while asking the wrong\n  question, and only a wire-level assertion sees those.\n\n`PROJECT-LOG.md` records why things are the way they are, including the\nalternatives that were rejected. Read it before changing a decision.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n",
  "bytes": 26194,
  "sha": "ecbb33d48a9eda250634756fbe21be992c68a306ad7213487743434fbd1e45f7",
  "repo_slug": "itsbreeze/google-multi-account-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_itsbreeze_grounders_mcp_75e72790/readme"
}