cms-mcp-hub
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@cms-mcp-hubCreate a new page in Drupal called 'About Us'."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
CMS MCP Hub
Multi-tenant MCP (Model Context Protocol) gateway. Proxies Claude.ai tool calls to one or more remote CMS MCP servers, with per-workspace SSO, connector management, and an admin dashboard.
Claude.ai ──MCP──▶ cms-mcp-hub ──HTTP──▶ Drupal MCP server
──HTTP──▶ WordPress MCP server
──SSE───▶ Any CMS MCP serverQuick Start
# 1. Install dependencies
npm install
# 2. (Recommended) Set encryption key
export ENCRYPTION_KEY=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")
# 3. Start dev server (auto-reloads on src/ changes)
npm run dev
# 4. Open browser
open http://localhost:3456Default port: 3456. Override with PORT=4000 npm run dev.
Related MCP server: mcp-api-gateway
Control Flow
1. Server Startup (src/index.js)
node src/index.js
└── new WorkspaceRegistry() reads data/workspaces/ from disk
└── new McpHubServer(registry)
└── _setupRoutes() registers all Express routes + app.param workspace validation
└── app.listen(PORT)On startup, app.param('workspace_id', ...) runs for every route that has :workspace_id. It loads the workspace config from disk, attaches it to req.workspace, and returns 404 if the workspace doesn't exist.
2. Creating a Workspace
Browser → POST /api/workspaces { name }
└── WorkspaceRegistry.create()
├── generates workspace_id = "ws_" + 12 random bytes (hex)
├── generates admin_key = "ak_" + 24 random bytes (hex)
├── stores admin_key_hash = SHA-256(admin_key)
└── writes data/workspaces/{workspace_id}/config.json
Response → { workspace_id, admin_key, name }
⚠ admin_key shown ONCE — save it immediatelyconfig.json structure:
{
"workspace_id": "ws_abc123...",
"name": "My Org",
"admin_key_hash": "sha256hex...",
"idp": null,
"created_at": "2025-01-01T00:00:00.000Z"
}3. Admin Login → Session Cookie
Browser → POST /ws/:id/auth/admin { admin_key }
└── WorkspaceRegistry.verifyAdminKey()
└── SHA-256(admin_key) == stored admin_key_hash (timing-safe compare)
└── TokenStore.signSession({ workspace_id, exp: now + 24h })
└── base64url(payload) + "." + HMAC-SHA256(payload, ENCRYPTION_KEY)
└── Set-Cookie: ws_session=<token>; HttpOnly; SameSite=Lax;
Path=/ws/{workspace_id}/; Max-Age=86400
Response → { ok: true }
Browser → redirects to /ws/:id/uiSession cookie is path-scoped to /ws/{workspace_id}/. A session for workspace A is not sent to workspace B requests.
4. Dashboard Load (/ws/:id/ui)
Page is static HTML served from src/web/ui.js. On load, JavaScript calls 4 APIs in parallel:
Browser JS load()
├── GET /ws/:id/health (no auth — workspace health + tool count)
├── GET /ws/:id/api/connectors (session required)
├── GET /ws/:id/auth/sso/status (session required)
└── GET /ws/:id/api/idp (session required)
If any session-required endpoint returns 401:
└── showAuthWall() ← overlay with "Session expired" + link to login
If all succeed:
└── renders workspace name, stats bar, SSO panel, connector cards5. Connector Connection Flow
Admin → POST /ws/:id/api/connectors { name, url, token?, transport?, sseUrl? }
└── ConnectorRegistry.connectOne()
└── transport auto-detection (if not specified):
1. Direct HTTP — JSON-RPC POST to url
2. Streamable HTTP — MCP SDK StreamableHTTPClientTransport
3. SSE — MCP SDK SSEClientTransport
└── calls tools/list on remote server
└── registers each tool as {safePrefix(name)}__{toolName}
e.g. drupal-prod__get_node
└── stores in memory (ConnectorRegistry.connectors Map)
└── saves to data/workspaces/{id}/connectors.json
On server restart:
└── ConnectorRegistry.initialize() reconnects all saved connectors in parallel
Non-blocking: registry returned immediately, connections run in backgroundTool prefix rule: safePrefix(name) replaces characters outside [a-zA-Z0-9_-] with _. Required by Claude.ai tool naming rules.
Fetch timeout: FETCH_TIMEOUT_MS = 120,000 (2 minutes) per tool call.
6. Claude.ai MCP Request Flow
Claude.ai → POST /ws/:id/mcp
└── checks Mcp-Session-Id header
├── existing session → route to cached StreamableHTTPServerTransport
└── new session → create MCP Server + StreamableHTTPServerTransport
store in httpSessions Map (keyed by session ID)
└── MCP Server handles:
├── tools/list → ConnectorRegistry.getAllTools()
│ all tools from all connected connectors (prefixed)
└── tools/call { name: "drupal-prod__get_node", arguments: {...} }
└── ConnectorRegistry.callTool()
├── strips prefix → finds connector by name
├── calls remote MCP server with original tool name
└── returns result to Claude.ai
Claude.ai GET /ws/:id/mcp ← SSE stream for streaming responses
Claude.ai DELETE /ws/:id/mcp ← close sessionConnect Claude.ai to: http://your-host/ws/{workspace_id}/mcp
7. SSO / OAuth 2.0 Authorization Code Flow
Step 1 — Configure IdP
Admin → PATCH /ws/:id/api/idp
{ authorize_url, token_url, client_id, client_secret, scope }
└── saved to config.json idp field
Step 2 — Start SSO
Admin → GET /ws/:id/auth/sso/start
└── OAuthManager.buildStartUrl()
├── generates random state (20 bytes hex), stores with 10-min expiry
└── builds IdP redirect URL:
{authorize_url}?response_type=code
&client_id=...
&redirect_uri={origin}/ws/{id}/auth/callback
&scope=...
&state=...
└── 302 redirect → IdP login page
Step 3 — IdP Callback
IdP → GET /ws/:id/auth/callback?code=...&state=...
└── OAuthManager.handleCallback()
├── validates state (exists + not expired)
├── POST to token_url to exchange code → tokens
├── decodes JWT id_token to extract sub claim
└── WorkspaceRegistry.saveToken(workspace_id, '_service', {
access_token, refresh_token, expires_at, token_type, sub
})
└── AES-256-GCM encrypted → data/workspaces/{id}/tokens/_service.enc
└── 302 redirect → /ws/:id/ui?sso=ok
Step 4 — Token Auto-Refresh
On every tool call: WorkspaceRegistry.getToken()
└── if expires_at - 60s < now && has refresh_token
POST to idp.token_url with grant_type=refresh_token
saves updated token
└── returns current access_token
└── ConnectorRegistry injects Authorization: Bearer {access_token}8. Session Verification (all protected routes)
_requireAdmin(req, res)
└── reads Cookie: ws_session=<token>
└── TokenStore.verifySession(token)
├── splits "data.sig"
├── recomputes HMAC-SHA256(data, key)
├── crypto.timingSafeEqual(sig, expected)
├── decodes base64url payload
└── checks payload.exp > Date.now()
└── verifies payload.workspace_id === req.params.workspace_id
└── on failure: sends { error: "..." } with status 4019. Token Encryption (TokenStore)
Algorithm: AES-256-GCM (authenticated encryption)
Key source:
ENCRYPTION_KEYenv var (64 hex chars = 32 bytes). Falls back toscrypt("cms-mcp-hub-dev-key", "cms-salt-v1", 32)if unset — not safe for productionKey is cached after first derivation —
scryptSynconly runs once per processFormat stored in
.encfiles:{ "iv": "<12-byte hex>", "tag": "<16-byte hex>", "data": "<ciphertext hex>" }
File Structure
cms-mcp-hub/
├── src/
│ ├── index.js entry point — creates WorkspaceRegistry + McpHubServer
│ ├── proxy/
│ │ └── McpHubServer.js Express app, all route handlers, MCP session factory
│ ├── connectors/
│ │ └── ConnectorRegistry.js connector lifecycle, transport detection, tool routing
│ ├── workspaces/
│ │ ├── WorkspaceRegistry.js workspace CRUD, per-workspace ConnectorRegistry factory
│ │ ├── OAuthManager.js OAuth 2.0 authorization code flow
│ │ └── TokenStore.js AES-256-GCM encrypt/decrypt, HMAC session sign/verify
│ └── web/
│ └── ui.js landing page + dashboard HTML (served as template strings)
└── data/
└── workspaces/
└── {workspace_id}/
├── config.json workspace name, admin_key_hash, idp config
├── connectors.json saved connector configs
└── tokens/
└── _service.enc encrypted SSO service tokenAll API Endpoints
Public (no auth required)
Method | Path | Description |
|
| Landing page — create or open a workspace |
|
| Create a new workspace |
|
| Workspace admin dashboard (HTML) |
|
| Workspace health — connectors + tool count |
POST /api/workspaces
Body: { "name": "My Org" }
Response: { "workspace_id": "ws_...", "admin_key": "ak_...", "name": "My Org" }GET /ws/:id/health
{
"status": "ok",
"workspace": "My Org",
"sso": { "connected": false },
"connectors": [{ "name": "drupal-prod", "status": "connected", "toolCount": 8 }],
"totalTools": 8
}Admin Auth
Method | Path | Description |
|
| Login with admin key — sets session cookie |
|
| Logout — clears session cookie |
POST /ws/:id/auth/admin
Body: { "admin_key": "ak_..." }
Response: { "ok": true }
Set-Cookie: ws_session=...; HttpOnly; SameSite=Lax; Path=/ws/{id}/; Max-Age=86400SSO / OAuth (requires admin session)
Method | Path | Description |
|
| Redirect to IdP authorization page |
|
| OAuth callback — handles code→token exchange |
|
| Current SSO token status |
|
| Delete stored service token |
GET /ws/:id/auth/sso/status
{ "connected": true, "expired": false, "expires_at": 1749123456789, "has_refresh": true }
{ "connected": false }Connector Management (requires admin session)
Method | Path | Description |
|
| List all connectors, status, and tools |
|
| Add and connect a new connector |
|
| Update connector config and reconnect |
|
| Disconnect and remove a connector |
|
| Re-fetch tool list (no reconnect) |
|
| Full disconnect + reconnect |
POST /ws/:id/api/connectors — request body
{
"name": "drupal-prod",
"url": "https://cms.example.com/mcp",
"token": "Basic abc123",
"transport": "streamable-http",
"sseUrl": "https://cms.example.com/mcp/sse",
"description": "Production Drupal"
}token, transport, sseUrl, description are all optional. If transport is omitted, auto-detection runs.
GET /ws/:id/api/connectors — response item
{
"name": "drupal-prod",
"url": "https://...",
"status": "connected",
"transport": "direct-http",
"toolCount": 8,
"tools": [
{
"name": "get_node",
"title": "Get Node",
"description": "...",
"inputSchema": { "type": "object", "properties": {} }
}
],
"error": null
}IdP Configuration (requires admin session)
Method | Path | Description |
|
| Get current IdP config (client_secret masked) |
|
| Create or update IdP config |
PATCH /ws/:id/api/idp
{
"authorize_url": "https://idp.example.com/oauth/authorize",
"token_url": "https://idp.example.com/oauth/token",
"client_id": "your-client-id",
"client_secret": "your-secret",
"scope": "openid profile email"
}Send "client_secret": "••••••••" (or omit it) to keep the existing secret.
MCP Endpoint (Claude.ai connects here)
Method | Path | Description |
|
| Create new MCP session or send to existing |
|
| SSE stream for active session |
|
| Close MCP session |
New session: POST without Mcp-Session-Id header.
Existing session: POST/GET with Mcp-Session-Id: <id> header.
In Claude.ai → Settings → Integrations: enter http://your-host/ws/{workspace_id}/mcp
Connector Transport Auto-Detection
When transport is not specified, tried in order:
Order | Transport | Method | Works with |
1 | Direct HTTP | JSON-RPC POST to | Drupal |
2 | Streamable HTTP | MCP SDK | Modern MCP servers |
3 | SSE | MCP SDK | Legacy MCP servers |
Force a specific transport: "transport": "streamable-http" or "transport": "sse".
Environment Variables
Variable | Required | Description |
| No | HTTP port (default: |
| Recommended | 64-char hex string (32 bytes) for AES-256-GCM + HMAC. Falls back to hardcoded dev key if unset |
Generate a key:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"In dev, source before starting:
export ENCRYPTION_KEY=your64hexchars
npm run devOr use a .env file with a loader (e.g. dotenv) — the export ENCRYPTION_KEY=... syntax in .env.dev only works after source .env.dev, not with dotenv.
npm Scripts
Script | Command | Use |
|
| Production |
|
| Dev — auto-reloads on any |
|
| Only reloads on connector config JSON changes |
|
| CLI interface |
Use npm run dev for development. npm run sync does not watch JS files.
Troubleshooting
Symptom | Cause | Fix |
Dashboard stuck on "Loading..." | JS not reloaded after code change | Hard refresh: |
"Session expired" overlay immediately | Cookie not set, or wrong workspace path | Log in at |
| Another process on port 3456 |
|
| Env var missing | Set |
Connector stuck in "connecting" | Remote MCP server unreachable | Check URL + network; see |
SSO button disabled | IdP not configured | Configure IdP first via "Configure IdP" button |
This server cannot be deployed
Maintenance
Related MCP Connectors
One MCP endpoint for Claude, GPT & Gemini: 100+ tools + no-code connectors + agent workers.
MCP server unifying ERPs, CRMs, APIs and knowledge base for Claude, ChatGPT and Gemini.
- QuallaaOAuthcom.quallaa
Talk to your public-facing AI from any MCP client — Claude, ChatGPT, Cursor, Cline, Windsurf.
Unified MCP Server is a remote MCP connector for AI agents and vertical AI products that provides access to 22,000+ authorized SaaS tools across 400+ integrations and 24 categories directly inside LLMs (Claude, GPT, Gemini, Cohere). Tools operate only on explicitly authorized customer connections, enabling agents to safely read and write against live third-party systems.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceHosted MCP server that bridges Claude.ai with one or more WordPress sites.7 npmISC
- FlicenseAqualityDmaintenanceA universal MCP server that integrates any API with Claude Desktop by pointing it at a Swagger/OpenAPI spec, automatically generating tools for interaction.344-
- AlicenseAqualityDmaintenanceA Model Context Protocol server that gives Claude (or any MCP-compatible LLM) direct access to a self-hosted WordPress site over its REST API.73MIT
- FlicenseNot gradedqualityBmaintenanceMulti-tenant MCP server that connects Claude.ai to WordPress sites through Pluggio, enabling AI-driven site management, content operations, database queries, and file manipulation.-