GlassCloud
MCP Relay Server for GlassBridge - a cloud service that bridges the GlassBridge Android app with Google services and third-party tools via the Model Context Protocol (MCP).
Purpose
GlassCloud solves a fundamental challenge in mobile AI assistants: how do you give a voice assistant on smart glasses access to your personal data (email, calendar) securely?
The answer is a cloud relay that:
- Authenticates users via Google OAuth on a web browser
- Links devices via QR code scanning (no typing passwords on glasses)
- Proxies tool calls from the Android app to Google APIs
- Manages OAuth tokens securely with encryption at rest
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Smart Glasses │────▶│ GlassCloud │────▶│ Google APIs │
│ + Android App │ WS │ (This Server) │ │ Gmail/Calendar │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Web Console │
│ (OAuth + QR) │
└─────────────────┘
Key Features
- WebSocket Relay - Real-time bidirectional communication with Android devices
- Google OAuth - Secure authentication without exposing credentials to the mobile app
- QR Code Linking - Scan-to-link flow for easy device pairing
- MCP Tool Execution - Gmail and Calendar tools with automatic token refresh
- Voice-First Design - Progress messages for immediate audio feedback during tool execution
Quick Start
# Install dependencies
npm install
# Copy and configure environment
cp .env.example .env
# Edit .env with your settings (see Configuration below)
# Development (auto-reload)
npm run dev
# Production
npm run build
npm start
Then open: http://localhost:3000/console
Configuration
Required Environment Variables
# Security - MUST be unique random values (32+ chars)
# Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
JWT_SECRET=your-random-secret-here
ENCRYPTION_KEY=your-random-key-here
# Google OAuth (optional for dev, required for production)
# Create at: https://console.cloud.google.com/apis/credentials
GOOGLE_CLIENT_ID=xxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxx
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback
Optional Settings
PORT=3000 # Server port
NODE_ENV=development # development | production
LOG_LEVEL=debug # trace | debug | info | warn | error
DATABASE_PATH=./data/glasscloud.db
CORS_ORIGINS=http://localhost:3000
Architecture
System Components
┌─────────────────────────────────────────────────────────────────┐
│ GlassCloud Server │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ WebSocket │ │ REST API │ │ MCP Proxy │ │
│ │ Server │ │ (Express) │ │ Manager │ │
│ │ │ │ │ │ │ │
│ │ - Device │ │ - OAuth │ │ - Gmail │ │
│ │ connections│ │ - QR codes │ │ - Calendar │ │
│ │ - Tool │ │ - Devices │ │ - Token │ │
│ │ routing │ │ - Console │ │ refresh │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └─────────────────┼─────────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ SQLite + WAL │ │
│ │ │ │
│ │ - Users │ │
│ │ - Devices │ │
│ │ - OAuth tokens │ │
│ │ - Link tokens │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Directory Structure
src/
├── index.ts # Entry point, server startup
├── config/
│ ├── env.ts # Zod environment validation
│ ├── mcp-services.ts # Built-in service definitions
│ └── index.ts
├── server/
│ ├── express.ts # Express app setup (CORS, helmet, rate limiting)
│ └── websocket.ts # WebSocket server with zombie cleanup
├── routes/
│ ├── auth.ts # Google OAuth flow
│ ├── console.ts # Web console UI
│ ├── devices.ts # Device management API
│ ├── health.ts # Health check endpoint
│ ├── link.ts # QR code token generation
│ └── mcp.ts # MCP services API
├── websocket/
│ ├── handler.ts # Message routing with progress feedback
│ ├── protocol.ts # Message type definitions
│ └── connection.ts # Connection tracking
├── services/
│ ├── auth.service.ts # OAuth + token refresh mutex
│ ├── device.service.ts # Device CRUD operations
│ ├── link.service.ts # QR code token handling
│ └── mcp-proxy.service.ts # Tool execution + input coercion
├── mcp/
│ ├── gmail.ts # Gmail API integration
│ ├── calendar.ts # Calendar API integration
│ └── registry.ts
├── db/
│ ├── index.ts # SQLite connection + WAL mode
│ └── schema.ts # Table definitions
├── utils/
│ ├── logger.ts # Pino structured logging
│ ├── crypto.ts # AES-256-GCM encryption
│ └── cache.ts # LRU cache for tool results
└── types/
├── api.ts # REST API types
├── mcp.ts # MCP types
└── websocket.ts # WebSocket message types
Design Decisions
Why a Cloud Relay?
-
OAuth Security - Google OAuth requires a web browser redirect flow. Smart glasses can't do this, but they can scan a QR code.
-
Token Management - OAuth tokens must be refreshed periodically. Doing this on-device means storing refresh tokens on the phone. The relay handles this centrally.
-
Connection Stability - Mobile connections are flaky. The relay maintains persistent connections to Google APIs while tolerating device disconnects.
SQLite with WAL Mode
We use SQLite instead of PostgreSQL for simplicity:
- Zero configuration - No separate database server
- Faster for single-instance - No network latency
- WAL mode - Enables concurrent reads during writes
db.pragma('journal_mode = WAL');
db.pragma('synchronous = NORMAL');
Token Refresh Mutex
When a user asks "Check my email and add a meeting", the LLM might fire two tool calls simultaneously. Without protection, both could try to refresh an expired OAuth token, causing one to fail.
Solution: A promise-based mutex that makes concurrent refresh requests wait for the first one.
const refreshPromises = new Map<string, Promise<Token>>();
async function getValidToken(userId: string) {
// If refresh in progress, wait for it
const existing = refreshPromises.get(userId);
if (existing) return existing;
// Start new refresh
const promise = refreshToken(userId);
refreshPromises.set(userId, promise);
// ...
}
Voice-First UX
Tool execution can take 2-5 seconds. In a voice app, silence feels broken.
Solution: Send tool_progress immediately when execution starts:
{"type": "tool_progress", "status": "executing", "message": "Checking your emails..."}
The Android app can play a "thinking" sound while waiting.
Content Truncation
Large emails (10MB with attachments) would crash the Android JSON parser.
Solution: Truncate to 10KB and tell the LLM:
[...Email truncated due to size. Full content not available...]
This prevents the LLM from hallucinating the rest of the email.
Input Coercion
LLMs often send "10" (string) when the schema expects 10 (number).
Solution: Use Zod with coercion:
z.coerce.number().int().min(1).max(50)
// Accepts both 10 and "10"
WebSocket Protocol
Connection
ws://localhost:3000/ws?deviceId=UNIQUE_DEVICE_ID
Client → Server Messages
// Execute a tool
{ "type": "tool_execute", "requestId": "uuid", "serverId": "gmail",
"toolName": "gmail.get_unread", "arguments": { "maxResults": 10 } }
// List available servers
{ "type": "get_servers", "requestId": "uuid" }
// Link device to user
{ "type": "link_device", "requestId": "uuid", "linkToken": "from-qr-code", "deviceId": "..." }
// Get user account info
{ "type": "get_user_account", "requestId": "uuid", "deviceId": "..." }
Server → Client Messages
// Tool execution started (for voice feedback)
{ "type": "tool_progress", "requestId": "uuid", "status": "executing",
"message": "Checking your emails..." }
// Tool result
{ "type": "tool_result", "requestId": "uuid",
"result": { "success": true, "isError": false, "content": "You have 3 unread emails..." } }
// Available servers
{ "type": "servers_list", "requestId": "uuid", "servers": [...] }
// Error
{ "type": "error", "requestId": "uuid", "error": "Token expired" }
REST API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/health | GET | Health check with connection stats |
/console | GET | Web console UI |
/auth/google | POST | Initiate OAuth flow |
/auth/google/callback | GET | OAuth callback |
/api/link/generate | POST | Generate QR code link token |
/api/devices | GET | List user's linked devices |
/api/devices/:id | DELETE | Unlink a device |
/api/mcp/services | GET | List available MCP services |
Available MCP Tools
Gmail (gmail.*)
| Tool | Description |
|---|---|
gmail.get_unread | Get unread email count and summaries |
gmail.search | Search emails by query |
gmail.get_message | Get full email content by ID |
Calendar (calendar.*)
| Tool | Description |
|---|---|
calendar.get_today | Get today's events |
calendar.get_events | Get events for N days |
calendar.create_event | Create a new event |
Database Schema
-- Users (from Google OAuth)
users (id, google_id, email, display_name, profile_picture_url, created_at, updated_at)
-- Linked devices
devices (id, user_id, device_name, device_model, last_seen_at, last_heartbeat_at, linked_at, created_at)
-- QR code link tokens (single-use, 5 min expiry)
link_tokens (id, user_id, expires_at, used_at, used_by_device_id, created_at)
-- Encrypted OAuth tokens
oauth_tokens (id, user_id, provider, access_token_encrypted, refresh_token_encrypted, ...)
Security Considerations
Token Encryption
OAuth tokens are encrypted at rest using AES-256-GCM. The encryption key comes from the ENCRYPTION_KEY environment variable.
Link Token Security
- Cryptographically random (32 bytes)
- Single-use (marked used after successful link)
- Short expiration (5 minutes)
- Stored as SHA-256 hash (original never stored)
Google API Scopes
This app requests restricted scopes (gmail.readonly, calendar.events). For public deployment, you'll need Google's CASA security assessment ($15K-$75K/year). For testing, keep the app in "Testing" mode (100 user limit).
Future Enhancements
- Third-party MCP server registration via console
- Push notifications via FCM
- Usage analytics and tool popularity metrics
- Multi-tenancy for organizations
- PostgreSQL migration for horizontal scaling
Related Documentation
License
MIT