Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions examples/multi-tenant-mcp/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Git
.git/
.gitignore

# Docker
Dockerfile
.dockerignore

# Logs
*.log

# Documentation
README.md
14 changes: 14 additions & 0 deletions examples/multi-tenant-mcp/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Descope MCP / OAuth — use DESCOPE_CONFIG_URL from your MCP server well-known URL, OR legacy project_id + DESCOPE_BASE_URL below.
DESCOPE_PROJECT_ID=P2ygkdk1hUDj1H7xh6flFxo5lMWs

# MCP server issuer (recommended)
DESCOPE_CONFIG_URL=https://api.descope.com/v1/apps/agentic/P2ygkdk1hUDj1H7xh6flFxo5lMWs/MS39o0PTWlxeDdTqeCB0ZgRuuMCiQ/.well-known/openid-configuration

# Legacy (used only when DESCOPE_CONFIG_URL is unset)
DESCOPE_BASE_URL=https://api.descope.com

# Public URL of this MCP server (OAuth metadata)
SERVER_URL=http://localhost:3000

# Required for switch_tenant: persist custom attribute currentTenant via Management API
DESCOPE_MANAGEMENT_KEY=K3CXoCreHLJdnnx6POIrZUSNOFPB8iQTp2zzR9Y4IOUYBA0kmrlEqnMPjhIAtxTSn5EGzFX
14 changes: 14 additions & 0 deletions examples/multi-tenant-mcp/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Descope MCP / OAuth — use DESCOPE_CONFIG_URL from your MCP server well-known URL, OR legacy project_id + DESCOPE_BASE_URL below.
DESCOPE_PROJECT_ID=

# MCP server issuer / OpenID discovery (recommended)
DESCOPE_CONFIG_URL=

# Legacy (used only when DESCOPE_CONFIG_URL is unset)
DESCOPE_BASE_URL=https://api.descope.com

# Public URL of this MCP server (OAuth metadata)
SERVER_URL=http://localhost:3000

# Required for switch_tenant: persist custom attribute currentTenant via Management API
DESCOPE_MANAGEMENT_KEY=
5 changes: 5 additions & 0 deletions examples/multi-tenant-mcp/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.env

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 HIGH: The multi-tenant-mcp directory only contains .env and .gitignore — no actual example code. The PR title says "Added Multi-Tenant MCP Example" but there's no implementation here (no package.json, no source files, no README). Is this intentionally just scaffolding, or was code missed from the commit?

dist/
node_modules/
.vercel
.next
32 changes: 32 additions & 0 deletions examples/multi-tenant-mcp/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM python:3.12-slim

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High IaC Finding

Missing User Instruction
on resource FROM python:3.12-slim AS python:3.12-slim

More Details
This rule checks whether a `USER` instruction is specified in the Dockerfile. The rule fails when the `USER` instruction is missing, causing the container to run with root privileges (UID 0). If an attacker compromises an application running as root, they gain the privileges needed to potentially escape the container and attack the host node. It also increases the blast radius of a breach, allowing full control to modify files or install malware within the container. Enforcing a non-root user is a fundamental security measure that minimizes the attack surface and contains the impact of a potential compromise.

Expected

The Dockerfile stage should contain the 'USER' instruction

Found

The Dockerfile stage does not contain any 'USER' instruction

Rule ID: fc0144c0-d1e9-4694-bd44-8eb9cbdd9a56


To ignore this finding as an exception, reply to this conversation with #wiz_ignore reason

If you'd like to ignore this finding in all future scans, add an exception in the .wiz file (learn more) or create an Ignore Rule (learn more).


To get more details on how to remediate this issue using AI, reply to this conversation with #wiz remediate


# Set working directory
WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*

# Copy requirements first for better caching
COPY pyproject.toml requirements.txt ./

# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy application code
COPY . .

# Expose the port the app runs on
EXPOSE 3000

# Set environment variables
ENV PYTHONPATH=/app
ENV PYTHONUNBUFFERED=1

# Health check
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1

# Run the application
CMD ["python", "server.py"]
75 changes: 75 additions & 0 deletions examples/multi-tenant-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Multi-tenant MCP Server (FastMCP + Descope)

