
Grounders MCP
io.github.ItsBreeze由 GitHub 帳號 ItsBreeze 發布v1.0.0更新於 Oct 24, 2026
Multiple Google accounts (Gmail, Calendar, Drive, Contacts, Tasks) in one Claude connector.
概覽
遠端 MCP 伺服器,可同時連結多個 Google 帳號,讓助理一次呼叫就能跨 Gmail、日曆、雲端硬碟、聯絡人和待辦事項搜尋與操作。
- 可以做什麼
- 跨所有已連結的 Gmail 與 AOL 信箱搜尋、讀取、寄送、草擬、加標籤、移至垃圾桶並管理郵件,還可擷取附件文字
- 列出、搜尋、建立、更新、刪除日曆活動並回覆邀請,也能在所有已連結日曆中找出共同空檔
- 尋找、讀取、建立、複製、留言、共用、取消共用和移至垃圾桶雲端硬碟檔案,並從 PDF、Office 與 OpenDocument 檔案擷取文字
- 查詢聯絡人,管理 Google 待辦清單與項目
- 透過討論串、電子郵件喚醒或 webhook 在 AI 代理之間交換訊息
- 適用情境
- 當助理需要同時處理多個 Google 帳號時適用,因為內建連接器每個只能綁定一個帳號。也適合跨帳號郵件搜尋、合併日曆時間線與尋找空檔,或把郵件與雲端硬碟文件讀成文字。
- 執行需求
- 託管版本是遠端端點,透過網址連線,需以 Google 登入並逐一連結帳號。自架需要 Node.js、Postgres、已啟用 Gmail、日曆、雲端硬碟、People 和 Tasks API 的 Google Cloud OAuth 用戶端、公開 HTTPS 網址,以及 DATABASE_URL、PUBLIC_BASE_URL、GOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET、TOKEN_ENC_KEY 和 JWT_SECRET 等變數。AOL 信箱需要應用程式專用密碼。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 Grounders MCP,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Web executable,透過 Streamable HTTP。 遠端服務在工作區中設定後即可從網頁執行環境執行。
交給代理程式安裝
把這段提示詞貼上到 Claude Code、Codex、Cursor 等代理程式,或 SourceWeft 對話中。代理程式會讀取該伺服器的安裝指南,先向你展示發布者、端點和登入方式,經你同意後再把它新增到你的客戶端。金鑰不會寫進任何命令。
閱讀 https://sourceweft.com/mcp/io.github.ItsBreeze%2Fgrounders-mcp/install.md,並按其中說明把這個 MCP 伺服器接入我的客戶端。修改任何設定之前,先告訴我它的發布者、端點和登入方式,等我確認。不要把 API 金鑰寫進命令或我會提交的檔案;問我金鑰應從哪裡取得。其他 MCP 客戶端
把它新增到你客戶端的 mcpServers 設定中。
{
"mcpServers": {
"grounders": {
"type": "http",
"url": "https://mcp.grounders.app/mcp"
}
}
}README
Hub — a multi-account Google connector for Claude
A remote MCP server that gives Claude access to several Google accounts at once — Gmail, Calendar, Drive, Contacts and Tasks. Claude's built-in Google connectors each hold exactly one account; connecting a second replaces the first. This holds as many as you link, and every search tool fans out across all of them in a single call.
AOL mailboxes link too. They are reached over IMAP and SMTP with an app password, sit behind the same mail tools, and are searched together with Gmail — see AOL mailboxes.
Run it yourself and it holds your Google tokens in your own database, encrypted, and talks to Google directly — nothing passes through a third party. Or try the hosted one first.
Node and Postgres, six tables. Dependencies are Express, pg and jsonwebtoken,
plus imapflow, mailparser and nodemailer for AOL — IMAP, MIME and SMTP
are protocols, not parsing jobs, and each library is the maintained standard for its
one. Document text extraction — PDF, Word, Excel, PowerPoint, OpenDocument — uses
only Node's standard library.
Status: running in production against real mailboxes, with 564 automated checks. Built for one person's needs and published because the one-account limit is not unique to them. Treat it as working software with a small user base, not as a hardened product.
Try the hosted one
A copy runs at hub.grounders.app. It is in testing, and free while it is.
- In Claude: Settings → Connectors → Add custom connector, with the URL
https://hub.grounders.app/mcp. ChatGPT, Cursor and other MCP clients take the same URL as a remote MCP server. - Sign in with Google once when asked.
- Link each account at hub.grounders.app/gmail/connect.
Google's review of the app is still in progress, so you will see an "unverified app" screen the first time.
Quick start
Then deploy it somewhere with a public HTTPS URL (Railway, Fly, Render, a VPS — anything that runs Node and reaches Postgres) and follow Setup.
Endpoints
Tools (59)
Every tool takes an optional account. On a search, omitting it fans the call
out across every linked account and merges the results — the thing no
single-account connector can do. On a write it names the one account to act on,
so "create this event" is never ambiguous about whose calendar it lands in.
Mail — Gmail and AOL (23)
Calendar (9)
Drive (15)
Contacts (2) and Tasks (5)
AI agents (5)
AIs talk to each other through Hub instead of by email. Every message has a thread, a hop count and a read time, so nothing is lost to subject matching and a sender can see whether it was read.
An agent without MCP needs no account: each wake carries a signed reply_url
(30 days, one owner, one agent). POST {"text":"..."} to it to answer, GET it to
read the thread, or POST {"new_thread":true,"to":"claude","text":"..."} to
start a new conversation.
AOL mailboxes
Link one at /gmail/aol with the address and an app password generated at
AOL Account Security — AOL has no OAuth
for third-party mail clients outside an approved partner list, so this is the only
door. The password is signed in to IMAP before it is stored (one app password
covers IMAP and SMTP), so a typo is reported on the link page, not on Claude's
first tool call. It is stored
AES-256-GCM encrypted under TOKEN_ENC_KEY, like the Google tokens. Hosts come
from a preset in code (imap_providers.js), never from the request.
Sending goes through a relay where SMTP is blocked. Railway blocks outbound
SMTP below its Pro plan, so hub.grounders.app hands each AOL submission over
HTTPS to a Cloudflare Worker (smtp-relay/) that speaks SMTP to
smtp.aol.com:465. The Worker stores and logs nothing and reaches only preset
hosts. It is used when SMTP_RELAY_URL and SMTP_RELAY_TOKEN are set; without
them the server talks SMTP directly, as a self-hosted copy that can reach port
465 should. Gmail sending is unaffected — it goes through Google's API.
The 23 mail tools work on an AOL mailbox unchanged, and a search with no
account covers Gmail and AOL in one merged, date-sorted result. What differs
is IMAP itself, and the tools say so rather than pretend:
No permanent delete, enforced in code. Gmail's guarantee is structural — the
scope cannot expunge. An app password can do anything, so here it is a property of
imap_api.js: its one expunge is removeDraft, which acts on a single UID in the
Drafts folder by UID EXPUNGE (and sends the draft to Trash on a server without
it, since plain EXPUNGE would take every other \Deleted message too). Moves
refuse a server that would emulate MOVE with a mailbox-wide expunge.
npm run test:imap drives the client against a scripted server and asserts every
expunge it ever sees was one draft, in Drafts.
Document text extraction
read_file_content and get_attachment both return text for PDF, Word, Excel,
PowerPoint and OpenDocument files, using only Node's standard library — no
parsing dependency to vendor, audit or keep current.
Where it stops, and what happens then. Two kinds of PDF have no text to extract: a scan, which is a picture of a page, and one whose fonts are CID-keyed or subset, where the bytes in the content stream are glyph numbers that need the font's own tables to become letters. Both would decode into confident-looking nonsense, so the result is scored for readability and refused rather than returned — the refusal names the cause and points at the fix.
That fix is ocr: true on read_file_content, which copies the file as a Google
Doc (Drive runs OCR during that conversion), exports the text, and deletes the
copy in a finally block. It reads scans, photographs and images. Two things
worth knowing: it is the one place this server writes to Drive during a read, and
that delete is the only permanent delete in the whole server — its target is a
file created seconds earlier by that same call, never anything the user put
there. If the cleanup itself fails, the result says so and names the file.
Extraction is capped at 60 KB like every other body, with the cut flagged.
Shared drives
A shared drive is owned by an organisation rather than by a person, which is
where a company's actual documents live. Drive's own default hides them: a
files.list without corpora=allDrives searches only My Drive and files shared
directly with the account, and a request naming a file inside a shared drive
comes back 404 File not found unless it carries supportsAllDrives — an error
that reads as a wrong id rather than as a missing capability.
Every Drive request that accepts that parameter now carries it, applied in one
place rather than at each of the eighteen call sites, so the next endpoint added
cannot quietly reintroduce the gap. Which methods accept it is not a guess: it
is taken from the v3 discovery document, and files.export, comments and
replies are deliberately left out because they do not take it.
Searches therefore span My Drive and every shared drive by default, and results
name the drive a file came from rather than just its id. Google recommends
narrowing where you can, so list_shared_drives gives the ids and drive_id
confines a search to one.
This widens what writes can reach as well as reads: a file in a shared drive was
previously unreachable by update_file, share_file or trash_file, and now is.
Since a shared drive holds an organisation's documents rather than one person's,
that is exactly the case the ownership rule below exists for.
A drive is named where it can be, and says so where it cannot. drives.list
covers the drives this account is a member of, paged rather than cut off at
Drive's hundred-per-call — an account in more drives than that would otherwise
have the overflow look like non-membership.
A file can also reach you from a drive you are not in, through a folder shared
directly. Such a drive cannot be named at all. Its id is also its root folder's
id, but reaching a file inside a drive grants nothing on the root folder above
it, so asking answers File not found — verified against a real Workspace
account, where two of three drives holding recent files were reached this way.
Those carry shared_drive_member: false rather than a bare id, so "no name
available" is distinguishable from "lookup failed".
Writes land on files you own
A file owned by someone else — or by an organisation, which is every file in a shared drive, including ones you created there — is not this connector's to change on a model's judgement.
The draft is the point. It states each field as from → to, and for a content
replacement it reads what is there now and shows the lines that actually
change — not just "this will overwrite 40 KB". Binary content says a preview is
impossible rather than faking one. Nothing is written until a second call
arrives, so a person sees the specific change before it lands rather than being
told afterwards which of their colleague's documents moved.
The private copy names parents: ['root'] deliberately: files.copy with no
parent puts the copy beside the source, which for a shared-drive file would leave
it in that same shared drive — still not private.
share_file and trash_file draft too, but have no copy path: copying a
colleague's document and sharing that spreads their content further, not less.
Revoking and restoring are never gated. unshare_file and untrash_file
work without confirmation on any file, yours or not. Widening access needs
approval; narrowing never does. A brake that needs permission is not a brake.
Repeating events
list_events expands a series into its occurrences — "what's on Tuesday" means
the standup that Tuesday, not the rule that generates it. The consequence is that
the id in hand is almost always one occurrence, and Google offers no flag saying
whether an edit was meant for that occurrence or for all of them.
So update_event and delete_event take a scope:
Reads carry recurring_event_id, which is what makes that resolution possible at
all, and the result of every write says which it actually did. That matters in
one direction especially: an id naming the series itself changes every occurrence
even under this_event, because that is what patching a series does — so the
result says applies_to: "series" rather than letting it pass silently.
Creating a repeating event takes repeat: "daily" | "weekly" | "monthly" | "yearly", with repeat_count or repeat_until to end it. Anything those cannot
say — every second Tuesday, weekdays only — goes in recurrence as RFC 5545
lines. Those are checked here rather than at Google: a rule with no FREQ, an
unknown frequency, or a DTSTART line all come back naming the problem, where
Google answers a generic 400. A repeat rule belongs to a series and is silently
dropped if written to one occurrence, so that is refused too.
Setup
- Google Cloud — create a project; enable the Gmail, Calendar, Drive, People
and Tasks APIs; configure the consent screen as External, and add every
Google address you plan to link as a test user. Create an OAuth client of type
Web application with the authorized redirect URI set to exactly
<PUBLIC_BASE_URL>/gmail/oauth/callback. - Env — set
DATABASE_URL,PUBLIC_BASE_URL,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,TOKEN_ENC_KEYandJWT_SECRET(see.env.example), then deploy. The schema applies itself on boot. - Link accounts — visit
<PUBLIC_BASE_URL>/gmail/connect, sign in with Google, then link each account once. The account you sign in with and the accounts you link are independent; sign in as yourself and link whichever mailboxes you like beneath that identity. - Add to Claude — Settings → Connectors → Add custom connector →
<PUBLIC_BASE_URL>/mcp. Claude registers itself and sends you to the consent screen, where you sign in with Google once.
Who sees what
Every mailbox is stored against the identity that linked it, and every MCP token
carries that identity as its subject — through refreshes as well as the first
grant. Two people using the same deployment reach only their own accounts, and
naming somebody else's address in a tool call is refused rather than served.
npm run test:isolation asserts exactly that.
Upgrading from the single-operator build, where every token carried the same
subject: set LEGACY_OWNER_EMAIL to the address those mailboxes belong to, and
the first sign-in from that address adopts them. Nothing needs re-linking.
Unset it afterwards.
If you linked accounts before adding a product: the granted scopes are fixed at link time, so an account linked before Calendar, Drive, Contacts and Tasks existed holds a Gmail-only grant. Google will not extend it retroactively. Visit
/gmail/connectagain for each account — that adds the missing access and changes nothing else. Until then the new tools name the account and say to re-link it, rather than failing with an opaque 403.
Deploying
Any host that runs Node and reaches Postgres works. There is nothing to build, no container to define, and the schema applies itself on boot — a fresh deployment needs only environment variables.
Environment
/gmail/check diagnoses a bad Google client without running the whole consent
round-trip, and reports which of the two variables Google rejected. Values are
never echoed back.
On Railway
Create a project from this repository and add a Postgres service, which
provides DATABASE_URL for the app service to reference. Set the rest above, take the generated domain as
PUBLIC_BASE_URL, and register <PUBLIC_BASE_URL>/gmail/oauth/callback as an
authorized redirect URI on the OAuth client. Pushes to the default branch
redeploy automatically.
Moving an existing deployment
Two facts make this a cutover rather than a migration, with no downtime and no re-linking:
- Point the new deployment at the same database. The schema is
CREATE TABLE IF NOT EXISTS, so it is a no-op against tables that already exist, and every linked account carries over untouched. - Carry
TOKEN_ENC_KEYacross unchanged. Tokens are AES-256-GCM, which is authenticated: a different key fails the tag check and throws rather than returning nonsense, so the failure is loud — but every account is unusable until the original key is restored.
An OAuth client accepts several redirect URIs, so add the new deployment's callback rather than replacing the old one. Both deployments can then run side by side against the same database — nothing else writes those five tables — which leaves room to verify the new one before retiring the old.
Expect Claude to authenticate once more afterwards if JWT_SECRET changed.
Linked Google accounts survive it: they are keyed to a constant subject, not to
any particular token.
Security notes
- Google refresh and access tokens are AES-256-GCM encrypted at rest under
TOKEN_ENC_KEY. The database never holds a usable token. - Mail cannot be permanently deleted. The Gmail scope is
gmail.modify: read, send, label, archive, trash. It deliberately excludesmail.google.com, sotrash_messageis recoverable for 30 days and nothing here can destroy mail. - Drive is different, and the difference is worth knowing. Drive has no
equivalent middle scope:
drive.fileonly sees files this app itself created, which cannot answer "find my lease", so searching and editing existing files needs fulldrive— which does permit permanent deletion. There the limit is enforced by the tool surface instead:trash_filetrashes, and no tool reaches Drive's permanent-delete endpoint. That is a weaker guarantee than Gmail's, because it is a matter of what is exposed rather than what is possible. - Every outward action has an undo, which is the condition for offering it.
unshare_filewithdraws any grantshare_filecan make, anduntrash_filerestores whattrash_fileremoved. The first-party connector has neither, so a permission it grants cannot be taken back through the same interface. This is the one place the wider surface here is also the safer one. - AOL app passwords are the one stored credential that is not an OAuth grant. They are encrypted like the tokens, checked before storage, never logged (both protocol clients run with logging off), and cannot be revoked from here — unlinking deletes the stored copy, and the link and deletion pages say to delete the app password at AOL too. The link form is behind the sign-in rate limit, so it cannot be used to guess AOL passwords through this server.
- Contacts are read-only by scope, not just by omission.
- Drive writes are guarded where the first-party connector is simply narrower.
Claude's Drive connector shares with one email address and a role — it has no
way to publish a file — and its
update_filechanges only the title and parent. Two guards match that default, because both failures are one-way and neither looks alarming in a tool result:share_filetakes a named person only. Domain-wide and public-link sharing needDRIVE_ALLOW_PUBLIC_SHARING=true, and are refused outright otherwise rather than quietly narrowed to something safer. Even enabled, they needconfirm_public: trueas a second signal, and the refusal spells out what the grant would actually mean before it is repeated.- Overwriting a file's contents needs
replace_content: truealongsidecontent, so a rename can never destroy a document as a side effect.
- Authorization codes are single-use and hashed; PKCE S256 is required; refresh
tokens rotate on every use;
redirect_urimust match the registration exactly. - While the Google app stays in Testing, refresh tokens expire after 7 days and accounts must be re-linked. Publishing the app stops that, but Gmail, Drive and Contacts are all restricted scopes, so Google verification applies.
Architecture
Rate limits are respected rather than discovered: Google meters quota per user
per second, so per-item detail fetches run five at a time, and a fetch that fails
anyway is counted in unavailable_* rather than silently dropped from the
results.
Tests
All plain Node — no framework. 564 checks.
Mounting it inside an existing app
src/app.js is the whole standalone server and contains nothing but the
connector, so embedding it is a copy of that file's body: require the two
routers (routes/mcp, routes/gmail_link), keep the rate limits and the four
.well-known discovery routes, and run the migration alongside your own.
One thing not to skip: MCP tokens are signed with a key derived from
JWT_SECRET by HMAC rather than with JWT_SECRET itself. If your app signs its
own user tokens with that secret and verifies them without an audience check,
sharing the key would make every connector token a valid user token for your
app. npm run test:tokens asserts the separation in both directions.
Contributing
Issues and pull requests are welcome. Two conventions worth knowing first:
- Suites are plain Node — no framework, no new dependencies. Each is a file
under
test/with annpm run test:<name>script, one line per check, and a non-zero exit on failure. - Tests assert on what goes over the wire. They stub
global.fetchand check the query parameters, the request path, the multipart framing. The failures worth catching here are calls that succeed while asking the wrong question, and only a wire-level assertion sees those.
PROJECT-LOG.md records why things are the way they are, including the
alternatives that were rejected. Read it before changing a decision.
License
MIT — see LICENSE.
來源:README.md,提交 2197711
工具
0版本歷史
1- v1.0.0最新Sep 16, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通必需25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式必需10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分