Skip to content

Commit 3a683bd

Browse files
committed
feat: change middleware behaviour
1 parent c91c504 commit 3a683bd

File tree

5 files changed

+159
-168
lines changed

5 files changed

+159
-168
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ app.use(mcpAuthServer.router());
2020
app.use('/api', publicRoutes);
2121

2222
// Protected routes with MCP authentication
23-
app.use('/api/protected', mcpAuthServer.protect(protectedRoutes));
23+
app.use('/api/protected', mcpAuthServer.protect(), protectedRoutes);
2424

2525
// Error handling middleware
2626
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: 141 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ config();
2929

3030
const app: Express = express();
3131

32-
const mcpAuthServer = new McpAuthServer({
32+
const mcpAuthServer: McpAuthServer = new McpAuthServer({
3333
baseUrl: process.env.BASE_URL as string,
3434
});
3535

@@ -48,170 +48,167 @@ const SESSION_TIMEOUT_MS: number = 30 * 60 * 1000;
4848

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

51-
app.post(
52-
'/mcp',
53-
mcpAuthServer.protect(async (req: Request, res: Response): Promise<void> => {
54-
try {
55-
const sessionId: string | undefined = req.headers['mcp-session-id'] as string | undefined;
56-
let transport: StreamableHTTPServerTransport;
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;
5755

58-
if (sessionId && transports[sessionId]) {
59-
if (isSessionExpired(transports[sessionId].lastAccess)) {
60-
// eslint-disable-next-line no-console
61-
console.log(`Session expired: ${sessionId}`);
62-
transports[sessionId].transport.close();
63-
delete transports[sessionId];
64-
65-
res.status(401).json({
66-
error: {
67-
code: -32000,
68-
message: 'Session expired',
69-
},
70-
id: null,
71-
jsonrpc: '2.0',
72-
});
73-
return;
74-
}
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];
7562

76-
transport = transports[sessionId].transport;
77-
transports[sessionId].lastAccess = Date.now();
78-
} else if (!sessionId && isInitializeRequest(req.body)) {
79-
let bearerToken: string | undefined;
80-
const authHeader: string | undefined = req.headers.authorization as string | undefined;
81-
if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
82-
bearerToken = authHeader.substring(7);
83-
// eslint-disable-next-line no-console
84-
console.log(`Bearer token captured for new session.`);
85-
} else {
86-
// eslint-disable-next-line no-console
87-
console.warn('MCP session initialized: No Bearer token found in Authorization header.');
88-
}
89-
transport = new StreamableHTTPServerTransport({
90-
onsessioninitialized: (newSessionId: string): void => {
91-
transports[newSessionId] = {
92-
lastAccess: Date.now(),
93-
transport,
94-
};
95-
// eslint-disable-next-line no-console
96-
console.log(`Session initialized: ${newSessionId}`);
63+
res.status(401).json({
64+
error: {
65+
code: -32000,
66+
message: 'Session expired',
9767
},
98-
sessionIdGenerator: (): string => randomUUID(),
99-
});
100-
101-
transport.onclose = (): void => {
102-
if (transport.sessionId) {
103-
// eslint-disable-next-line no-console
104-
console.log(`Session closed: ${transport.sessionId}`);
105-
delete transports[transport.sessionId];
106-
}
107-
};
108-
109-
const server: McpServer = new McpServer({
110-
name: 'example-server',
111-
version: '1.0.0',
68+
id: null,
69+
jsonrpc: '2.0',
11270
});
71+
return;
72+
}
11373

114-
server.tool(
115-
'get_pet_vaccination_info',
116-
'Retrieves the vaccination history and upcoming vaccination dates for a specific pet. Requires user authentication and explicit consent via an authorization token.',
117-
{
118-
petId: z.string().describe('The unique identifier for the pet.'),
119-
},
120-
async ({petId}: {petId: string}) => {
121-
try {
122-
return {
123-
content: [
124-
{
125-
text: `Retrieved vaccination info for pet ID: ${petId}. Token was ${
126-
bearerToken ? 'present' : 'absent'
127-
}.`,
128-
type: 'text',
129-
},
130-
],
131-
};
132-
} catch (error) {
133-
const errorMessage: string = error instanceof Error ? error.message : String(error);
134-
throw new Error(`Failed to retrieve vaccination information: ${errorMessage}`);
135-
}
136-
},
137-
);
138-
139-
server.tool(
140-
'book_vet_appointment',
141-
'Books a new veterinary appointment for a specific pet. Requires user authentication and explicit consent via an authorization token.',
142-
{
143-
date: z.string().describe('Desired date for the appointment (e.g., YYYY-MM-DD).'),
144-
petId: z.string().describe('The unique identifier for the pet.'),
145-
reason: z.string().describe('The reason for the vet visit.'),
146-
time: z.string().describe('Desired time for the appointment (e.g., HH:MM AM/PM).'),
147-
},
148-
async ({date, petId, reason, time}: {date: string; petId: string; reason: string; time: string}) => {
149-
try {
150-
return {
151-
content: [
152-
{
153-
text: `Booked vet appointment for pet ID: ${petId} on ${date} at ${time} for: ${reason}. Token was ${
154-
bearerToken ? 'present' : 'absent'
155-
}.`,
156-
type: 'text',
157-
},
158-
],
159-
};
160-
} catch (error) {
161-
const errorMessage: string = error instanceof Error ? error.message : String(error);
162-
throw new Error(`Failed to book appointment: ${errorMessage}`);
163-
}
164-
},
165-
);
166-
167-
try {
168-
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+
};
16993
// eslint-disable-next-line no-console
170-
console.log('Server connected to transport');
171-
} catch (error) {
94+
console.log(`Session initialized: ${newSessionId}`);
95+
},
96+
sessionIdGenerator: (): string => randomUUID(),
97+
});
98+
99+
transport.onclose = (): void => {
100+
if (transport.sessionId) {
172101
// eslint-disable-next-line no-console
173-
console.error(`Error connecting server to transport: ${error}`);
174-
res.status(500).json({
175-
error: {
176-
code: -32000,
177-
message: 'Internal server error: Failed to connect to MCP server',
178-
},
179-
id: null,
180-
jsonrpc: '2.0',
181-
});
182-
return;
102+
console.log(`Session closed: ${transport.sessionId}`);
103+
delete transports[transport.sessionId];
183104
}
184-
} else {
185-
let message: string = 'Bad Request: No valid session ID provided for existing session.';
186-
if (!isInitializeRequest(req.body)) {
187-
message = 'Bad Request: Not an initialization request and no session ID found.';
188-
}
189-
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({
190173
error: {
191174
code: -32000,
192-
message,
175+
message: 'Internal server error: Failed to connect to MCP server',
193176
},
194-
id: req.body?.id || null,
177+
id: null,
195178
jsonrpc: '2.0',
196179
});
197180
return;
198181
}
199-
200-
await transport.handleRequest(req, res, req.body);
201-
} catch (error) {
202-
const requestId: string | number | null | undefined =
203-
typeof req.body === 'object' && req.body !== null && 'id' in req.body ? req.body.id : null;
204-
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({
205188
error: {
206189
code: -32000,
207-
message: 'Internal server error',
190+
message,
208191
},
209-
id: requestId,
192+
id: req.body?.id || null,
210193
jsonrpc: '2.0',
211194
});
195+
return;
212196
}
213-
}),
214-
);
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+
});
215212

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

