Files
hermes-mcp/ARCHITECTURE.md
Garfield 02398258a5 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>
2026-05-14 13:48:01 -04:00

222 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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