Environment Setup

Applies to Tenant Delegated connections only. Identity Brokering needs no configuration; Token Exchange needs only two URLs entered in the wizard.

This is a how-to guide. The Contract Specification is the normative reference for environment variables and their canonical defaults.

Environment variables

Your tenant application must set the following environment variables:

VariableDescriptionExample
MCP_CONTRACT_HMAC_KEY64-character hex shared secret (from hub admin panel)4cec2d76a8b1...
MCP_CONTRACT_SESSION_TTL_SECSession token lifetime in seconds3600
MCP_CONTRACT_AUTH_CODE_TTL_SECAuth code lifetime in seconds (recommended: 60)60
MCP_CONTRACT_ALLOWED_CALLBACK_HOSTSComma-separated allowlist of hub callback hostsmcpweb.aircwou.in

The MCP_CONTRACT_HMAC_KEY must be exactly 64 hex characters (representing 32 bytes). This key is shared between the hub and your app — never expose it in client-side code or version control.

javascript
// lib/mcp-contract/config.ts
export const MCP_CONTRACT_HMAC_KEY_HEX = process.env.MCP_CONTRACT_HMAC_KEY ?? "";
export const MCP_CONTRACT_SESSION_TTL_SEC = parseInt(
  process.env.MCP_CONTRACT_SESSION_TTL_SEC ?? "3600", 10
);
export const MCP_CONTRACT_AUTH_CODE_TTL_SEC = parseInt(
  process.env.MCP_CONTRACT_AUTH_CODE_TTL_SEC ?? "60", 10
);

export const MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS = (
  process.env.MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS ?? ""
)
  .split(",")
  .map((s) => s.trim())
  .filter(Boolean);

export function isAllowedCallbackUrl(url: string): boolean {
  try {
    const u = new URL(url);
    return MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS.includes(u.host);
  } catch {
    return false;
  }
}
javascript
// lib/mcp-contract/config.js
export const MCP_CONTRACT_HMAC_KEY_HEX = process.env.MCP_CONTRACT_HMAC_KEY ?? "";
export const MCP_CONTRACT_SESSION_TTL_SEC = parseInt(
  process.env.MCP_CONTRACT_SESSION_TTL_SEC ?? "3600", 10
);
export const MCP_CONTRACT_AUTH_CODE_TTL_SEC = parseInt(
  process.env.MCP_CONTRACT_AUTH_CODE_TTL_SEC ?? "60", 10
);

export const MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS = (
  process.env.MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS ?? ""
)
  .split(",")
  .map((s) => s.trim())
  .filter(Boolean);

export function isAllowedCallbackUrl(url) {
  try {
    const u = new URL(url);
    return MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS.includes(u.host);
  } catch {
    return false;
  }
}
java
// src/main/resources/application.properties

mcp.contract.hmac-key=${MCP_CONTRACT_HMAC_KEY}
mcp.contract.session-ttl-sec=${MCP_CONTRACT_SESSION_TTL_SEC:3600}
mcp.contract.auth-code-ttl-sec=${MCP_CONTRACT_AUTH_CODE_TTL_SEC:60}
mcp.contract.allowed-callback-hosts=${MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS:}
java
// McpContractProperties.java
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Arrays;
import java.util.List;

@ConfigurationProperties(prefix = "mcp.contract")
public record McpContractProperties(
    String hmacKey,
    int sessionTtlSec,
    int authCodeTtlSec,
    String allowedCallbackHosts
) {
    public List<String> allowedCallbackHostList() {
        if (allowedCallbackHosts == null || allowedCallbackHosts.isBlank()) {
            return List.of();
        }
        return Arrays.stream(allowedCallbackHosts.split(","))
            .map(String::trim)
            .filter(s -> !s.isEmpty())
            .toList();
    }

    public boolean isAllowedCallbackUrl(String url) {
        try {
            var uri = java.net.URI.create(url);
            String host = uri.getPort() > 0
                ? uri.getHost() + ":" + uri.getPort()
                : uri.getHost();
            return allowedCallbackHostList().contains(host);
        } catch (Exception e) {
            return false;
        }
    }
}
typescript
// lib/mcp-contract/config.ts  (Node.js / Express)
export const MCP_CONTRACT_HMAC_KEY_HEX: string = process.env.MCP_CONTRACT_HMAC_KEY ?? "";
export const MCP_CONTRACT_SESSION_TTL_SEC: number = parseInt(
  process.env.MCP_CONTRACT_SESSION_TTL_SEC ?? "3600", 10
);
export const MCP_CONTRACT_AUTH_CODE_TTL_SEC: number = parseInt(
  process.env.MCP_CONTRACT_AUTH_CODE_TTL_SEC ?? "60", 10
);

