Skip to content

Commit ba6c815

Browse files
authored
Merge pull request #4 from 3nethz/main
fix: update protectedRoute for Express 5 compatibility and improve MCP usage documentation
2 parents 63eb1f5 + d82bc25 commit ba6c815

File tree

8 files changed

+360
-227
lines changed

8 files changed

+360
-227
lines changed

.changeset/ready-waves-lay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@asgardeo/mcp-express': minor
3+
---
4+
5+
First stable release 🎉

.changeset/spotty-sloths-work.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@asgardeo/mcp-node': minor
3+
---
4+
5+
First stable release 🎉

examples/express-mcp-server/src/index.ts

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import express from 'express';
22
import {config} from 'dotenv';
3-
import {AsgardeoMcpAuth, protectedRoute} from '@asgardeo/mcp-express';
3+
import {McpAuthServer} from '@asgardeo/mcp-express';
44
import {protectedRoutes} from './routes/protected';
55
import {publicRoutes} from './routes/public';
66

@@ -9,25 +9,18 @@ config();
99
const app = express();
1010
const port = process.env.PORT || 3000;
1111

12-
app.use(express.json());
12+
const mcpAuthServer = new McpAuthServer({
13+
baseUrl: process.env.BASE_URL as string,
14+
});
1315

14-
app.use(
15-
AsgardeoMcpAuth({
16-
baseUrl: process.env.BASE_URL as string,
17-
}),
18-
);
16+
app.use(express.json());
17+
app.use(mcpAuthServer.router());
1918

2019
// Public routes
2120
app.use('/api', publicRoutes);
2221

2322
// Protected routes with MCP authentication
24-
app.use(
25-
'/api/protected',
26-
protectedRoute({
27-
baseUrl: process.env.BASE_URL as string,
28-
}),
29-
protectedRoutes,
30-
);
23+
app.use('/api/protected', mcpAuthServer.protect(), protectedRoutes);
3124

