MCP server that lets Lobs/OpenClaw talk to a local Gmail+Google Calendar bridge over localhost HTTP (internal-only).
- Gmail access is read-only.
- Calendar access is read by default, with per-calendar write allowlisting enforced by the bridge.
This repo contains two layers that must be kept in sync:
- Bridge protocol (methods + params + result shapes) — what the privileged “other user” service implements.
- This MCP server + CLI wrappers — what Lobs/OpenClaw calls.
If you add/change a bridge method, you must update:
- the MCP tool mapping in
src/index.ts - the docs below
- and (optionally) any local CLI conveniences
The bridge listens on localhost HTTP (default: http://127.0.0.1:17381) and speaks JSON.
GET /health→{ ok: true }POST /callwith{ id, method, params }→{ id, ok, result | error }
Request:
{"id":"...","method":"calendar.upcoming","params":{"hours":72,"tz":"America/New_York"}}Response:
{"id":"...","ok":true,"result":{}}Methods used by this MCP:
pinggmail.unread{ max }gmail.search{ q, max }calendar.upcoming{ hours, tz, calendarId? }calendar.create{ calendarId?, event }calendar.update{ calendarId?, eventId, patch }calendar.delete{ calendarId?, eventId }
npm install
npm run buildOne command build+run (bash script):
./bin/start-mcp(If you already built it and just want to run the compiled server: npm start.)
npm run devA small CLI that calls the localhost HTTP bridge directly (so you don’t have to remember URLs or write ad-hoc test code).
# after npm install && npm run build
lobs-bridge ping
lobs-bridge health
lobs-bridge gmail-unread --max 10
lobs-bridge gmail-search "is:unread newer_than:7d" --max 20
lobs-bridge calendar-upcoming --hours 48 --tz "America/New_York"
lobs-bridge calendar-listNice ergonomics:
- global overrides:
--url,--timeout-ms,--token - JSON input can be inline, from a file (
@event.json), or stdin (-) --jsonfor pretty JSON output
If you need to hit an arbitrary bridge method:
lobs-bridge call calendar.upcoming --params-json '{"hours":48,"tz":"America/New_York"}'If you want a dead-simple shell way to send one request and block until a response:
./bin/bridge-call ping
./bin/bridge-call gmail.unread '{"max":10}'This is intentionally “dumb”: it’s for quick manual testing that the bridge is alive and returning the expected shapes.
- Bring up the bridge (socket) reliably:
cd ~/lobs-mcp
./bin/upThis ensures:
- the
gcal-bridgeuser service is installed + started /tmp/gcal-bridge.sockexists
Logs:
journalctl --user -u gcal-bridge -f- Set up Google auth (one time)
Put your OAuth client JSON at:
~/.config/lobs-mcp/google-credentials.json
Then run:
cd ~/lobs-mcp
npm run build
lobs-google-authThis writes:
~/.config/lobs-mcp/google-token.json
Scopes:
- Calendar: read+write API scope (Google scopes aren’t per-calendar; we enforce RO/RW locally via
calendar-acl.json) - Gmail: read + mark read/unread (NO send)
- Calendar access control (RW vs RO)
The bridge enforces per-calendar permissions via:
~/.config/lobs-mcp/calendar-acl.json
Example (default read-only; explicitly allow write only where you want it):
{
"default": "read",
"calendars": {
"primary": "write",
"someone@group.calendar.google.com": "read"
}
}If the file is missing, the safe fallback is effectively:
{ "default": "read", "calendars": {} }To discover calendar IDs, use the new MCP tool calendar_list (or bridge method calendar.list).
Defaults:
- HTTP:
http://127.0.0.1:17381
For the “bridge runs as another user” case, HTTP is the easiest option (no Unix-socket permission headaches).
- Bridge runs as
mcpuservia systemd and binds to127.0.0.1only - MCP server runs as
rafeand talks tohttp://127.0.0.1:17381
Override (optional):
LOBS_BRIDGE_SOCKETLOBS_BRIDGE_TIMEOUT_MSGOOGLE_CREDENTIALS_PATHGOOGLE_TOKEN_PATH
Resolution order (highest → lowest priority):
- Environment variables
- Repo-local
.env ~/.config/lobs-mcp/config.env- Built-in defaults