export const MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS: string[] = (
  process.env.MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS ?? ""
)
  .split(",")
  .map((s) => s.trim())
  .filter(Boolean);

export function isAllowedCallbackUrl(url: string): boolean {
  try {
    const u = new URL(url);
    return MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS.includes(u.host);
  } catch {
    return false;
  }
}
javascript
// lib/mcp-contract/config.js
const MCP_CONTRACT_HMAC_KEY_HEX = process.env.MCP_CONTRACT_HMAC_KEY ?? "";
const MCP_CONTRACT_SESSION_TTL_SEC = parseInt(
  process.env.MCP_CONTRACT_SESSION_TTL_SEC ?? "3600", 10
);
const MCP_CONTRACT_AUTH_CODE_TTL_SEC = parseInt(
  process.env.MCP_CONTRACT_AUTH_CODE_TTL_SEC ?? "60", 10
);

const MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS = (
  process.env.MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS ?? ""
)
  .split(",")
  .map((s) => s.trim())
  .filter(Boolean);

function isAllowedCallbackUrl(url) {
  try {
    const u = new URL(url);
    return MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS.includes(u.host);
  } catch {
    return false;
  }
}

module.exports = {
  MCP_CONTRACT_HMAC_KEY_HEX,
  MCP_CONTRACT_SESSION_TTL_SEC,
  MCP_CONTRACT_AUTH_CODE_TTL_SEC,
  MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS,
  isAllowedCallbackUrl,
};
python
# mcp_contract/config.py
import os
from urllib.parse import urlparse

MCP_CONTRACT_HMAC_KEY_HEX: str = os.environ.get("MCP_CONTRACT_HMAC_KEY", "")
MCP_CONTRACT_SESSION_TTL_SEC: int = int(
    os.environ.get("MCP_CONTRACT_SESSION_TTL_SEC", "3600")
)
MCP_CONTRACT_AUTH_CODE_TTL_SEC: int = int(
    os.environ.get("MCP_CONTRACT_AUTH_CODE_TTL_SEC", "60")
)

MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS: list[str] = [
    h.strip()
    for h in os.environ.get("MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS", "").split(",")
    if h.strip()
]


def is_allowed_callback_url(url: str) -> bool:
    try:
        return urlparse(url).netloc in MCP_CONTRACT_ALLOWED_CALLBACK_HOSTS
    except Exception:
        return False

Domain Verification

Before your tenant can be activated, the hub requires you to prove ownership of your domain. During onboarding, the hub generates a verification token (visible in the admin panel). You must make this token discoverable via one of two methods:

Method 1: DNS TXT record (recommended)

Add a TXT record to your domain's DNS:

FieldValue
Name / Host_mcp-verify
TypeTXT
ValueThe token from the admin panel (e.g. mcp-hub-verify=4cec2d76a8b1...)

For example, if your domain is myapp.example.com, the full record name is:

_mcp-verify.example.com  TXT  "mcp-hub-verify=4cec2d76a8b1..."

DNS propagation can take anywhere from a few minutes to 48 hours depending on your DNS provider. You can verify propagation locally by running: nslookup -type=TXT _mcp-verify.example.com

Method 2: Well-known JSON file

If you cannot modify DNS records, you can instead serve a JSON file at:

https://myapp.example.com/.well-known/mcp-hub-verification.json

The file must return application/json with this content:

json
{
  "token": "mcp-hub-verify=4cec2d76a8b1..."
}

The well-known file must be served over HTTPS, must not redirect, and must return a Content-Type: application/json header. The hub will reject responses that don't meet these requirements.

Triggering verification

Once you've added the DNS record or well-known file, ask your hub administrator to click "Check DNS TXT" or "Check Well-Known File" in the tenant onboarding panel. The hub will attempt to look up your token and mark the domain as verified.


DNS setup

Your tenant app must be accessible over HTTPS. You can use any hosting provider:

  • Vercel — automatic HTTPS via *.vercel.app
  • Railway — automatic HTTPS via *.railway.app
  • Self-hosted — use a reverse proxy (Nginx, Caddy) with Let's Encrypt

During hub onboarding, the admin enters your app's base URL (e.g., https://myapp.example.com). The hub constructs endpoint URLs relative to that base:

EndpointFull URL
Login pagehttps://myapp.example.com/mcp-login
Exchangehttps://myapp.example.com/api/mcp/exchange
Validatehttps://myapp.example.com/api/mcp/validate
Refreshhttps://myapp.example.com/api/mcp/refresh
Logouthttps://myapp.example.com/api/mcp/logout

The login page URL (/mcp-login) must be a GET-able HTML page. All API endpoints accept POST requests with JSON bodies (except /api/mcp/login which accepts form data).