Skip to main content
Glama
aircwou

jhipsterSampleApplication

by aircwou

jhipsterSampleApplication — JHipster + MCPHub tenant

A JHipster 9 application (Spring Boot 4 + Angular) that also acts as an MCPHub TENANT_DELEGATED tenant: MCPHub sends users to this app's login page, then exchanges and validates sessions through HMAC-signed API calls.

This guide covers four ways to run it:

Option

Use it for

Database

URL

1. Run locally

Day-to-day development

H2 in-memory

http://localhost:8085

2. Docker Compose

Production-like run on your machine

PostgreSQL container

http://localhost:8080

3. Render

Hosted deployment

Render PostgreSQL

https://<service>.onrender.com

4. Cloudflare Tunnel

Making a local app reachable from MCPHub

(whatever is running locally)

https://<your-host>


Contents


Related MCP server: genpark-event-driven-webhook-signature-verifier-skill

Prerequisites

Tool

Version

Needed for

Git

any

Cloning

JDK

21

Options 1 and the Jib image build

Node.js

≥ 24.14 (optional)

Only if you run npm directly. Maven downloads its own Node 24.14 / npm 11.11 into target/

Docker + Compose v2

recent

Option 2 (and building the image yourself)

cloudflared

recent

Option 4

A Render account

—

Option 3

You do not need to install Maven: the repo ships the Maven wrapper (./mvnw, or .\mvnw.cmd on Windows PowerShell/cmd).

Windows: the examples use bash syntax. In Git Bash they work as written. In PowerShell, use .\mvnw.cmd instead of ./mvnw, .\npmw.cmd instead of ./npmw, and $env:NAME = "value" instead of export NAME=value.


Project setup

git clone https://github.com/aircwou/jhipster-mcp-demo.git
cd jhipster-mcp-demo

# Optional: install client dependencies up front (Maven also does this on first build).
# CYPRESS_INSTALL_BINARY=0 skips the ~500 MB Cypress download if you don't run e2e tests.
CYPRESS_INSTALL_BINARY=0 ./npmw install

The first build downloads Maven dependencies, Node, and npm packages, so it takes a few minutes. Later builds are much faster.


Configuration

Spring profiles

Profile

Activated by

What it does

dev

Default for ./mvnw, -Pdev

H2 in-memory DB, port 8085, debug logging. Also pulls in secret-samples and api-docs

secret-samples

Included by dev

Supplies the JWT secret and the MCPHub settings from application-secret-samples.yml

prod

-Pprod, the Docker image

PostgreSQL, port 8080, all secrets must come from environment variables

In dev, values in application-secret-samples.yml override the MCP_CONTRACT_* environment variables (the profile file wins over the ${…} placeholders in application.yml). To change them in dev, edit that file, or use Spring's own variable names, which take priority over YAML files: e.g. MCP_CONTRACT_ALLOWEDCALLBACKHOSTS (no underscores between words).

Environment variables (prod / Docker / Render)

Variable

Required

Description

JHIPSTER_SECURITY_AUTHENTICATION_JWT_BASE64_SECRET

Yes

Base64 of ≥ 64 random bytes; signs user JWTs (HS512).

MCP_CONTRACT_HMAC_KEY

Yes

Exactly 64 hex characters. Must equal the key configured for this tenant in MCPHub. The app refuses to start in prod if it is missing, malformed, or the sample placeholder.

MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS

Yes, for MCP login

Comma-separated host names MCPHub may redirect back to, e.g. lockmcp.ai,www.lockmcp.ai. Host only: no https:// and no path.

MCP_CONTRACT_DOMAIN_VERIFICATION_TOKEN

No

Served at /.well-known/mcp-hub-verification.json for domain verification.

MCP_CONTRACT_SESSION_TTL_SEC

No

Session lifetime, default 3600.

MCP_CONTRACT_AUTH_CODE_TTL_SEC

No

Auth-code lifetime, default 60.

SPRING_DATASOURCE_URL

Yes (prod)

JDBC URL, e.g. jdbc:postgresql://host:5432/dbname.

SPRING_DATASOURCE_USERNAME / SPRING_DATASOURCE_PASSWORD

Yes (prod)

Database credentials.

PORT

No

Port the Docker image listens on (default 8080). Render sets this automatically.

Generate secrets:

