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.

已验证Streamable HTTP可网页运行效率与工作流文件与存储沟通与协作

概览

AI 生成的概览

远程 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 邮箱需要应用专用密码。
安装前请注意
该服务器可以发送邮件、创建和删除日历事件、共享与取消共享云端硬盘文件,并永久删除任务。云端硬盘使用完整 drive 权限,该权限允许永久删除,尽管没有工具暴露此操作。AOL 应用专用密码加密存储,且无法从服务器端撤销。必须保管好 TOKEN_ENC_KEY,丢失后已存凭据将无法解密。可选的 Stripe 变量会启用付费计费。托管版本仍在测试中,首次使用会显示未验证应用提示。

安装

在 SourceWeft 中

  1. 打开 控制台中的 Grounders MCP,将其添加到工作区。
  2. 为需要使用其工具的对话启用该服务。

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.

  1. 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.
  2. Sign in with Google once when asked.
  3. 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

bash
git clone https://github.com/ItsBreeze/google-multi-account-mcp.gitcd google-multi-account-mcpnpm installcp .env.example .env      # fill it in — see Setup belownpm run migrate           # creates the six tablesnpm 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

MethodPathDescription
POST/mcpMCP Streamable HTTP endpoint (Bearer auth)
GET/mcp/oauth/authorizeConsent screen — operator password
POST/mcp/oauth/tokenToken + refresh grants
POST/mcp/oauth/registerRFC 7591 dynamic client registration
GET/.well-known/oauth-protected-resourceRFC 9728 discovery
GET/.well-known/oauth-authorization-serverRFC 8414 discovery
GET/gmail/connectLink an account (repeat per account)
GET/POST/gmail/aolLink an AOL mailbox with an app password (checked against AOL before it is stored)
POST/gmail/unlinkUnlink one, revoking the grant at Google (or deleting the stored AOL app password)
GET/POST/ai/k/<link>Reply link for an AI agent without MCP: GET reads its thread, POST {"text"} answers

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)