![Descope Banner](https://github.com/descope/.github/assets/32936811/d904d37e-e3fa-4331-9f10-2880bb708f64)

## Introduction

This example demonstrates **tenant-scoped MCP tools** with Descope:

- The **`switch_tenant`** tool validates the tenant with **`mgmt.tenant.load`** (and session settings for SSO), then persists the active tenant id in the user’s **`currentTenant`** custom attribute using **`mgmt.user.patch`**.
- **Tenant-specific tools** are tagged for `tenant-a` vs `tenant-b`; on **every tool execution** the server calls Descope **`GET …/oauth2/v1/userinfo`** with the caller’s bearer token and verifies that `currentTenant` matches the tool’s tenant before running business logic.

## Prerequisites (Descope Console)

1. Create an **[MCP Server](https://docs.descope.com/agentic-identity-hub/mcp-servers/settings)** in Descope and note **Well-Known URL** (`DESCOPE_CONFIG_URL`) — or use legacy `DESCOPE_PROJECT_ID` + `DESCOPE_BASE_URL` for local dev.
2. Define a **custom attribute** named **`currentTenant`** on users (same key used in Management API patches).
3. Generate a **[Management Key](https://docs.descope.com/settings/project-management)** with permission to update users (`DESCOPE_MANAGEMENT_KEY`). Required only for **`switch_tenant`**.
4. Ensure MCP clients request scopes that include **`descope.custom_claims`** (and related OIDC scopes) so **UserInfo** returns custom attributes.

## Requirements

- Python 3.12+
- Dependencies in `pyproject.toml` (`uv sync`)

## Quick Start

### 1. Clone and enter this example

```bash
git clone https://github.com/descope/ai.git
cd ai/examples/multi-tenant-mcp
```

### 2. Environment variables

Copy `.env.example` to `.env` and fill in:

| Variable | Purpose |
|----------|---------|
| `DESCOPE_PROJECT_ID` | Descope project id |
| `DESCOPE_CONFIG_URL` | MCP server Well-Known URL (recommended) |
| `DESCOPE_BASE_URL` | Legacy; used only if `DESCOPE_CONFIG_URL` is unset |
| `SERVER_URL` | Public base URL of this MCP server |
| `DESCOPE_MANAGEMENT_KEY` | Enables **`switch_tenant`** (updates `currentTenant`) |

### 3. Install and run

```bash
uv sync
uv run python server.py
```

Visit [http://localhost:3000](http://localhost:3000) for the landing page.

## Tools

| Tool | Tenant | Description |
|------|--------|-------------|
| `switch_tenant` | *(any)* | Looks up tenant via Management API, rejects unknown tenants / SSO tenants, then sets `currentTenant` |
| `tenant_a_inventory_snapshot` | `tenant-a` | Dummy “inventory” tool |
| `tenant_b_metrics_ping` | `tenant-b` | Dummy metrics ping |
| `tenant_b_notes_echo` | `tenant-b` | Echoes a note string |

If UserInfo **`currentTenant`** does not match the tool’s tenant, the handler returns an error explaining to call **`switch_tenant`** first.

**`switch_tenant`** requires the tenant id to exist in Descope (`mgmt.tenant.load`). If **`enforceSSO`** is set, **`authType`** is SAML/OIDC, **SSO Setup Suite** is enabled, or **federated SSO app IDs** are configured, the tool returns an error instructing the user to sign in via SSO instead of switching from MCP.

## Implementation notes

- **UserInfo URL:** `{DESCOPE_BASE_URL}/oauth2/v1/userinfo` with `Authorization: Bearer <access_token>`.
- **Login id for Management API:** Resolved from JWT claims (`email`, `loginIds`, etc.).
- Demo tenant ids are **`tenant-a`** and **`tenant-b`** (`ALLOWED_TENANTS` in `server.py`). Align these with real tenant ids if you attach users to Descope tenants.

### FastMCP component visibility

Tools for **tenant-a** and **tenant-b** are tagged and **disabled globally** via `mcp.disable(...)`. Each MCP session **re-enables only the tools for the user’s current tenant** using `ctx.enable_components` / `ctx.disable_components` after reading UserInfo (`sync_session_tenant_visibility`), so clients typically **do not see** the other tenant’s tools. Call **`switch_tenant`** once after connecting so an existing Descope `currentTenant` is applied to the session’s tool list.
Binary file not shown.
Loading