MCPHub
Overview
MCPHub is an open-source, Apache-2.0 licensed gateway for the Model Context Protocol (MCP). You connect MCP servers to it once. These can be local stdio servers started with npx or uvx, or remote SSE and streamable HTTP servers. MCPHub publishes them to AI clients such as Claude Code, Cursor, Cherry Studio and OpenWebUI through stable, authenticated MCP endpoints. A web dashboard manages servers, tools, groups, users and keys.
This guide covers the deployment and operation of MCPHub on Ubuntu 24.04 on Azure, using SecureAnyCloud Azure Marketplace images.
What makes MCPHub different: SecureAnyCloud also publishes the MetaMCP and ContextForge MCP gateways, and MCPHub is the lightest of the three. It is a single Node.js process with one JSON settings file, no database server and no container runtime. MCPHub is organized around routes and groups:
- One endpoint per scope.
/mcpserves every server,/mcp/<group>serves one group, and/mcp/<server>serves a single server. Each client or agent can be pointed at exactly the set of servers it needs. - Per-tool control. Each tool of each server can be switched off or have its description rewritten. A group can include only chosen tools from a server, for example the read-only tools of a database server.
- Scoped bearer keys. A key can be limited to particular groups or servers, so an agent's key cannot reach anything else.
- Per-user credentials. Users can bind their own API keys to a shared server, stored encrypted, so one server definition serves many people with their own accounts.
- Smart routing (optional). Upstream's
$smartendpoint finds the right tool by semantic search. It needs PostgreSQL with pgvector and an embedding API, which this image does not include. See Step 11.
This image builds MCPHub v1.1.4 from the upstream release tag (the commit is verified) with the upstream lockfile. It runs MCPHub as an unprivileged mcphub service account on loopback only, and puts nginx in front of it with HTTPS.
Out of the box, upstream MCPHub creates its admin account with a random password that it prints to the server log. On this image, the password is generated for your VM on first boot and passed to MCPHub from a root-only file, so it is never logged. A per-VM MCP bearer key is also created. nginx does not start until first boot has proved that the password works and that MCP refuses callers without the key. The image is backed by 24/7 SecureAnyCloud support.
MCPHub is an independent open-source project by samanhappy and its contributors. This image is produced by SecureAnyCloud and is not affiliated with, endorsed by, or sponsored by the MCPHub project or its contributors. It ships the free and open-source Apache-2.0 licensed software, unmodified.
What is included:
- MCPHub v1.1.4 built from the upstream tag with
pnpm --frozen-lockfile, production dependencies only, with itsLICENSEin/usr/share/doc/mcphub - Node.js 22 LTS and
uv/uvxfrom their official releases, pinned by checksum, so you can addnpxanduvxbased MCP servers - Two example MCP servers from the Model Context Protocol project, memory (a small knowledge graph) and sequential-thinking, installed locally from a pinned lockfile. They need no network and no API key, and are grouped as starter
- nginx serving the dashboard and MCP endpoints over HTTPS on port 443 with a certificate generated for your VM. Port
80only redirects to HTTPS - A unique admin password, MCP bearer key, JWT signing secret, credential encryption key and TLS certificate generated per VM on first boot, never baked into the image
mcphub-selftest, a one-command, end-to-end check of the gateway
Prerequisites
- An Azure subscription with permission to create VMs
- A
Standard_B2sVM or larger. MCPHub itself is light, so size the VM for the MCP servers you will run on it (eachstdioserver is its own process) - Inbound TCP
443(and optionally80, which only redirects) open to the networks your AI clients and administrators connect from - Outbound internet access if you will add servers with
npxoruvx. They download packages fromregistry.npmjs.organdpypi.orgthe first time they start. The two example servers need no network - An SSH key pair for administrative access
Deploy from the Azure Portal
- In the Azure Portal, search the Marketplace for MCPHub on Ubuntu 24.04 LTS by SecureAnyCloud, and select Create.
- Choose your subscription, resource group and region.
- Set the VM size to
Standard_B2sor larger. - Set the authentication type to SSH public key and supply your key.
- Under Inbound port rules, allow SSH (22) and HTTPS (443). Add HTTP (80) if you want plain
http://links to redirect. - On the Disks tab, choose
StandardSSD_LRSfor a good balance of cost and performance. - Select Review + create, then Create.
First boot takes well under a minute. During it, the appliance generates your VM's credentials and certificate, creates the admin account, and proves that MCP refuses unauthenticated callers. nginx does not start until that has been proved.
Deploy from the Azure CLI
az vm image terms accept --urn sac1647283583153:mcphub-ubuntu-24-04:default:latest
az vm create \
--resource-group my-resource-group \
--name mcphub-gateway \
--image sac1647283583153:mcphub-ubuntu-24-04:default:latest \
--size Standard_B2s \
--storage-sku StandardSSD_LRS \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku Standard
az vm open-port \
--resource-group my-resource-group \
--name mcphub-gateway \
--port 443 --priority 1001Restrict that rule to your own address ranges. A gateway can run tools on your behalf, so the fewer networks that can reach it, the better.
Connect to your VM
Connect over SSH:
ssh azureuser@<vm-ip>Confirm the services are running
Check that all three units are active:
sudo systemctl is-active mcphub-firstboot.service mcphub.service nginx.serviceExpected output:
active active activenginx listens on 80 and 443. MCPHub listens on port 3000 on loopback only:
sudo ss -lntp | grep -E ':(80|443|3000)\b'Expected output (process ids differ):
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=4549,fd=7),...) LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=4549,fd=5),...) LISTEN 0 511 127.0.0.1:3000 0.0.0.0:* users:(("node",pid=4668,fd=24)) LISTEN 0 511 [::]:443 [::]:* users:(("nginx",pid=4549,fd=8),...) LISTEN 0 511 [::]:80 [::]:* users:(("nginx",pid=4549,fd=6),...)Upstream MCPHub has no listen address option, so the appliance starts it with a small shim (/opt/mcphub/loopback-listen.cjs) that binds it to 127.0.0.1.
You can prove from the VM that port 3000 answers on loopback and not on the VM's own address, while nginx serves HTTPS and port 80 only redirects:
curl -s -o /dev/null -w 'loopback 127.0.0.1:3000 -> HTTP %{http_code}\n' http://127.0.0.1:3000/health
curl -s -o /dev/null -m 4 -w "own address $(hostname -I | awk '{print $1}'):3000 -> HTTP %{http_code}\n" "http://$(hostname -I | awk '{print $1}'):3000/health" || true
curl -s -o /dev/null -w 'http://127.0.0.1/mcp -> HTTP %{http_code} %{redirect_url}\n' http://127.0.0.1/mcp
curl -sk https://127.0.0.1/health | jq -c .serversExpected output (your private address differs):
loopback 127.0.0.1:3000 -> HTTP 200 own address 10.0.0.10:3000 -> HTTP 000 http://127.0.0.1/mcp -> HTTP 301 https://127.0.0.1/mcp {"total":2,"connected":2,"disconnected":0}HTTP 000 means nothing answered. Nothing outside the VM can reach MCPHub except through nginx.
Read your VM's credentials
Every credential on this appliance was generated for this VM on its first boot. Read them from the credentials file:
sudo cat /root/mcphub-credentials.txtExpected output (your values differ):
# MCPHub - generated for this VM on first boot by mcphub-firstboot.service.
# Nothing here is shared with any other VM. Changing the password or keys in the
# dashboard does not update this file. MCPHUB_URL=https://<your VM's public IP>/ MCPHUB_ADMIN_USERNAME=admin MCPHUB_ADMIN_PASSWORD=<24 characters generated on your VM> MCPHUB_BEARER_KEY=mcphub_<64 hex characters generated on your VM> MCPHUB_MCP_URL=https://<your VM's public IP>/mcp MCPHUB_GROUP_MCP_URL=https://<your VM's public IP>/mcp/starter MCPHUB_TLS_SHA256_FINGERPRINT=<unique to your VM>The JWT signing secret MCPHub uses for dashboard sessions is in /etc/mcphub/mcphub.env. Only root and the mcphub service account can read it. The settings file /var/lib/mcphub/mcp_settings.json stores the admin password only as a bcrypt hash.
Sign in to the dashboard
Open MCPHUB_URL from the credentials file in your browser. The certificate is generated for your VM and is self-signed, so the browser warns once. Compare the fingerprint it shows with MCPHUB_TLS_SHA256_FINGERPRINT before you accept it. Sign in as admin with MCPHUB_ADMIN_PASSWORD.
The dashboard shows how many servers are online and their tools. An MCP Endpoints card lists the URLs to give your clients: every server, each group, and smart routing. The URLs use your VM's public address, which is set on first boot under Settings > Installation > Base URL. Change it there if you give the VM a DNS name.
The sidebar holds Servers, Groups, Prompts, Resources, Market, Users, Logs, Credentials and Settings.
Change the admin password under Settings if you want one of your own, and record it somewhere safe. The credentials file keeps the original.
Check the MCP endpoints
Every MCP endpoint requires a bearer key. Without one, or with a wrong one, MCPHub answers 401:
for p in /mcp /mcp/starter /mcp/memory; do printf 'anonymous POST %-13s -> HTTP %s\n' "$p" "$(curl -sk -o /dev/null -w '%{http_code}' -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}' https://127.0.0.1$p)" done printf 'anonymous GET %-13s -> HTTP %s\n' /sse "$(curl -sk -o /dev/null -m 5 -w '%{http_code}' -H 'Accept: text/event-stream' https://127.0.0.1/sse)"Expected output:
anonymous POST /mcp -> HTTP 401 anonymous POST /mcp/starter -> HTTP 401 anonymous POST /mcp/memory -> HTTP 401 anonymous GET /sse -> HTTP 401/mcp/memory is the single-server route. Every connected server also gets its own endpoint.
mcphub-selftest then proves the whole path with your VM's key. No key and a wrong key are refused on both the all-servers endpoint and the starter group endpoint. With the key, each one completes an MCP initialize, lists the tools and runs a real tool call:
sudo mcphub-selftestExpected output:
PASS /mcp: refused without a key and with a wrong key (401) PASS /mcp/starter: refused without a key and with a wrong key (401) PASS /mcp: initialize (mcphub), 10 tools listed, tools/call sequential-thinking-sequentialthinking returned a result PASS /mcp/starter: initialize (mcphub_starter_group), 10 tools listed, tools/call sequential-thinking-sequentialthinking returned a result MCPHUB_SELFTEST_OKYou can drive the protocol yourself with curl. MCPHub's streamable HTTP endpoint returns a session ID on initialize, which later requests send back.
The commands below store a fact in the memory server and read the knowledge graph back:
KEY=$(sudo sed -n 's/^MCPHUB_BEARER_KEY=//p' /root/mcphub-credentials.txt) H=(-H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream')
SID=$(curl -sk -D - -o /dev/null "${H[@]}" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' https://127.0.0.1/mcp | sed -n 's/^mcp-session-id: //Ip' | tr -d '\r')
curl -sk "${H[@]}" -H "mcp-session-id: $SID" -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' https://127.0.0.1/mcp | sed -n 's/^data: //p' | jq -r '.result.tools[].name'
curl -sk "${H[@]}" -H "mcp-session-id: $SID" -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"memory-create_entities","arguments":{"entities":[{"name":"sac","entityType":"organization","observations":["publishes the MCPHub image"]}]}}}' https://127.0.0.1/mcp >/dev/null
curl -sk "${H[@]}" -H "mcp-session-id: $SID" -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"memory-read_graph","arguments":{}}}' https://127.0.0.1/mcp | sed -n 's/^data: //p' | jq -r '.result.content[0].text'Expected output:
memory-create_entities memory-create_relations memory-add_observations memory-delete_entities memory-delete_observations memory-delete_relations memory-read_graph memory-search_nodes memory-open_nodes sequential-thinking-sequentialthinking { "entities": [ { "name": "sac", "entityType": "organization", "observations": [ "publishes the MCPHub image" ] } ], "relations": [] }Tools are named <server>-<tool>, so tools from different servers never collide.
The memory server keeps its graph in /var/lib/mcphub/memory.jsonl.
Connect an AI client
Point any MCP client that supports streamable HTTP (or SSE at /sse) at MCPHUB_MCP_URL (every server) or MCPHUB_GROUP_MCP_URL (one group). Send the header Authorization: Bearer <MCPHUB_BEARER_KEY>.
For Claude Code, run:
claude mcp add --transport http mcphub https://<vm-ip>/mcp \
--header "Authorization: Bearer mcphub_your-key"For Cursor (~/.cursor/mcp.json) and other clients that take a JSON server list, use:
{ "mcpServers": { "mcphub": { "url": "https://<vm-ip>/mcp", "headers": { "Authorization": "Bearer mcphub_your-key" } } } }Until you install a certificate from a public authority (Step 12), clients must trust this VM's self-signed certificate. Copy /etc/mcphub/tls/mcphub.crt to the client machine and trust it. For Node.js based clients, set NODE_EXTRA_CA_CERTS to that file.
MCPHub also includes an OAuth 2.0 server, so clients that support MCP OAuth can sign in through the dashboard instead of using a static key.
Add your own MCP servers
Open Servers and select Add Server, or install one from Market, the catalog bundled with MCPHub. A server is one of:
- stdio: a command MCPHub starts on the VM, typically
npx -y <package>oruvx <package>, with any environment variables (API keys) it needs - SSE or streamable HTTP: a remote MCP server URL, with headers or OAuth
Each server's tools are listed under it. Switch a tool off to hide it from every endpoint, or edit its description to steer how AI clients use it. Changes apply without a restart.
Egress. stdio servers started with npx or uvx download their package from registry.npmjs.org or pypi.org the first time they start. They run as the unprivileged mcphub account.
Add only servers you trust. An MCP server runs code on this VM and can use whatever credentials you give it.
MCPHub also checks registry.npmjs.org for a newer MCPHub release, so the dashboard can tell you about it.
Create groups, per-tool selections and scoped keys
A group is a named set of servers with its own endpoint, /mcp/<group>. A group can include only some of a server's tools. Combined with a bearer key scoped to that group, this gives an agent exactly the tools it needs and nothing else.
Create them in the dashboard under Groups and Settings > Keys, or with the API as below. The commands sign in with the admin password and create a group memory-readonly that holds just the three read-only tools of the memory server. They then create a key that may use only that group, and compare what each key can see:
PW=$(sudo sed -n 's/^MCPHUB_ADMIN_PASSWORD=//p' /root/mcphub-credentials.txt)
KEY=$(sudo sed -n 's/^MCPHUB_BEARER_KEY=//p' /root/mcphub-credentials.txt) B=https://127.0.0.1
T=$(printf '{"username":"admin","password":"%s"}' "$PW" | curl -sk -H 'Content-Type: application/json' --data-binary @- $B/api/auth/login | jq -r .token)
curl -sk -H "x-auth-token: $T" -H 'Content-Type: application/json' \
-d '{"name":"memory-readonly","description":"Read-only knowledge graph tools","servers":[{"name":"memory","tools":["read_graph","search_nodes","open_nodes"]}]}' \
$B/api/groups | jq -c '{success, group: .data.name}'
SCOPED=$(curl -sk -H "x-auth-token: $T" -H 'Content-Type: application/json' \
-d '{"name":"reporting-agent","accessType":"groups","allowedGroups":["memory-readonly"]}' $B/api/auth/keys | jq -r .data.token) tools() { local H=(-H "Authorization: Bearer $2" -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream') local code sid code=$(curl -sk -o /dev/null -D /tmp/mcp-h -w '%{http_code}' "${H[@]}" -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"guide","version":"1"}}}' "$B$1") [ "$code" = 200 ] || { echo "$1 -> HTTP $code"; return; } sid=$(sed -n 's/^mcp-session-id: //Ip' /tmp/mcp-h | tr -d '\r')
curl -sk "${H[@]}" -H "mcp-session-id: $sid" -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' "$B$1" \
| sed -n 's/^data: //p' | jq -r --arg p "$1" '"\($p) -> \(.result.tools | length) tools"' } echo "default key:"; tools /mcp "$KEY"; tools /mcp/memory-readonly "$KEY" echo "scoped key:"; tools /mcp/memory-readonly "$SCOPED"; tools /mcp "$SCOPED"; tools /mcp/starter "$SCOPED" rm -f /tmp/mcp-hExpected output:
{"success":true,"group":"memory-readonly"} default key: /mcp -> 10 tools /mcp/memory-readonly -> 3 tools scoped key: /mcp/memory-readonly -> 3 tools /mcp -> HTTP 401 /mcp/starter -> HTTP 401The API prints the new key only once, when it is created. The dashboard shows it masked afterwards.
Smart routing (optional)
MCPHub's /mcp/$smart endpoint lets an AI client describe what it wants to do and find the right tool by semantic search across every connected server, instead of loading every tool definition. It stores tool embeddings in PostgreSQL with the pgvector extension and needs an embedding API (OpenAI or a compatible service).
This image runs MCPHub in its file mode and does not include a database. To use smart routing, provide a PostgreSQL server with pgvector, for example Azure Database for PostgreSQL flexible server with the vector extension allowed. Then set DB_URL in /etc/mcphub/mcphub.env.
Setting DB_URL switches MCPHub to its database mode. On its next start, it migrates the servers, groups, users and keys from the JSON settings file into PostgreSQL, so back up /var/lib/mcphub first (see Day-to-day operation).
Then restart mcphub.service and configure the embedding provider under Settings > Smart Routing. Follow the upstream documentation at https://docs.mcphub.app/features/smart-routing and https://docs.mcphub.app/configuration/database-configuration.
Use your own domain and certificate
Point a DNS name at the VM's public IP, then replace the self-signed certificate with one from a public authority. This example uses certbot's webroot plugin. Install certbot first:
sudo certbot certonly --webroot -w /var/www/html -d mcp.example.com
sudo ln -sf /etc/letsencrypt/live/mcp.example.com/fullchain.pem /etc/mcphub/tls/mcphub.crt
sudo ln -sf /etc/letsencrypt/live/mcp.example.com/privkey.pem /etc/mcphub/tls/mcphub.key
sudo nginx -t && sudo systemctl reload nginxFor that challenge to pass, add a location /.well-known/acme-challenge/ { root /var/www/html; } block to the port 80 server in /etc/nginx/sites-available/mcphub.conf, and open port 80.
Clients then use https://mcp.example.com/mcp with normal certificate verification.
Check what is in the image and its license
Check the installed versions and the source record:
jq -r .version /opt/mcphub/app/package.json node --version uv --version cat /usr/share/doc/mcphub/SOURCEExpected output:
1.1.4 v22.23.3 uv 0.13.1 (x86_64-unknown-linux-gnu) MCPHub v1.1.4, built from the unmodified upstream source. Source: https://github.com/samanhappy/mcphub/tree/b3ec898335c5fb8269adaa99bf6ce7804684139e Release: https://github.com/samanhappy/mcphub/releases/tag/v1.1.4 Licence: Apache License 2.0 (see LICENSE in this directory). Built with the upstream pnpm-lock.yaml (--frozen-lockfile) on Node.js v22.23.3; the package.json version is stamped from the tag, as upstream's own release builds do. Example MCP servers in /opt/mcphub/servers: @modelcontextprotocol/server-memory 1.0.0 and @modelcontextprotocol/server-sequential-thinking 1.0.0 from https://github.com/modelcontextprotocol/servers (see example-servers-LICENSE).MCPHub is licensed under the Apache License 2.0. The example servers are released by the Model Context Protocol project under Apache-2.0 and MIT. The license texts are in /usr/share/doc/mcphub.
Day-to-day operation
sudo systemctl status mcphub.service --no-pager sudo journalctl -u mcphub.service -n 30 --no-pager sudo ls -l /var/lib/mcphub/- Restart the gateway with
sudo systemctl restart mcphub.service, and nginx withsudo systemctl reload nginx. MCPHub restarts everystdioserver it manages. - Configuration lives in
/var/lib/mcphub/mcp_settings.json: servers, groups, users, keys and system settings. The dashboard edits it for you. If you edit it by hand, restart the service afterwards. - Back up by copying the state directory, which holds the settings, the credential encryption key and the memory server's data:
sudo tar czf /root/mcphub-backup-$(date +%F).tgz -C /var/lib/mcphub .Keep /etc/mcphub/mcphub.env with any backup you restore elsewhere. Keep mcp_settings.json.credentials.key with the settings, because it decrypts the per-user credentials stored in them.
- Upgrades. Unattended upgrades keep Ubuntu and nginx patched. To move MCPHub to a newer upstream release, back up first and read its release notes. Then rebuild /opt/mcphub/app from the new tag the same way (see SOURCE), or contact SecureAnyCloud support.
Security notes
- Restrict ports 443 and 80 to your own networks in the network security group. MCP servers run tools with the credentials you give them.
- Give each client or agent its own key, scoped to the groups or servers it needs, and delete keys you no longer use (Settings > Keys).
- Keep bearer authentication on. It is on in this image. Turning it off in Settings > Keys opens every MCP endpoint to anyone who can reach the VM.
- Add only MCP servers you trust, and switch off tools a client should not call.
- Use a certificate from a public authority for a real hostname (Step 12), so clients can verify the gateway.
- Keep unattended upgrades on. They are enabled in this image.
Troubleshooting
Nothing answers on port 443. nginx starts only after first boot has completed. Check it:
sudo systemctl status mcphub-firstboot.service --no-pager sudo journalctl -u mcphub-firstboot.service --no-pager | tail -30The browser warns about the certificate. This is expected until you install your own certificate. Compare the fingerprint with MCPHUB_TLS_SHA256_FINGERPRINT in the credentials file.
The credentials file shows a private address. Azure did not expose the public IP to the VM when it first started. Use the VM's public IP from the Azure Portal instead. Everything else is unaffected.
A client gets 401. The Authorization: Bearer header is missing, or the key is wrong, disabled or scoped to other groups. Check the key under Settings > Keys.
A server shows as disconnected. Open it under Servers to see its error, and check sudo journalctl -u mcphub.service. For npx and uvx servers, the VM needs outbound access to the package registries.
Support
This image is published and supported by SecureAnyCloud. For help with the image, deployment or configuration, contact SecureAnyCloud support 24/7 through the Azure Marketplace listing.
Deploy on Azure
Launch MCPHub on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com