AreaToolsNotes
Accountslist_accountsWhich accounts are linked, with token health
Searchsearch_messages, search_threadsGmail 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
Readget_message, get_thread, get_attachmentBodies 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)
Sendsend_message, reply_to_message, forward_messageReplies thread via In-Reply-To/References; forwards carry attachments (10 MB cap, skipped ones named); optional from + fromName send as a verified Gmail send-as alias
Draftscreate_draft, list_drafts, get_draft, update_draft, send_draft, delete_draftcreate_draft with reply_to_message_id drafts an in-thread reply for review — the safe path for AI-written mail; create_draft and send_draft also take from/fromName (send_draft rewrites the draft's From first, Gmail only)
Labelsmodify_labels, list_labels, create_label, update_label, delete_labelmodify_labels takes message_id or thread_id; removing INBOX archives
Trash & spamtrash_message, untrash_message, mark_spamAll take message_id or thread_id; trash is recoverable for 30 days; mark_spam with unmark: true restores

Calendar (9)

AreaToolsNotes
Readlist_calendars, list_events, search_events, get_eventlist_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
Writecreate_event, update_event, delete_eventTimes 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
Repeatscreate_event, update_event, delete_eventrepeat: "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
RSVPrespond_to_eventaccepted / declined / tentative, as the account that was invited; notifies the organiser by default
Schedulingsuggest_timeFree 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

Drive (15)

AreaToolsNotes
Findsearch_files, list_recent_files, get_file_metadata, list_shared_drivesText 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
Readread_file_content, download_file_contentDocs, 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
Writecreate_file, update_file, copy_fileText 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
Commentcomment_on_fileLeave a comment on a draft, or reply to a thread — the review path that changes nothing in the document
Sharingget_file_permissions, share_file, unshare_fileShares 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
Removetrash_file, untrash_fileTrash and restore, within the 30-day window — see the scope note below

Contacts (2) and Tasks (5)

AreaToolsNotes
Contactssearch_contacts, list_contactsRead-only. Searches saved contacts and people the account has corresponded with, so "email Ann" resolves to an address instead of a guess
Taskslist_task_lists, list_tasks, create_task, update_task, delete_tasklist_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

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.

AreaToolsNotes
Talkai_send, ai_inbox, ai_threadai_send with thread answers in place and to defaults to the other side. ai_inbox with wait_seconds (up to 50) holds until a reply arrives. Threads stop at hop 10
Agentsai_agents, ai_set_agentAn agent can be woken by email (one JSON object from a linked mailbox, subject template like Claude -> Lute (hop {hop})) or by webhook (POST {"text"}, headers stored encrypted). A failed wake keeps the message unread and tells the sender

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:

GmailAOL over IMAP
Labels, several per messageFolders, one per message. UNREAD/STARRED are flags; INBOX, SPAM, TRASH and folder names move the message. A change that would need two folders is refused; IMPORTANT and categories do not exist
Ids are permanentAn id is folder + UID, so a move gives the message a new id — every moving call returns it as new_message_id. An id from a folder the server renumbered is refused as stale, never resolved to a different message
Gmail query syntaxTranslated to IMAP SEARCH: from: to: cc: bcc: subject: is:unread/read/starred has:attachment newer_than: older_than: after: before: in: label: larger: smaller:, phrases, OR, -, parentheses. Anything else is refused by name — dropping category:promotions would return every message instead of a subset
Threads by idThreads by the server's THREADID where it has OBJECTID (AOL does), read off each folder's recent mail, plus a Message-ID search for ancestors — gathered across folders, so your replies in Sent count. Servers without THREADID fall back to the References/In-Reply-To chain. AOL cannot search References at all, which is why this is not one search
Sent copies are automaticAfter an SMTP send, Sent is checked for the message's Message-ID and the copy appended only if AOL did not file one itself. Bcc goes on the SMTP envelope only, never in the transmitted header
update_draft edits in placeIMAP cannot edit: the new version is saved, then the old removed, so the draft id changes
Deleting a label keeps the mailDeleting a folder deletes its mail, so only an empty folder can be deleted

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.

FormatHowNotes
.docx / .odtZIP + XMLParagraphs, tabs and line breaks preserved; styles and revision marks dropped
.xlsx / .odsZIP + XMLOne CSV block per sheet, named from the workbook relationships. Cells are placed by column letter, so a gap stays a gap
.pptx / .odpZIP + XMLOne block per slide, in presentation order
.pdfContent-stream parsingFlate, ASCII85 and ASCIIHex filter chains; literal, hex and octal strings; positioning operators become line breaks

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.

SituationWhat happens
You own itThe write happens
You do notA private copy is made in your My Drive and edited there
You want the originaledit_original: true returns a draft and writes nothing
The user approvesconfirm_edit: true applies it

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:

scopeEffect
this_event (default)Changes only the occurrence named by event_id — "move tomorrow's standup"
seriesResolves the occurrence back to its series and changes every one — "make it 10am from now on"

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

  1. 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.
  2. Env — set DATABASE_URL, PUBLIC_BASE_URL, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, TOKEN_ENC_KEY and JWT_SECRET (see .env.example), then deploy. The schema applies itself on boot.
  3. 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.
  4. 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/connect again 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

VariableNotes
DATABASE_URLPostgres. The six tables create themselves on first boot
PUBLIC_BASE_URLThe 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
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETFrom the Web application OAuth client
TOKEN_ENC_KEY32 bytes, base64. Encrypts Google tokens and AOL app passwords at rest. Losing or changing it makes every stored credential undecryptable
JWT_SECRETAny long random string; signs MCP tokens and browser sessions via two separately derived keys. Changing it only forces everyone to authenticate again
LEGACY_OWNER_EMAILOptional, one-time. Adopts mailboxes linked before per-user sign-in existed, on that address's first sign-in
PORTOptional, defaults to 3000. Most platforms set this for you
STRIPE_SECRET_KEY / STRIPE_PRICE_IDOptional. Set both to charge a subscription for tool calls (sign-in, linking and deletion stay free). Unset, billing is off entirely
STRIPE_WEBHOOK_SECRETSigning secret for the <PUBLIC_BASE_URL>/billing/webhook endpoint (events: checkout.session.completed, customer.subscription.*)
BILLING_PRICE_LABELPrice text on the public pages, default $0.99/month — keep it matching the Stripe price
BILLING_EXEMPT_EMAILSComma-separated. Whoever has linked one of these mailboxes never pays

/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_KEY across 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 excludes mail.google.com, so trash_message is 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.file only sees files this app itself created, which cannot answer "find my lease", so searching and editing existing files needs full drive — which does permit permanent deletion. There the limit is enforced by the tool surface instead: trash_file trashes, 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_file withdraws any grant share_file can make, and untrash_file restores what trash_file removed. 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_file changes 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_file takes a named person only. Domain-wide and public-link sharing need DRIVE_ALLOW_PUBLIC_SHARING=true, and are refused outright otherwise rather than quietly narrowed to something safer. Even enabled, they need confirm_public: true as 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: true alongside content, 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_uri must 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

src/services/google_http.js     shared transport: URL building, bearer auth,                                error unwrapping, bounded-concurrency mapLimitsrc/services/gmail_api.js       one thin client per Google API, each returningsrc/services/calendar_api.js    shaped results rather than raw payloadssrc/services/drive_api.jssrc/services/people_api.jssrc/services/tasks_api.jssrc/services/office_text.js     .docx/.xlsx/.pptx/.odt text, via a ZIP readersrc/services/pdf_text.js        PDF content-stream text, with a readability guardsrc/services/text_extract.js    one entry point both Drive and Gmail read throughsrc/services/gmail_accounts.js  linked accounts, token refresh, scope gatingsrc/services/imap_api.js        AOL over IMAP/SMTP, the same surface as gmail_apisrc/services/imap_query.js      Gmail search syntax → IMAP SEARCH, refusing what it can't mapsrc/services/imap_accounts.js   linked AOL mailboxes, app passwords encrypted at restsrc/services/imap_providers.js  host presets — the only place a mail host comes fromsrc/mcp/shared.js               resolveAccount / tokenFor / fanOut / mergeSearch, and                                mailFor / fanOutMail, which pick Gmail or IMAP per mailboxsrc/mcp/tools/                  one module per product, assembled by index.js

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.

bash
# No database, no network:npm run test:tokens     # MCP and user tokens cannot be swapped, in either directionnpm run test:query      # Google query-string construction (repeated array params)npm run test:mime       # entity decoding, body truncation, MIME constructionnpm run test:threads    # address parsing, thread summaries, cross-account fan-outnpm run test:products   # free-slot arithmetic, Drive query quoting, multipart                        # upload framing, task dates, scope gating, tool-surface shapenpm run test:drive-safety   # the sharing and overwrite guards, both env statesnpm run test:extract    # PDF and Office text extraction, against real fixturesnpm run test:drive-parity   # upload framing, conversion, export and comments, on the wirenpm run test:reach      # shared-drive request parameters, recurrence rules, and which                        # event a scoped write actually lands onnpm run test:ownership  # which file id a write reaches, that a draft writes nothing,                        # and that revoking is never gatednpm run test:imap       # AOL: query translation, and the client against a scripted                        # IMAP/SMTP server — moves, ids, Bcc, Sent copies, no expungenpm run test:isolation  # one person's mailboxes, Gmail or AOL, are unreachable by anothernpm run test:ai         # AI messages on in-memory Postgres: threads, wakes, reply links, hops
# Needs a database:DATABASE_URL=postgres://…  npm run test:accounts        # resolution + encryption at restDATABASE_URL=postgres://…  npm run test:imap-accounts   # AOL app passwords at rest, owner scoping
# Needs the API running:npm start &TEST_BASE_URL=http://127.0.0.1:3000  npm run test:mcp   # OAuth + MCP protocol

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 an npm 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.fetch and 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
SourceWeft 尚未读取此服务器的工具。

版本历史

1
  1. v1.0.0最新Sep 16, 2026

评分

已满足适用检查项的 50%

该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。

  • 近 30 天内可连通必需25 分

    请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。

  • 已声明认证方式必需10 分

    请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。

  • 已列出工具15 分

    请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。

  • 描述必需15 分
  • 图标10 分
  • 文档15 分
  • 许可证10 分