openssl rand -base64 64 | tr -d '\n'   # JHIPSTER_SECURITY_AUTHENTICATION_JWT_BASE64_SECRET
openssl rand -hex 32                   # MCP_CONTRACT_HMAC_KEY (only if you are creating the key; otherwise copy it from MCPHub)

For Docker Compose, put these in a .env file. Start from the template; .env is git-ignored:

cp .env.example .env

1. Run locally

Uses the dev profile: an in-memory H2 database (data, including MCP sessions, is lost on restart) and sample users loaded at startup.

Option A: single process (simplest)

Builds the Angular client once and serves everything from Spring Boot:

CYPRESS_INSTALL_BINARY=0 ./mvnw spring-boot:run -Pdev,webapp -ntp

PowerShell:

$env:CYPRESS_INSTALL_BINARY = "0"; .\mvnw.cmd spring-boot:run "-Pdev,webapp" -ntp

Open http://localhost:8085 and sign in with admin / admin or user / user.

Option B: live reload for front-end work

Run the back end and the Angular dev server in two terminals:

./npmw run backend:start   # Spring Boot on :8085 (skips the client build)
./npmw start               # Angular dev server, proxies API calls to :8085

Open http://localhost:9000. The browser refreshes when you change client files.

Option C: production profile on your machine

./npmw run docker:db:up                      # PostgreSQL in Docker on 127.0.0.1:5432
./mvnw -Pprod -DskipTests -ntp clean verify  # builds target/*.jar

export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost:5432/jhipsterSampleApplication
export SPRING_DATASOURCE_USERNAME=jhipsterSampleApplication
export JHIPSTER_SECURITY_AUTHENTICATION_JWT_BASE64_SECRET="$(openssl rand -base64 64 | tr -d '\n')"
export MCP_CONTRACT_HMAC_KEY=<64-hex-key>
export MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS=lockmcp.ai,www.lockmcp.ai

