- Replace FETCHERPAY with generic CUSTOM account examples - Update README.md, .env.example, and DEPLOY.md with generic configurations - Remove hardcoded IPs, email addresses, and domain names - Update package.json description to be more generic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
205 lines
5.6 KiB
Markdown
205 lines
5.6 KiB
Markdown
# Hermes MCP — Setup & Deployment
|
|
|
|
Hermes is a multi-account email MCP server for Claude AI.
|
|
It supports **Yahoo Mail** (IMAP App Password) and any **custom IMAP/SMTP server**.
|
|
|
|
---
|
|
|
|
## 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 (Kubernetes example)
|
|
|
|
Example deployment using MicroK8s single-node cluster.
|
|
SSL is handled by `cert-manager` with a Let's Encrypt certificate.
|
|
|
|
### 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 and secret
|
|
```bash
|
|
microk8s kubectl create namespace hermes-mcp # if it doesn't exist
|
|
|
|
microk8s kubectl create secret generic hermes-mcp-env -n hermes-mcp \
|
|
--from-literal=YAHOO_EMAIL=your@yahoo.com \
|
|
--from-literal=YAHOO_APP_PASSWORD=your-app-password \
|
|
--from-literal=CUSTOM_EMAIL=your@domain.com \
|
|
--from-literal=CUSTOM_PASSWORD=your-password \
|
|
--from-literal=CUSTOM_IMAP_HOST=mail.yourdomain.com \
|
|
--from-literal=CUSTOM_IMAP_PORT=993 \
|
|
--from-literal=CUSTOM_SMTP_HOST=mail.yourdomain.com \
|
|
--from-literal=CUSTOM_SMTP_PORT=587 \
|
|
--from-literal=PORT=3456
|
|
```
|
|
|
|
### Build & push image
|
|
```bash
|
|
# Option 1: Build locally and push to your registry
|
|
docker build -t localhost:32000/hermes-mcp:latest .
|
|
docker push localhost:32000/hermes-mcp:latest
|
|
|
|
# Option 2: Copy to server and build there
|
|
scp -r src/ package*.json tsconfig.json Dockerfile user@your-server:~/hermes-mcp/
|
|
ssh user@your-server "cd ~/hermes-mcp && docker build -t localhost:32000/hermes-mcp:latest . && docker push localhost:32000/hermes-mcp:latest"
|
|
```
|
|
|
|
### Apply K8s manifests
|
|
```yaml
|
|
# hermes-k8s.yaml
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: hermes-mcp
|
|
namespace: hermes-mcp
|
|
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: hermes-mcp
|
|
spec:
|
|
selector:
|
|
app: hermes-mcp
|
|
ports:
|
|
- port: 3456
|
|
targetPort: 3456
|
|
---
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: hermes-ingress
|
|
namespace: hermes-mcp
|
|
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.yourdomain.com
|
|
http:
|
|
paths:
|
|
- path: /
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: hermes-mcp
|
|
port:
|
|
number: 3456
|
|
tls:
|
|
- hosts:
|
|
- hermes.yourdomain.com
|
|
secretName: hermes-tls
|
|
```
|
|
|
|
Apply the manifests:
|
|
```bash
|
|
microk8s kubectl apply -f hermes-k8s.yaml
|
|
```
|
|
|
|
### Redeploy after code changes
|
|
```bash
|
|
# Rebuild and push the image
|
|
docker build -t localhost:32000/hermes-mcp:latest .
|
|
docker push localhost:32000/hermes-mcp:latest
|
|
|
|
# Restart the deployment
|
|
microk8s kubectl rollout restart deployment/hermes-mcp -n hermes-mcp
|
|
microk8s kubectl rollout status deployment/hermes-mcp -n hermes-mcp
|
|
```
|
|
|
|
---
|
|
|
|
## Useful commands
|
|
|
|
```bash
|
|
# Logs
|
|
microk8s kubectl logs -n hermes-mcp -l app=hermes-mcp --tail=100 -f
|
|
|
|
# Pod status
|
|
microk8s kubectl get pods -n hermes-mcp -l app=hermes-mcp
|
|
|
|
# Update a single env var without rebuild (takes effect on next rollout)
|
|
microk8s kubectl set env deployment/hermes-mcp -n hermes-mcp KEY=value
|
|
microk8s kubectl rollout restart deployment/hermes-mcp -n hermes-mcp
|
|
|
|
# Health check
|
|
curl https://hermes.yourdomain.com/health
|
|
```
|
|
|
|
---
|
|
|
|
## Add to Claude.ai
|
|
|
|
1. Go to **Claude.ai → Settings → Connectors → Add custom connector**
|
|
2. Enter URL: `https://hermes.yourdomain.com/mcp` (or your server's URL)
|
|
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 and defaults to `"yahoo"`. Configure your second account in the code if needed.
|
|
|
|
---
|
|
|
|
## Known issues & fixes
|
|
|
|
| Issue | Cause | Fix |
|
|
|-------|-------|-----|
|
|
| `read_message` timeout | `source: true` downloads full raw RFC822 | Use `bodyParts: ['TEXT']` instead |
|
|
| `messageFlagsAdd` deadlock | Called inside `for await` loop while FETCH active | Moved to after the loop |
|
|
| Stale session after pod restart | Session ID guard blocked re-initialize | Accept initialize regardless of session ID |
|
|
| `EAI_AGAIN` DNS errors | K8s internal DNS resolution issues | Use direct IP instead of hostname |
|