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:
221
ARCHITECTURE.md
Normal file
221
ARCHITECTURE.md
Normal 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
|
||||
Reference in New Issue
Block a user