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
6 changes: 5 additions & 1 deletion demos/remote-mcp-server-descope-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@ To explore your new MCP api, you can use the [MCP Inspector](https://modelcontex
<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:
Expand Down Expand Up @@ -120,11 +119,16 @@ Then, using the `Streamable HTTP` transport, enter the `workers.dev` URL (ex: `h

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.

## Architecture

This server uses the **stateless MCP handler** from the [MCP SDK v2](https://developers.cloudflare.com/agents/model-context-protocol/guides/migrate-to-mcp-sdk-v2/) (protocol revision `2026-07-28`). Instead of the old stateful `McpAgent` Durable Object, requests are served by [`createMcpHandler`](https://developers.cloudflare.com/agents/model-context-protocol/mcp-handler-api/) from `agents/mcp/server` — so there is no Durable Object binding or migration to configure.


## Features

The MCP server implementation includes:

- ⚡ Stateless MCP SDK v2 handler (no Durable Object required)
- 🔐 OAuth 2.0/2.1 Authorization Server Metadata (RFC 8414)
- 🔑 Dynamic Client Registration (RFC 7591)
- 🔒 PKCE Support
Expand Down
7 changes: 5 additions & 2 deletions demos/remote-mcp-server-descope-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@
"format": "oxfmt --write .",
"lint:fix": "oxlint --fix",
"start": "wrangler dev",
"cf-typegen": "wrangler types"
"cf-typegen": "wrangler types",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@cloudflare/workers-oauth-provider": "^0.8.1",
"agents": "^0.17.1",
"@modelcontextprotocol/sdk": "1.30.0",
"@modelcontextprotocol/server": "2.0.0",
"agents": "^0.20.1",
"hono": "^4.12.27",
"zod": "^4.4.3"
},
Expand Down
2 changes: 1 addition & 1 deletion demos/remote-mcp-server-descope-auth/src/descope-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export async function fetchDescopeAuthToken({
return [null, new Response("Failed to fetch access token", { status: 500 })];
}

const body = await resp.json();
const body = (await resp.json()) as { access_token?: string };
const accessToken = body.access_token as string;
if (!accessToken) {
return [null, new Response("Missing access token", { status: 400 })];
Expand Down
14 changes: 14 additions & 0 deletions demos/remote-mcp-server-descope-auth/src/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
interface Env {
DESCOPE_CLIENT_ID: string;
DESCOPE_CLIENT_SECRET: string;
COOKIE_ENCRYPTION_KEY: string;
}


declare namespace Cloudflare {
interface Env {
DESCOPE_CLIENT_ID: string;
DESCOPE_CLIENT_SECRET: string;
COOKIE_ENCRYPTION_KEY: string;
}
}
125 changes: 72 additions & 53 deletions demos/remote-mcp-server-descope-auth/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,91 @@
import OAuthProvider from "@cloudflare/workers-oauth-provider";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/server";
import { createMcpHandler, getMcpAuthContext } from "agents/mcp/server";
import { z } from "zod";
import { DescopeHandler } from "./descope-handler";
import type { Props } from "./descope-utils";

// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = {
sub: string;
name: string;
email: string;
accessToken: string;
};
function requireProps(): Props {
const props = getMcpAuthContext()?.props as Props | undefined;
if (!props) {
throw new Error("Missing authenticated Descope context");
}
return props;
}

export class MyMCP extends McpAgent<Env, Record<string, never>, Props> {
server = new McpServer({

function createServer() {
const server = new McpServer({
name: "Descope OAuth Proxy Demo",
version: "1.0.0",
});

async init() {
// Hello, world!
this.server.tool(
"add",
"Add two numbers the way only MCP can",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ text: String(a + b), type: "text" }],
}),
);

// Use the upstream access token to access user info
this.server.tool(
"getUserInfo",
"Get authenticated user info from Descope",
{},
async () => {
return {
content: [
{
text: JSON.stringify({
email: this.props!.email,
name: this.props!.name,
sub: this.props!.sub,
}),
type: "text",
},
],
};
},
);
server.registerTool(
"add",
{
description: "Add two numbers the way only MCP can",
inputSchema: z.object({ a: z.number(), b: z.number() }),
},
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
}),
);

// Return the access token
this.server.tool("getToken", "Get the Descope access token", {}, async () => ({
content: [
{
text: String(`User's token: ${this.props!.accessToken}`),
type: "text",
},
],
}));
}
// Use the authenticated context to return user info
server.registerTool(
"getUserInfo",
{
description: "Get authenticated user info from Descope",
inputSchema: z.object({}),
},
async () => {
const props = requireProps();
return {
content: [
{
type: "text",
text: JSON.stringify({
email: props.email,
name: props.name,
sub: props.sub,
}),
},
],
};
},
);

// Return the access token
server.registerTool(
"getToken",
{
description: "Get the Descope access token",
inputSchema: z.object({}),
},
async () => {
const props = requireProps();
return {
content: [{ type: "text", text: `User's token: ${props.accessToken}` }],
};
},
);

return server;
}

const mcpHandler = createMcpHandler(createServer);

export default new OAuthProvider({
apiHandler: MyMCP.serve("/mcp"),
// OAuthProvider verifies the bearer token and populates the request
// context with the decrypted props before delegating to the MCP handler.
apiHandler: {
fetch: (request: Request, env: unknown, ctx: ExecutionContext) =>
mcpHandler(request, env, ctx),
},
apiRoute: "/mcp",
authorizeEndpoint: "/authorize",
clientIdMetadataDocumentEnabled: true,
clientRegistrationEndpoint: "/register",
defaultHandler: DescopeHandler as any,
tokenEndpoint: "/token",
Expand Down
Loading