account-vault-mcp-server
A local MCP server that manages multiple Gmail accounts simultaneously, allowing MCP clients to switch between accounts per request without logging out or in. It securely stores OAuth tokens in the macOS Keychain.
README
account-vault-mcp-server
Repo: https://github.com/joeshwoa/account-vault-mcp-server
Claude's (and ChatGPT's, and every other app's) built-in Gmail connector holds exactly one Google account at a time — connecting a second one logs the first one out. This is a local MCP server that fixes that: it holds many logged-in accounts at once and lets any MCP client pick the right one per request, with no logout/login switching, ever again.
This is a plain Model Context Protocol server talking stdio — nothing in it is specific to any one app. It works identically in Claude Desktop, Claude Code, Cursor, Windsurf, Cline, Continue, VS Code's MCP support, or any other MCP-compatible client or agent, including ones not listed here, since MCP is an open standard, not a Claude or Cursor feature.
It runs entirely on your machine. Nothing is hosted, nothing but Google ever sees your tokens, and this README tells you exactly where they're stored.
Quick install
Already installed? The easiest way, zero typing, ever: double-click
Open Account Vault.command, the file sitting right in this folder. It opens the
control panel in your browser. That's the whole interaction. (The very first time, macOS
may warn "unidentified developer" since you made this file yourself — right-click it and
choose Open once instead of double-clicking, confirm, and it'll open normally every
time after that.)
Not installed yet, and would rather an AI handle that part too? Open
AI_SETUP_PROMPT.md, copy everything in it, and paste it as a
message to an AI coding assistant that can run terminal commands on your Mac — Claude
Code, Cursor, or similar. It installs everything, wires it into your AI app, then hands
you off to the same double-click panel above for the account part.
Prefer doing it all by hand? Keep reading — everything below is the same steps typed by you, using the command line instead of double-clicking.
How this actually works (read this before you rely on it)
You asked for something that works like a wallet — log an account in once, and it's just available forever, and it should be dynamic: adding things shouldn't require rewriting code. Here's exactly how much of that is true, and where the real limits are:
- Adding another account for a service that's already supported (e.g. a 3rd, 4th, 5th Gmail account) is fully dynamic. Run one command, log in once in the browser, done — zero code changes, and every MCP client using this server picks it up immediately.
- Adding support for a brand-new kind of connector (Google Calendar, Notion, Slack, ...) is not literally automatic — no system can do that, because every service has a different API and a different login flow. What this project gives you instead is a small, consistent template (an "adapter") so adding one is a contained, ~30-60 minute task instead of a rewrite. The vault core, the CLI, and every MCP client config never change when you add one. See Adding a new connector below.
This ships with one adapter built and fully tested: Gmail. It's built as the reference implementation the pattern is proven against.
Not Google-only, and not a replacement for your other MCP connectors
Two things worth being precise about:
It's not Google/OAuth-specific. The adapter interface (src/types.ts) supports two
kinds of login: a browser OAuth2 flow (what Gmail uses) and a static API-key flow — paste
a token/key once, no browser involved — for services like Supabase, most SaaS APIs, or
anything else that authenticates with an API key instead of an OAuth dance. See
Adding a new connector for a complete worked example of the
API-key style.
It doesn't replace, duplicate, or touch your other MCP connectors — the official
Gmail connector, Supabase's own MCP, or anything else you already have connected. This
server's tools live under their own names (gmail_search_messages, not whatever the
official Gmail connector calls its search tool), so it sits alongside what you already
use rather than competing with it.
One important, honest limit: this vault cannot make an already-existing MCP connector (the official Gmail connector, Supabase's official MCP, etc.) switch which account it is using. Those run as hosted services — their credentials live server-side with Anthropic, or with Supabase, with no local hook for anything (this tool included) to reach in and swap. That's an access boundary, not a missing feature — no locally-run tool could do that for a hosted connector, by design, since allowing it would be a security hole. What this vault gives you instead is a second, independent way to reach the same kind of account (Gmail today), one that was built from the ground up to hold many accounts and switch between them live, with its own tools, no restart, no re-auth. For a connector you self-host and run as a local process yourself (rather than through Claude's hosted connector settings), a credential-switching proxy in front of it is sometimes possible depending on how that specific server reads its credentials — but it's a per-server integration, not something generic, and most self-hosted MCP servers cache their credentials at startup, so even that path usually needs a reconnect rather than a truly live mid-conversation switch. If you have a specific self-hosted server in mind, say which one and that's a real, scoped thing to look into.
Security notes
- Every account's OAuth tokens are stored in your macOS Keychain (via the built-in
securitycommand), not in a plaintext file — even though this project lives on a portable drive. Only non-secret metadata (an account's label and email address) sits indata/accounts.jsonon disk. - This currently only works on macOS, because it's built on the macOS Keychain. See Troubleshooting if you ever need to run it elsewhere.
- Because this drive is portable, turn on FileVault (or equivalent full-disk encryption) on any machine it's plugged into — the Keychain protects the tokens, but there's no reason to make it easier than necessary for someone with physical access.
- The Gmail scope this server requests (
gmail.compose) technically permits sending mail, because Google doesn't offer a narrower "drafts only, never send" scope. This server intentionally never calls the send endpoint — every write tool only creates or updates drafts, which you review and send yourself from Gmail. If you deliberately want auto-send later, that's a conscious change tosrc/adapters/gmail/tools.ts, not something this ships with. npm run buildruns TypeScript compilation only; nothing here phones home or reports usage anywhere.
One-time setup
0. Get the code built
Already have this exact folder (e.g. this is Joshua's original copy on the portable
SSD)? npm install and npm run build have already been run — dist/ is ready to go,
skip to step 1.
Starting fresh from GitHub instead (a different machine, or someone else trying this)?
git clone https://github.com/joeshwoa/account-vault-mcp-server.git
cd account-vault-mcp-server
npm install
npm run build
Either way, if you ever change the source or pull updates, re-run npm install && npm run build.
1. Create a Google OAuth client (one-time, reused for every Gmail account you add)
You only do this once — the same client ID/secret works for unlimited Gmail accounts.
- Go to console.cloud.google.com and create a new project (any name).
- APIs & Services → Library → search "Gmail API" → Enable.
- APIs & Services → OAuth consent screen (may be labeled "Google Auth Platform") → Get started → fill in an app name and your email → user type External (unless you're on Google Workspace and want to restrict this to your org) → step through the wizard.
- Still on the consent screen, open Data Access → Add or remove scopes → add
these four, then save:
https://www.googleapis.com/auth/gmail.readonlyhttps://www.googleapis.com/auth/gmail.labelshttps://www.googleapis.com/auth/gmail.composehttps://www.googleapis.com/auth/userinfo.email
- Open Audience → Test users → add every Gmail address you plan to connect (your personal Gmail, your work Gmail, etc.). Google blocks OAuth for External apps in Testing mode unless the account is listed here — this step is easy to miss.
- APIs & Services → Credentials (or Google Auth Platform → Clients) → Create Client → Application type Desktop app → name it anything → Create. Copy the Client ID and Client Secret it gives you.
2. Save the OAuth client, then log in each account
Easiest: skip everything below and run npm run panel instead — see
Local control panel. What follows is the command-line version of
the same thing.
From this folder:
cd /Volumes/PortableSSD/MCP/account-vault-mcp-server
# one-time: paste the Client ID / Client Secret from step 1
node dist/cli.js config gmail <CLIENT_ID> <CLIENT_SECRET>
# once per account — repeat with whatever labels make sense to you
node dist/cli.js add gmail personal
node dist/cli.js add gmail work
Each add opens your browser to Google's normal sign-in + consent screen. Because the
app is in "Testing" publishing status, Google will show an "unverified app" warning —
that's expected for a personal-use app; click Advanced → Go to (your app name) to
continue. Once you approve, the tab confirms and the terminal prints the account's email.
Check what's configured any time with:
node dist/cli.js list
Local control panel
Double-click Open Account Vault.command in this folder. Or, from a terminal:
npm run panel
Either way, this starts a small web page at http://127.0.0.1:4790 (only reachable from this computer —
never exposed to your network) and opens it in your browser automatically. From there you
can, without touching a terminal again:
- Paste your Google OAuth Client ID/Secret (the one-time step from above), for Gmail or any future OAuth2 adapter.
- Connect a Gmail account — click "Connect Gmail," give it a label, and it sends you through the normal Google sign-in in your browser, then brings you back with the account added.
- Add an account for any API-key-based adapter — a plain form for whatever fields that service needs.
- See every account currently configured, and remove any of them.
- Check for and apply updates from GitHub with one click.
- Copy the exact MCP config snippet for Claude Desktop, Claude Code, or Cursor, with the real path to this folder already filled in.
It's built on the same underlying functions as the CLI (node dist/cli.js ...) — nothing
it does is different from, or less safe than, running those commands by hand. Close the
terminal window/tab running it (or Ctrl+C) when you're done; it doesn't need to stay
running for the MCP server itself to work.
Connect it to your MCP client(s)
This is a standard MCP server (stdio transport) — the exact same server works in every client. The entry point is:
/Volumes/PortableSSD/MCP/account-vault-mcp-server/dist/index.js
Claude Desktop
Edit (or create) ~/Library/Application Support/Claude/claude_desktop_config.json. If it
already has an mcpServers block from something else, add account-vault alongside it
rather than replacing the file:
{
"mcpServers": {
"account-vault": {
"command": "node",
"args": ["/Volumes/PortableSSD/MCP/account-vault-mcp-server/dist/index.js"]
}
}
}
Restart Claude Desktop afterward. To confirm it connected: start a new chat and click the
tools/hammer icon near the message box — account-vault-mcp-server's tools
(vault_list_accounts, gmail_search_messages, etc.) should be listed there. (That
config file also lives under Settings -> Developer -> Edit Config if you'd rather get
there through the UI than find the file yourself.)
Claude Code
claude mcp add --transport stdio --scope user account-vault -- node /Volumes/PortableSSD/MCP/account-vault-mcp-server/dist/index.js
--scope user makes it available in every project, not just the current one. Verify with
claude mcp list. (If this exact flag syntax has changed by the time you run it, claude mcp add --help will show the current form — the underlying idea, pointing it at
node .../dist/index.js, doesn't change.)
Cursor
Edit (or create) ~/.cursor/mcp.json for a global setup (available in every project), or
.cursor/mcp.json inside one project folder to scope it there:
{
"mcpServers": {
"account-vault": {
"command": "node",
"args": ["/Volumes/PortableSSD/MCP/account-vault-mcp-server/dist/index.js"]
}
}
}
Any other MCP-compatible app
Same pattern everywhere: point it at command node with one argument, the absolute path
to dist/index.js shown above. That's the entire integration surface — nothing about the
server changes per client.
Using it
No new commands to learn — once it's connected, just talk to Claude/Cursor/whatever app you wired it into, the same way you'd talk about anything else. It figures out which account you mean from context, and asks if it's genuinely unsure.
Things you can say:
- "What's in my personal Gmail unread right now?"
- "Check my work inbox for anything from finance"
- "Search my personal Gmail for that flight confirmation from last week"
- "Draft a reply in my work account to the thread about the Q3 budget" (it'll create the draft and tell you it's waiting in Gmail for you to review and send — it never sends on its own)
- "What Gmail accounts do I have connected?" (this calls
vault_list_accountsdirectly) - "Archive that email" / "mark it read" / "star it" (once you've looked at a message together)
If you have more than one Gmail account configured and don't say which one, it will ask
(or check vault_list_accounts itself) instead of guessing and picking the wrong inbox.
It will never send an email for you. Every write action is a draft — search, reading, labels, and drafts are the only things it can touch. You always send from Gmail yourself.
Tools reference
| Tool | What it does |
|---|---|
vault_list_accounts |
List every account configured, across every connector |
vault_list_services |
List which connectors this server supports |
gmail_search_messages |
Search one account with Gmail's native search syntax |
gmail_get_message |
Full headers + plain-text body of one message |
gmail_get_thread |
Every message in a conversation, in order |
gmail_list_labels |
List labels and their IDs |
gmail_list_drafts |
List saved drafts |
gmail_create_draft |
Create a draft (never sends) |
gmail_update_draft |
Edit an existing draft (never sends) |
gmail_modify_labels |
Add/remove labels on a message (archive, star, mark read, ...) |
Adding a new connector
The vault core (src/vault/), the CLI (src/cli.ts), and src/index.ts are all generic
— they loop over whatever's in src/adapters/index.ts and switch on each adapter's
auth.kind, and never need to change. Every adapter picks one of two login styles:
oauth2— a browser sign-in + consent screen (what Gmail uses). Right for Google services, and most other modern SaaS APIs (Slack, Notion, etc.).apikey— paste a token/key once, no browser involved. Right for services like Supabase, or anything else authenticated with an API key or personal access token.
To add one:
- Copy
src/adapters/gmail/tosrc/adapters/<service>/as a starting template. - In
adapter.ts, implementauthfor whichever kind fits the service (see theOAuth2AuthHooks/ApiKeyAuthHookstypes insrc/types.ts, and the example below). - In
tools.ts, write the MCP tools you want (search, read, write, whatever the service needs), following theToolDefshape insrc/types.ts. - Add the new adapter to the array in
src/adapters/index.ts. That's it —vault_list_accounts, the CLI'sadd/list/removecommands, and every connected MCP client pick it up automatically, with no other code touched. npm run build. OAuth2 services neednode dist/cli.js config <service> ...once first; API-key services skip straight toadd.
Worked example: an API-key adapter
This is the entire auth block for a hypothetical API-key-based service — no browser
flow, no app-level config step:
// src/adapters/example-service/adapter.ts
import type { Adapter, ApiKeyAuthHooks } from "../../types.js";
import { exampleServiceTools } from "./tools.js";
const auth: ApiKeyAuthHooks = {
kind: "apikey",
fields: [
{ name: "apiKey", label: "API key", secret: true },
{ name: "projectUrl", label: "Project URL" },
],
toSecret(values) {
return { apiKey: values.apiKey, projectUrl: values.projectUrl };
},
async verifyAndFetchDisplayName(secret) {
// Make one lightweight authenticated call to confirm the key works, and return
// something human-readable (a project/workspace name) for vault_list_accounts.
const res = await fetch(`${secret.projectUrl}/whoami`, {
headers: { Authorization: `Bearer ${secret.apiKey}` },
});
if (!res.ok) throw new Error(`Couldn't verify credentials (${res.status}). Check the API key and URL.`);
const data = await res.json();
return data.projectName ?? String(secret.projectUrl);
},
};
export const exampleServiceAdapter: Adapter = {
service: "example-service",
displayName: "Example Service",
auth,
tools: exampleServiceTools,
};
Running node dist/cli.js add example-service work then prompts for API key (masked)
and Project URL, verifies them, and stores the result in the Keychain — same
vault_list_accounts, same automatic per-call account switching, same everything else,
for free.
Publish this on GitHub
This folder is already a git repository with an initial commit — node_modules/, dist/,
and your account data/OAuth secrets (data/*.json) are all git-ignored, so nothing
sensitive is in the commit. Publishing it is one command if you have the
GitHub CLI installed:
cd /Volumes/PortableSSD/MCP/account-vault-mcp-server
gh auth login # one-time, opens your browser — skip if already logged in
gh repo create account-vault-mcp-server --source=. --remote=origin --push --private
Use --public instead of --private if you want it open. That's it — gh creates the
repository on your GitHub account and pushes this commit in one step.
No gh installed, or don't want to use a terminal at all? Two other options:
- Install it first:
brew install gh(if you have Homebrew), then run the block above. - Fully manual: create a new, empty repository at github.com/new
(don't check "Add a README" — this folder already has one), then run:
cd /Volumes/PortableSSD/MCP/account-vault-mcp-server git remote add origin <the URL GitHub gives you> git push -u origin main
AI_SETUP_PROMPT.md step 9 also covers this if you'd rather have an AI assistant drive it
interactively with you.
Why I couldn't just do this step for you: publishing needs your GitHub credentials, and I intentionally don't have a way to take those from you in chat — pasting a token here would be a real way to leak it. Everything up to the push is already done for you either way.
Staying up to date
Every time the server starts, it does one quick, read-only, non-blocking check against
GitHub (git ls-remote — it doesn't download or change anything) and prints a note to
the logs if a newer version is available. It never updates itself silently; applying an
update is always something you explicitly run:
node dist/cli.js check-update # just look, changes nothing
node dist/cli.js update # pull + npm install + npm run build
update only fast-forwards — if you've made local edits (like a custom adapter) it
refuses instead of overwriting them, and tells you to commit or stash first. After it
finishes, fully restart your MCP app (or reconnect the server) so it picks up the new
code.
Troubleshooting
- "No accounts configured yet" — run
node dist/cli.js listto confirm, thenadd. - "Gmail rejected the request (401/403)" — the token was likely revoked. Re-run
node dist/cli.js add gmail <label>to log in again. - Google did not return a refresh_token — Google only issues one the first time an
account consents. Go to
myaccount.google.com/permissions, remove
access for your app, then run
addagain. - "Port 8765 is already in use" (during
add) — something else is using that port; close it and retry, or changeREDIRECT_PORTinsrc/cli.tsand rebuild. - The control panel won't start / port 4790 in use — something else is already using
that port (maybe you already have the panel open in another tab/window — check there
first). Close whatever's using it, or change
PORTinsrc/web/server.tsand rebuild. - Running on something other than macOS — this build refuses to start because it
relies on the macOS Keychain (
src/vault/keychain.ts). To run elsewhere, swap that module for a different secret store (e.g. an OS-appropriate credential manager, or an encrypted file) — the rest of the codebase only talks to it through theToolContextinterface insrc/types.ts, so nothing else needs to change.
Removing an account
node dist/cli.js remove gmail <label>
This deletes both the Keychain entry and the entry in data/accounts.json.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。