Skip to main content
Glama
index.ts14.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", });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/anoopt/remote-mcp-entra-id-todo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server