jb_gateway_mcp
A local MCP server that acts as a secure credential-holding gateway to Google APIs (Gmail, Calendar, Drive), with deny-by-default policy enforcement so agents never see OAuth tokens.
README
jb_gateway_mcp
A local MCP server that acts as a credential-holding gateway to Google APIs (Gmail, Calendar, Drive) and, via Enable Banking, read-only bank account data (DNB, Nordea, Revolut, ...). AI agents call MCP tools; the server holds every credential and decides — via a deny-by-default policy — what each caller is allowed to do. Agents never see a token, password, API key, or raw account number.
Full design/architecture: DESIGN.md.
Skills & plugins (recommended if you're using Claude Code)
This repo ships one project skill directly, in .claude/skills/:
run-jb-gateway-mcp— launches the server and drives a real MCP session against it end-to-end (handshake, tool discovery,ping, policy-gated tool calls, audit log integrity check). Ask e.g. "run jb_gateway_mcp" or "smoke-test the gateway." This one stays here since it's a server-maintainer concern, not something plugin users need.
The account-connection and finance-report skills — plus domain subagents
— are distributed separately as Claude Code plugins, so they can be
installed standalone without cloning this repo:
jb_claude_pluggins
(jb-finance-mcp-plugin: bank onboarding + finance reports;
jb-google-notify-plugin: Google onboarding + report notifications via
Gmail). Both depend on this server being installed standalone first — see
"Standalone install" below — and walk through app/consent registration,
running the onboarding CLI, and adding the right policy.yaml grants,
verifying the result against live data before calling it done.
The rest of this README is the manual reference for each step: useful if you're not driving this through Claude Code, or want to understand exactly what the skills automate.
Standalone install (for plugin users, or any MCP client)
If you just want the server itself on PATH — e.g. to use the plugins
above, or to point a non-Claude-Code MCP client at it without cloning this
repo — install it as a tool instead of uv sync-ing a clone:
uv tool install --python 3.13 git+https://github.com/jitheshb83/jb_gateway_mcp.git
This puts jb-gateway-mcp, onboard-google, onboard-bank, and
uninstall-google on your PATH. Also create a policy file — the server
won't start without one, even to serve ping:
mkdir -p ~/.jb_gateway_mcp
echo 'callers: {}' > ~/.jb_gateway_mcp/policy.yaml
That's a valid, safe, deny-everything starting point (see §4 below for the
shape once you're ready to add grants) — ~/.jb_gateway_mcp/policy.yaml
is the default JB_GATEWAY_POLICY_FILE path a standalone install resolves
to automatically, unlike the dev-clone path below where it's the repo's
own tracked policy.yaml.
The rest of this README (steps 1–7) still applies for OAuth client setup,
onboarding, and policy.yaml grants — those are one-time, per-account
steps independent of how the server itself got installed — with one
difference: steps 3 and 7b
below show uv run onboard-google ... / uv run onboard-bank ... run
from inside a cloned repo (the uv sync dev path). With a standalone
install, drop both the cd and the uv run prefix — just
onboard-google ... / onboard-bank ... directly, since they're already
on PATH. If you're developing on this repo itself, uv sync + uv run
(as in "Install" below) is the right mode instead.
Already onboarded accounts via a dev-clone install? You don't need to
re-run onboarding for the standalone path — tokens/sessions live in the OS
keychain (keyring), not tied to which install method wrote them. Only
policy.yaml is per-install (each has its own default location), so the
one thing a switch between install modes always needs is its own grants.
Prerequisites
- Python 3.13 (managed automatically by
uv) uv- For Google tools: a Google account you're willing to grant read-only (or send/write) API access to, and a Google Cloud project to create OAuth credentials in
- For bank tools: a free Enable Banking account and the bank(s) you want to connect (DNB, Nordea, Revolut, ... currently — see §7)
1. Install
cd jb_gateway_mcp
uv sync
2. Create a Google OAuth client (one-time, in Google Cloud Console)
The gateway needs its own OAuth client to run the consent flow. You do this once, in your own Google account — nothing here can do it for you:
- Go to Google Cloud Console and create (or pick) a project.
- APIs & Services → Library — enable the Gmail API, Google Calendar API, and Google Drive API.
- APIs & Services → OAuth consent screen — configure it (External is fine for personal use; add your own account as a test user if the app stays in "Testing" mode).
- APIs & Services → Credentials → Create Credentials → OAuth client ID
— Application type: Desktop app. Download the resulting JSON — this
is your
client_secret.json. - Keep this file out of the repo. Store it somewhere outside the
project (e.g.
~/.secrets/jb_gateway_mcp/client_secret.json). The.gitignorehere already blocksclient_secret*.jsonas a backstop, but don't rely on that — don't put it in the repo directory at all.
3. Onboard a Google account
This is a one-time, human-run step per Google account. It opens a browser for you to log in and grant consent; the resulting token is written to your OS keychain — it never touches disk in plaintext and is never visible to any agent.
uv run onboard-google \
--account you@example.com \
--client-secrets ~/.secrets/jb_gateway_mcp/client_secret.json
By default this requests read-only scopes (Gmail, Calendar, Drive). To
also allow sending mail or creating events, pass --scopes explicitly:
uv run onboard-google \
--account you@example.com \
--client-secrets ~/.secrets/jb_gateway_mcp/client_secret.json \
--scopes \
https://www.googleapis.com/auth/gmail.readonly \
https://www.googleapis.com/auth/gmail.send \
https://www.googleapis.com/auth/calendar \
https://www.googleapis.com/auth/drive.readonly
On success it prints the account and granted scopes — never a token value. Re-run this any time a refresh token is revoked (the server will raise a clear re-consent error if that happens mid-use).
4. Grant policy access
The server ships with policy.yaml denying everything by default — no
caller can use any tool until you explicitly grant it. Edit policy.yaml:
callers:
local:
allow:
- tool: gmail.list_messages
scope: gmail.readonly
- tool: gmail.read_message
scope: gmail.readonly
- tool: calendar.list_events
scope: calendar.readonly
- tool: drive.list_files
scope: drive.readonly
- tool: drive.read_file
scope: drive.readonly
# Only add these if you actually want an agent to be able to send
# mail / create events on your behalf:
# - tool: gmail.send_message
# scope: gmail.send
# - tool: calendar.create_event
# scope: calendar.events
local is the default caller identity for v1 (single-user, local stdio
deployment — see DESIGN.md §7). Override it with the
JB_GATEWAY_CALLER_ID environment variable if you want distinct policies
per launching client (see §6 below).
5. Run it standalone (quick manual test)
The server is started via scripts/start.sh — a thin
wrapper that resolves the project root, checks uv and policy.yaml are
present, and execs into uv run jb-gateway-mcp (so a launching client's
process management/signals reach the real server directly, no wrapper
process left in between). This is the same command every client config
below points at.
./scripts/start.sh
This blocks, speaking MCP over stdio — it's meant to be launched by a client, not run interactively. To sanity-check it without a full client, run the automated test suite instead:
uv run pytest -q # 115 tests: unit + a real stdio round-trip test
uv run ruff check .
uv run mypy .
The stdio round-trip test in tests/test_server.py spawns the real server
process and calls ping over a real MCP session — the same mechanism any
client uses.
For a fuller live check (handshake, all 13 tools discovered, ping, policy
enforcement on the Google tools, and an audit-log integrity check), run the
project skill's smoke test:
uv run python .claude/skills/run-jb-gateway-mcp/scripts/smoke_test.py
6. Connect a real client
Every client config below launches scripts/start.sh
with JB_GATEWAY_CALLER_ID=local — the same caller id already granted
read-only Gmail/Calendar/Drive access in policy.yaml and verified working
end-to-end. This repo is a single-user, local deployment (see
DESIGN.md §7), so every client sharing one caller id is
intentional, not a shortcut — they all run as you, on your machine. If you
later want per-client policies (e.g. a stricter grant set for one client),
give it its own JB_GATEWAY_CALLER_ID and add a matching entry under
callers: in policy.yaml — until you do, any caller id with no entry
there is denied everything by default.
Claude Desktop
Copy config/claude_desktop_config.example.json
into your Claude Desktop config
(~/Library/Application Support/Claude/claude_desktop_config.json on
macOS), replacing the placeholder path with this repo's absolute path, then
restart Claude Desktop.
Claude Code
A ready-to-use .mcp.json already exists at this repo's root
(project-scoped — Claude Code picks it up automatically when you open this
folder). If you'd rather register it globally instead, claude mcp add is
the CLI route — run claude mcp --help to confirm the exact current flags
for your installed version.
Any other MCP client (Cursor, Windsurf, Cline, etc.)
Most MCP clients use the same mcpServers JSON shape. See
config/mcp_client_generic.example.json
and that client's own docs for where its config file lives.
7. Connect a bank account (DNB, Nordea, Revolut, ...)
Independent of the Google setup above and §6 — do this before, after, or
without ever doing them; it's a separate provider with its own app
registration and onboarding CLI. bank.* tools are backed by Enable
Banking, a licensed AISP aggregator (direct bank PSD2 APIs require being a
regulated TPP with an eIDAS certificate — not viable for a personal
project).
7a. Register an Enable Banking application (one-time, in their Control Panel)
- Sign in at enablebanking.com/sign-in/ (email + magic link — no business registration needed).
- Control Panel → API applications → Add a new application.
- Name: anything identifiable.
- Redirect URL: exactly
https://localhost:8080/callback— Enable Banking requireshttps://, with no plain-http localhost exception (unlike Google). - Privacy/Terms URL: required fields, but not validated while the app stays in Restricted mode (own-accounts-only, which is what this project uses) — any placeholder URL works.
- Let the browser generate the private key rather than supplying your
own — it downloads once as
<application-id>.pemand never leaves your machine.
- Keep the
.pemout of the repo — e.g.~/.secrets/jb_gateway_mcp/enablebanking/<application-id>.pem, same convention asclient_secret.json. - Activate the application. A freshly registered app starts
"Inactive" and returns
403 Forbiddenon every API call until you click "Activate by linking accounts" in the Control Panel and complete one bank login through their hosted UI. Do this once per institution you plan to connect (DNB, Nordea, Revolut, ...) — Restricted mode only ever serves accounts that have gone through this linking step.
7b. Onboard each institution
uv run onboard-bank --institution dnb \
--application-id <uuid> \
--private-key ~/.secrets/jb_gateway_mcp/enablebanking/<uuid>.pem
--application-id/--private-key are only needed the first time — every
institution after that reuses the stored app credential:
uv run onboard-bank --institution nordea
uv run onboard-bank --institution revolut
This is interactive: it opens a bank login URL in your browser, and after
you complete BankID/SCA login, the browser fails to load the final
redirect page (https://localhost:8080/callback?...) — that's expected,
nothing is listening there. Copy the full URL from the address bar and
paste it back at the terminal prompt; the CLI extracts the authorization
code from it. On success it prints e.g. dnb onboarded: 1 account(s) linked, consent valid until 2026-10-30 — never a secret value.
Consent is SCA-backed and valid for 90 days; re-run the same command for the same institution to refresh it — there's no separate "refresh" command, and no way to extend a session without a fresh login (PSD2 requires it).
Supported institution aliases (see
src/jb_gateway_mcp/cli/onboard_bank.py):
dnb, nordea, revolut — all currently Norway (NO). Adding a new one is
a two-line code change.
7c. Grant policy access
callers:
local:
allow:
- tool: bank.list_accounts
scope: bank.readonly
- tool: bank.get_balance
scope: bank.readonly
- tool: bank.summarize_spending
scope: bank.readonly
- tool: bank.list_transactions_summary
scope: bank.readonly
# Adds counterparty name + payment description to transaction results
# (IBANs stay masked either way). Off by default:
# - tool: bank.list_transactions_detailed
# scope: bank.transactions.detailed
Tiered by design: the default read-only tools never return counterparty
names, payment descriptions, or raw IBANs (every IBAN — the account
holder's own, and any counterparty's — is masked to its last 4 digits).
bank.list_transactions_detailed is the only tool that adds
counterparty/description text, and it needs its own explicit grant.
7d. Verify
The connect-bank-account skill's status-check script (in the
jb_claude_pluggins
jb-finance-mcp-plugin) reports connection status (or "not
connected"/"EXPIRED") and a live balance check for every onboarded
institution:
uv run python skills/connect-bank-account/scripts/check_bank_status.py --live
(run from a clone of that plugin's repo, at its own root directory — see
its README for the one-time uv sync setup; the Claude Code plugin
install itself doesn't give you a directory to cd into by hand).
Environment variables
| Variable | Default | Purpose |
|---|---|---|
JB_GATEWAY_CALLER_ID |
local |
Identity used for every policy check and audit entry in this process. Must match a callers: key in policy.yaml to be granted anything. |
JB_GATEWAY_POLICY_FILE |
~/.jb_gateway_mcp/policy.yaml |
Path to the policy file. Must exist and be valid YAML — the server fails to start if it's missing, even just to serve ping, since policy loads at startup. This repo's own .mcp.json/example client configs set it explicitly to this repo's tracked policy.yaml; a standalone install (see above) needs you to create the default path yourself (mkdir -p ~/.jb_gateway_mcp && echo 'callers: {}' > ~/.jb_gateway_mcp/policy.yaml is a valid, safe, deny-everything starting point) or point this at wherever you keep your own. |
JB_GATEWAY_AUDIT_LOG |
~/.jb_gateway_mcp/audit.jsonl |
Path to the audit log (JSON Lines, one entry per tool call, secrets redacted). Parent directory is created automatically. |
Tool catalog
| Tool | Scope | Notes |
|---|---|---|
ping |
— (ungated smoke-test tool) | Always available, not policy-gated |
gmail.list_messages |
gmail.readonly |
account, query |
gmail.read_message |
gmail.readonly |
account, message_id |
gmail.send_message |
gmail.send |
account, to, subject, body — not granted by default |
calendar.list_events |
calendar.readonly |
account, calendar_id, max_results |
calendar.create_event |
calendar.events |
account, calendar_id, summary, start_iso, end_iso — not granted by default |
drive.list_files |
drive.readonly |
account, query, page_size |
drive.read_file |
drive.readonly |
account, file_id |
bank.list_accounts |
bank.readonly |
institution — masked IBAN only; local keychain read, no live API call |
bank.get_balance |
bank.readonly |
institution, account_uid — cached 60min (see "Bank tool result caching" below) |
bank.summarize_spending |
bank.readonly |
institution, account_uid, date_from, date_to — aggregated totals only, no line items; cached 60min |
bank.list_transactions_summary |
bank.readonly |
institution, account_uid, date_from, date_to — date/amount/currency only; cached 60min |
bank.list_transactions_detailed |
bank.transactions.detailed |
adds counterparty name/description (IBANs still masked) — not granted by default; cached 60min |
Bank tool result caching
Enable Banking enforces a daily, not short-term, per-consent access cap
("consented multiplicity without PSU involvement per day") — a 429 means
that institution's whole day is spent, not "wait and retry." The four
bank.* tools that actually reach the live API (everything above except
bank.list_accounts, which is a local keychain read) cache their result
in memory for 60 minutes, keyed by tool name + exact parameters. A repeat
call with identical parameters within that window returns the cached
result instead of making another live request — still fully audit-logged
(outcome: "cached", same params, distinguishable from "success" in
JB_GATEWAY_AUDIT_LOG), just without reaching the handler. The cache is
in-process memory only — never written to disk, and cleared on every
server restart. This is opt-in per tool (ToolSpec.cache_ttl_seconds in
src/jb_gateway_mcp/adapters/base.py); write/send tools are never
cached.
Network & ports
The gateway itself listens on nothing. It's a stdio MCP server — the client (Claude Desktop/Code, etc.) launches it as a subprocess and talks to it over the process's stdin/stdout pipes. There's no port, no host, no URL, no listening socket at any point during normal operation — it isn't reachable over the network at all, by design (see DESIGN.md).
The one exception is the one-time onboard-google step: it briefly
starts a local HTTP server on localhost:8080 (via
google_auth_oauthlib's InstalledAppFlow.run_local_server) purely to
catch Google's OAuth redirect after you approve consent in the browser. It
shuts down immediately once the redirect arrives — nothing is listening
before or after that single command runs. If port 8080 is already in use on
your machine, that command will fail; there's currently no flag to change
the port, so free up 8080 or temporarily stop whatever else is using it
before running onboard-google.
Troubleshooting
- "no grant for caller X on tool Y" — expected deny-by-default behavior.
Add the grant to
policy.yamlunder the caller id you're using. - Re-consent error mentioning a revoked/expired refresh token — re-run
onboard-googlefor that account. 403 Forbiddenfromonboard-bank— the Enable Banking application (or that specific institution) hasn't been through "Activate by linking accounts" in their Control Panel yet — see §7a step 4.multiple ASPSPs matched institution=...fromonboard-bank— the institution name is genuinely ambiguous in that country (e.g. "DNB" vs. "DNB Corporate Mastercard"); narrow_INSTITUTION_NAME_HINTinsrc/jb_gateway_mcp/cli/onboard_bank.pyfor that alias and retry.NeedsReconsentError/ "consent ... expired" from a bank tool call — the 90-day bank consent lapsed; re-runonboard-bank --institution <alias>.- Audit log — every call (
success,cached,denied, orerror) is recorded atJB_GATEWAY_AUDIT_LOG. Tokens/secrets are redacted before writing.cachedmeans abank.*tool returned a result from the in-memory cache instead of reaching the live API — see "Bank tool result caching" above.
Security notes
- Never commit
client_secret.json, any Enable Banking.pemprivate key, or any file matching*credentials*.json—.gitignoreblocks these as a backstop, but treat it as a backstop, not a guarantee. - Tokens and bank private keys live only in the OS keychain; they're never logged, never returned in a tool response, and never appear in an audit log entry — the audit log only ever records tool call parameters, never results.
gmail.send_messageandcalendar.create_eventare the only write-capable Google tools; they are not granted in the defaultpolicy.yaml— add them deliberately, only for callers that actually need them.- Bank tools are architecturally read-only — the adapter's HTTP helper only
ever issues GET requests; there is no code path capable of initiating a
payment, even though Enable Banking's API separately supports one. Every
IBAN (the account holder's own, and any transaction counterparty's) is
masked to its last 4 digits before it leaves the adapter.
bank.list_transactions_detailedis the only tool that surfaces counterparty names/payment descriptions, and it requires its own, off-by-defaultpolicy.yamlgrant — the default tool set never sends that level of financial detail into an agent's context.
Uninstalling
Deleting the repo folder alone is not enough — stored tokens and the Google-side consent grant live outside it. Full teardown, in order:
-
Remove it from every client you connected it to:
- Claude Desktop — delete the
jb-gateway-mcpentry fromclaude_desktop_config.json, then restart Claude Desktop. - Claude Code — remove/delete
.mcp.json(project-scoped), orclaude mcp remove jb-gateway-mcpif you registered it globally instead. - Any other client — remove its equivalent
mcpServersentry.
- Claude Desktop — delete the
-
Run the uninstall command — revokes the account's grant on Google's side (RFC 7009 token revocation) and deletes its token from the OS keychain, in one step:
uv run uninstall-google --account you@example.comPrompts for confirmation per account (add
--yesto skip); repeatable with multiple--accountflags to clean up more than one at once. If the network call to Google fails, it still deletes the local keychain entry and tells you to revoke access manually at myaccount.google.com/permissions —--keep-remote-grantskips the network call entirely and only deletes locally (e.g. if you already revoked access on Google's side, or the grant was for a different app).Deleting the repo without running this leaves the token sitting in your keychain, and the grant active on Google's side, indefinitely.
-
Delete local state you don't want lingering (all outside the repo, so
rm -rf-ing the project directory won't touch these):- Audit log:
JB_GATEWAY_AUDIT_LOG(default~/.jb_gateway_mcp/) - Your
client_secret.jsoncopy, wherever you stored it outside the repo
- Audit log:
-
Remove the project itself:
rm -rf /path/to/jb_gateway_mcp # deletes .venv and all repo files together
Steps 1–4 are the parts people usually forget — the repo directory is the least sensitive thing to clean up here.
Uninstalling bank access
There's no uninstall-bank command yet (unlike uninstall-google) — bank
access is currently removed in two manual steps instead of one:
- Revoke on Enable Banking's side — in their Control Panel, revoke the linked account or delete the application entirely. This is the step that actually matters for security; it's the equivalent of myaccount.google.com/permissions for banks.
- Delete the local keychain entries —
keyringstores these under the OS's native secret store (Keychain on macOS, Credential Manager on Windows, Secret Service on Linux), under service namesjb_gateway_mcp:enablebanking_app(the app credential, one entry) andjb_gateway_mcp:enablebanking_session(one entry per institution alias you onboarded, e.g.dnb/nordea/revolut). Search forjb_gateway_mcp:enablebankingin your OS's credential manager UI (e.g. Keychain Access.app on macOS) and remove them, or delete a specific institution's private key file if you also want that gone (~/.secrets/jb_gateway_mcp/enablebanking/).
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.