Skip to content

Commit 868ba4a

Browse files
committed
Merge remote-tracking branch 'origin/main' into add-example-xmcp-mcp
2 parents 4056497 + 4f5502a commit 868ba4a

28 files changed

Lines changed: 2914 additions & 3076 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ The following examples can be found in the examples directory:
2222
- [Fastify API Example](./examples/example-fastify-api/README.md)
2323
- [NestJS API Example](./examples/example-nestjs-api/README.md)
2424
- [Express Web App Example](./examples/example-express-web/README.md)
25+
- [FastMCP MCP Example](./examples/example-fastmcp-mcp/README.md)
26+
- [Express MCP Server Example](./examples/example-express-mcp/README.md)
2527
- [XMCP MCP Server Example](./examples/example-xmcp-mcp/README.md)
2628

2729
Before running the examples, you need to install the dependencies for the monorepo and build all the packages.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
AUTH0_DOMAIN=
2+
AUTH0_AUDIENCE=
3+
PORT=3001
4+
MCP_SERVER_URL=http://localhost:3001
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Example Express MCP Server with Auth0 Integration
2+
3+
This is a practical example of securing a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/docs) server
4+
with Auth0 using the Express framework.
5+
6+
## Install dependencies
7+
8+
Install the dependencies using npm:
9+
10+
```bash
11+
npm install
12+
```
13+
14+
## Auth0 Tenant Setup
15+
16+
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.
17+
18+
## Configuration
19+
20+
Rename `.env.example` to `.env` and configure the domain and audience:
21+
22+
```
23+
# Auth0 tenant domain
24+
AUTH0_DOMAIN=example-tenant.us.auth0.com
25+
26+
# Auth0 API Identifier
27+
AUTH0_AUDIENCE=http://localhost:3001
28+
```
29+
30+
With the configuration in place, the example can be started by running:
31+
32+
```bash
33+
npm run start
34+
```
35+
36+
## Testing
37+
38+
Use an MCP client like [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to test your server interactively:
39+
40+
```bash
41+
npx @modelcontextprotocol/inspector
42+
```
43+
44+
The server will start up and the UI will be accessible at http://localhost:6274.
45+
46+
In the MCP Inspector, select `Streamable HTTP` as the `Transport Type` and enter `http://localhost:3001/mcp` as the URL.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "example-express-mcp",
3+
"version": "0.1.0-beta.1",
4+
"description": "Express MCP Server Example — Example implementation of an MCP server using Express.js with Auth0",
5+
"type": "module",
6+
"scripts": {
7+
"start": "tsx src/index.ts --project tsconfig.json"
8+
},
9+
"dependencies": {
10+
"@auth0/auth0-api-js": "*",
11+
"@modelcontextprotocol/sdk": "^1.17.0",
12+
"cors": "^2.8.5",
13+
"dotenv": "^17.2.1",
14+
"express": "^5.1.0",
15+
"zod": "^3.25.76"
16+
},
17+
"devDependencies": {
18+
"@types/cors": "^2.8.19",
19+
"@types/express": "^5.0.3",
20+
"ts-node": "^10.9.2",
21+
"tsx": "^4.20.3",
22+
"typescript": "^5.8.3"
23+
}
24+
}
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import { ApiClient, VerifyAccessTokenError } from "@auth0/auth0-api-js";
2+
import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
3+
import { InvalidTokenError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
4+
import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
5+
import {
6+
getOAuthProtectedResourceMetadataUrl,
7+
mcpAuthMetadataRouter,
8+
} from "@modelcontextprotocol/sdk/server/auth/router.js";
9+
import {
10+
McpServer,
11+
ToolCallback,
12+
} from "@modelcontextprotocol/sdk/server/mcp.js";
13+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
14+
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
15+
import type { NextFunction, Request, Response } from "express";
16+
import express from "express";
17+
import { ZodRawShape } from "zod";
18+
import { MCP_TOOL_SCOPES } from "./tools.js";
19+
import { Auth, Auth0McpOptions } from "./types.js";
20+
21+
declare module "express-serve-static-core" {
22+
interface Request {
23+
auth0Mcp: ReturnType<typeof createAuth0Mcp>;
24+
mcpTransport?: StreamableHTTPServerTransport;
25+
mcpServer?: McpServer;
26+
}
27+
}
28+
29+
export function auth0Mcp(options: Auth0McpOptions) {
30+
//@ts-expect-error TypeScript doesnt like this
31+
const router = new express.Router();
32+
33+
router.use(async (req: Request, res: Response, next: NextFunction) => {
34+
req.auth0Mcp = createAuth0Mcp({
35+
resourceName: options.resourceName,
36+
resourceServerUrl: options.resourceServerUrl,
37+
domain: options.domain,
38+
audience: options.audience,
39+
});
40+
next();
41+
});
42+
43+
// Add metadata routes
44+
router.use(async (req: Request, res: Response, next: NextFunction) => {
45+
const metadataRouter = await req.auth0Mcp.authMetadataRouter();
46+
return metadataRouter(req, res, next);
47+
});
48+
49+
return router;
50+
}
51+
52+
export function createAuth0Mcp(opts: Auth0McpOptions) {
53+
const verify = createVerifier(opts);
54+
const requireScopes = createScopeValidator();
55+
56+
const authMetadataRouter = createAuthMetadataRouter(opts);
57+
const authMiddleware = createAuthMiddleware(opts, verify);
58+
59+
return {
60+
/**
61+
* Human-readable name for the protected resource (MCP server).
62+
*/
63+
resourceName: opts.resourceName,
64+
65+
/**
66+
* Creates an Express router that exposes OAuth metadata endpoints needed for MCP clients.
67+
*/
68+
authMetadataRouter: () => authMetadataRouter,
69+
70+
/**
71+
* Creates Express middleware for protecting MCP endpoints.
72+
* Validates Bearer tokens and populates req.auth with user information.
73+
*/
74+
authMiddleware: () => authMiddleware,
75+
76+
/**
77+
* Wraps an MCP tool handler to enforce required OAuth scopes.
78+
*/
79+
requireScopes,
80+
};
81+
}
82+
83+
function isNonEmptyString(value: unknown): value is string {
84+
return typeof value === "string" && value.length > 0;
85+
}
86+
87+
/**
88+
* Creates a JWT token verifier for Auth0-issued access tokens.
89+
*
90+
* This function returns a reusable `verify` function that validates JWT signatures,
91+
* token claims, and extracts user identity information for MCP integration using
92+
* the official @auth0/auth0-api-js library.
93+
*/
94+
function createVerifier(opts: Auth0McpOptions) {
95+
const apiClient = new ApiClient({
96+
domain: opts.domain,
97+
audience: opts.audience,
98+
});
99+
return async function verify(token: string): Promise<Auth> {
100+
try {
101+
const decoded = await apiClient.verifyAccessToken({
102+
accessToken: token,
103+
});
104+
105+
if (!isNonEmptyString(decoded.sub)) {
106+
throw new InvalidTokenError(
107+
"Token is missing required subject (sub) claim"
108+
);
109+
}
110+
111+
let clientId: string | null = null;
112+
if (isNonEmptyString(decoded.client_id)) {
113+
clientId = decoded.client_id;
114+
} else if (isNonEmptyString(decoded.azp)) {
115+
clientId = decoded.azp;
116+
}
117+
118+
if (!clientId) {
119+
throw new InvalidTokenError(
120+
"Token is missing required client identification (client_id or azp claim)."
121+
);
122+
}
123+
124+
return {
125+
token,
126+
clientId,
127+
scopes:
128+
typeof decoded.scope === "string"
129+
? decoded.scope.split(" ").filter(Boolean)
130+
: [],
131+
...(decoded.exp && { expiresAt: decoded.exp }),
132+
extra: {
133+
sub: decoded.sub,
134+
...(isNonEmptyString(decoded.client_id) && {
135+
client_id: decoded.client_id,
136+
}),
137+
...(isNonEmptyString(decoded.azp) && { azp: decoded.azp }),
138+
...(isNonEmptyString(decoded.name) && { name: decoded.name }),
139+
...(isNonEmptyString(decoded.email) && { email: decoded.email }),
140+
},
141+
};
142+
} catch (error) {
143+
if (error instanceof VerifyAccessTokenError) {
144+
throw new InvalidTokenError(error.message);
145+
}
146+
throw error;
147+
}
148+
};
149+
}
150+
151+
/**
152+
* Returns a router that includes MCP authorization metadata endpoints.
153+
*/
154+
async function createAuthMetadataRouter(opts: Auth0McpOptions) {
155+
const oauthMetadata = await discoverAuthorizationServerMetadata(
156+
new URL(`https://${opts.domain}`)
157+
);
158+
159+
if (!oauthMetadata) {
160+
throw new Error(`Failed to fetch OAuth metadata from ${opts.domain}`);
161+
}
162+
163+
return mcpAuthMetadataRouter({
164+
oauthMetadata,
165+
resourceServerUrl: opts.resourceServerUrl,
166+
resourceName: opts.resourceName,
167+
scopesSupported: ["openid", ...MCP_TOOL_SCOPES],
168+
});
169+
}
170+
171+
/**
172+
* Returns an Express middleware that protects MCP endpoints.
173+
*/
174+
function createAuthMiddleware(
175+
opts: Auth0McpOptions,
176+
verifier: (token: string) => Promise<Auth>
177+
) {
178+
return requireBearerAuth({
179+
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(
180+
opts.resourceServerUrl
181+
),
182+
verifier: { verifyAccessToken: verifier },
183+
});
184+
}
185+
186+
/**
187+
* Wraps an MCP tool handler to enforce required OAuth scopes.
188+
*
189+
* This is a higher-order function that adds scope-based authorization to MCP tools.
190+
* It validates that the authenticated user's JWT token contains all required scopes
191+
* before allowing access to the wrapped tool.
192+
*/
193+
function createScopeValidator() {
194+
/**
195+
* Wraps a tool handler with scope validation.
196+
* This function ensures that the tool can only be executed if the user has the required OAuth scopes.
197+
*/
198+
return function requireScopes<T extends ZodRawShape>(
199+
requiredScopes: readonly string[],
200+
handler: (args: T, extra: { authInfo: Auth }) => Promise<CallToolResult>
201+
): ToolCallback<T> {
202+
return (async (args, extra) => {
203+
// To support both context-only and payload+context handlers
204+
let context = extra;
205+
if (!extra) {
206+
context = args as Parameters<ToolCallback<T>>[1];
207+
}
208+
209+
if (!context.authInfo) {
210+
throw new Error(
211+
"Authentication information is required to execute this tool."
212+
);
213+
}
214+
const userScopes = context.authInfo.scopes;
215+
const hasScopes = requiredScopes.every((scope) =>
216+
userScopes.includes(scope)
217+
);
218+
219+
if (!hasScopes) {
220+
throw new Error(
221+
`Missing required scopes: ${requiredScopes.join(", ")}`
222+
);
223+
}
224+
225+
return handler(args as T, { authInfo: context.authInfo as Auth });
226+
}) as ToolCallback<T>;
227+
};
228+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import "dotenv/config";
2+
import { createExpressApp } from "./server.js";
3+
4+
async function main() {
5+
try {
6+
const PORT = parseInt(process.env.PORT ?? "3001", 10);
7+
const app = await createExpressApp();
8+
9+
app.listen(PORT, () => {
10+
console.log(`Example Express MCP Server listening on port ${PORT}`);
11+
});
12+
} catch (error) {
13+
console.error("Failed to start server:", error);
14+
process.exit(1);
15+
}
16+
}
17+
18+
main();
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { allowedMethods } from "@modelcontextprotocol/sdk/server/auth/middleware/allowedMethods.js";
2+
import cors from "cors";
3+
import express, { type Application } from "express";
4+
import { auth0Mcp } from "./auth0.js";
5+
import { requireAuth, withMcpServer } from "./utils.js";
6+
7+
const PORT = parseInt(process.env.PORT ?? "3001", 10);
8+
const MCP_SERVER_RESOURCE_NAME = "Example Express MCP Server";
9+
const MCP_SERVER_URL = process.env.MCP_SERVER_URL ?? `http://localhost:${PORT}`;
10+
const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN as string;
11+
const AUDIENCE = process.env.AUTH0_AUDIENCE ?? MCP_SERVER_URL;
12+
13+
// Validate required environment variables
14+
if (!AUTH0_DOMAIN) {
15+
throw new Error("AUTH0_DOMAIN environment variable is required");
16+
}
17+
18+
/**
19+
* Create Express app with CORS, Auth0, and MCP handling.
20+
*/
21+
export async function createExpressApp(): Promise<Application> {
22+
const app = express();
23+
24+
// Enable CORS
25+
app.use(
26+
cors({
27+
origin: "*", // Adjust as needed for production
28+
exposedHeaders: ["Mcp-Session-Id"],
29+
allowedHeaders: ["Content-Type", "mcp-session-id"],
30+
})
31+
);
32+
33+
app.use(express.json());
34+
35+
app.use(
36+
auth0Mcp({
37+
resourceName: MCP_SERVER_RESOURCE_NAME,
38+
resourceServerUrl: new URL(MCP_SERVER_URL),
39+
domain: AUTH0_DOMAIN,
40+
audience: AUDIENCE,
41+
})
42+
);
43+
44+
app.post("/mcp", requireAuth, withMcpServer, async (req, res) => {
45+
try {
46+
await req.mcpTransport?.handleRequest(req, res, req.body);
47+
} catch (err) {
48+
console.error("Error handling MCP request:", err);
49+
if (!res.headersSent) {
50+
res.status(500).json({
51+
jsonrpc: "2.0",
52+
error: {
53+
code: -32603,
54+
message: "Internal server error",
55+
},
56+
id: null,
57+
});
58+
}
59+
}
60+
});
61+
62+
app.use("/mcp", allowedMethods(["POST"]));
63+
64+
return app;
65+
}

0 commit comments

Comments
 (0)