
Security News
The Changelog Podcast: Practical Steps to Stay Safe on npm
Learn the essential steps every developer should take to stay secure on npm and reduce exposure to supply chain attacks.
@descope/mcp-express
Advanced tools
Drop‑in Express middleware and helpers to add secure auth to your Model Context Protocol (MCP) server with Descope. Ship an authenticated /mcp endpoint and register tools in minutes.
npm install @descope/mcp-express
.envDESCOPE_PROJECT_ID=your_project_id
SERVER_URL=http://localhost:3000
import "dotenv/config";
import express from "express";
import { descopeMcpAuthRouter, defineTool, DescopeMcpProvider } from "@descope/mcp-express";
import { z } from "zod";
const app = express();
// Required: so /mcp can read JSON bodies
app.use(express.json());
// Optional: explicit provider config (env work out of the box)
const provider = new DescopeMcpProvider({
projectId: process.env.DESCOPE_PROJECT_ID,
serverUrl: process.env.SERVER_URL,
baseUrl: process.env.DESCOPE_BASE_URL, // optional
});
// Define an authenticated tool (requires 'openid')
const hello = defineTool({
name: "hello",
description: "Say hello to the authenticated user",
input: {
name: z.string().describe("Name to greet").optional(),
},
scopes: ["openid"],
handler: async (args, extra) => {
const result = {
message: `Hello ${args.name || "there"}!`,
authenticatedUser: extra.authInfo.clientId,
};
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
},
});
// Wire the MCP router and register your tools
app.use(
descopeMcpAuthRouter((server) => {
hello(server);
}, provider),
);
app.listen(3000, () => {
console.log("MCP endpoint: POST http://localhost:3000/mcp");
});
Pro tips
Content-Type: application/json to /mcp./mcp requires a valid Bearer token./mcp handler is wired only when you pass a toolRegistration function.Pick your flavor: the ergonomic defineTool or the flexible registerAuthenticatedTool.
defineTool (with input)import { defineTool } from "@descope/mcp-express";
import { z } from "zod";
const getUser = defineTool({
name: "get_user",
description: "Get user information",
input: { userId: z.string().describe("The user ID to fetch") },
scopes: ["profile", "email"],
handler: async (args, extra) => {
return {
content: [{ type: "text", text: JSON.stringify({ userId: args.userId, scopes: extra.authInfo.scopes }, null, 2) }],
};
},
});
registerAuthenticatedTool
import { registerAuthenticatedTool } from "@descope/mcp-express";
import { z } from "zod";
const getUser = registerAuthenticatedTool(
"get_user",
{
description: "Get user information",
inputSchema: { userId: z.string().describe("The user ID to fetch") },
},
async (args, extra) => {
return { content: [{ type: "text", text: JSON.stringify({ userId: args.userId }, null, 2) }] };
},
["profile", "email"],
);
const whoami = registerAuthenticatedTool(
"whoami",
{ description: "Return authenticated identity info" },
async (extra) => {
const result = {
clientId: extra.authInfo.clientId,
scopes: extra.authInfo.scopes || [],
};
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
},
["openid"],
);
Short answer: choose defineTool for brevity and great type inference; choose registerAuthenticatedTool if you prefer explicit overloads and a closer-to-the-metal API.
defineTool is a thin wrapper over registerAuthenticatedTool.defineTool
(args, extra) when you provide input.registerAuthenticatedTool
registerTool shape.There isn’t anything you can do with one that you can’t do with the other. Pick the style you prefer.
MCP 2025‑06‑18 compliant Resource Server.
/mcp endpoint with bearer token authenticationOptional (Authorization Server)
/authorize endpoint (disabled by default)Resource Server (always enabled)
Authorization Server (optional)
All OAuth schemas use Zod for runtime validation.
By default, this SDK runs as a Resource Server only. That’s the recommended path and aligns with the MCP 2025‑06‑18 spec. The features below are for legacy compatibility and testing. Enabling them exposes additional endpoints (/authorize, /register). Consider the added surface area before turning them on.
Requirements
Example .env
DESCOPE_PROJECT_ID=your_project_id
SERVER_URL=http://localhost:3000
DESCOPE_MANAGEMENT_KEY=your_management_key
Configuration example
import { DescopeMcpProvider } from "@descope/mcp-express";
const provider = new DescopeMcpProvider({
projectId: process.env.DESCOPE_PROJECT_ID,
serverUrl: process.env.SERVER_URL,
authorizationServerOptions: {
isDisabled: false, // enable Authorization Server mode
enableAuthorizeEndpoint: true, // expose /authorize
enableDynamicClientRegistration: true, // optionally expose /register
},
// Only needed if you enable dynamic client registration
dynamicClientRegistrationOptions: {
authPageUrl: `https://api.descope.com/login/${process.env.DESCOPE_PROJECT_ID}?flow=consent`,
permissionScopes: [
{ name: "get-schema", description: "Allow getting the SQL schema" },
{ name: "run-query", description: "Allow executing a SQL query", required: false },
],
nonConfidentialClient: true,
},
});
Notes
enableDynamicClientRegistration: true and provide dynamicClientRegistrationOptions if you want to expose /register.import { DescopeMcpProvider } from "@descope/mcp-express";
const provider = new DescopeMcpProvider({
verifyTokenOptions: {
requiredScopes: ["get-schema", "run-query"],
// resourceIndicator: "your-resource", // optional
// audience: "your-audience", // optional (single value supported currently)
},
});
Already have a plain MCP server using server.registerTool? Here’s the simplest path:
app.use(express.json()).descopeMcpAuthRouter((server) => { /* register tools */ }, provider).POST /mcp with bearer auth when you provide a registration function.server.registerTool("whoami", { description: "Return identity" }, async (_args, _extra) => {
const data = { ok: true };
return { content: [{ type: "text", text: JSON.stringify(data) }] };
});
const whoami = defineTool({
name: "whoami",
description: "Return identity",
scopes: ["openid"],
handler: async (extra) => {
const data = { ok: true };
return { content: [{ type: "text", text: JSON.stringify(data) }] };
},
});
app.use(
descopeMcpAuthRouter((server) => {
whoami(server);
}, provider),
);
const whoami = registerAuthenticatedTool(
"whoami",
{ description: "Return identity" },
async (extra) => {
const data = { ok: true };
return { content: [{ type: "text", text: JSON.stringify(data) }] };
},
["openid"],
);
StreamableHTTPServerTransport or your own /.well-known/* endpoints. The router handles them.(args, extra) => CallToolResult.(extra) => CallToolResult.CallToolResult as { content: [{ type: "text", text: "..." }] }.extra.getOutboundToken(appId, scopes?) to fetch outbound tokens.If you can’t use the router, the lower-level pieces exist (descopeMcpBearerAuth and createMcpServerHandler on POST /mcp), but the router is the simplest and safest path.
/mcp now uses StreamableHTTPServerTransport from the official MCP SDK.descopeMcpAuthRouter.This SDK adapts code from the Model Context Protocol TypeScript SDK (MIT).
MIT
FAQs
Descope Express MCP SDK
We found that @descope/mcp-express demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 5 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Learn the essential steps every developer should take to stay secure on npm and reduce exposure to supply chain attacks.

Security News
Experts push back on new claims about AI-driven ransomware, warning that hype and sponsored research are distorting how the threat is understood.

Security News
Ruby's creator Matz assumes control of RubyGems and Bundler repositories while former maintainers agree to step back and transfer all rights to end the dispute.