# 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
