Dev MCP Server on Easysource-internal — Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking. This is an ops/infra plan: most "tests" are verification commands or observations, not unit tests.
Goal: Stand up a dev deployment of hq-mcp-server on the Easysource-internal box, reachable at https://mcp-internal.hirequotient.com/mcp, deployable on-demand from any branch via a new deploy-dev.yml, and verified end-to-end through a real claude.ai connector before feature branches merge to main.
Architecture: Additive dev path parallel to prod. Prod (uat-vm, main-only, mcp.hirequotient.com) is untouched. Dev = one ~/hq-mcp-server clone on the box with a dev .env, a Docker container on 127.0.0.1:5011 behind an nginx TLS vhost, fed by a manual deploy-dev.yml that builds the chosen branch's image on-demand then SSH-deploys it via the existing scripts/deploy.sh.
Tech Stack: GitHub Actions, Docker, GHCR, nginx, certbot, PM2/Docker on Ubuntu, Node 20, MCP Streamable-HTTP.
Global Constraints¶
- Target box:
Easysource-internal, instancei-0aa0dcabda4ba8f5e, accountbcg-tech-access, EIP35.153.20.238. - Public host:
mcp-internal.hirequotient.com(DNS A → EIP, done). - Dev container: name
hq-mcp,127.0.0.1:5011→ container:4100(deploy.sh defaults). - Reuse internal secrets from the existing local/box
.env; change onlyPUBLIC_BASE_URL,ALLOWED_HOSTS,ENV/NODE_ENV. - Never commit
.envor certs. Never touchmain,build.yml,deploy.yml, or the prod box. - Git identity
jeevan-hq <jeevan@hirequotient.com>; noCo-Authored-By/Claude-Sessiontrailers. - Work branch:
feat/dev-mcp-deploy(already created).
Task 1: Author deploy-dev.yml (on-demand build + SSH deploy)¶
Files:
- Create: .github/workflows/deploy-dev.yml
Interfaces:
- Consumes: scripts/deploy.sh (existing) via MCP_CONTAINER/MCP_HOST_PORT defaults; GHCR image ghcr.io/hirequotient/hq-mcp-server:<sha>.
- Produces: a manually-dispatchable workflow with a ref input that builds that ref's image and deploys it to the dev box.
- Secrets required (Task 3): DEV_DEPLOY_SSH_HOST, DEV_DEPLOY_SSH_USER, DEV_DEPLOY_SSH_KEY, DEV_DEPLOY_SSH_PORT (optional).
- [ ] Step 1: Write the workflow
name: Deploy (dev)
# MANUAL ONLY. Builds the chosen ref's image on-demand, pushes to GHCR, then
# SSH-deploys it to the Easysource-internal dev box. Never triggered by push.
# Prod's build.yml / deploy.yml (uat-vm, main-only) are untouched.
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch / tag / SHA to deploy to dev'
required: true
default: 'feat/via-claude-mail-source'
concurrency:
group: deploy-dev
cancel-in-progress: false
jobs:
build:
name: Build & push dev image (GHCR)
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
sha: ${{ steps.resolve.outputs.sha }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- id: resolve
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/hirequotient/hq-mcp-server:${{ steps.resolve.outputs.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Deploy to Easysource-internal
needs: build
runs-on: ubuntu-latest
environment: internal
steps:
- name: SSH -> checkout ref + deploy.sh
uses: appleboy/ssh-action@v1.2.0
with:
host: ${{ secrets.DEV_DEPLOY_SSH_HOST }}
username: ${{ secrets.DEV_DEPLOY_SSH_USER }}
key: ${{ secrets.DEV_DEPLOY_SSH_KEY }}
port: ${{ secrets.DEV_DEPLOY_SSH_PORT || 22 }}
command_timeout: 20m
script_stop: true
envs: DEPLOY_REF
script: |
set -euo pipefail
cd ~/hq-mcp-server
git fetch origin --prune --tags
git checkout "${{ inputs.ref }}"
git pull --ff-only || true
./scripts/deploy.sh
- [ ] Step 2: Lint the workflow YAML
Run: cd /Users/admin/hq-mcp-server && python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/deploy-dev.yml')); print('YAML OK')"
Expected: YAML OK
(If actionlint is available: actionlint .github/workflows/deploy-dev.yml → no errors.)
- [ ] Step 3: Commit
cd /Users/admin/hq-mcp-server
git add .github/workflows/deploy-dev.yml
git -c user.name="jeevan-hq" -c user.email="jeevan@hirequotient.com" \
commit -m "ci(dev): add manual deploy-dev workflow (build ref image + SSH deploy to Easysource-internal)"
Task 2: Add .env.dev.example documenting the dev deltas¶
Files:
- Create: .env.dev.example
Interfaces:
- Produces: a committed, secret-free reference of the exact dev .env deltas, so the on-box .env (Task 4) is reproducible.
- [ ] Step 1: Write the example (NON-SECRET keys + placeholders only)
# Dev overrides for the Easysource-internal box. Copy the box's existing/local
# working .env, then set these three keys. All other keys (MONGO_URI, REDIS_URL,
# JWT_SECRET_TOKEN, GOOGLE_CLIENT_ID/SECRET, etc.) are REUSED unchanged from the
# internal values — do NOT commit real secrets here.
PORT=4100
ENV=development
# BASE ONLY — no /mcp suffix (app appends /mcp and /oauth/google/callback itself).
PUBLIC_BASE_URL=https://mcp-internal.hirequotient.com
ALLOWED_HOSTS=mcp-internal.hirequotient.com
BE_BASE_URL=https://easysource-internal.hirequotient.com/api
EASYSOURCE_WEB_BASE=https://easysource-internal.hirequotient.com
# Mongo cluster uses MONGODB-X509 → REQUIRED (deploy.sh mounts certs/db-cert.pem
# at /run/secrets/db-cert.pem; cert = box BE's /etc/databaseCert/dev_prod_mongodb.pem).
MONGO_CERT_PATH=/run/secrets/db-cert.pem
- [ ] Step 2: Verify no real secret values are present
Run: cd /Users/admin/hq-mcp-server && grep -nE "(mongodb\+srv|redis://|eyJ|AIza|GOCSPX)" .env.dev.example && echo "LEAK" || echo "clean"
Expected: clean
- [ ] Step 3: Commit
cd /Users/admin/hq-mcp-server
git add .env.dev.example
git -c user.name="jeevan-hq" -c user.email="jeevan@hirequotient.com" \
commit -m "docs(dev): add .env.dev.example documenting dev host/env overrides"
Task 3: Deploy SSH key + GitHub secrets (manual, out-of-repo)¶
Files: none (GitHub repo settings + box ~/.ssh/authorized_keys).
- [ ] Step 1: Generate a dedicated deploy keypair (local)
-
[ ] Step 2: Add the PUBLIC key to the box (as the deploy user, e.g.
ubuntu): Append~/.ssh/hq-mcp-dev-deploy.pubto the box's~/.ssh/authorized_keys. Verify:ssh -i ~/.ssh/hq-mcp-dev-deploy <user>@35.153.20.238 'echo ok'→ok. -
[ ] Step 3: Set repo secrets (Settings → Secrets → Actions, or
gh): DEV_DEPLOY_SSH_HOST=35.153.20.238(ormcp-internal.hirequotient.com)DEV_DEPLOY_SSH_USER=<deploy user>DEV_DEPLOY_SSH_KEY=<contents of ~/.ssh/hq-mcp-dev-deploy>(private key)-
DEV_DEPLOY_SSH_PORT=22Verify:gh secret list -R HireQuotient/hq-mcp-servershows the fourDEV_DEPLOY_*. -
[ ] Step 4: Create the
internalEnvironment (optional approval gate) in repo Settings → Environments.
Task 4: On-box provisioning (clone + dev .env + cert + GHCR login)¶
Files: on box ~/hq-mcp-server/.env, ~/hq-mcp-server/certs/db-cert.pem.
-
[ ] Step 1: Confirm prereqs & free port
-
[ ] Step 2: Clone the repo
-
[ ] Step 3: Create the dev
.env— copy the internal values, then apply the three deltas from.env.dev.example. Placecerts/db-cert.pemif the box uses Mongo X509 (else keep the plainMONGO_URIand dropMONGO_CERT_PATH). Verify:test -f .env && echo ".env present". -
[ ] Step 4: GHCR login (read:packages)
Verify:Login Succeeded.
Task 5: nginx TLS vhost for mcp-internal.hirequotient.com¶
Files: on box /etc/nginx/sites-available/mcp-internal.conf (+ symlink to sites-enabled).
-
[ ] Step 1: Write the server block
server { server_name mcp-internal.hirequotient.com; location / { proxy_pass http://127.0.0.1:5011; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; # Streamable-HTTP/SSE proxy_read_timeout 3600s; proxy_send_timeout 3600s; } listen 80; } -
[ ] Step 2: Enable + test config
Expected:syntax is ok/test is successful. -
[ ] Step 3: Issue TLS cert (certbot rewrites the block to 443)
Verify: cert issued, block nowsudo certbot --nginx -d mcp-internal.hirequotient.com --non-interactive --agree-tos -m jeevan@hirequotient.com sudo systemctl reload nginxlisten 443 ssl.
Task 6: First dev deploy + health¶
-
[ ] Step 1: Add the dev redirect URI in Google Cloud (owner):
https://mcp-internal.hirequotient.com/oauth/google/callbackon the existing OAuth client. -
[ ] Step 2: Run the workflow — Actions → "Deploy (dev)" → Run workflow →
ref=feat/via-claude-mail-source. Or:gh workflow run deploy-dev.yml -R HireQuotient/hq-mcp-server -f ref=feat/via-claude-mail-source. -
[ ] Step 3: Verify container + health on the box
Expected: container running;/healthz200. -
[ ] Step 4: Verify public endpoint Verify from an allowed network:
https://mcp-internal.hirequotient.com/healthz→ 200;https://mcp-internal.hirequotient.com/mcpreachable.
Task 7: End-to-end connector test (acceptance gate)¶
-
[ ] Step 1: In claude.ai, add a second connector "Easysource (dev)" →
https://mcp-internal.hirequotient.com/mcp; complete Google OAuth. Verify tools list loads. -
[ ] Step 2: Against a test org, run a tool that emits an internal-team mail (create project / update vetting). Verify the internal mail subject carries
[via MCP](sync sites), and for async (insights / outreach EMAIL STATS) thatproject.vettingSource/workflowLog.source=mcpin the DB and the outreach-handler mail is tagged. Loki:service_name="outreach-handler". -
[ ] Step 3: Confirm the prod connector (
mcp.hirequotient.com) and prod mail are unaffected (spot-check a prod mail has no tag change from this). -
[ ] Step 4: Green → the feature branch is cleared to merge to
main(existing prod build + manualdeploy.ymlpath).
Self-Review¶
- Spec coverage: exposure (Task 5), hostname (Tasks 4/5/6), deploy trigger + on-demand build (Task 1), BE target (Task 2
.env), EIP/DNS (done pre-plan), OAuth (Task 6 Step 1), end-to-end test (Task 7), runbook order preserved. ✓ - Placeholder scan: none (
.env.dev.exampleintentionally uses documented placeholders for secrets, verified secret-free in Task 2 Step 2). - Consistency: container
hq-mcp/ port5011/ hostmcp-internal.hirequotient.com/ EIP35.153.20.238used identically across tasks. ✓
Execution status — 2026-09-09¶
LIVE: https://mcp-internal.hirequotient.com/mcp (dev MCP on Easysource-internal).
Done:
- EIP 35.153.20.238 allocated + attached; easysource-internal + mcp-internal DNS re-pointed. ✓
- Task 1 deploy-dev.yml (ref-injection hardened) + Task 2 .env.dev.example — committed on feat/dev-mcp-deploy. ✓
- Box provisioning (Task 4/5): ayush added to docker group; repo seeded via git bundle on feat/via-claude-mail-source (731ba13); dev .env (reused secrets + host/env overrides + MONGO_CERT_PATH=/run/secrets/db-cert.pem); Mongo X509 cert copied from /etc/databaseCert/dev_prod_mongodb.pem → certs/db-cert.pem; one-off BUILD_LOCAL=1 image built; container hq-mcp on 127.0.0.1:5011 (--restart unless-stopped). ✓
- nginx vhost + certbot TLS (expires 2026-12-08, auto-renew) + 80→443 redirect. ✓
- Verified: /healthz → 200 {"ok":true}; /mcp → 401 (OAuth challenge, proxy OK). ✓
Remaining (user-driven / CI wiring):
- Google Cloud: add redirect URI https://mcp-internal.hirequotient.com/oauth/google/callback to the existing OAuth client.
- Task 7: add claude.ai connector → the dev URL, OAuth login, run a tool, verify [via MCP].
- Task 3 + CI path: generate deploy keypair → box authorized_keys; set DEV_DEPLOY_* GitHub secrets; give the box a read-only git credential (CI deploy job runs git fetch origin); then deploy-dev.yml replaces the manual bootstrap as the repeatable deploy.
Gotchas hit during bring-up (2026-09-09)¶
PUBLIC_BASE_URLmust be the BASE, no/mcp— the app appends/mcp(resource) and/oauth/*itself. A/mcpsuffix doubles it (resource=.../mcp/mcp) and breaks OAuth discovery + the Google callback path. Verify via/.well-known/oauth-authorization-servermatching prod's shape.- Mongo cert must be world-readable — the container runs as non-root
appuser, but the cert copied from the BE is0640 ayush:ayush, so the app throwsEACCESon/run/secrets/db-cert.pem→ Mongo never connects →users.findOne()buffers 10s → the OAuth callback hangs. Fix:chmod 644 certs/db-cert.pem. (Re-apply whenever the cert is re-copied.)