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

9.9 KiB
Raw Permalink Blame History

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

# 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 importsmoduleResolution: 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 internalstripAccountParam() 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