skills-ws
Version:
87 agent skills for AI coding assistants — marketing, growth, web3, dev, design & operations. Built for OpenClaw, Claude Code, Cursor, and Codex.
169 lines (140 loc) • 4.25 kB
Markdown
- 11. Deployment
- systemd + cloudflared Tunnel
- Docker
- Vercel Edge Proxy Pattern
```bash
cd /opt/my-mcp-server
npm ci && npm run build
sudo tee /etc/systemd/system/mcp-server.service << 'EOF'
[]
Description=MCP Server
After=network.target
[]
Type=simple
User=mcp
WorkingDirectory=/opt/my-mcp-server
ExecStart=/usr/bin/node dist/http-server.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3100
EnvironmentFile=/opt/my-mcp-server/.env
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/my-mcp-server/logs
PrivateTmp=true
[]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-server
cloudflared tunnel create mcp-server
cloudflared tunnel route dns mcp-server mcp.yourdomain.com
sudo tee /etc/cloudflared/config.yml << 'EOF'
tunnel: YOUR_TUNNEL_ID
credentials-file: /root/.cloudflared/YOUR_TUNNEL_ID.json
ingress:
- hostname: mcp.yourdomain.com
service: http://localhost:3100
- service: http_status:404
EOF
sudo tee /etc/systemd/system/cloudflared-tunnel.service << 'EOF'
[]
Description=Cloudflare Tunnel
After=network.target
[]
Type=simple
ExecStart=/usr/bin/cloudflared tunnel --config /etc/cloudflared/config.yml run
Restart=always
RestartSec=5
[]
WantedBy=multi-user.target
EOF
sudo systemctl enable --now cloudflared-tunnel
```
```dockerfile
FROM node:22-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ src/
RUN npm run build
FROM node:22-slim
WORKDIR /app
RUN addgroup --system mcp && adduser --system --ingroup mcp mcp
COPY --from=builder /app/dist dist/
COPY --from=builder /app/node_modules node_modules/
COPY package.json ./
USER mcp
EXPOSE 3100
HEALTHCHECK --interval=30s --timeout=5s CMD wget -qO- http://localhost:3100/health || exit 1
CMD ["node", "dist/http-server.js"]
```
```yaml
services:
mcp-server:
build: .
ports:
- "3100:3100"
env_file: .env
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3100/health"]
interval: 30s
timeout: 5s
retries: 3
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
```
For remote (Streamable HTTP) servers, Vercel can act as an edge auth proxy:
```typescript
// vercel-proxy/api/mcp.ts
// NOTE: Vercel doesn't support long-lived SSE streams natively.
// Use Vercel as an auth proxy that FORWARDS to your actual MCP server.
// Do NOT redirect with the token in a query string: the MCP authorization spec
// forbids access tokens in the URI, and query strings leak into CDN/proxy logs.
// Pass the token in the Authorization header instead.
import type { VercelRequest, VercelResponse } from "@vercel/node";
export default async function handler(req: VercelRequest, res: VercelResponse) {
const apiKey = req.headers["x-api-key"] as string;
if (!apiKey) {
return res.status(401).json({ error: "API key required" });
}
// Verify key against your DB (Vercel KV, Upstash Redis, etc.)
const valid = await verifyKeyAtEdge(apiKey);
if (!valid) return res.status(401).json({ error: "Invalid API key" });
// Proxy to the actual MCP server with a short-lived token in the Authorization header
const token = generateShortLivedToken(apiKey);
const upstream = await fetch("https://mcp.yourdomain.com/mcp", {
method: req.method,
headers: {
"Content-Type": (req.headers["content-type"] as string) || "application/json",
Accept: "application/json, text/event-stream",
Authorization: `Bearer ${token}`,
...(req.headers["mcp-session-id"] ? { "Mcp-Session-Id": req.headers["mcp-session-id"] as string } : {}),
},
body: req.method === "POST" ? JSON.stringify(req.body) : undefined,
});
const sid = upstream.headers.get("mcp-session-id");
if (sid) res.setHeader("Mcp-Session-Id", sid);
res.status(upstream.status).send(Buffer.from(await upstream.arrayBuffer()));
}
```
---