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
5 changes: 5 additions & 0 deletions .changeset/ready-waves-lay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@asgardeo/mcp-express': minor
---

First stable release 🎉
5 changes: 5 additions & 0 deletions .changeset/spotty-sloths-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@asgardeo/mcp-node': minor
---

First stable release 🎉
21 changes: 7 additions & 14 deletions examples/express-mcp-server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express from 'express';
import {config} from 'dotenv';
import {AsgardeoMcpAuth, protectedRoute} from '@asgardeo/mcp-express';
import {McpAuthServer} from '@asgardeo/mcp-express';
import {protectedRoutes} from './routes/protected';
import {publicRoutes} from './routes/public';

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

app.use(express.json());
const mcpAuthServer = new McpAuthServer({
baseUrl: process.env.BASE_URL as string,
});

app.use(
AsgardeoMcpAuth({
baseUrl: process.env.BASE_URL as string,
}),
);
app.use(express.json());
app.use(mcpAuthServer.router());

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

// Protected routes with MCP authentication
app.use(
'/api/protected',
protectedRoute({
baseUrl: process.env.BASE_URL as string,
}),
protectedRoutes,
);
app.use('/api/protected', mcpAuthServer.protect(), protectedRoutes);

// Error handling middleware
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
Expand Down
301 changes: 148 additions & 153 deletions examples/express-mcp-vet-ai-assist-app/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*/

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

const app: Express = express();

const mcpAuthServer: McpAuthServer = new McpAuthServer({
baseUrl: process.env.BASE_URL as string,
});

app.use(express.json());
app.use(
AsgardeoMcpAuth({
baseUrl: process.env.BASE_URL as string,
}),
);
app.use(mcpAuthServer.router());

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

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

