diff --git a/demos/remote-mcp-server-descope-auth/.dev.vars.example b/demos/remote-mcp-server-descope-auth/.dev.vars.example index ac1663fee..67c38fe00 100644 --- a/demos/remote-mcp-server-descope-auth/.dev.vars.example +++ b/demos/remote-mcp-server-descope-auth/.dev.vars.example @@ -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" diff --git a/demos/remote-mcp-server-descope-auth/README.md b/demos/remote-mcp-server-descope-auth/README.md index 5062c10dc..98655a27c 100644 --- a/demos/remote-mcp-server-descope-auth/README.md +++ b/demos/remote-mcp-server-descope-auth/README.md @@ -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://.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: @@ -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" ``` @@ -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.
- MCP Inspector with the above config + MCP Inspector with the above config
+ ## 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://.workers.dev/callback`) to the Inbound App client's redirect URLs in **Agentic Identity Hub → Clients**. + 3. Deploy the worker: ```bash @@ -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 diff --git a/demos/remote-mcp-server-descope-auth/img/mcp-inspector-mcp-config.png b/demos/remote-mcp-server-descope-auth/img/mcp-inspector-mcp-config.png new file mode 100644 index 000000000..24dd163a9 Binary files /dev/null and b/demos/remote-mcp-server-descope-auth/img/mcp-inspector-mcp-config.png differ diff --git a/demos/remote-mcp-server-descope-auth/img/mcp-inspector-sse-config.png b/demos/remote-mcp-server-descope-auth/img/mcp-inspector-sse-config.png deleted file mode 100644 index ab9b7fc2d..000000000 Binary files a/demos/remote-mcp-server-descope-auth/img/mcp-inspector-sse-config.png and /dev/null differ diff --git a/demos/remote-mcp-server-descope-auth/src/descope-handler.ts b/demos/remote-mcp-server-descope-auth/src/descope-handler.ts index 22a36ae2b..3b95d00c3 100644 --- a/demos/remote-mcp-server-descope-auth/src/descope-handler.ts +++ b/demos/remote-mcp-server-descope-auth/src/descope-handler.ts @@ -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, }), @@ -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; diff --git a/demos/remote-mcp-server-descope-auth/src/descope-utils.ts b/demos/remote-mcp-server-descope-auth/src/descope-utils.ts index 45d1d82f3..b7165ebb9 100644 --- a/demos/remote-mcp-server-descope-auth/src/descope-utils.ts +++ b/demos/remote-mcp-server-descope-auth/src/descope-utils.ts @@ -1,50 +1,56 @@ /** - * 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) { @@ -52,14 +58,15 @@ export async function fetchDescopeAuthToken({ } 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", }); diff --git a/demos/remote-mcp-server-descope-auth/static/README.md b/demos/remote-mcp-server-descope-auth/static/README.md index e02e96cc6..da11c31b5 100644 --- a/demos/remote-mcp-server-descope-auth/static/README.md +++ b/demos/remote-mcp-server-descope-auth/static/README.md @@ -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 @@ -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. -
- MCP Inspector with the above config -
+> **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: @@ -81,6 +90,8 @@ wrangler secret put DESCOPE_MANAGEMENT_KEY npm run deploy ``` +After deploying, add your production callback URL (`https://.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: @@ -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 diff --git a/demos/remote-mcp-server-descope-auth/worker-configuration.d.ts b/demos/remote-mcp-server-descope-auth/worker-configuration.d.ts index f4799b890..5920efa30 100644 --- a/demos/remote-mcp-server-descope-auth/worker-configuration.d.ts +++ b/demos/remote-mcp-server-descope-auth/worker-configuration.d.ts @@ -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; OAUTH_PROVIDER: import("@cloudflare/workers-oauth-provider").OAuthHelpers; diff --git a/demos/remote-mcp-server-descope-auth/wrangler.jsonc b/demos/remote-mcp-server-descope-auth/wrangler.jsonc index 00959edee..553297e2b 100644 --- a/demos/remote-mcp-server-descope-auth/wrangler.jsonc +++ b/demos/remote-mcp-server-descope-auth/wrangler.jsonc @@ -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": [