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
4 changes: 2 additions & 2 deletions demos/remote-mcp-server-descope-auth/.dev.vars.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
DESCOPE_PROJECT_ID="your_project_id"
DESCOPE_MANAGEMENT_KEY="your_management_key"
DESCOPE_CLIENT_ID="your_inbound_app_client_id"
DESCOPE_CLIENT_SECRET="your_inbound_app_client_secret"
COOKIE_ENCRYPTION_KEY="your_cookie_encryption_key_generate_with_openssl_rand_hex_32"
50 changes: 32 additions & 18 deletions demos/remote-mcp-server-descope-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,24 @@ Let's get a Remote MCP server up-and-running on Cloudflare Workers with Descope

Before you begin, ensure you have:

- A [Descope](https://www.descope.com/) account and project
- A [Descope](https://www.descope.com/) account
- A Descope **Inbound App client** (created in the [Agentic Identity Hub → Clients](https://app.descope.com/) section — see below)
- Node.js version `18.x` or higher
- A Cloudflare account (for deployment)

## Develop locally

1. Get your credentials from the Descope Console:
- [Project ID](https://app.descope.com/settings/project)
- [Management Key](https://app.descope.com/settings/company/managementkeys)
1. Create an Inbound App client in the Descope Console:
- Go to **Agentic Identity Hub → Clients** and create a new client.
- Allow Authorization code grant type.
- Set the redirect / callback URL to `http://localhost:8787/callback` (add your deployed `https://<worker>.workers.dev/callback` too when you deploy).
- From the client's **Connection Information**, copy the **Client ID** and **Client Secret** — these are the credentials this server uses to authorize against Descope.
- **Define the scopes** the server requests (in the client's Scopes section). This server asks for `openid profile email`, so add:
- `profile` → mapped to the `name` claim
- `email` → mapped to the `email` claim
- (`openid` is built-in and does not need to be added.)

> Scopes requested at `/authorize` **must** be pre-defined on the Inbound App, or Descope rejects the request (`Received invalid scope`). The `openid` scope is required for the `/userinfo` call to succeed; `profile`/`email` populate the `name`/`email` claims returned by the `getUserInfo` tool.

2. Create a KV namespace for OAuth state storage:

Expand All @@ -30,8 +39,8 @@ npx wrangler kv namespace create OAUTH_KV

```bash
# .dev.vars
DESCOPE_PROJECT_ID="your_project_id"
DESCOPE_MANAGEMENT_KEY="your_management_key"
DESCOPE_CLIENT_ID="your_inbound_app_client_id"
DESCOPE_CLIENT_SECRET="your_inbound_app_client_secret"
COOKIE_ENCRYPTION_KEY="your_cookie_encryption_key"
```

Expand Down Expand Up @@ -62,33 +71,37 @@ You should be able to open [`http://localhost:8787/`](http://localhost:8787/) in
To explore your new MCP api, you can use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector).

1. Start it with `npx @modelcontextprotocol/inspector`
2. [Within the inspector](http://localhost:5173), switch the Transport Type to `SSE` and enter `http://localhost:8787/sse` as the URL of the MCP server to connect to.
3. Add a bearer token and click "Connect"
4. Click "List Tools"
5. Run the "getToken" tool, which should return the Authorization header that you set in the inspector
2. [Within the inspector](http://localhost:5173), set the Transport Type to `Streamable HTTP` and enter `http://localhost:8787/mcp` as the URL of the MCP server to connect to.
3. Click "Connect" (or run the **Quick OAuth Flow** from the Authentication panel). The inspector registers itself via Dynamic Client Registration, then redirects you to Descope to log in. This server is OAuth-protected — you do **not** paste a bearer token manually; the token is obtained through the OAuth flow.
4. After you authenticate, click "List Tools".
5. Run the "getUserInfo" tool to see your authenticated Descope profile, or "getToken" to see the Descope access token the server received.

<div align="center">
<img src="img/mcp-inspector-sse-config.png" alt="MCP Inspector with the above config" width="600"/>
<img src="img/mcp-inspector-mcp-config.png" alt="MCP Inspector with the above config" width="600"/>
</div>


## Deploy to Cloudflare

1. Create a KV namespace for production:

```bash
npx wrangler kv:namespace create OAUTH_KV --env production
# Update the KV namespace ID in wrangler.jsonc
npx wrangler kv namespace create OAUTH_KV
# Copy the returned ID into the production kv_namespaces binding in wrangler.jsonc
```

2. Set up your secrets in Cloudflare:

```bash
# Set Descope credentials as secrets
npx wrangler secret put DESCOPE_PROJECT_ID
npx wrangler secret put DESCOPE_MANAGEMENT_KEY
# Set Descope Inbound App credentials as secrets
npx wrangler secret put DESCOPE_CLIENT_ID
npx wrangler secret put DESCOPE_CLIENT_SECRET
npx wrangler secret put COOKIE_ENCRYPTION_KEY
```

> [!IMPORTANT]
> After deploying, add your production callback URL (`https://<worker>.workers.dev/callback`) to the Inbound App client's redirect URLs in **Agentic Identity Hub → Clients**.

3. Deploy the worker:

```bash
Expand All @@ -103,9 +116,10 @@ Just like you did above in "Develop locally", run the MCP inspector:
npx @modelcontextprotocol/inspector@latest
```

Then enter the `workers.dev` URL (ex: `worker-name.account-name.workers.dev/sse`) of your Worker in the inspector as the URL of the MCP server to connect to, and click "Connect".
Then, using the `Streamable HTTP` transport, enter the `workers.dev` URL (ex: `https://worker-name.account-name.workers.dev/mcp`) of your Worker in the inspector as the URL of the MCP server to connect to, and click "Connect".

You've now connected to your MCP server from a remote MCP client. Authentication runs through the Descope OAuth flow — no manual bearer token needed.

You've now connected to your MCP server from a remote MCP client. You can pass in a bearer token like mentioned above.

## Features

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
6 changes: 3 additions & 3 deletions demos/remote-mcp-server-descope-auth/src/descope-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ async function redirectToDescope(
headers: {
...headers,
location: getDescopeAuthorizeUrl({
project_id: env.DESCOPE_PROJECT_ID,
client_id: env.DESCOPE_CLIENT_ID,
redirect_uri: new URL("/callback", request.url).href,
state: stateToken,
}),
Expand Down Expand Up @@ -162,9 +162,9 @@ app.get("/callback", async (c) => {

// Exchange the code for an access token
const [accessToken, errResponse] = await fetchDescopeAuthToken({
client_id: c.env.DESCOPE_CLIENT_ID,
client_secret: c.env.DESCOPE_CLIENT_SECRET,
code: c.req.query("code"),
management_key: c.env.DESCOPE_MANAGEMENT_KEY,
project_id: c.env.DESCOPE_PROJECT_ID,
redirect_uri: new URL("/callback", c.req.url).href,
});
if (errResponse) return errResponse;
Expand Down
37 changes: 22 additions & 15 deletions demos/remote-mcp-server-descope-auth/src/descope-utils.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,72 @@
/**
* Constructs an authorization URL for Descope OAuth.
* Constructs an authorization URL for Descope OAuth (Inbound App).
*
* @param {Object} options
* @param {string} options.project_id - The Descope project ID.
* @param {string} options.client_id - The Descope Inbound App Client ID.
* @param {string} options.redirect_uri - The redirect URI of the application.
* @param {string} [options.state] - The state parameter.
*
* @returns {string} The authorization URL.
*/
export function getDescopeAuthorizeUrl({
project_id,
client_id,
redirect_uri,
state,
scope = "openid profile email",
}: {
project_id: string;
client_id: string;
redirect_uri: string;
state?: string;
scope?: string;
}) {
const upstream = new URL("https://api.descope.com/oauth2/v1/apps/authorize");
upstream.searchParams.set("client_id", project_id);
upstream.searchParams.set("client_id", client_id);
upstream.searchParams.set("redirect_uri", redirect_uri);
upstream.searchParams.set("response_type", "code");
// The `openid` scope is required for the /apps/userinfo endpoint to accept the
// resulting access token; `profile`/`email` add the name/email claims.
// These scopes must be pre-defined on the Inbound App in the Descope Console.
upstream.searchParams.set("scope", scope);
if (state) upstream.searchParams.set("state", state);
return upstream.href;
}

/**
* Fetches an authorization token from Descope.
* Fetches an authorization token from Descope (Inbound App token exchange).
*
* @param {Object} options
* @param {string} options.project_id - The Descope project ID.
* @param {string} options.management_key - The Descope management key.
* @param {string} options.client_id - The Descope Inbound App Client ID.
* @param {string} options.client_secret - The Descope Inbound App Client Secret.
* @param {string} options.code - The authorization code.
* @param {string} options.redirect_uri - The redirect URI of the application.
*
* @returns {Promise<[string, null] | [null, Response]>} A promise that resolves to an array containing the access token or an error response.
*/
export async function fetchDescopeAuthToken({
project_id,
management_key,
client_id,
client_secret,
code,
redirect_uri,
}: {
code: string | undefined;
project_id: string;
management_key: string;
client_id: string;
client_secret: string;
redirect_uri: string;
}): Promise<[string, null] | [null, Response]> {
if (!code) {
return [null, new Response("Missing code", { status: 400 })];
}

const resp = await fetch("https://api.descope.com/oauth2/v1/apps/token", {
body: JSON.stringify({
body: new URLSearchParams({
client_id,
client_secret,
code,
grant_type: "authorization_code",
redirect_uri,
}),
headers: {
Authorization: `Bearer ${project_id}:${management_key}`,
"Content-Type": "application/json",
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
});
Expand Down
67 changes: 39 additions & 28 deletions demos/remote-mcp-server-descope-auth/static/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,39 @@ Let's get a Remote MCP server up-and-running on Cloudflare Workers with Descope

Before you begin, ensure you have:

- A [Descope](https://www.descope.com/) account and project
- A [Descope](https://www.descope.com/) account
- A Descope **Inbound App client** (created in **Agentic Identity Hub → Clients**)
- Node.js version `18.x` or higher
- A Cloudflare account (for deployment)

## Develop locally

1. Get your credentials from the Descope Console:
- [Project ID](https://app.descope.com/settings/project)
- [Management Key](https://app.descope.com/settings/company/managementkeys)
1. Create an Inbound App client in the Descope Console:
- Go to **Agentic Identity Hub → Clients** and create a new client.
- Set the redirect / callback URL to `http://localhost:8787/callback`.
- From the client's **Connection Information**, copy the **Client ID** and **Client Secret**.
- Under **User information scopes**, define the scopes the server requests (mapped to a user attribute):
- `email` → **Email** attribute
- `profile` → **Display Name** attribute
- (`openid` is built-in and is required for `getUserInfo` to work. Scopes requested at `/authorize` must be pre-defined on the Inbound App.)

2. Create a `.dev.vars` file in your project root (this file is gitignored):
2. Create a KV namespace for OAuth state storage:

```bash
npx wrangler kv namespace create OAUTH_KV
# Copy the ID and update wrangler.jsonc
```

3. Create a `.dev.vars` file in your project root (this file is gitignored):

```bash
# .dev.vars
DESCOPE_PROJECT_ID="your_project_id"
DESCOPE_MANAGEMENT_KEY="your_management_key"
# For local development
SERVER_URL="http://localhost:8787"
DESCOPE_CLIENT_ID="your_inbound_app_client_id"
DESCOPE_CLIENT_SECRET="your_inbound_app_client_secret"
COOKIE_ENCRYPTION_KEY="your_cookie_encryption_key" # openssl rand -hex 32
```

3. Clone and set up the repository:
4. Clone and set up the repository:

```bash
# clone the repository
Expand All @@ -47,32 +59,29 @@ You should be able to open [`http://localhost:8787/`](http://localhost:8787/) in
To explore your new MCP api, you can use the [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector).

1. Start it with `npx @modelcontextprotocol/inspector`
2. [Within the inspector](http://localhost:5173), switch the Transport Type to `SSE` and enter `http://localhost:8787/sse` as the URL of the MCP server to connect to.
3. Add a bearer token and click "Connect"
4. Click "List Tools"
5. Run the "getToken" tool, which should return the Authorization header that you set in the inspector
2. [Within the inspector](http://localhost:5173), set the Transport Type to `Streamable HTTP` and enter `http://localhost:8787/mcp` as the URL of the MCP server to connect to.
3. Click "Connect" (or run the **Quick OAuth Flow** from the Authentication panel). The inspector registers itself via Dynamic Client Registration, then redirects you to Descope to log in — no manual bearer token needed.
4. After you authenticate, click "List Tools".
5. Run the "getUserInfo" tool to see your authenticated Descope profile, or "getToken" to see the Descope access token the server received.

<div align="center">
<img src="img/mcp-inspector-sse-config.png" alt="MCP Inspector with the above config" width="600"/>
</div>
> **Note:** The `SSE` transport is deprecated. This server exposes the modern **Streamable HTTP** transport at `/mcp`.

## Deploy to Cloudflare

1. Set up your secrets in Cloudflare:
1. Create a KV namespace for production:

```bash
# Set Descope credentials as secrets
wrangler secret put DESCOPE_MANAGEMENT_KEY
npx wrangler kv namespace create OAUTH_KV
# Copy the returned ID into the production kv_namespaces binding in wrangler.jsonc
```

2. Set your Descope `project_id` and your hosted worker's `server_url` to the `wrangler.jsonc` file:
2. Set up your secrets in Cloudflare:

```bash
# wrangler.jsonc
"vars": {
"DESCOPE_PROJECT_ID": "your_project_id",
"SERVER_URL": "https://your_worker_slug.your_account_name.workers.dev"
}
# Set Descope Inbound App credentials as secrets
npx wrangler secret put DESCOPE_CLIENT_ID
npx wrangler secret put DESCOPE_CLIENT_SECRET
npx wrangler secret put COOKIE_ENCRYPTION_KEY
```

3. Deploy the worker:
Expand All @@ -81,6 +90,8 @@ wrangler secret put DESCOPE_MANAGEMENT_KEY
npm run deploy
```

After deploying, add your production callback URL (`https://<worker>.workers.dev/callback`) to the Inbound App client's redirect URLs in **Agentic Identity Hub → Clients**.

## Call your newly deployed remote MCP server from a remote MCP client

Just like you did above in "Develop locally", run the MCP inspector:
Expand All @@ -89,9 +100,9 @@ Just like you did above in "Develop locally", run the MCP inspector:
npx @modelcontextprotocol/inspector@latest
```

Then enter the `workers.dev` URL (ex: `worker-name.account-name.workers.dev/sse`) of your Worker in the inspector as the URL of the MCP server to connect to, and click "Connect".
Then, using the `Streamable HTTP` transport, enter the `workers.dev` URL (ex: `https://worker-name.account-name.workers.dev/mcp`) of your Worker in the inspector as the URL of the MCP server to connect to, and click "Connect".

You've now connected to your MCP server from a remote MCP client. You can pass in a bearer token like mentioned above.
You've now connected to your MCP server from a remote MCP client. Authentication runs through the Descope OAuth flow.

## Features

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
declare namespace Cloudflare {
interface Env {
OAUTH_KV: KVNamespace;
DESCOPE_PROJECT_ID: string;
DESCOPE_MANAGEMENT_KEY: string;
DESCOPE_CLIENT_ID: string;
DESCOPE_CLIENT_SECRET: string;
COOKIE_ENCRYPTION_KEY: string;
MCP_OBJECT: DurableObjectNamespace<import("./src/index").MyMCP>;
OAUTH_PROVIDER: import("@cloudflare/workers-oauth-provider").OAuthHelpers;
Expand Down
5 changes: 3 additions & 2 deletions demos/remote-mcp-server-descope-auth/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
"name": "remote-mcp-server-bearer-auth",
"main": "src/index.ts",
"compatibility_date": "2025-03-10",
"compatibility_flags": ["nodejs_compat"],
/**
* Note: Use secrets to store sensitive data.
* https://developers.cloudflare.com/workers/configuration/secrets/
*
* Commands to set secrets:
* npx wrangler secret put DESCOPE_PROJECT_ID
* npx wrangler secret put DESCOPE_MANAGEMENT_KEY
* npx wrangler secret put DESCOPE_CLIENT_ID
* npx wrangler secret put DESCOPE_CLIENT_SECRET
* npx wrangler secret put COOKIE_ENCRYPTION_KEY
*/
"migrations": [
Expand Down