3225
// Error handling middleware
3326
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {

examples/express-mcp-vet-ai-assist-app/src/index.ts

Lines changed: 148 additions & 153 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
*/
1818

1919
import {randomUUID} from 'node:crypto';
20-
import {AsgardeoMcpAuth, protectedRoute} from '@asgardeo/mcp-express';
20+
import {McpAuthServer} from '@asgardeo/mcp-express';
2121
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp';
2222
import {StreamableHTTPServerTransport} from '@modelcontextprotocol/sdk/server/streamableHttp';
2323
import {isInitializeRequest} from '@modelcontextprotocol/sdk/types';
@@ -28,12 +28,13 @@ import {z} from 'zod';
2828
config();
2929

3030
const app: Express = express();
31+
32+
const mcpAuthServer: McpAuthServer = new McpAuthServer({
33+
baseUrl: process.env.BASE_URL as string,
34+
});
35+
3136
app.use(express.json());
32-
app.use(
33-
AsgardeoMcpAuth({
34-
baseUrl: process.env.BASE_URL as string,
35-
}),
36-
);
37+
app.use(mcpAuthServer.router());
3738

3839
interface TransportMap {
3940
[sessionId: string]: {
@@ -47,173 +48,167 @@ const SESSION_TIMEOUT_MS: number = 30 * 60 * 1000;
4748

4849
const isSessionExpired = (lastAccessTime: number): boolean => Date.now() - lastAccessTime > SESSION_TIMEOUT_MS;
4950

50-
app.post(
51-
'/mcp',
52-
protectedRoute({
53-
baseUrl: process.env.BASE_URL as string,
54-
}),
55-
async (req: Request, res: Response): Promise<void> => {
56-
try {
57-
const sessionId: string | undefined = req.headers['mcp-session-id'] as string | undefined;
58-
let transport: StreamableHTTPServerTransport;
59-
60-
if (sessionId && transports[sessionId]) {
61-
if (isSessionExpired(transports[sessionId].lastAccess)) {
62-
// eslint-disable-next-line no-console
63-
console.log(`Session expired: ${sessionId}`);
64-
transports[sessionId].transport.close();
65-
delete transports[sessionId];
66-
67-
res.status(401).json({
68-
error: {
69-
code: -32000,
70-
message: 'Session expired',
71-
},
72-
id: null,
73-
jsonrpc: '2.0',
74-
});
75-
return;
76-
}
77-
78-
transport = transports[sessionId].transport;
79-
transports[sessionId].lastAccess = Date.now();
80-
} else if (!sessionId && isInitializeRequest(req.body)) {
81-
let bearerToken: string | undefined;
82-
const authHeader: string | undefined = req.headers.authorization as string | undefined;
83-
if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
84-
bearerToken = authHeader.substring(7);
85-
// eslint-disable-next-line no-console
86-
console.log(`Bearer token captured for new session.`);
87-
} else {
88-
// eslint-disable-next-line no-console
89-
console.warn('MCP session initialized: No Bearer token found in Authorization header.');
90-
}
91-
transport = new StreamableHTTPServerTransport({
92-
onsessioninitialized: (newSessionId: string): void => {
93-
transports[newSessionId] = {
94-
lastAccess: Date.now(),
95-
transport,
96-
};
97-
// eslint-disable-next-line no-console
98-
console.log(`Session initialized: ${newSessionId}`);
99-
},
100-
sessionIdGenerator: (): string => randomUUID(),
101-
});
102-
103-
transport.onclose = (): void => {
104-
if (transport.sessionId) {
105-
// eslint-disable-next-line no-console
106-
console.log(`Session closed: ${transport.sessionId}`);
107-
delete transports[transport.sessionId];
108-
}
109-
};
51+
app.post('/mcp', mcpAuthServer.protect(), async (req: Request, res: Response): Promise<void> => {
52+
try {
53+
const sessionId: string | undefined = req.headers['mcp-session-id'] as string | undefined;
54+
let transport: StreamableHTTPServerTransport;
11055

111-
const server: McpServer = new McpServer({
112-
name: 'example-server',
113-
version: '1.0.0',
114-
});
56+
if (sessionId && transports[sessionId]) {
57+
if (isSessionExpired(transports[sessionId].lastAccess)) {
58+
// eslint-disable-next-line no-console
59+
console.log(`Session expired: ${sessionId}`);
60+
transports[sessionId].transport.close();
61+
delete transports[sessionId];
11562

116-
server.tool(
117-
'get_pet_vaccination_info',
118-
'Retrieves the vaccination history and upcoming vaccination dates for a specific pet. Requires user authentication and explicit consent via an authorization token.',
119-
{
120-
petId: z.string().describe('The unique identifier for the pet.'),
121-
},
122-
async ({petId}: {petId: string}) => {
123-
try {
124-
return {
125-
content: [
126-
{
127-
text: `Retrieved vaccination info for pet ID: ${petId}. Token was ${
128-
bearerToken ? 'present' : 'absent'
129-
}.`,
130-
type: 'text',
131-
},
132-
],
133-
};
134-
} catch (error) {
135-
const errorMessage: string = error instanceof Error ? error.message : String(error);
136-
throw new Error(`Failed to retrieve vaccination information: ${errorMessage}`);
137-
}
138-
},
139-
);
140-
141-
server.tool(
142-
'book_vet_appointment',
143-
'Books a new veterinary appointment for a specific pet. Requires user authentication and explicit consent via an authorization token.',
144-
{
145-
date: z.string().describe('Desired date for the appointment (e.g., YYYY-MM-DD).'),
146-
petId: z.string().describe('The unique identifier for the pet.'),
147-
reason: z.string().describe('The reason for the vet visit.'),
148-
time: z.string().describe('Desired time for the appointment (e.g., HH:MM AM/PM).'),
149-
},
150-
async ({date, petId, reason, time}: {date: string; petId: string; reason: string; time: string}) => {
151-
try {
152-
return {
153-
content: [
154-
{
155-
text: `Booked vet appointment for pet ID: ${petId} on ${date} at ${time} for: ${reason}. Token was ${
156-
bearerToken ? 'present' : 'absent'
157-
}.`,
158-
type: 'text',
159-
},
160-
],
161-
};
162-
} catch (error) {
163-
const errorMessage: string = error instanceof Error ? error.message : String(error);
164-
throw new Error(`Failed to book appointment: ${errorMessage}`);
165-
}
63+
res.status(401).json({
64+
error: {
65+
code: -32000,
66+
message: 'Session expired',
16667
},
167-
);
68+
id: null,
69+
jsonrpc: '2.0',
70+
});
71+
return;
72+
}
16873

169-
try {
170-
await server.connect(transport);
74+
transport = transports[sessionId].transport;
75+
transports[sessionId].lastAccess = Date.now();
76+
} else if (!sessionId && isInitializeRequest(req.body)) {
77+
let bearerToken: string | undefined;
78+
const authHeader: string | undefined = req.headers.authorization as string | undefined;
79+
if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
80+
bearerToken = authHeader.substring(7);
81+
// eslint-disable-next-line no-console
82+
console.log(`Bearer token captured for new session.`);
83+
} else {
84+
// eslint-disable-next-line no-console
85+
console.warn('MCP session initialized: No Bearer token found in Authorization header.');
86+
}
87+
transport = new StreamableHTTPServerTransport({
88+
onsessioninitialized: (newSessionId: string): void => {
89+
transports[newSessionId] = {
90+
lastAccess: Date.now(),
91+
transport,
92+
};
17193
// eslint-disable-next-line no-console
172-
console.log('Server connected to transport');
173-
} catch (error) {
94+
console.log(`Session initialized: ${newSessionId}`);
95+
},
96+
sessionIdGenerator: (): string => randomUUID(),
97+
});
98+
99+
transport.onclose = (): void => {
100+
if (transport.sessionId) {
174101
// eslint-disable-next-line no-console
175-
console.error(`Error connecting server to transport: ${error}`);
176-
res.status(500).json({
177-
error: {
178-
code: -32000,
179-
message: 'Internal server error: Failed to connect to MCP server',
180-
},
181-
id: null,
182-
jsonrpc: '2.0',
183-
});
184-
return;
185-
}
186-
} else {
187-
let message: string = 'Bad Request: No valid session ID provided for existing session.';
188-
if (!isInitializeRequest(req.body)) {
189-
message = 'Bad Request: Not an initialization request and no session ID found.';
102+
console.log(`Session closed: ${transport.sessionId}`);
103+
delete transports[transport.sessionId];
190104
}
191-
res.status(400).json({
105+
};
106+
107+
const server: McpServer = new McpServer({
108+
name: 'example-server',
109+
version: '1.0.0',
110+
});
111+
112+
server.tool(
113+
'get_pet_vaccination_info',
114+
'Retrieves the vaccination history and upcoming vaccination dates for a specific pet. Requires user authentication and explicit consent via an authorization token.',
115+
{
116+
petId: z.string().describe('The unique identifier for the pet.'),
117+
},
118+
async ({petId}: {petId: string}) => {
119+
try {
120+
return {
121+
content: [
122+
{
123+
text: `Retrieved vaccination info for pet ID: ${petId}. Token was ${
124+
bearerToken ? 'present' : 'absent'
125+
}.`,
126+
type: 'text',
127+
},
128+
],
129+
};
130+
} catch (error) {
131+
const errorMessage: string = error instanceof Error ? error.message : String(error);
132+
throw new Error(`Failed to retrieve vaccination information: ${errorMessage}`);
133+
}
134+
},
135+
);
136+
137+
server.tool(
138+
'book_vet_appointment',
139+
'Books a new veterinary appointment for a specific pet. Requires user authentication and explicit consent via an authorization token.',
140+
{
141+
date: z.string().describe('Desired date for the appointment (e.g., YYYY-MM-DD).'),
142+
petId: z.string().describe('The unique identifier for the pet.'),
143+
reason: z.string().describe('The reason for the vet visit.'),
144+
time: z.string().describe('Desired time for the appointment (e.g., HH:MM AM/PM).'),
145+
},
146+
async ({date, petId, reason, time}: {date: string; petId: string; reason: string; time: string}) => {
147+
try {
148+
return {
149+
content: [
150+
{
151+
text: `Booked vet appointment for pet ID: ${petId} on ${date} at ${time} for: ${reason}. Token was ${
152+
bearerToken ? 'present' : 'absent'
153+
}.`,
154+
type: 'text',
155+
},
156+
],
157+
};
158+
} catch (error) {
159+
const errorMessage: string = error instanceof Error ? error.message : String(error);
160+
throw new Error(`Failed to book appointment: ${errorMessage}`);
161+
}
162+
},
163+
);
164+
165+
try {
166+
await server.connect(transport);
167+
// eslint-disable-next-line no-console
168+
console.log('Server connected to transport');
169+
} catch (error) {
170+
// eslint-disable-next-line no-console
171+
console.error(`Error connecting server to transport: ${error}`);
172+
res.status(500).json({
192173
error: {
193174
code: -32000,
194-
message,
175+
message: 'Internal server error: Failed to connect to MCP server',
195176
},
196-
id: req.body?.id || null,
177+
id: null,
197178
jsonrpc: '2.0',
198179
});
199180
return;
200181
}
201-
202-
await transport.handleRequest(req, res, req.body);
203-
} catch (error) {
204-
const requestId: string | number | null | undefined =
205-
typeof req.body === 'object' && req.body !== null && 'id' in req.body ? req.body.id : null;
206-
res.status(500).json({
182+
} else {
183+
let message: string = 'Bad Request: No valid session ID provided for existing session.';
184+
if (!isInitializeRequest(req.body)) {
185+
message = 'Bad Request: Not an initialization request and no session ID found.';
186+
}
187+
res.status(400).json({
207188
error: {
208189
code: -32000,
209-
message: 'Internal server error',
190+
message,
210191
},
211-
id: requestId,
192+
id: req.body?.id || null,
212193
jsonrpc: '2.0',
213194
});
195+
return;
214196
}
215-
},
216-
);
197+
198+
await transport.handleRequest(req, res, req.body);
199+
} catch (error) {
200+
const requestId: string | number | null | undefined =
201+
typeof req.body === 'object' && req.body !== null && 'id' in req.body ? req.body.id : null;
202+
res.status(500).json({
203+
error: {
204+
code: -32000,
205+
message: 'Internal server error',
206+
},
207+
id: requestId,
208+
jsonrpc: '2.0',
209+
});
210+
}
211+
});
217212

218213
const handleSessionRequest = async (expressReq: Request, expressRes: Response): Promise<void> => {
219214
try {

0 commit comments

Comments
 (0)