java -jar target/*.jar                       # http://localhost:8080

2. Run with Docker Compose

src/main/docker/app.yml starts the app (prod profile) and PostgreSQL.

Step 1: create .env with at least the two required secrets (see Configuration):

cp .env.example .env
# edit .env

Step 2: build the image (either way produces jhipstersampleapplication:latest):

# Option 1: Dockerfile. Needs only Docker.
docker build -t jhipstersampleapplication .

# Option 2: Jib. Needs JDK 21, no Dockerfile involved.
./npmw run java:docker            # amd64
./npmw run java:docker:arm64      # Apple Silicon / arm64

Step 3: start it. Pass --env-file .env. Compose does not pick up the repo-root .env automatically when the compose file is in another folder.

docker compose --env-file .env -f src/main/docker/app.yml up -d --wait

Open http://localhost:8080. --wait returns once the app's health check (/management/health) passes. First start takes about 30 to 60 seconds while Liquibase creates the schema.

docker compose --env-file .env -f src/main/docker/app.yml logs -f app   # follow logs
docker compose --env-file .env -f src/main/docker/app.yml down          # stop
docker compose --env-file .env -f src/main/docker/app.yml down -v       # stop and delete the database

Notes:

  • Ports are bound to 127.0.0.1 only. Use Cloudflare Tunnel to expose the app.

  • The PostgreSQL container uses trust auth and has no volume by default. Fine locally, not for production (see the comments in postgresql.yml).

  • Only need the database (e.g. for local Option C)? ./npmw run docker:db:up / ./npmw run docker:db:down.


3. Deploy to Render

Render has no native Java runtime, so the service is built from the repo's Dockerfile. It compiles the client and server inside Docker and listens on Render's PORT.

Step 1: create the database

  1. Render Dashboard → New → Postgres.

  2. Pick a name and a region. Use the same region for the web service so they can talk over the private network.

  3. Create it, then open Connections and note Hostname (internal), Port, Database, Username, and Password.

Step 2: create the web service

  1. Push this repo to GitHub/GitLab. Render deploys from your remote, not from your machine.

  2. Render Dashboard → New → Web Service → connect the repository.

  3. Settings:

    • Language / Runtime: Docker

    • Branch: main

    • Dockerfile Path: ./Dockerfile (Root Directory: leave empty)

    • Region: same as the database

    • Instance type: at least 512 MB RAM. The free tier works for a demo but sleeps after inactivity (see Troubleshooting).

  4. Environment Variables: add these. Use the internal hostname from Step 1:

    Key

    Value

    SPRING_DATASOURCE_URL

    jdbc:postgresql://<internal-hostname>:5432/<database>

    SPRING_DATASOURCE_USERNAME

    <username>

    SPRING_DATASOURCE_PASSWORD

    <password>

    JHIPSTER_SECURITY_AUTHENTICATION_JWT_BASE64_SECRET

    output of openssl rand -base64 64 | tr -d '\n'

    MCP_CONTRACT_HMAC_KEY

    the 64-hex key from MCPHub

    MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS

    e.g. lockmcp.ai,www.lockmcp.ai

    MCP_CONTRACT_DOMAIN_VERIFICATION_TOKEN

    (optional) token from MCPHub

    Render shows a postgresql://… URL. Spring needs the jdbc:postgresql:// form with the username and password in separate variables, so don't paste the Render URL as-is.

    Don't set PORT. Render provides it and the image binds to it.

  5. Advanced → Health Check Path: /management/health

  6. Click Create Web Service. The first build takes several minutes (Maven + Angular production build). Watch the Logs tab until you see Application 'jhipsterSampleApplication' is running!.

Step 3: after the first deploy

  • Your app is at https://<service-name>.onrender.com. Optionally add a custom domain under Settings → Custom Domains.

  • Change the admin and user passwords immediately. The sample accounts are created in every profile.

  • Register the public URL in MCPHub (see Registering the app in MCPHub).

  • Every push to main triggers a redeploy (Auto-Deploy is on by default).


4. Expose a local app with Cloudflare Tunnel

Use this when MCPHub (or anyone else) needs to reach an app running on your machine. The tunnel connects outbound to Cloudflare, so there are no open ports and no port forwarding.

Pick the local port that matches how you run the app:

How it runs

Local URL to expose

Option 1A / 1B (dev)

http://localhost:8085

Option 1C or Docker Compose (prod)

http://localhost:8080

Install cloudflared

# Windows
winget install --id Cloudflare.cloudflared

# macOS
brew install cloudflared

# Debian / Ubuntu (amd64)
curl -L -o cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb

cloudflared --version

Quick tunnel (no account, random URL)

cloudflared tunnel --url http://localhost:8085

cloudflared prints a URL like https://<random-words>.trycloudflare.com. It changes every time you restart, so it's good for a quick test and bad for anything you register in MCPHub.

Named tunnel on your own domain (stable URL)

Requires a domain whose DNS is managed by Cloudflare. The example uses jhipster-raw.example.com; replace it with yours.

# 1. Authenticate. Opens a browser; pick the zone (domain). Saves ~/.cloudflared/cert.pem
cloudflared tunnel login

# 2. Create the tunnel. Prints a tunnel UUID and writes ~/.cloudflared/<UUID>.json
cloudflared tunnel create jhipster-raw

# 3. Point a hostname at it (creates a proxied CNAME in Cloudflare DNS)
cloudflared tunnel route dns jhipster-raw jhipster-raw.example.com
  1. Create ~/.cloudflared/config.yml (Windows: %USERPROFILE%\.cloudflared\config.yml):

tunnel: jhipster-raw
credentials-file: /home/<you>/.cloudflared/<UUID>.json # Windows: C:\Users\<you>\.cloudflared\<UUID>.json
ingress:
  - hostname: jhipster-raw.example.com
    service: http://localhost:8085
  - service: http_status:404
  1. Validate and run:

cloudflared tunnel ingress validate
cloudflared tunnel run jhipster-raw

The app is now at https://jhipster-raw.example.com. Cloudflare terminates TLS; your app still serves plain HTTP locally. Keep the terminal open, or install it as a background service with cloudflared service install (see Cloudflare's docs).

Dashboard-managed alternative: in Cloudflare Zero Trust → Networks → Tunnels, create a tunnel, add a Public Hostname pointing to http://localhost:8085, and run the command it shows: cloudflared tunnel run --token <TOKEN>.

The app must be running before traffic arrives. If it isn't, Cloudflare returns a 502 Bad Gateway.


Registering the app in MCPHub

Replace https://<your-host> with your Render URL or tunnel hostname.

MCPHub field

URL

Method

Login URL

https://<your-host>/mcp-login (dash, not /mcp/login)

GET (browser)

Exchange URL

https://<your-host>/api/mcp/exchange

POST, HMAC-signed

Validate URL

https://<your-host>/api/mcp/validate

POST, HMAC-signed

Refresh

https://<your-host>/api/mcp/refresh

POST, HMAC-signed

Logout

https://<your-host>/api/mcp/logout

POST, HMAC-signed

Users directory

https://<your-host>/api/mcp/users

GET, HMAC-signed

Roles directory

https://<your-host>/api/mcp/roles

GET, unsigned

Domain verification

https://<your-host>/.well-known/mcp-hub-verification.json

GET

Checklist:

  • The HMAC key in MCPHub and in the app are identical.

  • The host of MCPHub's callback_url is listed in MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS.

  • The OpenAPI contract the hub uses is mcp-openapi.yaml.


Users, roles, and MCP tool scopes

Sample accounts

Created by Liquibase on first start, in every profile (dev, Docker, Render):

Login

Password

Email

Roles

admin

admin

admin@localhost

ROLE_ADMIN, ROLE_USER

user

user

user@localhost

ROLE_USER

Roles

Role

Exists in the database?

In the app

MCP tool scopes (from application.yml)

ROLE_ADMIN

Yes (seeded)

Full access, including the Administration menu (user management, authorities, metrics, logs)

All 31 operations: account, users, bank accounts, labels, operations, authorities

ROLE_USER

Yes (seeded)

Normal user: own account plus bank accounts, labels, operations

getAccount, saveAccount, changePassword, getAllUsers, createUser, updateUser, getUser, deleteUser

ROLE_MANAGER

No

Not usable until you create it (see below)

getAccount, saveAccount, changePassword, getAllUsers

How the scopes work:

  • The scopes are configured under mcp.contract.tool-scopes in application.yml, keyed by role name. Each name is an operationId from mcp-openapi.yaml.

  • A user's toolScopes, returned by /api/mcp/exchange and /api/mcp/validate, is the union of the scopes of all their roles. For example, admin gets the ROLE_ADMIN and ROLE_USER scopes combined.

  • /api/mcp/roles lists the roles that exist in the database with their scopes. Roles that appear only in application.yml (like ROLE_MANAGER today) are not reported to MCPHub.

  • Scopes are not permissions. The app still enforces its own access rules. The user-management operations (getAllUsers, createUser, updateUser, getUser, deleteUser) call /api/admin/users, which requires ROLE_ADMIN. A ROLE_USER or ROLE_MANAGER session is offered those tools but gets 403 Forbidden when it calls them. Remove them from those roles if you don't want MCPHub to offer them.

Adding a role (e.g. ROLE_MANAGER)

  1. Create the role. Sign in as admin, go to Administration → Authority (/authority), and create ROLE_MANAGER. Or call the API:

    curl -X POST http://localhost:8085/api/authorities \
      -H "Authorization: Bearer <admin JWT>" -H "Content-Type: application/json" \
      -d '{"name":"ROLE_MANAGER"}'

    Don't edit liquibase/data/authority.csv on an existing database. Liquibase has already applied it, and changing the file makes startup fail with a checksum error.

  2. Assign it. Go to Administration → User management → Edit user → Profiles, and tick the role.

  3. Set its tool scopes in application.yml under tool-scopes. Keep the bracket-and-quotes form, which preserves the exact key:

    '[ROLE_MANAGER]':
      - getAccount
      - getAllBankAccounts

    Restart the app.

  4. Re-import roles in MCPHub so it picks up the new role from /api/mcp/roles.

In dev, the H2 database is in-memory. Roles and users created through the UI or API disappear on restart, but changes to application.yml stay.


Common commands

Task

Command

Run dev (single process)

./mvnw spring-boot:run -Pdev,webapp -ntp

Run back end only (dev)

./npmw run backend:start

Run Angular dev server

./npmw start

Build production jar

./mvnw -Pprod -DskipTests -ntp clean verify

Build Docker image (Dockerfile)

docker build -t jhipstersampleapplication .

Build Docker image (Jib)

./npmw run java:docker

Start app + DB in Docker

docker compose --env-file .env -f src/main/docker/app.yml up -d --wait

Start only PostgreSQL

./npmw run docker:db:up

Back-end tests

./mvnw verify

Front-end tests

./npmw test

Lint client

./npmw run lint

Health check

curl http://localhost:8080/management/health


Troubleshooting

Symptom

Cause / fix

MCP_CONTRACT_HMAC_KEY is not set / …must be exactly 64 hexadecimal characters at startup

Running a non-dev profile without a valid key. Set it to 64 hex chars (no quotes, no spaces).

Could not resolve placeholder 'jhipster.security.authentication.jwt.base64-secret'

prod profile without JHIPSTER_SECURITY_AUTHENTICATION_JWT_BASE64_SECRET.

required variable … is missing a value: set it in .env

You ran Docker Compose without --env-file .env, or the value is empty in .env.

MCP login page says "The callback URL is not permitted for this tenant."

The callback_url host isn't in MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS. List host names only (lockmcp.ai, not https://lockmcp.ai/...). In dev, edit application-secret-samples.yml.

Exchange/validate return 401 unauthorized

Signature check failed. Either (a) the key differs from MCPHub's; (b) the URL registered in MCPHub differs from the one the request arrives on (scheme, host, and path are all signed, e.g. www. vs bare domain); or (c) the server clock is off, since requests outside the replay window are rejected.

/mcp/login shows the normal JHipster UI

Wrong path. It's /mcp-login.

Port 8085 (or 8080) was already in use

Stop the other process, or change server.port (dev: application-dev.yml; also update webpack/proxy.conf.js).

Docker: Bind for 127.0.0.1:5432 failed: port is already allocated (or :8080)

Another container or a local PostgreSQL is using the port. Stop it, or change the host side of ports: in src/main/docker/app.yml / postgresql.yml (e.g. 127.0.0.1:18080:8080).

/bin/sh^M: bad interpreter running ./mvnw in Linux/WSL

Windows line endings: sed -i 's/\r$//' mvnw. The Dockerfile already does this.

npm install is very slow or fails downloading Cypress

Set CYPRESS_INSTALL_BINARY=0.

Cloudflare 502 Bad Gateway / Unable to reach the origin service

App not running, or the tunnel points at the wrong port (8085 dev vs 8080 prod).

Render deploy fails with No open ports detected or health check failures

The app is still starting (Liquibase + JVM). Check the logs for the real error, usually a missing env var or a wrong SPRING_DATASOURCE_URL (must start with jdbc:postgresql://).

Render: Connection refused / UnknownHostException to the database

Web service and database are in different regions, or you used the external hostname without SSL. Use the internal hostname in the same region.

Render free tier: first request after idle takes about a minute

Free services spin down when idle. MCPHub calls during spin-up may time out; use a paid instance for anything real.

Render build killed / out of memory

The Angular production build is memory-hungry. Retry, or use a larger instance type.

git push → 403 Permission … denied to <user>

Git is signed in as a GitHub account without write access to the remote. Get added as a collaborator, or sign in with the right account.


Security notes

  • Never commit real secrets. application-secret-samples.yml is for local development only. In prod, use environment variables (.env locally, the Render dashboard on Render). .env is git-ignored.

  • If a real HMAC key or JWT secret was ever committed or pushed, rotate it. Removing it in a later commit does not remove it from git history.

  • The sample accounts admin/admin and user/user exist in every profile. Change those passwords on any publicly reachable deployment, including Cloudflare quick tunnels.

  • /api/mcp/roles and /.well-known/mcp-hub-verification.json are intentionally unauthenticated. Every other MCP back-channel endpoint requires a valid HMAC signature.


Testing

./mvnw verify          # Spring Boot unit + integration tests
./npmw test            # Angular unit tests (Vitest)
./npmw run e2e         # Cypress e2e; start the app first with ./npmw run app:start
./mvnw gatling:test    # Gatling performance tests

For e2e, set CYPRESS_E2E_USERNAME / CYPRESS_E2E_PASSWORD to override the default credentials.


References

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables clients to access multiple backend MCP servers through a single endpoint, with OAuth 2.1 authorization, namespaced tools, and secure credential management.
    2
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables managing and observing Technitium DNS Server through MCP, including read-only or read-write tool surfaces, scoped bearer authentication, audit logging, and confirmation gates for destructive operations.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables turning selected OpenAPI operations into authenticated MCP tools with branded OAuth, operation allowlisting, and live schema refreshes.
    Apache 2.0