Skip to main content
Glama
prashanthambalavanan-okta

ProGear MCP Servers

ProGear MCP Servers

One MCP gateway for the ProGear basketball-equipment demo, hosting four domains — Inventory, Customer, Sales, and Pricing — each speaking the real MCP protocol (Streamable HTTP transport) and secured by your own Okta org (its own Custom Authorization Server + scope set per domain).

This is a deliberately smaller sibling of ProGearSalesAI: no Auth0 FGA, no LangGraph orchestrator, no frontend. The gateway itself just validates whatever bearer token it's given — it doesn't care how the caller got it. packages/local-tester (local-only, not deployed) demonstrates one way a caller might: human PKCE login + an agent doing the ID-JAG exchange to mint that token, mirroring the original app's Cross-App Access flow.

Deployment shape

One process, one Render service, one build/start command. packages/gateway mounts all 4 domains at different paths behind a single Express app:

Mount

Scopes

Tools

/inventory/mcp

inventory:read, inventory:write, inventory:alert

list_products, search_inventory, check_stock, get_low_stock_alerts, get_inventory_summary, update_inventory_quantity

/customer/mcp

customer:read, customer:lookup, customer:history

get_customer, search_customers, get_customers_by_tier, get_top_customers, get_customer_summary

/sales/mcp

sales:read, sales:quote, sales:order

list_orders, get_order, get_pipeline, create_quote, create_order, cancel_order

/pricing/mcp

pricing:read, pricing:margin, pricing:discount

get_price, get_category_pricing, calculate_bulk_price, get_discount_structure

Each mount validates against its own Okta Custom Authorization Server (different issuer/audience per domain) even though they all run in the same process — a token issued for the inventory auth server can't be used against /customer/mcp, and within a mount, each tool call checks its required scope against the granted scopes in the caller's token (a token missing inventory:write can call check_stock but not update_inventory_quantity).

packages/mcp-inventory, mcp-customer, mcp-sales, mcp-pricing also still work as standalone servers (their own server.ts + /mcp + /health, single-domain env vars) if you ever want to split them back into separate deployments — the gateway just imports each one's tool-registration logic (./tools export) and mounts it under its own auth config instead of calling .listen() itself.

Related MCP server: Auth0 MCP Server Demo — CRM

Data

Seeded from a ported snapshot of the ProGearSalesAI demo dataset: 90 inventory SKUs, 90 pricing entries, 34 customers, tier/volume discount tables (packages/shared/src/data/initial_data.json). Sales orders/quotes are in-memory only. All state resets on process restart — this is a tools server, not a system of record.

Project structure

packages/
  shared/            # ported data + store, JWKS auth + scope enforcement, HTTP/MCP transport helper
  mcp-inventory/      mcp-customer/      mcp-sales/      mcp-pricing/   # tool definitions + standalone entrypoint each
  gateway/            # the actual deployment: mounts all 4 at /inventory, /customer, /sales, /pricing
  local-tester/       # local-only: PKCE login + agent ID-JAG exchange, calls the deployed gateway (see its own README)

Local development

npm install
npm run build            # builds shared + all 4 domains + gateway, in dependency order

npm run dev:gateway       # tsx watch, all 4 mounts on one port (default 3000)

Without the relevant Okta env vars set for a mount, that mount returns 500 on every /mcp request unless you set ALLOW_INSECURE=true, which skips token validation and grants every scope on every mount — local dev only, never set this in a deployed environment.

Okta setup

Set OKTA_DOMAIN once, plus OKTA_<DOMAIN>_AUTH_SERVER_ID + OKTA_<DOMAIN>_AUDIENCE per domain — these are the exact same env var names already used in the ProGearSalesAI backend (OKTA_CUSTOMER_AUTH_SERVER_ID, OKTA_INVENTORY_AUDIENCE, etc.), so existing values can be copied over as-is:

OKTA_DOMAIN=https://your-org.okta.com

OKTA_INVENTORY_AUTH_SERVER_ID=...   OKTA_INVENTORY_AUDIENCE=api://progear-inventory
OKTA_CUSTOMER_AUTH_SERVER_ID=...    OKTA_CUSTOMER_AUDIENCE=api://progear-customer
OKTA_SALES_AUTH_SERVER_ID=...       OKTA_SALES_AUDIENCE=api://progear-sales
OKTA_PRICING_AUTH_SERVER_ID=...     OKTA_PRICING_AUDIENCE=api://progear-pricing

