index.ts•14.9 kB
import OAuthProvider from "@cloudflare/workers-oauth-provider";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { McpAgent } from "agents/mcp";
import { Client } from "@microsoft/microsoft-graph-client";
import { z } from "zod";
import { EntraHandler } from "./entra-handler";
// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
type Props = {
userPrincipalName: string;
displayName: string;
mail: string;
id: string;
accessToken: string;
};
export class EntraIDTodoMCP extends McpAgent<Env, Record<string, never>, Props> {
// @ts-expect-error - Type mismatch due to duplicate @modelcontextprotocol/sdk versions in dependency tree
server = new McpServer({
name: "Microsoft Entra OAuth Todo MCP Server",
version: "1.0.0",
});
async init() {
// Simple test tool
this.server.tool(
"add",
"Add two numbers",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ text: String(a + b), type: "text" }],
}),
);
// Get current user profile
this.server.tool(
"getUserProfile",
"Get the authenticated user's profile from Microsoft Graph",
{},
async () => {
const client = Client.init({
authProvider: (done) => {
done(null, this.props!.accessToken);
},
});
try {
const user = await client.api("/me").get();
return {
content: [
{
text: JSON.stringify(user, null, 2),
type: "text",
},
],
};
} catch (error) {
return {
content: [
{
text: `Error fetching user profile: ${error instanceof Error ? error.message : String(error)}`,
type: "text",
},
],
isError: true,
};
}
},
);
// List all todo task lists
this.server.tool(
"listTodoLists",
"Get all todo task lists for the authenticated user",
{},
async () => {
const client = Client.init({
authProvider: (done) => {
done(null, this.props!.accessToken);
},
});
try {
const lists = await client.api("/me/todo/lists").get();
return {
content: [
{
text: JSON.stringify(lists, null, 2),
type: "text",
},
],
};
} catch (error) {
return {
content: [
{
text: `Error fetching todo lists: ${error instanceof Error ? error.message : String(error)}`,
type: "text",
},
],
isError: true,
};
}
},
);
// Create a new todo task list
this.server.tool(
"createTodoList",
"Create a new todo task list",
{
displayName: z.string().describe("The name of the task list"),
},
async ({ displayName }) => {
const client = Client.init({
authProvider: (done) => {
done(null, this.props!.accessToken);
},
});
try {
const newList = await client.api("/me/todo/lists").post({
displayName,
});
return {
content: [
{
text: JSON.stringify(newList, null, 2),
type: "text",
},
],
};
} catch (error) {
return {
content: [
{
text: `Error creating todo list: ${error instanceof Error ? error.message : String(error)}`,
type: "text",
},
],
isError: true,
};
}
},
);
// Get tasks from a specific todo list
this.server.tool(
"listTasks",
"Get all tasks from a specific todo list",
{
listId: z.string().describe("The ID of the todo list"),
},
async ({ listId }) => {
const client = Client.init({
authProvider: (done) => {
done(null, this.props!.accessToken);
},
});
try {
const tasks = await client.api(`/me/todo/lists/${listId}/tasks`).get();
// Get the user's timezone
const userTimeZone = this.env.DEFAULT_TIMEZONE || "Europe/London";
// Transform the tasks to include timezone-aware dates
const transformedTasks = {
...tasks,
value: tasks.value.map((task: any) => {
const transformedTask = { ...task };
// Convert due date to user's timezone if it exists
if (task.dueDateTime?.dateTime) {
const utcDate = new Date(task.dueDateTime.dateTime + 'Z');
transformedTask.dueDateTime = {
dateLocal: utcDate.toLocaleDateString('en-GB', {
timeZone: userTimeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit'
})
};
}
// Convert reminder date to user's timezone if it exists
if (task.reminderDateTime?.dateTime) {
const utcDate = new Date(task.reminderDateTime.dateTime + 'Z');
transformedTask.reminderDateTime = {
dateTimeLocal: utcDate.toLocaleString('en-GB', {
timeZone: userTimeZone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
})
};
}
return transformedTask;
})
};
return {
content: [
{
text: JSON.stringify(transformedTasks, null, 2),
type: "text",
},
],
};
} catch (error) {
return {
content: [
{
text: `Error fetching tasks: ${error instanceof Error ? error.message : String(error)}`,
type: "text",
},
],
isError: true,
};
}
},
);
// Create a new task in a todo list
this.server.tool(
"createTask",
"Create a new task in a specific todo list",
{
listId: z.string().describe("The ID of the todo list"),
title: z.string().describe("The title of the task"),
body: z.string().optional().describe("The task body/description"),
dueDateTime: z.string().optional().describe("Due date in ISO format (e.g., 2025-01-15T00:00:00 or 2025-01-15T16:00:00)"),
reminderDateTime: z.string().optional().describe("Reminder date in ISO format (e.g., 2025-01-15T09:00:00 or 2025-01-15T16:00:00)"),
isReminderOn: z.boolean().optional().describe("Whether reminder is enabled for this task"),
importance: z.enum(["low", "normal", "high"]).optional().default("normal").describe("Task importance level"),
categories: z.array(z.string()).optional().describe("Categories/tags for the task"),
timeZone: z.string().optional().nullable().describe("Time zone for dates. If not specified, set to null to use server default. Examples: 'Europe/London', 'America/New_York', 'Asia/Tokyo'"),
},
async ({ listId, title, body, dueDateTime, reminderDateTime, isReminderOn, importance, categories, timeZone }) => {
const effectiveTimeZone = timeZone || this.env.DEFAULT_TIMEZONE || "Europe/London";
const client = Client.init({
authProvider: (done) => {
done(null, this.props!.accessToken);
},
});
try {
const taskData: any = {
title,
importance,
};
if (body) {
taskData.body = {
content: body,
contentType: "text",
};
}
if (dueDateTime) {
taskData.dueDateTime = {
dateTime: dueDateTime,
timeZone: effectiveTimeZone,
};
}
if (reminderDateTime) {
taskData.reminderDateTime = {
dateTime: reminderDateTime,
timeZone: effectiveTimeZone,
};
}
if (isReminderOn !== undefined) {
taskData.isReminderOn = isReminderOn;
}
if (categories && categories.length > 0) {
taskData.categories = categories;
}
const newTask = await client.api(`/me/todo/lists/${listId}/tasks`).post(taskData);
return {
content: [
{
text: JSON.stringify(newTask, null, 2),
type: "text",
},
],
};
} catch (error) {
return {
content: [
{
text: `Error creating task: ${error instanceof Error ? error.message : String(error)}`,
type: "text",
},
],
isError: true,
};
}
},
);
// Update an existing task
this.server.tool(
"updateTask",
"Update an existing task in a todo list",
{
listId: z.string().describe("The ID of the todo list"),
taskId: z.string().describe("The ID of the task to update"),
title: z.string().optional().describe("New title for the task"),
body: z.string().optional().describe("New task body/description"),
status: z.enum(["notStarted", "inProgress", "completed", "waitingOnOthers", "deferred"]).optional().describe("Task status"),
importance: z.enum(["low", "normal", "high"]).optional().describe("Task importance level"),
dueDateTime: z.string().optional().describe("New due date in ISO format (e.g., 2025-01-15T00:00:00 or 2025-01-15T16:00:00)"),
reminderDateTime: z.string().optional().describe("New reminder date in ISO format (e.g., 2025-01-15T09:00:00 or 2025-01-15T16:00:00)"),
isReminderOn: z.boolean().optional().describe("Whether reminder is enabled for this task"),
categories: z.array(z.string()).optional().describe("Categories/tags for the task"),
timeZone: z.string().optional().nullable().describe("Time zone for dates. If not specified, set to null to use server default. Examples: 'Europe/London', 'America/New_York', 'Asia/Tokyo'"),
},
async ({ listId, taskId, title, body, status, importance, dueDateTime, reminderDateTime, isReminderOn, categories, timeZone }) => {
const effectiveTimeZone = timeZone || this.env.DEFAULT_TIMEZONE || "Europe/London";
const client = Client.init({
authProvider: (done) => {
done(null, this.props!.accessToken);
},
});
try {
const updates: any = {};
if (title) updates.title = title;
if (status) updates.status = status;
if (importance) updates.importance = importance;
if (body) {
updates.body = {
content: body,
contentType: "text",
};
}
if (dueDateTime) {
updates.dueDateTime = {
dateTime: dueDateTime,
timeZone: effectiveTimeZone,
};
}
if (reminderDateTime) {
updates.reminderDateTime = {
dateTime: reminderDateTime,
timeZone: effectiveTimeZone,
};
}
if (isReminderOn !== undefined) {
updates.isReminderOn = isReminderOn;
}
if (categories) {
updates.categories = categories;
}
const updatedTask = await client
.api(`/me/todo/lists/${listId}/tasks/${taskId}`)
.patch(updates);
return {
content: [
{
text: JSON.stringify(updatedTask, null, 2),
type: "text",
},
],
};
} catch (error) {
return {
content: [
{
text: `Error updating task: ${error instanceof Error ? error.message : String(error)}`,
type: "text",
},
],
isError: true,
};
}
},
);
// Delete a task
this.server.tool(
"deleteTask",
"Delete a task from a todo list",
{
listId: z.string().describe("The ID of the todo list"),
taskId: z.string().describe("The ID of the task to delete"),
},
async ({ listId, taskId }) => {
const client = Client.init({
authProvider: (done) => {
done(null, this.props!.accessToken);
},
});
try {
await client.api(`/me/todo/lists/${listId}/tasks/${taskId}`).delete();
return {
content: [
{
text: `Task ${taskId} successfully deleted from list ${listId}`,
type: "text",
},
],
};
} catch (error) {
return {
content: [
{
text: `Error deleting task: ${error instanceof Error ? error.message : String(error)}`,
type: "text",
},
],
isError: true,
};
}
},
);
// Get a specific task
this.server.tool(
"getTask",
"Get details of a specific task",
{
listId: z.string().describe("The ID of the todo list"),
taskId: z.string().describe("The ID of the task"),
},
async ({ listId, taskId }) => {
const client = Client.init({
authProvider: (done) => {
done(null, this.props!.accessToken);
},
});
try {
const task = await client.api(`/me/todo/lists/${listId}/tasks/${taskId}`).get();
return {
content: [
{
text: JSON.stringify(task, null, 2),
type: "text",
},
],
};
} catch (error) {
return {
content: [
{
text: `Error fetching task: ${error instanceof Error ? error.message : String(error)}`,
type: "text",
},
],
isError: true,
};
}
},
);
// Delete a todo list
this.server.tool(
"deleteTodoList",
"Delete a todo task list",
{
listId: z.string().describe("The ID of the todo list to delete"),
},
async ({ listId }) => {
const client = Client.init({
authProvider: (done) => {
done(null, this.props!.accessToken);
},
});
try {
await client.api(`/me/todo/lists/${listId}`).delete();
return {
content: [
{
text: `Todo list ${listId} successfully deleted`,
type: "text",
},
],
};
} catch (error) {
return {
content: [
{
text: `Error deleting todo list: ${error instanceof Error ? error.message : String(error)}`,
type: "text",
},
],
isError: true,
};
}
},
);
// Update a todo list
this.server.tool(
"updateTodoList",
"Update a todo task list",
{
listId: z.string().describe("The ID of the todo list"),
displayName: z.string().describe("New name for the task list"),
},
async ({ listId, displayName }) => {
const client = Client.init({
authProvider: (done) => {
done(null, this.props!.accessToken);
},
});
try {
const updatedList = await client
.api(`/me/todo/lists/${listId}`)
.patch({ displayName });
return {
content: [
{
text: JSON.stringify(updatedList, null, 2),
type: "text",
},
],
};
} catch (error) {
return {
content: [
{
text: `Error updating todo list: ${error instanceof Error ? error.message : String(error)}`,
type: "text",
},
],
isError: true,
};
}
},
);
}
}
export default new OAuthProvider({
// NOTE - during the summer 2025, the SSE protocol was deprecated and replaced by the Streamable-HTTP protocol
// https://developers.cloudflare.com/agents/model-context-protocol/transport/#mcp-server-with-authentication
apiHandlers: {
"/sse": EntraIDTodoMCP.serveSSE("/sse"), // deprecated SSE protocol - use /mcp instead
"/mcp": EntraIDTodoMCP.serve("/mcp"), // Streamable-HTTP protocol
},
authorizeEndpoint: "/authorize",
clientRegistrationEndpoint: "/register",
defaultHandler: EntraHandler as any,
tokenEndpoint: "/token",
});