Um segundo cérebro poderoso para a sua IA preferida. Entregue o contexto certo e tenha o melhor assistente onde você preferir.
Zinom.ai une suas reuniões (Granola), sua base de conhecimento (Notion) e sua rotina (Calendar) num índice pesquisável, e expõe tudo isso para o seu assistente de IA via MCP — para ele buscar contexto e ainda criar tarefas, páginas e sistemas no Notion por você.
It's a self-hosted MCP server (Node + Express + PostgreSQL/pgvector + PM2) with a multi-tenant, self-service portal on top. No SaaS — everything runs on one box.
Your AI (Claude Code, claude.ai, Cursor, …) ──MCP──▶ Zinom.ai ──▶ your Notion · Granola · Calendar
- Ask your brain anything —
brain_searchdoes hybrid semantic + keyword retrieval (cross-encoder reranker) over everything you've indexed, with real relevance scores and source links. - Read & write Notion — 25 tools to search, fetch, create, update, move, and comment on pages; query and manage databases / data sources; upload files.
- Index more than Notion — Granola meeting notes and Google calendars (via iCal, no Google Cloud), searchable alongside your pages.
- Multi-tenant & self-service — friends onboard themselves through the portal, connect their own sources, and query only their own brain.
1. The hosted product (Zinom.ai). A non-technical person opens the portal,
joins by invite, signs in with a magic link, connects their sources, and plugs
their AI assistant in. No .env, no psql, no command line. See
The portal.
2. Self-host the MCP server. Run it on your own box for a single operator (or many). See Quickstart and Run with Docker.
The portal (served at the site root, e.g. https://zinom.ai) lets each person run
their own second brain, fully isolated from everyone else's.
Onboarding flow
- Invite — the operator mints a single-use code:
npm run make-invite. - Sign in — the friend enters the code + their email and gets a magic link (passwordless). No password is ever set.
- Connect sources — from one page they connect:
- Notion — 1-click OAuth (recommended) or paste a Personal Access Token (easier to integrate; see the in-app tutorial).
- Calendars — add one or more secret iCal links.
- Granola — paste their API key.
- Index — their Notion + Granola + iCal index into their own account.
- Connect their AI — they generate a per-account access token (for Claude Code / Cursor) or add Zinom as a custom connector in claude.ai and authorize with an emailed 6-digit code.
Isolation (the top invariant). Every row carries an account_id; every search
is hard-scoped to the authenticated account, derived from the session/token and
never from request input. One account can never see another's data. Secrets
live in a per-account AES-256-GCM vault (account_secrets) and are shown only
masked, never in plaintext.
Admin. GET /admin (operator BEARER_TOKEN-gated, like /status) renders a
read-only dashboard: accounts, connected sources, MCP tokens, last index runs,
usage metering, and invite stats.
Portal surface
| Path | What |
|---|---|
/ |
Landing + sign-in (invite / magic link) |
/portal/register, /portal/login, /portal/verify |
Magic-link auth |
/portal/sources, /portal/ical, /portal/granola, /portal/notion/connect, /portal/notion/pat |
Manage credentials |
/portal/mcp-token, /portal/reindex |
Per-account MCP token + indexing |
/admin |
Operator dashboard |
/mcp, /oauth/* |
MCP endpoint + OAuth (operator + per-account friend flow) |
git clone https://github.com/BrunooMoniz/notion-mcp.git
cd notion-mcp
npm install
cp .env.example .env # fill in values (see Configuration)
npm run migrate # apply DB schema (Postgres + pgvector)
npm run build
npm start # http://localhost:3456 (GET /health to check)At minimum you need one Notion token (NOTION_<WORKSPACE>_TOKEN) and a
BEARER_TOKEN. The brain, portal, and email are optional layers — add them when
you want them.
Requirements: Node.js 20+. For the brain: PostgreSQL 16 + pgvector and a Voyage AI key. For the portal: a
SECRETS_KEYand (for real emails) a Resend key.
The included docker compose stack brings the whole thing up from zero —
Postgres + pgvector, the MCP server, the brain indexer, and the classifier — and
runs the DB migrations automatically.
cp .env.example .env # fill in tokens/keys
docker compose up -d # builds the image, starts db -> migrate -> appServer on http://localhost:3456 (GET /health). Compose provides Postgres
(pgvector/pgvector:pg16) and sets POSTGRES_URL for you; a one-shot migrate
service applies the schema before the app boots. You still fill in tokens/keys in
.env. (Production on the VPS uses PM2 — see Production.)
- claude.ai / Claude Desktop: Settings → Connectors → Add custom connector →
paste the server URL (e.g.
https://zinom.ai/mcp). The authorize screen signs the friend in (portal session = 1 click, otherwise email + 6-digit code) and issues a token scoped to their account. - Claude Code / Cursor: generate a token in the portal and run the shown
command:
claude mcp add --transport http zinom https://zinom.ai/mcp --header "Authorization: Bearer acct_…"
For claude.ai as the operator: open the registration window
(curl -X POST .../admin/open-registration -H "Authorization: Bearer YOUR_BEARER_TOKEN"),
add the server URL in claude.ai, and on the consent screen pick workspaces + enter
your admin password (PKCE S256). The token is scoped to those workspaces.
For claude.ai as a friend (no operator, no curl): sign into the portal and click
Liberar conexão ao Claude.ai in the connector card. That opens a short
self-service registration window (POST /portal/connect-window, session-gated,
default 5 min — override with PORTAL_CONNECT_WINDOW_MINUTES); then add the server
URL in claude.ai and authorize with your email + 6-digit code within the window.
A local PostgreSQL + pgvector index. A background brain-indexer pulls from
your sources, embeds it, and brain_search retrieves it.
| Source | How |
|---|---|
| Notion | indexer crawls data sources shared with the integration; brain_index_url adds a specific page/DB on demand |
| Granola | via the Granola API (summary by default; raw transcript opt-in) |
| Calendars | each calendar's private iCal URL — multiple calendars, even across Google accounts, no Google Cloud |
| Web | brain_index_web adds any URL on demand; optional periodic feed via WEB_SOURCES |
Retrieval: Voyage voyage-3-large embeddings + accent-insensitive Portuguese
full-text, fused with Reciprocal Rank Fusion over an over-fetched pool, then
reranked by Voyage rerank-2.5-lite. Reads are scoped by account (multi-tenant
hard guard) and by workspace (defense in depth).
Calendars (iCal): for each calendar, Google Calendar → Settings and sharing →
Integrate calendar → Secret address in iCal format (.../basic.ics). The operator
lists them in GOOGLE_CAL_ICS; portal friends add them per-account. iCal URLs are
secrets — .env/vault only.
Além do iCal (que é leitura/RAG), cada usuário pode conectar suas próprias
contas Google via OAuth no portal (botão "Conectar conta Google") e operar as
agendas ao vivo pelas tools MCP list_calendars, list_events,
create_calendar_event, update_calendar_event, delete_calendar_event. Os
refresh tokens ficam no vault criptografado (account_secrets, kind
google_oauth), isolados por conta — o calendar_ref é validado contra as
contas conectadas do tenant antes de qualquer operação. delete_calendar_event
exige confirm: true; toda escrita vai pro audit log.
Configuração única no Google Cloud (reusa o projeto OAuth existente):
- Tela de consentimento → adicionar os escopos
https://www.googleapis.com/auth/calendar.readonlyehttps://www.googleapis.com/auth/calendar.events. - Publishing status = "In production" (NÃO "Testing"): evita a expiração de 7 dias do refresh token e a verificação do Google (mostra só o aviso "app não verificado", que o usuário aceita clicando).
- Redirect URI
/google/callbackjá está registrado (reusado). - Env já existentes:
GOOGLE_OAUTH_CLIENT_ID,GOOGLE_OAUTH_CLIENT_SECRET,BASE_URL,SECRETS_KEY.
Dono via Claude Code: o bearer estático resolve account_id = 'bruno'. Para o
dono enxergar pelas tools as contas Google que conectou pelo portal, a conta
'bruno' precisa ter o email do portal dele (account.email), para que o login
do portal caia no mesmo account_id.
Copy .env.example to .env. Key variables:
| Variable | For | Purpose |
|---|---|---|
NOTION_<WS>_TOKEN |
operator | Notion token per workspace (ntn_…) |
BEARER_TOKEN |
operator | static token for Claude Code / /admin / scripts (min 32 chars) |
OAUTH_PASSWORD_HASH |
OAuth | scrypt hash for the operator consent (node scripts/hash-password.mjs '<pwd>') |
BASE_URL |
OAuth/portal | public URL (e.g. https://zinom.ai) — used for links, OAuth metadata, MCP URL |
POSTGRES_URL |
brain | Postgres + pgvector connection string |
VOYAGE_API_KEY |
brain | embeddings + reranker |
SECRETS_KEY |
portal | 64 hex chars (openssl rand -hex 32) — AES-256-GCM vault key |
NOTION_OAUTH_CLIENT_ID / NOTION_OAUTH_CLIENT_SECRET |
portal | Notion public-OAuth app (for friend "Connect Notion") |
RESEND_API_KEY / PORTAL_EMAIL_FROM |
portal | transactional email (magic links + login codes) |
GRANOLA_<WS>_TOKEN, GOOGLE_CAL_ICS, INDEXER_CRON, RERANK_*, NORA_READONLY |
no | sources / schedule / tuning |
PORTAL_COOKIE_SECURE, PORTAL_SESSION_COOKIE_DOMAIN, PORTAL_PAGES_ORIGIN |
no | cookie/CORS tuning for split-domain deploys |
In dev/test, PORTAL_EMAIL_DEV=1 logs the magic link / code instead of sending.
See .env.example for the full, commented list.
pm2 start ecosystem.config.cjs # notion-mcp + brain-indexer + brain-classifier + nightly reindexThe hosted Zinom.ai routes zinom.ai → the VPS (the app at localhost:3456) via
Cloudflare. Operational runbook: specs/001-account-portal/DEPLOY.md.
src/
index.ts Express server, MCP session lifecycle, auth middleware
tools.ts 25 Notion tool definitions (Zod schemas + handlers)
clients.ts Notion API clients per workspace
oauth.ts OAuth 2.1 server — operator (password) + friend (per-account) flows
account-bearer.ts per-account MCP bearer tokens (hash at rest)
account-tokens.ts per-account vault-token resolution
secrets.ts AES-256-GCM per-account secret vault
context.ts AsyncLocalStorage — per-request account + workspace scope
notion-oauth.ts Notion public-OAuth + PAT onboarding
portal/ friend portal: session, invites, magic-link, email, sources, routes
admin/ operator dashboard (read-only)
rag/ brain indexer + hybrid search (pgvector)
index-account.ts per-account indexing (Notion + Granola + iCal)
*-source.ts notion / granola / calendar-ics / web sources
classifier/ LLM page classifier + spaced-repetition Revisitar
scripts/ migrate.mts + migrations/0001..0007, make-invite.mts, reindex, eval
portal/ static front (landing, dashboard) served at the site root
tests/e2e/ Playwright e2e (portal flows)
Multi-tenant primitives: account / account_workspaces / account_secrets
/ account_api_tokens / usage_log (migrations 0005–0006) + invite_codes /
magic_links / portal_sessions + account.email (0007). Per-account chunks are
namespaced via prefixChunkIds(accountId) and every read is account_id-scoped.
Notion API pinned to 2025-09-03. Security: OAuth 2.1 (PKCE S256, scrypt
password, brute-force lockout), per-account magic links + bearers stored as SHA-256
hashes, AES-256-GCM secret vault, Helmet, CORS, rate limiting, a JSONL audit log of
every write, and confirm: true guard rails on destructive tools.
Read: notion_search, notion_fetch, notion_get_page,
notion_get_block_children, notion_query_database, notion_get_database_schema,
notion_list_data_sources, notion_get_data_source_schema,
notion_query_data_source, notion_list_users, notion_get_self,
notion_list_comments.
Write (non-destructive): notion_create_page, notion_update_page,
notion_append_blocks, notion_update_page_content, notion_move_page,
notion_create_comment, notion_create_database, notion_update_database.
Write (DESTRUCTIVE — require confirm: true): notion_replace_page_content,
notion_delete_page, notion_update_database with remove_columns.
Files: notion_create_file_upload, notion_send_file_upload,
notion_complete_file_upload.
Brain: brain_search (hybrid semantic + keyword, reranked, account/workspace
scoped), brain_index_url (index a Notion URL/ID on demand), brain_index_web
(index any web page/article).
Google Calendar (account-scoped, multi-conta): list_calendars, list_events
(read), create_calendar_event, update_calendar_event (write),
delete_calendar_event (DESTRUCTIVE — requires confirm: true).
main is protected — changes land via pull request.
git checkout -b feat/your-change
# ...commit...
gh pr create --base mainRun npm test (node:test), npm run build, and npx playwright test (e2e) before
opening a PR.
MIT