See .env.example for the full list, including which vars from a ProGearSalesAI-style .env don't apply here (Anthropic key, CORS, the AI Agent's own private key/client ID — this gateway validates incoming tokens, it doesn't self-issue any). Tokens are validated by signature + issuer + audience against each domain's own Okta JWKS endpoint (jose's createRemoteJWKSet) — no shared secret needed on this side.

Deploying to Render

Single service, either via the dashboard or the included Blueprint.

Manual (New → Web Service):

Field

Value

Language

Node

Root Directory

(blank — npm workspaces monorepo, build runs from repo root)

Build Command

npm install && npm run build

Start Command

node packages/gateway/dist/server.js

Health Check Path

/health

Blueprint: render.yaml at the repo root defines the same single progear-mcp-gateway service — New → Blueprint, point at this repo, then fill in the 9 Okta env vars it prompts for (marked sync: false).

Connecting an agent

Each mount exposes MCP over Streamable HTTP at POST/GET/DELETE <mount>/mcp (stateless — no session persistence across requests) plus its own GET <mount>/health; there's also a top-level GET /health listing all mounts.

Get an access token from Okta for the relevant Custom Authorization Server + scopes (e.g. client-credentials grant for a service/agent identity), then:

Claude Code CLI:

claude mcp add --transport http progear-inventory \
  https://<your-render-url>/inventory/mcp \
  --header "Authorization: Bearer <token>"

Repeat per domain (/customer/mcp, /sales/mcp, /pricing/mcp) with a token scoped to that domain's audience.

Any other MCP client / agent SDK: point it at the mount's /mcp URL with an Authorization: Bearer <token> header on every request.

OAuth discovery (no static token)

Clients that implement the MCP authorization spec can find Okta on their own instead of being handed a token. Each mount publishes RFC 9728 protected-resource metadata at the root of the gateway, path-scoped to the endpoint it describes:

GET /.well-known/oauth-protected-resource/inventory/mcp
GET /.well-known/oauth-protected-resource/customer/mcp
GET /.well-known/oauth-protected-resource/sales/mcp
GET /.well-known/oauth-protected-resource/pricing/mcp
{
  "resource": "https://<your-render-url>/inventory/mcp",
  "authorization_servers": ["https://your-org.okta.com/oauth2/<inventory-auth-server-id>"],
  "scopes_supported": ["inventory:read", "inventory:write", "inventory:alert"],
  "bearer_methods_supported": ["header"],
  "resource_name": "ProGear Inventory MCP"
}

A 401 from /mcp now also carries the pointer, so a client that calls the endpoint cold learns where to authenticate:

WWW-Authenticate: Bearer resource_metadata="https://<your-render-url>/.well-known/oauth-protected-resource/inventory/mcp"

(When a token was presented but failed validation, the challenge additionally carries error="invalid_token" and error_description.)

The URLs in these documents are derived from the incoming request (X-Forwarded-Proto + Host, with trust proxy on — correct on Render). Set PUBLIC_BASE_URL=https://<your-render-url> only if something in front of the gateway rewrites the Host header.

To let a client complete the flow, register an OIDC public client (Authorization Code + PKCE) in your Okta org, add the client's redirect URI (Claude.ai uses https://claude.ai/api/mcp/auth_callback), and grant the domain's scopes in that Custom Authorization Server's access policy. Two things to note about these documents: there is no unscoped /.well-known/oauth-protected-resource — it would be ambiguous across four resources, so it 404s on purpose — and the gateway serves no /.well-known/oauth-authorization-server. That second document comes from Okta at {issuer}/.well-known/oauth-authorization-server, which is what the client follows next. Both endpoints exist only on the gateway; the standalone per-domain dev servers (packages/mcp-*) don't have them.

Connecting Claude Code with a pre-registered client ID

claude mcp add takes a client_id, plus a fixed callback port — which you need, because Okta requires exact redirect URIs and won't accept a random port:

claude mcp add --transport http \
  --client-id 0oaXXXXXXXXXXXXXXXX \
  --callback-port 33418 \
  progear-inventory https://progear-mcp-servers.onrender.com/inventory/mcp

Then in Claude Code: /mcp → select progear-inventoryAuthenticate. That fetches the protected-resource document above, redirects you to the inventory Custom AS, and stores the token.

Repeat per domain (one Okta app can serve all four):

claude mcp add --transport http --client-id 0oaXXXXXXXXXXXXXXXX --callback-port 33418 \
  progear-customer https://progear-mcp-servers.onrender.com/customer/mcp
claude mcp add --transport http --client-id 0oaXXXXXXXXXXXXXXXX --callback-port 33418 \
  progear-sales    https://progear-mcp-servers.onrender.com/sales/mcp
claude mcp add --transport http --client-id 0oaXXXXXXXXXXXXXXXX --callback-port 33418 \
  progear-pricing  https://progear-mcp-servers.onrender.com/pricing/mcp

Okta side, before that works:

  • An OIDC app of type Native or SPA (public client): grant types Authorization Code + Refresh Token, PKCE required, no client secret.

  • Sign-in redirect URI http://localhost:33418/callback. If auth fails with a redirect_uri mismatch, read the actual redirect_uri out of the authorize URL in your browser's address bar and register that instead.

  • In each of the four Custom Authorization Servers, an access-policy rule that allows this client and grants that domain's scopes.

Okta ignores the RFC 8707 resource parameter Claude sends and stamps aud from the Custom AS's own audience setting — which is exactly why the per-domain AS split works here without extra wiring.

If you'd rather skip OAuth entirely for a quick test, the static-token path still works:

claude mcp add --transport http progear-inventory \
  https://progear-mcp-servers.onrender.com/inventory/mcp \
  --header "Authorization: Bearer <token from the demo app>"

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    A secure MCP server for CRM operations (contacts and deals) with Auth0 OIDC authentication, role-based access control (sales-rep read-only vs sales-manager full access), and on-behalf-of token exchange.
    -
  • F
    license
    Not graded
    quality
    B
    maintenance
    An MCP server for inventory management (list, add, delete, comment on items) that demonstrates Auth0 for MCP capabilities including JWT bearer auth, fine-grained authorization with FGA, CIBA step-up approval, and Token Vault integration.
    1
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    A remote MCP server on Cloudflare Workers protected by Okta custom authorization server, demonstrating tool-level authorization via JWT access tokens and scopes/groups.
    -