Personal Server
FreeNot checkedPrivate MCP server for managing Gmail, custom IMAP/SMTP email, Slack notifications, and personal notes stored in SQLite.
About
Private MCP server for managing Gmail, custom IMAP/SMTP email, Slack notifications, and personal notes stored in SQLite.
README
A private, single-user MCP (Model Context Protocol) server that gives AI agents access to your email, Slack notifications, and a personal knowledge base ("soul docs"). Written in TypeScript, runs over Streamable HTTP or stdio.
Capabilities at a glance:
- Search and read Gmail or any IMAP mailbox
- Send email via Gmail API or custom SMTP (with a prepare→confirm safety flow)
- Post Slack notifications via incoming webhooks
- CRUD a personal knowledge base backed by SQLite or Turso/libSQL
- Configure all credentials at runtime via MCP tools — no
.envediting required
Tools
Mail — Gmail
| Tool | Summary |
|---|---|
gmail_search_messages |
Search Gmail with typed filters (from, to, subject, text, label, unread, date range). Returns summaries. |
gmail_get_message |
Fetch a full Gmail message by ID — headers, plain text body, HTML body. |
Mail — Custom IMAP / SMTP
| Tool | Summary |
|---|---|
custom_mail_search_messages |
Search a custom IMAP mailbox with the same typed filters. |
custom_mail_get_message |
Fetch a full message from the custom IMAP mailbox by UID. |
email_prepare_send |
Validate and stage an email draft. Returns a confirmation ID; does not send. |
email_confirm_send |
Send a previously staged email using its confirmation ID. The two-step flow prevents accidental sends. |
Slack
| Tool | Summary |
|---|---|
send_slack_notification |
Post a message (plain text or Block Kit blocks) to a configured Slack incoming webhook. |
Soul Docs (personal knowledge base)
| Tool | Summary |
|---|---|
get_my_soul_docs |
Search docs by full-text query (title, content, source) or filter by tag. Returns newest first. |
write_my_soul_doc |
Create or update a doc. Pass an id to upsert (preserves original created_at); omit to create new. Each doc has a title, body content, tags, an optional source reference, and arbitrary metadata. |
Setup (runtime configuration)
When MCP_ENABLE_SETUP_TOOLS=true (the default), these tools let you configure every service at runtime and test connections immediately. All changes are in-memory — they override .env values but reset on restart.
| Tool | Summary |
|---|---|
setup_status |
Show which services are configured (no secrets exposed). |
setup_database |
Set a Turso/libSQL URL or local file path, then test the connection. Accepts libsql://, file:, or plain paths like /data/db.sqlite. |
setup_gmail_oauth_start |
Generate a Google OAuth authorization URL. Optionally override client ID, secret, and redirect URI. |
setup_gmail_oauth_complete |
Exchange the OAuth authorization code for a refresh token and store it in the runtime config. |
setup_custom_mail_imap |
Set IMAP host, port, credentials, and mailbox — then test the connection. |
setup_custom_mail_smtp |
Set SMTP host, port, credentials, and default from address — then verify the connection. |
setup_slack_webhook |
Set a Slack incoming webhook URL and send a test notification. |
Set MCP_ENABLE_SETUP_TOOLS=false to disable all setup tools and use .env-only configuration.
Configuration reference
Every setting can be provided via .env or overridden at runtime by the corresponding setup_* tool.
Runtime
| Variable | Default | Notes |
|---|---|---|
MCP_TRANSPORT |
streamable-http |
streamable-http, http (alias), or stdio |
MCP_PORT |
3000 |
HTTP listen port |
MCP_HOST |
127.0.0.1 |
Set to 0.0.0.0 for remote access |
MCP_BEARER_TOKEN |
— | Optional. When set, requires Authorization: Bearer <token> on all requests |
MCP_ALLOWED_ORIGINS |
* |
Comma-separated origins or * for all |
MCP_ENABLE_SETUP_TOOLS |
true |
Set to false to remove setup_* tools |
Database
| Variable | Default | Notes |
|---|---|---|
TURSO_DATABASE_URL |
file:local.db |
file: for local SQLite, libsql:// for Turso, or a plain path |
TURSO_AUTH_TOKEN |
— | Required for Turso remote databases |
TURSO_SYNC_URL |
— | Sync endpoint for embedded replicas |
TURSO_SYNC_INTERVAL_MS |
— | Sync interval in ms for embedded replicas |
Gmail OAuth
| Variable | Required | Notes |
|---|---|---|
GOOGLE_CLIENT_ID |
Yes | From Google Cloud Console |
GOOGLE_CLIENT_SECRET |
Yes | From Google Cloud Console |
GOOGLE_REDIRECT_URI |
Yes | e.g. http://127.0.0.1:3000/oauth/google/callback |
GOOGLE_REFRESH_TOKEN |
— | Obtained via OAuth flow; stored at runtime by setup_gmail_oauth_complete |
GOOGLE_OAUTH_STATE |
— | Optional CSRF state string |
Scopes requested: gmail.readonly, gmail.send.
Custom IMAP
| Variable | Default | Notes |
|---|---|---|
CUSTOM_IMAP_HOST |
— | IMAP server hostname |
CUSTOM_IMAP_PORT |
993 |
|
CUSTOM_IMAP_SECURE |
true |
TLS |
CUSTOM_IMAP_USER |
— | Usually the full email address |
CUSTOM_IMAP_PASSWORD |
— | App password recommended |
CUSTOM_IMAP_MAILBOX |
INBOX |
Custom SMTP
| Variable | Default | Notes |
|---|---|---|
CUSTOM_SMTP_HOST |
— | SMTP server hostname |
CUSTOM_SMTP_PORT |
587 |
|
CUSTOM_SMTP_SECURE |
false |
true for port 465, false for 587 (STARTTLS) |
CUSTOM_SMTP_USER |
— | Optional |
CUSTOM_SMTP_PASSWORD |
— | Optional |
EMAIL_DEFAULT_FROM |
— | Default sender address |
EMAIL_CONFIRMATION_TTL_SECONDS |
600 |
Expiry for staged-but-unsent emails |
Slack
| Variable | Notes |
|---|---|
SLACK_WEBHOOK_URL |
Incoming webhook URL |
Getting started
Path A: .env (static config)
npm install
cp .env.example .env
# Edit .env with your credentials
npm run build
npm start # HTTP on port 3000
# or: npm run start:stdio # stdio transport
Path B: setup tools (runtime config)
npm install
npm run build
npm start
Then, from your MCP client, call the setup_* tools in any order:
setup_database— point to your DB (or skip;file:local.dbis the default)setup_gmail_oauth_start→ open the URL →setup_gmail_oauth_completewith the codesetup_custom_mail_imap+setup_custom_mail_smtp— configure mailsetup_slack_webhook— configure Slacksetup_status— verify everything is wired up
Endpoints
| Method | Path | Description |
|---|---|---|
POST |
/mcp |
MCP Streamable HTTP — requires Accept: application/json, text/event-stream |
GET |
/healthz |
Health check |
GET |
/oauth/google/start |
Start Gmail OAuth flow (HTTP mode only) |
GET |
/oauth/google/callback |
Gmail OAuth callback (HTTP mode only) |
Architecture
src/
├── config.ts Env parsing (Zod), defaults, setConfigValue helper
├── index.ts Entry point — loads config, creates services, starts transport
├── runtime.ts Service factory — wires up all services with shared config
├── server.ts MCP server — registers all tools, conditionally includes setup tools
├── setup-tools.ts Runtime config tools — mutate config, test connections
├── tools.ts Core tool handlers — email, Slack, soul docs
├── errors.ts Custom error classes
├── logger.ts Structured JSON logging
├── types.ts Shared TypeScript interfaces
├── transports/
│ ├── http.ts Streamable HTTP transport + OAuth callback routes
│ └── stdio.ts Stdio transport
├── services/
│ ├── database.ts Turso/libSQL client — soul_docs, send_confirmations, audit_log
│ ├── gmail-api.ts Thin Gmail REST wrapper via google-auth-library + fetch (replaces googleapis)
│ ├── gmail.ts Gmail service — search, read, send, OAuth
│ ├── custom-mail.ts IMAP (imapflow) + SMTP (nodemailer) — search, read, send
│ ├── email-sender.tsComposite — delegates to Gmail or SMTP based on provider
│ └── slack.ts Slack incoming webhook via fetch
└── utils/
├── email.ts MIME builder, base64url helpers
└── mcp.ts jsonText() response formatter
Key design choices:
- Shared mutable config — All services hold a reference to the same
AppConfigobject. Setup tools mutate it directly; lazy services (Gmail, IMAP, SMTP, Slack) pick up changes on the next call. OnlyDatabaseServiceneeds an explicitreconnect()since it creates the libSQL client eagerly. - Audit logging — Every tool call is logged to the
audit_logtable with success/failure, args (secrets redacted), and a timestamp. - Two-step email send —
email_prepare_sendstages a draft (stored in DB with a TTL),email_confirm_sendconsumes it. Prevents accidental sends and gives the agent a chance to review. - OAuth works in both transports — Streamable HTTP mode has dedicated callback routes; stdio mode uses the
setup_gmail_oauth_start/setup_gmail_oauth_completetools where the user copies the code manually.
Database
Three tables are created automatically on startup:
| Table | Purpose |
|---|---|
soul_docs |
Personal knowledge base — title, content, tags, source, metadata, timestamps |
send_confirmations |
Staged email drafts with expiry and single-use tokens |
audit_log |
Immutable record of every tool invocation |
Local vs remote
# Local SQLite (relative to working directory)
TURSO_DATABASE_URL=file:local.db
# Local SQLite (absolute — for pods/containers that need persistent state)
TURSO_DATABASE_URL=file:/data/my-server.db
# Remote Turso DB
TURSO_DATABASE_URL=libsql://your-db.turso.io
TURSO_AUTH_TOKEN=your-token
Plain paths passed to setup_database are auto-prefixed with file: — so /data/db.sqlite becomes file:/data/db.sqlite.
Deployment
Docker (GHCR)
Pre-built multi-arch images (linux/amd64, linux/arm64) are published to GitHub Container Registry on every push to main.
# Pull and run
docker run -d --name personal-mcp \
--env-file .env \
-p 3000:3000 \
-v "$(pwd)/data:/app/data" \
ghcr.io/olamide226/personal-mcp-server:latest
# Or with a specific version
docker run -d --name personal-mcp \
--env-file .env \
-p 3000:3000 \
-v "$(pwd)/data:/app/data" \
ghcr.io/olamide226/personal-mcp-server:v1.0.0
Docker Compose (local build)
docker compose up --build
To pull from GHCR instead of building locally, swap the build: . line in docker-compose.yml for:
image: ghcr.io/olamide226/personal-mcp-server:latest
Bare metal / pod
npm ci --omit=dev
npm run build
MCP_HOST=0.0.0.0 MCP_PORT=3000 TURSO_DATABASE_URL=file:/data/server.db node dist/index.js
For production, set a strong MCP_BEARER_TOKEN and restrict MCP_ALLOWED_ORIGINS to your client origin(s).
Image tags
| Tag | When |
|---|---|
latest |
Every push to main |
<full-sha> |
Every push to main |
v1.2.3 |
Version tag (e.g. v1.2.3) |
1.2 |
Major.minor alias |
1 |
Major alias |
Semantic releases
Merging a PR to main triggers the release.yml workflow which parses conventional commits since the last tag:
| Commit prefix | Version bump |
|---|---|
feat: ... |
minor |
fix: ..., perf: ... |
patch |
BREAKING CHANGE: ... or feat!: ... |
major |
If any meaningful commits are found, a vX.Y.Z tag is created and pushed — which triggers a Docker build with semver tags.
Health check
curl http://localhost:3000/healthz
# {"ok":true,"name":"personal-mcp-server","version":"0.1.0"}
Development
npm install
npm run dev # tsx watch — auto-reload on changes
npm run build # tsc
npm test # vitest
npm run lint # eslint
npm run typecheck # tsc --noEmit (includes tests/)
Installing Personal Server
This server has no published package — it is built from source. Open the repository and follow its README.
▸ github.com/olamide226/personal-mcp-serverFAQ
Is Personal Server MCP free?
Yes, Personal Server MCP is free — one-click install via Unyly at no cost.
Does Personal Server need an API key?
No, Personal Server runs without API keys or environment variables.
Is Personal Server hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Personal Server in Claude Desktop, Claude Code or Cursor?
Open Personal Server on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Related MCPs
Gmail
Read, send and search emails from Claude
by GoogleSlack
Send, search and summarize Slack messages
by SlackRunbear
No-code MCP client for team chat platforms, such as Slack, Microsoft Teams, and Discord.
Discord Server
A community discord server dedicated to MCP by [Frank Fiegel](https://github.com/punkpeye)
Compare Personal Server with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All communication MCPs
