Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The following examples can be found in the examples directory:
- [Fastify API Example](./examples/example-fastify-api/README.md)
- [NestJS API Example](./examples/example-nestjs-api/README.md)
- [Express Web App Example](./examples/example-express-web/README.md)
- [Express MCP Server Example](./examples/example-express-mcp/README.md)

Before running the examples, you need to install the dependencies for the monorepo and build all the packages.

Expand Down
4 changes: 4 additions & 0 deletions examples/example-express-mcp/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AUTH0_DOMAIN=
AUTH0_AUDIENCE=
PORT=3001
MCP_SERVER_URL=http://localhost:3001
46 changes: 46 additions & 0 deletions examples/example-express-mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Example Express MCP Server with Auth0 Integration

This is a practical example of securing a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs) server
with Auth0 using the Express framework.

## Install dependencies

Install the dependencies using npm:

```bash
npm install
```

## Auth0 Tenant Setup

For detailed instructions on setting up your Auth0 tenant for MCP server integration, please refer to the [Auth0 Tenant Setup guide](../example-fastmcp-mcp/README.md#auth0-tenant-setup) in the FastMCP example.

## Configuration

Rename `.env.example` to `.env` and configure the domain and audience:

```
# Auth0 tenant domain
AUTH0_DOMAIN=example-tenant.us.auth0.com

# Auth0 API Identifier
AUTH0_AUDIENCE=http://localhost:3001
```

With the configuration in place, the example can be started by running:

```bash
npm run start
```

## Testing

Use an MCP client like [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to test your server interactively:

```bash
npx @modelcontextprotocol/inspector
```

The server will start up and the UI will be accessible at http://localhost:6274.

In the MCP Inspector, select `Streamable HTTP` as the `Transport Type` and enter `http://localhost:3001/mcp` as the URL.
24 changes: 24 additions & 0 deletions examples/example-express-mcp/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "example-express-mcp",
"version": "0.1.0-beta.1",
"description": "Express MCP Server Example — Example implementation of an MCP server using Express.js with Auth0",
"type": "module",
"scripts": {
"start": "tsx src/index.ts --project tsconfig.json"
},
"dependencies": {
"@auth0/auth0-api-js": "*",
"@modelcontextprotocol/sdk": "^1.17.0",
"cors": "^2.8.5",
"dotenv": "^17.2.1",
"express": "^5.1.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"ts-node": "^10.9.2",
"tsx": "^4.20.3",
"typescript": "^5.8.3"
}
}
228 changes: 228 additions & 0 deletions examples/example-express-mcp/src/auth0.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import { ApiClient, VerifyAccessTokenError } from "@auth0/auth0-api-js";
import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
import { InvalidTokenError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
import {
getOAuthProtectedResourceMetadataUrl,
mcpAuthMetadataRouter,
} from "@modelcontextprotocol/sdk/server/auth/router.js";
import {
McpServer,
ToolCallback,
} from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { NextFunction, Request, Response } from "express";
import express from "express";
import { ZodRawShape } from "zod";
import { MCP_TOOL_SCOPES } from "./tools.js";
import { Auth, Auth0McpOptions } from "./types.js";

declare module "express-serve-static-core" {
interface Request {
auth0Mcp: ReturnType<typeof createAuth0Mcp>;
mcpTransport?: StreamableHTTPServerTransport;
mcpServer?: McpServer;
}
}

export function auth0Mcp(options: Auth0McpOptions) {
//@ts-expect-error TypeScript doesnt like this
const router = new express.Router();

router.use(async (req: Request, res: Response, next: NextFunction) => {
req.auth0Mcp = createAuth0Mcp({
resourceName: options.resourceName,
resourceServerUrl: options.resourceServerUrl,
domain: options.domain,
audience: options.audience,
});
next();
});

// Add metadata routes
router.use(async (req: Request, res: Response, next: NextFunction) => {
const metadataRouter = await req.auth0Mcp.authMetadataRouter();
return metadataRouter(req, res, next);
});

return router;
}

export function createAuth0Mcp(opts: Auth0McpOptions) {
const verify = createVerifier(opts);
const requireScopes = createScopeValidator();

const authMetadataRouter = createAuthMetadataRouter(opts);
const authMiddleware = createAuthMiddleware(opts, verify);

return {
/**
* Human-readable name for the protected resource (MCP server).
*/
resourceName: opts.resourceName,

/**
* Creates an Express router that exposes OAuth metadata endpoints needed for MCP clients.
*/
authMetadataRouter: () => authMetadataRouter,

/**
* Creates Express middleware for protecting MCP endpoints.
* Validates Bearer tokens and populates req.auth with user information.
*/
authMiddleware: () => authMiddleware,

/**
* Wraps an MCP tool handler to enforce required OAuth scopes.
*/
requireScopes,
};
}

function isNonEmptyString(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
}

/**
* Creates a JWT token verifier for Auth0-issued access tokens.
*
* This function returns a reusable `verify` function that validates JWT signatures,
* token claims, and extracts user identity information for MCP integration using
* the official @auth0/auth0-api-js library.
*/
function createVerifier(opts: Auth0McpOptions) {
const apiClient = new ApiClient({
domain: opts.domain,
audience: opts.audience,
});
return async function verify(token: string): Promise<Auth> {
try {
const decoded = await apiClient.verifyAccessToken({
accessToken: token,
});

if (!isNonEmptyString(decoded.sub)) {
throw new InvalidTokenError(
"Token is missing required subject (sub) claim"
);
}

let clientId: string | null = null;
if (isNonEmptyString(decoded.client_id)) {
clientId = decoded.client_id;
} else if (isNonEmptyString(decoded.azp)) {
clientId = decoded.azp;
}

if (!clientId) {
throw new InvalidTokenError(
"Token is missing required client identification (client_id or azp claim)."
);
}

return {
token,
clientId,
scopes:
typeof decoded.scope === "string"
? decoded.scope.split(" ").filter(Boolean)
: [],
...(decoded.exp && { expiresAt: decoded.exp }),
extra: {
sub: decoded.sub,
...(isNonEmptyString(decoded.client_id) && {
client_id: decoded.client_id,
}),
...(isNonEmptyString(decoded.azp) && { azp: decoded.azp }),
...(isNonEmptyString(decoded.name) && { name: decoded.name }),
...(isNonEmptyString(decoded.email) && { email: decoded.email }),
},
};
} catch (error) {
if (error instanceof VerifyAccessTokenError) {
throw new InvalidTokenError(error.message);
}
throw error;
}
};
}

/**
* Returns a router that includes MCP authorization metadata endpoints.
*/
async function createAuthMetadataRouter(opts: Auth0McpOptions) {
const oauthMetadata = await discoverAuthorizationServerMetadata(
new URL(`https://${opts.domain}`)
);

if (!oauthMetadata) {
throw new Error(`Failed to fetch OAuth metadata from ${opts.domain}`);
}

return mcpAuthMetadataRouter({
oauthMetadata,
resourceServerUrl: opts.resourceServerUrl,
resourceName: opts.resourceName,
scopesSupported: ["openid", ...MCP_TOOL_SCOPES],
});
}

/**
* Returns an Express middleware that protects MCP endpoints.
*/
function createAuthMiddleware(
opts: Auth0McpOptions,
verifier: (token: string) => Promise<Auth>
) {
return requireBearerAuth({
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(
opts.resourceServerUrl
),
verifier: { verifyAccessToken: verifier },
});
}

/**
* Wraps an MCP tool handler to enforce required OAuth scopes.
*
* This is a higher-order function that adds scope-based authorization to MCP tools.
* It validates that the authenticated user's JWT token contains all required scopes
* before allowing access to the wrapped tool.
*/
function createScopeValidator() {
/**
* Wraps a tool handler with scope validation.
* This function ensures that the tool can only be executed if the user has the required OAuth scopes.
*/
return function requireScopes<T extends ZodRawShape>(
requiredScopes: readonly string[],
handler: (args: T, extra: { authInfo: Auth }) => Promise<CallToolResult>
): ToolCallback<T> {
return (async (args, extra) => {
// To support both context-only and payload+context handlers
let context = extra;
if (!extra) {
context = args as Parameters<ToolCallback<T>>[1];
}

if (!context.authInfo) {
throw new Error(
"Authentication information is required to execute this tool."
);
}
const userScopes = context.authInfo.scopes;
const hasScopes = requiredScopes.every((scope) =>
userScopes.includes(scope)
);

if (!hasScopes) {
throw new Error(
`Missing required scopes: ${requiredScopes.join(", ")}`
);
}

return handler(args as T, { authInfo: context.authInfo as Auth });
}) as ToolCallback<T>;
};
}
18 changes: 18 additions & 0 deletions examples/example-express-mcp/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import "dotenv/config";
import { createExpressApp } from "./server.js";

async function main() {
try {
const PORT = parseInt(process.env.PORT ?? "3001", 10);
const app = await createExpressApp();

app.listen(PORT, () => {
console.log(`Example Express MCP Server listening on port ${PORT}`);
});
} catch (error) {
console.error("Failed to start server:", error);
process.exit(1);
}
}

main();
65 changes: 65 additions & 0 deletions examples/example-express-mcp/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { allowedMethods } from "@modelcontextprotocol/sdk/server/auth/middleware/allowedMethods.js";
import cors from "cors";
import express, { type Application } from "express";
import { auth0Mcp } from "./auth0.js";
import { requireAuth, withMcpServer } from "./utils.js";

const PORT = parseInt(process.env.PORT ?? "3001", 10);
const MCP_SERVER_RESOURCE_NAME = "Example Express MCP Server";
const MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}`;
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN as string;
const AUDIENCE = process.env.AUTH0_AUDIENCE ?? MCP_SERVER_URL;

// Validate required environment variables
if (!AUTH0_DOMAIN) {
throw new Error("AUTH0_DOMAIN environment variable is required");
}

/**
* Create Express app with CORS, Auth0, and MCP handling.
*/
export async function createExpressApp(): Promise<Application> {
const app = express();

// Enable CORS
app.use(
cors({
origin: "*", // Adjust as needed for production
exposedHeaders: ["Mcp-Session-Id"],
allowedHeaders: ["Content-Type", "mcp-session-id"],
})
);

app.use(express.json());

app.use(
auth0Mcp({
resourceName: MCP_SERVER_RESOURCE_NAME,
resourceServerUrl: new URL(MCP_SERVER_URL),
domain: AUTH0_DOMAIN,
audience: AUDIENCE,
})
);

app.post("/mcp", requireAuth, withMcpServer, async (req, res) => {
try {
await req.mcpTransport?.handleRequest(req, res, req.body);
} catch (err) {
console.error("Error handling MCP request:", err);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
},
id: null,
});
}
}
});

app.use("/mcp", allowedMethods(["POST"]));

return app;
}
Loading