app.post(
'/mcp',
protectedRoute({
baseUrl: process.env.BASE_URL as string,
}),
async (req: Request, res: Response): Promise<void> => {
try {
const sessionId: string | undefined = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;

if (sessionId && transports[sessionId]) {
if (isSessionExpired(transports[sessionId].lastAccess)) {
// eslint-disable-next-line no-console
console.log(`Session expired: ${sessionId}`);
transports[sessionId].transport.close();
delete transports[sessionId];

res.status(401).json({
error: {
code: -32000,
message: 'Session expired',
},
id: null,
jsonrpc: '2.0',
});
return;
}

transport = transports[sessionId].transport;
transports[sessionId].lastAccess = Date.now();
} else if (!sessionId && isInitializeRequest(req.body)) {
let bearerToken: string | undefined;
const authHeader: string | undefined = req.headers.authorization as string | undefined;
if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
bearerToken = authHeader.substring(7);
// eslint-disable-next-line no-console
console.log(`Bearer token captured for new session.`);
} else {
// eslint-disable-next-line no-console
console.warn('MCP session initialized: No Bearer token found in Authorization header.');
}
transport = new StreamableHTTPServerTransport({
onsessioninitialized: (newSessionId: string): void => {
transports[newSessionId] = {
lastAccess: Date.now(),
transport,
};
// eslint-disable-next-line no-console
console.log(`Session initialized: ${newSessionId}`);
},
sessionIdGenerator: (): string => randomUUID(),
});

transport.onclose = (): void => {
if (transport.sessionId) {
// eslint-disable-next-line no-console
console.log(`Session closed: ${transport.sessionId}`);
delete transports[transport.sessionId];
}
};
app.post('/mcp', mcpAuthServer.protect(), async (req: Request, res: Response): Promise<void> => {
try {
const sessionId: string | undefined = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;

const server: McpServer = new McpServer({
name: 'example-server',
version: '1.0.0',
});
if (sessionId && transports[sessionId]) {
if (isSessionExpired(transports[sessionId].lastAccess)) {
// eslint-disable-next-line no-console
console.log(`Session expired: ${sessionId}`);
transports[sessionId].transport.close();
delete transports[sessionId];

server.tool(
'get_pet_vaccination_info',
'Retrieves the vaccination history and upcoming vaccination dates for a specific pet. Requires user authentication and explicit consent via an authorization token.',
{
petId: z.string().describe('The unique identifier for the pet.'),
},
async ({petId}: {petId: string}) => {
try {
return {
content: [
{
text: `Retrieved vaccination info for pet ID: ${petId}. Token was ${
bearerToken ? 'present' : 'absent'
}.`,
type: 'text',
},
],
};
} catch (error) {
const errorMessage: string = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to retrieve vaccination information: ${errorMessage}`);
}
},
);

server.tool(
'book_vet_appointment',
'Books a new veterinary appointment for a specific pet. Requires user authentication and explicit consent via an authorization token.',
{
date: z.string().describe('Desired date for the appointment (e.g., YYYY-MM-DD).'),
petId: z.string().describe('The unique identifier for the pet.'),
reason: z.string().describe('The reason for the vet visit.'),
time: z.string().describe('Desired time for the appointment (e.g., HH:MM AM/PM).'),
},
async ({date, petId, reason, time}: {date: string; petId: string; reason: string; time: string}) => {
try {
return {
content: [
{
text: `Booked vet appointment for pet ID: ${petId} on ${date} at ${time} for: ${reason}. Token was ${
bearerToken ? 'present' : 'absent'
}.`,
type: 'text',
},
],
};
} catch (error) {
const errorMessage: string = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to book appointment: ${errorMessage}`);
}
res.status(401).json({
error: {
code: -32000,
message: 'Session expired',
},
);
id: null,
jsonrpc: '2.0',
});
return;
}

try {
await server.connect(transport);
transport = transports[sessionId].transport;
transports[sessionId].lastAccess = Date.now();
} else if (!sessionId && isInitializeRequest(req.body)) {
let bearerToken: string | undefined;
const authHeader: string | undefined = req.headers.authorization as string | undefined;
if (authHeader && authHeader.toLowerCase().startsWith('bearer ')) {
bearerToken = authHeader.substring(7);
// eslint-disable-next-line no-console
console.log(`Bearer token captured for new session.`);
} else {
// eslint-disable-next-line no-console
console.warn('MCP session initialized: No Bearer token found in Authorization header.');
}
transport = new StreamableHTTPServerTransport({
onsessioninitialized: (newSessionId: string): void => {
transports[newSessionId] = {
lastAccess: Date.now(),
transport,
};
// eslint-disable-next-line no-console
console.log('Server connected to transport');
} catch (error) {
console.log(`Session initialized: ${newSessionId}`);
},
sessionIdGenerator: (): string => randomUUID(),
});

transport.onclose = (): void => {
if (transport.sessionId) {
// eslint-disable-next-line no-console
console.error(`Error connecting server to transport: ${error}`);
res.status(500).json({
error: {
code: -32000,
message: 'Internal server error: Failed to connect to MCP server',
},
id: null,
jsonrpc: '2.0',
});
return;
}
} else {
let message: string = 'Bad Request: No valid session ID provided for existing session.';
if (!isInitializeRequest(req.body)) {
message = 'Bad Request: Not an initialization request and no session ID found.';
console.log(`Session closed: ${transport.sessionId}`);
delete transports[transport.sessionId];
}
res.status(400).json({
};

const server: McpServer = new McpServer({
name: 'example-server',
version: '1.0.0',
});

server.tool(
'get_pet_vaccination_info',
'Retrieves the vaccination history and upcoming vaccination dates for a specific pet. Requires user authentication and explicit consent via an authorization token.',
{
petId: z.string().describe('The unique identifier for the pet.'),
},
async ({petId}: {petId: string}) => {
try {
return {
content: [
{
text: `Retrieved vaccination info for pet ID: ${petId}. Token was ${
bearerToken ? 'present' : 'absent'
}.`,
type: 'text',
},
],
};
} catch (error) {
const errorMessage: string = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to retrieve vaccination information: ${errorMessage}`);
}
},
);

server.tool(
'book_vet_appointment',
'Books a new veterinary appointment for a specific pet. Requires user authentication and explicit consent via an authorization token.',
{
date: z.string().describe('Desired date for the appointment (e.g., YYYY-MM-DD).'),
petId: z.string().describe('The unique identifier for the pet.'),
reason: z.string().describe('The reason for the vet visit.'),
time: z.string().describe('Desired time for the appointment (e.g., HH:MM AM/PM).'),
},
async ({date, petId, reason, time}: {date: string; petId: string; reason: string; time: string}) => {
try {
return {
content: [
{
text: `Booked vet appointment for pet ID: ${petId} on ${date} at ${time} for: ${reason}. Token was ${
bearerToken ? 'present' : 'absent'
}.`,
type: 'text',
},
],
};
} catch (error) {
const errorMessage: string = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to book appointment: ${errorMessage}`);
}
},
);

try {
await server.connect(transport);
// eslint-disable-next-line no-console
console.log('Server connected to transport');
} catch (error) {
// eslint-disable-next-line no-console
console.error(`Error connecting server to transport: ${error}`);
res.status(500).json({
error: {
code: -32000,
message,
message: 'Internal server error: Failed to connect to MCP server',
},
id: req.body?.id || null,
id: null,
jsonrpc: '2.0',
});
return;
}

await transport.handleRequest(req, res, req.body);
} catch (error) {
const requestId: string | number | null | undefined =
typeof req.body === 'object' && req.body !== null && 'id' in req.body ? req.body.id : null;
res.status(500).json({
} else {
let message: string = 'Bad Request: No valid session ID provided for existing session.';
if (!isInitializeRequest(req.body)) {
message = 'Bad Request: Not an initialization request and no session ID found.';
}
res.status(400).json({
error: {
code: -32000,
message: 'Internal server error',
message,
},
id: requestId,
id: req.body?.id || null,
jsonrpc: '2.0',
});
return;
}
},
);

await transport.handleRequest(req, res, req.body);
} catch (error) {
const requestId: string | number | null | undefined =
typeof req.body === 'object' && req.body !== null && 'id' in req.body ? req.body.id : null;
res.status(500).json({
error: {
code: -32000,
message: 'Internal server error',
},
id: requestId,
jsonrpc: '2.0',
});
}
});

const handleSessionRequest = async (expressReq: Request, expressRes: Response): Promise<void> => {
try {
Expand Down
Loading
Loading