feat: native OAuth login page, architecture docs, docs site update

- Add GET/POST /login to hermes for first-party cookie during OAuth popup
  (fixes browser CHIPS cookie partitioning that broke claude.ai connection)
- Add role column to all findCustomer* SQL queries in src/auth.ts
- Add claude.ai tab to docs/getting-started.html with OAuth flow steps
- Add ARCHITECTURE.md with system diagrams, data flow, and key invariants
- Rewrite README.md and DEPLOY.md to reflect actual MicroK8s deployment
- Deploy updated docs site (squaremcp-docs sha256 updated)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Garfield
2026-05-14 13:48:01 -04:00
parent 61dab40585
commit 02398258a5
13 changed files with 697 additions and 298 deletions

221
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,221 @@
# SquareMCP — Architecture
## System Overview
SquareMCP is a multi-tenant MCP (Model Context Protocol) gateway. It lets AI assistants (claude.ai, Codex CLI, opencode, Claude Desktop) control social media, email, and messaging platforms on behalf of authenticated users.
**Three public services:**
| Subdomain | Purpose | K8s pod | Port |
|-----------|---------|---------|------|
| `hermes.squaremcp.com` | MCP API + OAuth server | `hermes-mcp` | 3456 |
| `app.squaremcp.com` | Customer dashboard SPA | `squaremcp-app` | 8080 |
| `docs.squaremcp.com` | Developer documentation | `squaremcp-docs` | 80 |
---
## Infrastructure
```
Internet
┌─────────▼─────────┐
│ nginx ingress │
│ (MicroK8s addon) │
│ TLS via cert-mgr │
└─────────┬─────────┘
┌───────────┼───────────┐
│ │ │
┌─────────▼──┐ ┌─────▼──────┐ ┌▼────────────┐
│ hermes-mcp │ │squaremcp- │ │squaremcp- │
│ pod │ │ app pod │ │ docs pod │
│ :3456 │ │ :8080 │ │ :80 │
│hostNetwork │ └────────────┘ └─────────────┘
└──────┬──────┘
│ (same host network)
┌──────────┼──────────┐
│ │ │
┌────▼───┐ ┌───▼────┐ ┌───▼──────────────────┐
│ MySQL 8│ │Redis 7 │ │ obsidian vaults │
│ :3306 │ │ :6379 │ │ (hostPath volume) │
│(Docker)│ │(Docker)│ └──────────────────────┘
└────────┘ └────────┘
Namespace: fetcherpay
Registry: localhost:32000 (MicroK8s built-in)
TLS: Let's Encrypt via cert-manager
```
**`hermes-mcp` uses `hostNetwork: true`** — the pod binds port 3456 directly on the host interface, sharing MySQL and Redis at `127.0.0.1`.
---
## Deployment Pattern
```bash
# Every code change:
npm run build
docker build -t localhost:32000/hermes-mcp:latest .
docker push localhost:32000/hermes-mcp:latest
# Get the digest from the push output (or docker inspect)
# Update image sha256 in hermes-k8s.yaml — never use :latest tag in k8s
microk8s kubectl apply -f hermes-k8s.yaml
microk8s kubectl rollout status deployment/hermes-mcp -n fetcherpay
```
`hermes-k8s.yaml` is gitignored (contains plaintext secrets). Each deploy updates the sha256 image digest.
---
## Authentication
Three methods accepted in priority order:
```
1. x-api-key = global MCP_API_KEY → superadmin
2. x-api-key = customer API key → resolve Customer from MySQL
3. Authorization: Bearer <JWT> → decode JWT → resolve Customer
Authorization: Bearer <oauth-tok> → validate in oauth_tokens table
4. Cookie: session=<JWT> → decode → resolve Customer
```
**JWT payload:** `{ sub: customerId, email, plan, role }`
**Cookie settings:** `httpOnly`, `secure`, `sameSite: lax`, `domain: .squaremcp.com`, 7-day TTL
---
## OAuth 2.0 Flow (browser-based clients)
```
MCP Client (popup) hermes.squaremcp.com
│ │
├─ POST /oauth/register ────► Dynamic Client Registration
│◄─ { client_id, secret } ──┤
├─ GET /oauth/authorize ────►
│ no session? │
│◄─ 302 /login?return_to=.. ┤
├─ GET /login ─────────────►
│◄─ HTML login form ─────────
├─ POST /login ────────────► verify credentials
│ set-cookie: session (first-party!)
│◄─ 302 /oauth/authorize ───┤
├─ GET /oauth/authorize ────► session found ✓
│◄─ consent HTML ───────────┤
├─ POST /oauth/authorize ───► action=allow
│◄─ 302 redirect_uri?code=. ┤
├─ POST /oauth/token ───────► PKCE code exchange
│◄─ { access_token } ───────┤
├─ POST /mcp ───────────────► Bearer access_token
│◄─ MCP tools ──────────────┤
```
**Cookie partitioning note:** Browsers (CHIPS) partition cookies per top-level site. The OAuth popup runs on `hermes.squaremcp.com`, so login must happen on hermes — not on `app.squaremcp.com` — for the session cookie to be visible in that context. `/login` on hermes exists specifically for this reason.
---
## Multi-Tenancy: Tool Call Data Flow
```
AI assistant
POST /mcp Authorization: Bearer <token>
└─► requireAuth
resolves token → Customer { id, plan, getCredential() }
└─► createMcpServer(customer)
└─► handleToolCall("linkedin_create_post", args, customer)
├─ checkLimit(customerId, plan) → OK or error
├─ customer.getCredential("linkedin")
│ Redis GET creds:{customerId}:linkedin
│ AES-256-GCM decrypt → { accessToken }
├─ linkedin.createPost(accessToken, content)
│ POST api.linkedin.com/v2/ugcPosts
└─ recordUsage(customerId, "linkedin", "linkedin_create_post")
INSERT INTO usage_logs
```
**Credentials** are AES-256-GCM encrypted in Redis at `creds:{customerId}:{platform}`. Key is `CREDENTIAL_ENCRYPTION_KEY` env var — do not rotate without re-encrypting all stored values.
---
## Platform Clients (51 tools, 11 platforms)
| Platform | Key tools |
|----------|-----------|
| Email (IMAP/SMTP) | search_messages, read_message, send_email, create_draft, list_folders |
| Obsidian | search_notes, read_note, append_to_note, update_note, sync_status |
| WhatsApp Business | send_message, send_template, list_templates, get_message_status |
| LinkedIn | get_profile, create_post, search_connections, send_message, upload_video |
| TikTok | get_profile, get_creator_info, create_video, get_video_status |
| Facebook | get_page, get_posts, create_post, create_photo_post, create_video_post |
| Instagram | get_profile, get_media, create_post, create_reel |
| Twitter/X | get_user_profile, get_user_tweets, search_tweets, create_tweet, upload_video |
| Telegram | get_me, send_message, send_photo, get_updates, get_chat |
| Discord | get_me, get_guilds, get_channels, send_message, get_messages |
| Snapchat | get_me, get_ad_accounts, create_snap |
---
## Source Layout
```
src/
├── index.ts — Express app, all routes, MCP session management
├── tools.ts — handleToolCall(), tools[] registry
├── oauth.ts — OAuth 2.0 + PKCE + DCR
├── auth.ts — bcrypt, JWT, findCustomer* queries
├── db.ts — MySQL pool, ensureColumn() migration helper
├── redis.ts — shared Redis singleton
├── imap.ts / smtp.ts — multi-account email
├── manifest.ts — OpenAPI schema generation
├── billing/
│ ├── plans.ts — plan definitions + limits
│ ├── middleware.ts — Customer interface, meterMiddleware, API key resolution
│ ├── usage.ts — recordUsage(), checkLimit()
│ ├── invoices.ts — invoice generation + email
│ └── cron.ts — monthly invoice cron (Redis distributed lock)
├── multitenancy/
│ ├── credential-store.ts — AES-256-GCM per-customer credentials
│ ├── token-refresh.ts — OAuth token refresh per platform
│ ├── platform-health.ts — health checks + Redis cache
│ ├── webhook-router.ts — WhatsApp inbound phone → customerId routing
│ └── audit-log.ts — per-customer tool call audit trail
├── webhooks/
│ └── delivery.ts — outbound HMAC webhook + 3× retry + Redis DLQ
└── clients/
└── discord / facebook / instagram / linkedin / obsidian /
snapchat / telegram / tiktok / twitter / whatsapp .ts
product/
└── app/ — SaaS dashboard SPA (app.squaremcp.com)
docs/ — Developer docs site (docs.squaremcp.com)
```
---
## Key Invariants
1. **ESM `.js` extensions in imports**`moduleResolution: bundler`; always `import from './foo.js'` even for `.ts` files
2. **Digest pinning in K8s manifests** — always use `image: registry/name@sha256:...`, never `:latest` in production
3. **`hermes-k8s.yaml` is gitignored** — never force-add; contains plaintext credentials
4. **`CREDENTIAL_ENCRYPTION_KEY` is immutable** — rotating it requires re-encrypting all Redis-stored credentials
5. **`account` param is internal** — `stripAccountParam()` removes it from args before presenting tools in multi-tenant MCP sessions
6. **WhatsApp inbound webhook needs raw body** — uses `express.raw({ type: '*/*' })` for HMAC verification; do not switch to `express.json()`
7. **Cookie first-party requirement** — always set session cookies in the domain that will read them; the `/login` route on hermes exists for this reason