Initial commit: Hermes MCP - Yahoo Mail server for Claude AI
- Multi-account email support (Yahoo + self-hosted IMAP/SMTP) - MCP tools: get_profile, search_messages, read_message, list_folders, create_draft, send_email - Streamable HTTP transport with session recovery - Docker + Kubernetes deployment configuration - Express server with health endpoint 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
203
DEPLOY.md
Normal file
203
DEPLOY.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# Hermes MCP — Setup & Deployment
|
||||
|
||||
Hermes is a multi-account email MCP server for Claude AI.
|
||||
It supports **Yahoo Mail** (IMAP App Password) and any **self-hosted mail server** (Dovecot / Poste.io).
|
||||
|
||||
---
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
# 1. Install dependencies
|
||||
npm install
|
||||
|
||||
# 2. Copy and fill in credentials
|
||||
cp .env.example .env
|
||||
# edit .env with your email credentials
|
||||
|
||||
# 3. Run in dev mode (hot-reload)
|
||||
npm run dev
|
||||
|
||||
# 4. Test health
|
||||
curl http://localhost:3456/health
|
||||
# → {"status":"ok","service":"hermes-mcp"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Production deployment (MicroK8s — current setup)
|
||||
|
||||
The server runs as a Kubernetes Deployment on `23.120.207.35` (MicroK8s single-node cluster).
|
||||
SSL is handled by `cert-manager` with a Let's Encrypt cert for `hermes.fetcherpay.com`.
|
||||
|
||||
### Prerequisites on the server
|
||||
- MicroK8s with addons: `dns`, `ingress`, `registry`, `cert-manager`
|
||||
- Local registry at `localhost:32000`
|
||||
- A `ClusterIssuer` named `letsencrypt-prod` already configured
|
||||
|
||||
### One-time: create K8s namespace secret
|
||||
```bash
|
||||
microk8s kubectl create namespace fetcherpay # if it doesn't exist
|
||||
|
||||
microk8s kubectl create secret generic hermes-mcp-env -n fetcherpay \
|
||||
--from-literal=YAHOO_EMAIL=gheron01@yahoo.com \
|
||||
--from-literal=YAHOO_APP_PASSWORD=lzlleytmslxocxae \
|
||||
--from-literal=FETCHERPAY_EMAIL=garfield.heron@fetcherpay.com \
|
||||
--from-literal=FETCHERPAY_PASSWORD=onelove \
|
||||
--from-literal=FETCHERPAY_IMAP_HOST=23.120.207.35 \
|
||||
--from-literal=FETCHERPAY_IMAP_PORT=30993 \
|
||||
--from-literal=FETCHERPAY_SMTP_HOST=23.120.207.35 \
|
||||
--from-literal=FETCHERPAY_SMTP_PORT=30587 \
|
||||
--from-literal=PORT=3456
|
||||
```
|
||||
|
||||
### Build & push image
|
||||
```bash
|
||||
# From your local machine — SCP src to server first, then SSH in:
|
||||
scp -P 2222 -r src/ package*.json tsconfig.json Dockerfile garfield@23.120.207.35:~/hermes-mcp/
|
||||
|
||||
ssh -p 2222 garfield@23.120.207.35
|
||||
cd ~/hermes-mcp
|
||||
docker build -t localhost:32000/hermes-mcp:latest .
|
||||
docker push localhost:32000/hermes-mcp:latest
|
||||
```
|
||||
|
||||
### Apply K8s manifests (hermes-k8s.yaml on the server)
|
||||
```yaml
|
||||
# ~/hermes-mcp/hermes-k8s.yaml (already applied — shown for reference)
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: hermes-mcp
|
||||
namespace: fetcherpay
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: hermes-mcp
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: hermes-mcp
|
||||
spec:
|
||||
containers:
|
||||
- name: hermes-mcp
|
||||
image: localhost:32000/hermes-mcp:latest
|
||||
ports:
|
||||
- containerPort: 3456
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: hermes-mcp-env
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 3456
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: hermes-mcp
|
||||
namespace: fetcherpay
|
||||
spec:
|
||||
selector:
|
||||
app: hermes-mcp
|
||||
ports:
|
||||
- port: 3456
|
||||
targetPort: 3456
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: hermes-ingress
|
||||
namespace: fetcherpay
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: hermes.fetcherpay.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: hermes-mcp
|
||||
port:
|
||||
number: 3456
|
||||
tls:
|
||||
- hosts:
|
||||
- hermes.fetcherpay.com
|
||||
secretName: hermes-fetcherpay-tls
|
||||
```
|
||||
|
||||
### Redeploy after code changes
|
||||
```bash
|
||||
# On your local machine:
|
||||
scp -P 2222 src/imap.ts src/smtp.ts src/tools.ts src/index.ts \
|
||||
garfield@23.120.207.35:~/hermes-mcp/src/
|
||||
|
||||
ssh -p 2222 garfield@23.120.207.35 "
|
||||
cd ~/hermes-mcp &&
|
||||
docker build -t localhost:32000/hermes-mcp:latest . &&
|
||||
docker push localhost:32000/hermes-mcp:latest &&
|
||||
microk8s kubectl rollout restart deployment/hermes-mcp -n fetcherpay &&
|
||||
microk8s kubectl rollout status deployment/hermes-mcp -n fetcherpay
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Useful commands
|
||||
|
||||
```bash
|
||||
# Logs
|
||||
microk8s kubectl logs -n fetcherpay -l app=hermes-mcp --tail=100 -f
|
||||
|
||||
# Pod status
|
||||
microk8s kubectl get pods -n fetcherpay -l app=hermes-mcp
|
||||
|
||||
# Update a single env var without rebuild (takes effect on next rollout)
|
||||
microk8s kubectl set env deployment/hermes-mcp -n fetcherpay KEY=value
|
||||
microk8s kubectl rollout restart deployment/hermes-mcp -n fetcherpay
|
||||
|
||||
# Health check
|
||||
curl https://hermes.fetcherpay.com/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Add to Claude.ai
|
||||
|
||||
1. Go to **Claude.ai → Settings → Connectors → Add custom connector**
|
||||
2. Enter URL: `https://hermes.fetcherpay.com/mcp`
|
||||
3. Click Connect
|
||||
|
||||
### Available tools
|
||||
|
||||
| Tool | Description | Key params |
|
||||
|------|-------------|------------|
|
||||
| `get_profile` | Get email address for an account | `account` |
|
||||
| `search_messages` | Search INBOX by keyword/sender/subject | `q`, `maxResults`, `account` |
|
||||
| `read_message` | Read full message body by UID | `uid`, `account` |
|
||||
| `list_folders` | List all mailbox folders | `account` |
|
||||
| `create_draft` | Save a draft to the Drafts folder | `to`, `subject`, `body`, `account` |
|
||||
| `send_email` | Send an email | `to`, `subject`, `body`, `account` |
|
||||
|
||||
`account` is always optional — defaults to `"yahoo"`. Set to `"fetcherpay"` for the FetcherPay mailbox.
|
||||
|
||||
---
|
||||
|
||||
## Known issues & fixes
|
||||
|
||||
| Issue | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `yahoo_read_message` 5-min timeout | `source: true` downloads full raw RFC822 | Use `bodyParts: ['TEXT']` |
|
||||
| `messageFlagsAdd` deadlock | Called inside `for await` loop while FETCH active | Moved to after the loop |
|
||||
| Stale session after pod restart | `!sessionId` guard blocked re-initialize | Accept initialize regardless of session ID |
|
||||
| FetcherPay `EAI_AGAIN` DNS | K8s internal DNS cold-start for hostname | Use direct IP `23.120.207.35` |
|
||||
Reference in New Issue
Block a user