packages/mcp-express/README.md

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,9 @@ app.use(express.json());
5151
app.use(mcpAuthServer.router());
5252

5353
// Protect your MCP endpoint
54-
app.post(
55-
'/mcp',
56-
mcpAuthServer.protect(async (req, res) => {
57-
// Your MCP handling logic here
58-
}),
59-
);
54+
app.post('/mcp', mcpAuthServer.protect(), async (req, res) => {
55+
// Your MCP handling logic here
56+
});
6057
```
6158

6259
### API Reference
@@ -79,17 +76,15 @@ Returns an Express router that sets up the necessary endpoints for MCP authentic
7976
app.use(mcpAuthServer.router());
8077
```
8178

82-
#### mcpAuthServer.protect(handler)
79+
#### mcpAuthServer.protect()
8380

84-
Returns middleware that protects routes requiring authentication, passing control to the provided handler function when authentication succeeds.
81+
Returns middleware that protects routes requiring authentication. This middleware should be applied before your route
82+
handler.
8583

8684
```typescript
87-
app.post(
88-
'/api/protected',
89-
mcpAuthServer.protect(async (req, res) => {
90-
// Your protected route logic here
91-
})
92-
);
85+
app.post('/api/protected', mcpAuthServer.protect(), async (req, res) => {
86+
// Your protected route logic here
87+
});
9388
```
9489

9590
### Configuration
@@ -145,7 +140,8 @@ const isSessionExpired = (lastAccessTime: number): boolean => Date.now() - lastA
145140
// MCP endpoint with authentication
146141
app.post(
147142
'/mcp',
148-
mcpAuthServer.protect(async (req: Request, res: Response): Promise<void> => {
143+
mcpAuthServer.protect(),
144+
async (req: Request, res: Response): Promise<void> => {
149145
try {
150146
const sessionId: string | undefined = req.headers['mcp-session-id'] as string | undefined;
151147
let transport: StreamableHTTPServerTransport;
@@ -251,4 +247,4 @@ pnpm lint
251247

252248
## License
253249

254-
Apache-2.0 - see the [LICENSE](LICENSE) file for details.
250+
Apache-2.0 - see the [LICENSE](LICENSE) file for details.

0 commit comments

Comments
 (0)