microsoft-todo-mcp-server
Version:
Microsoft Todo MCP service for Claude and Cursor. Fork of @jhirono/todomcp
1,552 lines (1,535 loc) • 59.3 kB
JavaScript
#!/usr/bin/env node
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
// src/todo-index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import dotenv from "dotenv";
// src/token-manager.ts
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import { homedir } from "os";
var TokenManager = class {
tokenFilePath;
currentTokens = null;
constructor() {
const configDir = process.platform === "win32" ? join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "microsoft-todo-mcp") : join(homedir(), ".config", "microsoft-todo-mcp");
if (!existsSync(configDir)) {
__require("fs").mkdirSync(configDir, { recursive: true });
}
this.tokenFilePath = join(configDir, "tokens.json");
console.error(`Token file path: ${this.tokenFilePath}`);
}
// Try to get tokens from multiple sources
async getTokens() {
if (process.env.MS_TODO_ACCESS_TOKEN && process.env.MS_TODO_REFRESH_TOKEN) {
const envTokens = {
accessToken: process.env.MS_TODO_ACCESS_TOKEN,
refreshToken: process.env.MS_TODO_REFRESH_TOKEN,
expiresAt: Date.now() + 3600 * 1e3
// Assume 1 hour if not specified
};
if (Date.now() > envTokens.expiresAt) {
const refreshed = await this.refreshToken(envTokens.refreshToken);
if (refreshed) {
return refreshed;
}
}
return envTokens;
}
if (existsSync(this.tokenFilePath)) {
try {
const data = readFileSync(this.tokenFilePath, "utf8");
this.currentTokens = JSON.parse(data);
if (this.currentTokens) {
if (Date.now() > this.currentTokens.expiresAt) {
const refreshed = await this.refreshToken(this.currentTokens.refreshToken);
if (refreshed) {
return refreshed;
}
}
return this.currentTokens;
}
} catch (error) {
console.error("Error reading token file:", error);
}
}
const legacyPath = join(process.cwd(), "tokens.json");
if (existsSync(legacyPath)) {
try {
const data = readFileSync(legacyPath, "utf8");
const tokens = JSON.parse(data);
this.saveTokens(tokens);
return tokens;
} catch (error) {
console.error("Error reading legacy token file:", error);
}
}
return null;
}
async refreshToken(refreshToken2) {
var _a, _b, _c;
try {
const clientId = ((_a = this.currentTokens) == null ? void 0 : _a.clientId) || process.env.CLIENT_ID;
const clientSecret = ((_b = this.currentTokens) == null ? void 0 : _b.clientSecret) || process.env.CLIENT_SECRET;
const tenantId = ((_c = this.currentTokens) == null ? void 0 : _c.tenantId) || process.env.TENANT_ID || "organizations";
if (!clientId || !clientSecret) {
console.error("Missing client credentials for token refresh");
return null;
}
const tokenEndpoint = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const formData = new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
refresh_token: refreshToken2,
grant_type: "refresh_token",
scope: "offline_access Tasks.Read Tasks.ReadWrite Tasks.Read.Shared Tasks.ReadWrite.Shared User.Read"
});
const response = await fetch(tokenEndpoint, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: formData
});
if (!response.ok) {
const errorText = await response.text();
console.error(`Token refresh failed: ${errorText}`);
this.promptForReauth();
return null;
}
const data = await response.json();
const newTokens = {
accessToken: data.access_token,
refreshToken: data.refresh_token || refreshToken2,
expiresAt: Date.now() + data.expires_in * 1e3 - 5 * 60 * 1e3,
// 5 min buffer
clientId,
clientSecret,
tenantId
};
this.saveTokens(newTokens);
await this.updateClaudeConfig(newTokens);
return newTokens;
} catch (error) {
console.error("Error refreshing token:", error);
this.promptForReauth();
return null;
}
}
saveTokens(tokens) {
this.currentTokens = tokens;
writeFileSync(this.tokenFilePath, JSON.stringify(tokens, null, 2), "utf8");
}
// Update Claude config automatically
async updateClaudeConfig(tokens) {
try {
const claudeConfigPath = process.platform === "win32" ? join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json") : process.platform === "darwin" ? join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json") : join(homedir(), ".config", "Claude", "claude_desktop_config.json");
if (!existsSync(claudeConfigPath)) {
return;
}
const config = JSON.parse(readFileSync(claudeConfigPath, "utf8"));
if (config.mcpServers && config.mcpServers["microsoft-todo"]) {
config.mcpServers["microsoft-todo"].env = {
...config.mcpServers["microsoft-todo"].env,
MS_TODO_ACCESS_TOKEN: tokens.accessToken,
MS_TODO_REFRESH_TOKEN: tokens.refreshToken
};
writeFileSync(claudeConfigPath, JSON.stringify(config, null, 2), "utf8");
console.error("Updated Claude config with new tokens");
}
} catch (error) {
console.error("Could not update Claude config:", error);
}
}
promptForReauth() {
console.error(`
=================================================================
TOKEN REFRESH FAILED - REAUTHENTICATION REQUIRED
Your Microsoft To Do tokens have expired and could not be refreshed.
To fix this:
1. Open a new terminal
2. Navigate to the microsoft-todo-mcp-server directory
3. Run: pnpm run auth
4. Complete the authentication in your browser
5. Restart Claude Desktop to use the new tokens
Your tokens are stored in: ${this.tokenFilePath}
=================================================================
`);
}
// Store client credentials with tokens for future refreshes
async storeCredentials(clientId, clientSecret, tenantId) {
if (this.currentTokens) {
this.currentTokens.clientId = clientId;
this.currentTokens.clientSecret = clientSecret;
this.currentTokens.tenantId = tenantId;
this.saveTokens(this.currentTokens);
}
}
};
var tokenManager = new TokenManager();
// src/todo-index.ts
dotenv.config();
console.error("Current working directory:", process.cwd());
var MS_GRAPH_BASE = "https://graph.microsoft.com/v1.0";
var USER_AGENT = "microsoft-todo-mcp-server/1.0";
var server = new McpServer({
name: "mstodo",
version: "1.0.0"
});
async function makeGraphRequest(url, token, method = "GET", body) {
const headers = {
"User-Agent": USER_AGENT,
Accept: "application/json",
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
};
try {
const options = {
method,
headers
};
if (body && (method === "POST" || method === "PATCH")) {
options.body = JSON.stringify(body);
}
console.error(`Making request to: ${url}`);
console.error(
`Request options: ${JSON.stringify({
method,
headers: {
...headers,
Authorization: "Bearer [REDACTED]"
}
})}`
);
let response = await fetch(url, options);
if (response.status === 401) {
console.error("Got 401, attempting token refresh...");
const newToken = await getAccessToken();
if (newToken && newToken !== token) {
headers.Authorization = `Bearer ${newToken}`;
response = await fetch(url, { ...options, headers });
}
}
if (!response.ok) {
const errorText = await response.text();
console.error(`HTTP error! status: ${response.status}, body: ${errorText}`);
if (errorText.includes("MailboxNotEnabledForRESTAPI")) {
console.error(`
=================================================================
ERROR: MailboxNotEnabledForRESTAPI
The Microsoft To Do API is not available for personal Microsoft accounts
(outlook.com, hotmail.com, live.com, etc.) through the Graph API.
This is a limitation of the Microsoft Graph API, not an authentication issue.
Microsoft only allows To Do API access for Microsoft 365 business accounts.
You can still use Microsoft To Do through the web interface or mobile apps,
but API access is restricted for personal accounts.
=================================================================
`);
throw new Error(
"Microsoft To Do API is not available for personal Microsoft accounts. See console for details."
);
}
throw new Error(`HTTP error! status: ${response.status}, body: ${errorText}`);
}
const data = await response.json();
console.error(`Response received: ${JSON.stringify(data).substring(0, 200)}...`);
return data;
} catch (error) {
console.error("Error making Graph API request:", error);
return null;
}
}
async function getAccessToken() {
try {
console.error("getAccessToken called");
const tokens = await tokenManager.getTokens();
if (tokens) {
console.error(`Successfully retrieved valid token`);
return tokens.accessToken;
}
console.error("No valid tokens available");
return null;
} catch (error) {
console.error("Error getting access token:", error);
return null;
}
}
async function isPersonalMicrosoftAccount() {
var _a;
try {
const token = await getAccessToken();
if (!token) return false;
const url = `${MS_GRAPH_BASE}/me`;
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json"
}
});
if (!response.ok) {
console.error(`Error getting user info: ${response.status}`);
return false;
}
const userData = await response.json();
const email = userData.mail || userData.userPrincipalName || "";
const personalDomains = ["outlook.com", "hotmail.com", "live.com", "msn.com", "passport.com"];
const domain = (_a = email.split("@")[1]) == null ? void 0 : _a.toLowerCase();
if (domain && personalDomains.some((d) => domain.includes(d))) {
console.error(`
=================================================================
WARNING: Personal Microsoft Account Detected
Your Microsoft account (${email}) appears to be a personal account.
Microsoft To Do API access is typically not available for personal accounts
through the Microsoft Graph API, only for Microsoft 365 business accounts.
You may encounter the "MailboxNotEnabledForRESTAPI" error when trying to
access To Do lists or tasks. This is a limitation of the Microsoft Graph API,
not an issue with your authentication or this application.
You can still use Microsoft To Do through the web interface or mobile apps,
but API access is restricted for personal accounts.
=================================================================
`);
return true;
}
return false;
} catch (error) {
console.error("Error checking account type:", error);
return false;
}
}
server.tool(
"auth-status",
"Check if you're authenticated with Microsoft Graph API. Shows current token status and expiration time, and indicates if the token needs to be refreshed.",
{},
async () => {
const tokens = await tokenManager.getTokens();
if (!tokens) {
return {
content: [
{
type: "text",
text: "Not authenticated. Please run 'npx microsoft-todo-mcp-server setup' to authenticate with Microsoft."
}
]
};
}
const isExpired = Date.now() > tokens.expiresAt;
const expiryTime = new Date(tokens.expiresAt).toLocaleString();
const isPersonal = await isPersonalMicrosoftAccount();
let accountMessage = "";
if (isPersonal) {
accountMessage = "\n\n\u26A0\uFE0F WARNING: You are using a personal Microsoft account. Microsoft To Do API access is typically not available for personal accounts through the Microsoft Graph API. You may encounter 'MailboxNotEnabledForRESTAPI' errors. This is a Microsoft limitation, not an authentication issue.";
}
if (isExpired) {
return {
content: [
{
type: "text",
text: `Authentication expired at ${expiryTime}. Will attempt to refresh when you call any API.${accountMessage}`
}
]
};
} else {
return {
content: [
{
type: "text",
text: `Authenticated. Token expires at ${expiryTime}.${accountMessage}`
}
]
};
}
}
);
server.tool(
"get-task-lists",
"Get all Microsoft Todo task lists (the top-level containers that organize your tasks). Shows list names, IDs, and indicates default or shared lists.",
{},
async () => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const response = await makeGraphRequest(`${MS_GRAPH_BASE}/me/todo/lists`, token);
if (!response) {
return {
content: [
{
type: "text",
text: "Failed to retrieve task lists"
}
]
};
}
const lists = response.value || [];
if (lists.length === 0) {
return {
content: [
{
type: "text",
text: "No task lists found."
}
]
};
}
const formattedLists = lists.map((list) => {
let wellKnownInfo = "";
if (list.wellknownListName && list.wellknownListName !== "none") {
if (list.wellknownListName === "defaultList") {
wellKnownInfo = " (Default Tasks List)";
} else if (list.wellknownListName === "flaggedEmails") {
wellKnownInfo = " (Flagged Emails)";
}
}
let sharingInfo = "";
if (list.isShared) {
sharingInfo = list.isOwner ? " (Shared by you)" : " (Shared with you)";
}
return `ID: ${list.id}
Name: ${list.displayName}${wellKnownInfo}${sharingInfo}
---`;
});
return {
content: [
{
type: "text",
text: `Your task lists:
${formattedLists.join("\n")}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching task lists: ${error}`
}
]
};
}
}
);
server.tool(
"get-task-lists-organized",
"Get all task lists organized into logical folders/categories based on naming patterns, emoji prefixes, and sharing status. Provides a hierarchical view similar to folder organization.",
{
includeIds: z.boolean().optional().describe("Include list IDs in output (default: false)"),
groupBy: z.enum(["category", "shared", "type"]).optional().describe("Grouping strategy - 'category' (default), 'shared', or 'type'")
},
async ({ includeIds, groupBy }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const response = await makeGraphRequest(`${MS_GRAPH_BASE}/me/todo/lists`, token);
if (!response) {
return {
content: [
{
type: "text",
text: "Failed to retrieve task lists"
}
]
};
}
const lists = response.value || [];
if (lists.length === 0) {
return {
content: [
{
type: "text",
text: "No task lists found."
}
]
};
}
if (groupBy === "shared") {
const sharedLists = lists.filter((l) => l.isShared);
const personalLists = lists.filter((l) => !l.isShared);
let output2 = "\u{1F4C2} Microsoft To Do Lists - By Sharing Status\n";
output2 += "=".repeat(50) + "\n\n";
output2 += `\u{1F465} Shared Lists (${sharedLists.length})
`;
sharedLists.forEach((list) => {
const ownership = list.isOwner ? "Shared by you" : "Shared with you";
output2 += ` \u251C\u2500 ${list.displayName} [${ownership}]
`;
});
output2 += `
\u{1F512} Personal Lists (${personalLists.length})
`;
personalLists.forEach((list) => {
output2 += ` \u251C\u2500 ${list.displayName}
`;
});
return { content: [{ type: "text", text: output2 }] };
}
const organizeLists = (lists2) => {
const organized2 = {};
const patterns = {
archived: /\(([^)]+)\s*-\s*Archived\)$/i,
archive: /^📦\s*Archive/i,
shopping: /^🛒/,
property: /^🏡/,
family: /^👪/,
seasonal: /^(🎄|🎉)/,
work: /^(Work|SBIR)/i,
travel: /^(🚗|Rangeley)/i,
reading: /^📰/
};
lists2.forEach((list) => {
let placed = false;
const archiveMatch = list.displayName.match(patterns.archived);
if (archiveMatch) {
const category = `\u{1F4E6} Archived - ${archiveMatch[1]}`;
if (!organized2[category]) organized2[category] = [];
organized2[category].push(list);
placed = true;
} else if (patterns.archive.test(list.displayName)) {
if (!organized2["\u{1F4E6} Archives"]) organized2["\u{1F4E6} Archives"] = [];
organized2["\u{1F4E6} Archives"].push(list);
placed = true;
} else if (patterns.shopping.test(list.displayName)) {
if (!organized2["\u{1F6D2} Shopping Lists"]) organized2["\u{1F6D2} Shopping Lists"] = [];
organized2["\u{1F6D2} Shopping Lists"].push(list);
placed = true;
} else if (patterns.property.test(list.displayName)) {
if (!organized2["\u{1F3E1} Properties"]) organized2["\u{1F3E1} Properties"] = [];
organized2["\u{1F3E1} Properties"].push(list);
placed = true;
} else if (patterns.family.test(list.displayName)) {
if (!organized2["\u{1F46A} Family"]) organized2["\u{1F46A} Family"] = [];
organized2["\u{1F46A} Family"].push(list);
placed = true;
} else if (patterns.seasonal.test(list.displayName)) {
if (!organized2["\u{1F389} Seasonal & Events"]) organized2["\u{1F389} Seasonal & Events"] = [];
organized2["\u{1F389} Seasonal & Events"].push(list);
placed = true;
} else if (patterns.work.test(list.displayName)) {
if (!organized2["\u{1F4BC} Work"]) organized2["\u{1F4BC} Work"] = [];
organized2["\u{1F4BC} Work"].push(list);
placed = true;
} else if (patterns.travel.test(list.displayName)) {
if (!organized2["\u{1F697} Travel & Rangeley"]) organized2["\u{1F697} Travel & Rangeley"] = [];
organized2["\u{1F697} Travel & Rangeley"].push(list);
placed = true;
} else if (patterns.reading.test(list.displayName)) {
if (!organized2["\u{1F4DA} Reading"]) organized2["\u{1F4DA} Reading"] = [];
organized2["\u{1F4DA} Reading"].push(list);
placed = true;
} else if (list.wellknownListName && list.wellknownListName !== "none") {
if (!organized2["\u2B50 Special Lists"]) organized2["\u2B50 Special Lists"] = [];
organized2["\u2B50 Special Lists"].push(list);
placed = true;
} else if (list.isShared && !placed) {
if (!organized2["\u{1F465} Shared Lists"]) organized2["\u{1F465} Shared Lists"] = [];
organized2["\u{1F465} Shared Lists"].push(list);
placed = true;
} else {
if (!organized2["\u{1F4CB} Other Lists"]) organized2["\u{1F4CB} Other Lists"] = [];
organized2["\u{1F4CB} Other Lists"].push(list);
}
});
return organized2;
};
const organized = organizeLists(lists);
let output = "\u{1F4C2} Microsoft To Do Lists - Organized View\n";
output += "=".repeat(50) + "\n\n";
const sortedCategories = Object.keys(organized).sort((a, b) => {
const priority = {
"\u2B50 Special Lists": 1,
"\u{1F465} Shared Lists": 2,
"\u{1F4BC} Work": 3,
"\u{1F46A} Family": 4,
"\u{1F3E1} Properties": 5,
"\u{1F6D2} Shopping Lists": 6,
"\u{1F697} Travel & Rangeley": 7,
"\u{1F389} Seasonal & Events": 8,
"\u{1F4DA} Reading": 9,
"\u{1F4CB} Other Lists": 10,
"\u{1F4E6} Archives": 11
};
const aIsArchived = a.startsWith("\u{1F4E6} Archived -");
const bIsArchived = b.startsWith("\u{1F4E6} Archived -");
if (aIsArchived && !bIsArchived) return 1;
if (!aIsArchived && bIsArchived) return -1;
if (aIsArchived && bIsArchived) return a.localeCompare(b);
const aPriority = priority[a] || 999;
const bPriority = priority[b] || 999;
if (aPriority !== bPriority) return aPriority - bPriority;
return a.localeCompare(b);
});
sortedCategories.forEach((category) => {
const categoryLists = organized[category];
output += `${category} (${categoryLists.length})
`;
categoryLists.forEach((list, index) => {
const isLast = index === categoryLists.length - 1;
const prefix = isLast ? "\u2514\u2500" : "\u251C\u2500";
let listInfo = `${prefix} ${list.displayName}`;
const metadata = [];
if (list.wellknownListName === "defaultList") metadata.push("Default");
if (list.wellknownListName === "flaggedEmails") metadata.push("Flagged Emails");
if (list.isShared && list.isOwner) metadata.push("Shared by you");
if (list.isShared && !list.isOwner) metadata.push("Shared with you");
if (metadata.length > 0) {
listInfo += ` [${metadata.join(", ")}]`;
}
output += ` ${listInfo}
`;
if (!isLast) {
output += " \u2502\n";
}
});
output += "\n";
});
const totalLists = Object.values(organized).reduce((sum, l) => sum + l.length, 0);
const totalCategories = Object.keys(organized).length;
output += "-".repeat(50) + "\n";
output += `Summary: ${totalLists} lists in ${totalCategories} categories
`;
if (includeIds) {
output += "\n\n\u{1F4CB} List IDs Reference:\n" + "-".repeat(50) + "\n";
lists.forEach((list) => {
output += `${list.displayName}: ${list.id}
`;
});
}
return { content: [{ type: "text", text: output }] };
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching organized task lists: ${error}`
}
]
};
}
}
);
server.tool(
"create-task-list",
"Create a new task list (top-level container) in Microsoft Todo to help organize your tasks into categories or projects.",
{
displayName: z.string().describe("Name of the new task list")
},
async ({ displayName }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const requestBody = {
displayName
};
const response = await makeGraphRequest(`${MS_GRAPH_BASE}/me/todo/lists`, token, "POST", requestBody);
if (!response) {
return {
content: [
{
type: "text",
text: `Failed to create task list: ${displayName}`
}
]
};
}
return {
content: [
{
type: "text",
text: `Task list created successfully!
Name: ${response.displayName}
ID: ${response.id}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating task list: ${error}`
}
]
};
}
}
);
server.tool(
"update-task-list",
"Update the name of an existing task list (top-level container) in Microsoft Todo.",
{
listId: z.string().describe("ID of the task list to update"),
displayName: z.string().describe("New name for the task list")
},
async ({ listId, displayName }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const requestBody = {
displayName
};
const response = await makeGraphRequest(
`${MS_GRAPH_BASE}/me/todo/lists/${listId}`,
token,
"PATCH",
requestBody
);
if (!response) {
return {
content: [
{
type: "text",
text: `Failed to update task list with ID: ${listId}`
}
]
};
}
return {
content: [
{
type: "text",
text: `Task list updated successfully!
New name: ${response.displayName}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error updating task list: ${error}`
}
]
};
}
}
);
server.tool(
"delete-task-list",
"Delete a task list (top-level container) from Microsoft Todo. This will remove the list and all tasks within it.",
{
listId: z.string().describe("ID of the task list to delete")
},
async ({ listId }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const url = `${MS_GRAPH_BASE}/me/todo/lists/${listId}`;
console.error(`Deleting task list: ${url}`);
await makeGraphRequest(url, token, "DELETE");
return {
content: [
{
type: "text",
text: `Task list with ID: ${listId} was successfully deleted.`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error deleting task list: ${error}`
}
]
};
}
}
);
server.tool(
"get-tasks",
"Get tasks from a specific Microsoft Todo list. These are the main todo items that can contain checklist items (subtasks).",
{
listId: z.string().describe("ID of the task list"),
filter: z.string().optional().describe("OData $filter query (e.g., 'status eq \\'completed\\'')"),
select: z.string().optional().describe("Comma-separated list of properties to include (e.g., 'id,title,status')"),
orderby: z.string().optional().describe("Property to sort by (e.g., 'createdDateTime desc')"),
top: z.number().optional().describe("Maximum number of tasks to retrieve"),
skip: z.number().optional().describe("Number of tasks to skip"),
count: z.boolean().optional().describe("Whether to include a count of tasks")
},
async ({ listId, filter, select, orderby, top, skip, count }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const queryParams = new URLSearchParams();
if (filter) queryParams.append("$filter", filter);
if (select) queryParams.append("$select", select);
if (orderby) queryParams.append("$orderby", orderby);
if (top !== void 0) queryParams.append("$top", top.toString());
if (skip !== void 0) queryParams.append("$skip", skip.toString());
if (count !== void 0) queryParams.append("$count", count.toString());
const queryString = queryParams.toString();
const url = `${MS_GRAPH_BASE}/me/todo/lists/${listId}/tasks${queryString ? "?" + queryString : ""}`;
console.error(`Making request to: ${url}`);
const response = await makeGraphRequest(url, token);
if (!response) {
return {
content: [
{
type: "text",
text: `Failed to retrieve tasks for list: ${listId}`
}
]
};
}
const tasks = response.value || [];
if (tasks.length === 0) {
return {
content: [
{
type: "text",
text: `No tasks found in list with ID: ${listId}`
}
]
};
}
const formattedTasks = tasks.map((task) => {
let taskInfo = `ID: ${task.id}
Title: ${task.title}`;
if (task.status) {
const status = task.status === "completed" ? "\u2713" : "\u25CB";
taskInfo = `${status} ${taskInfo}`;
}
if (task.dueDateTime) {
taskInfo += `
Due: ${new Date(task.dueDateTime.dateTime).toLocaleDateString()}`;
}
if (task.importance) {
taskInfo += `
Importance: ${task.importance}`;
}
if (task.categories && task.categories.length > 0) {
taskInfo += `
Categories: ${task.categories.join(", ")}`;
}
if (task.body && task.body.content && task.body.content.trim() !== "") {
const previewLength = 50;
const contentPreview = task.body.content.length > previewLength ? task.body.content.substring(0, previewLength) + "..." : task.body.content;
taskInfo += `
Description: ${contentPreview}`;
}
return `${taskInfo}
---`;
});
let countInfo = "";
if (count && response["@odata.count"] !== void 0) {
countInfo = `Total count: ${response["@odata.count"]}
`;
}
return {
content: [
{
type: "text",
text: `Tasks in list ${listId}:
${countInfo}${formattedTasks.join("\n")}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching tasks: ${error}`
}
]
};
}
}
);
server.tool(
"create-task",
"Create a new task in a specific Microsoft Todo list. A task is the main todo item that can have a title, description, due date, and other properties.",
{
listId: z.string().describe("ID of the task list"),
title: z.string().describe("Title of the task"),
body: z.string().optional().describe("Description or body content of the task"),
dueDateTime: z.string().optional().describe("Due date in ISO format (e.g., 2023-12-31T23:59:59Z)"),
startDateTime: z.string().optional().describe("Start date in ISO format (e.g., 2023-12-31T23:59:59Z)"),
importance: z.enum(["low", "normal", "high"]).optional().describe("Task importance"),
isReminderOn: z.boolean().optional().describe("Whether to enable reminder for this task"),
reminderDateTime: z.string().optional().describe("Reminder date and time in ISO format"),
status: z.enum(["notStarted", "inProgress", "completed", "waitingOnOthers", "deferred"]).optional().describe("Status of the task"),
categories: z.array(z.string()).optional().describe("Categories associated with the task")
},
async ({
listId,
title,
body,
dueDateTime,
startDateTime,
importance,
isReminderOn,
reminderDateTime,
status,
categories
}) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const taskBody = { title };
if (body) {
taskBody.body = {
content: body,
contentType: "text"
};
}
if (dueDateTime) {
taskBody.dueDateTime = {
dateTime: dueDateTime,
timeZone: "UTC"
};
}
if (startDateTime) {
taskBody.startDateTime = {
dateTime: startDateTime,
timeZone: "UTC"
};
}
if (importance) {
taskBody.importance = importance;
}
if (isReminderOn !== void 0) {
taskBody.isReminderOn = isReminderOn;
}
if (reminderDateTime) {
taskBody.reminderDateTime = {
dateTime: reminderDateTime,
timeZone: "UTC"
};
}
if (status) {
taskBody.status = status;
}
if (categories && categories.length > 0) {
taskBody.categories = categories;
}
const response = await makeGraphRequest(
`${MS_GRAPH_BASE}/me/todo/lists/${listId}/tasks`,
token,
"POST",
taskBody
);
if (!response) {
return {
content: [
{
type: "text",
text: `Failed to create task in list: ${listId}`
}
]
};
}
return {
content: [
{
type: "text",
text: `Task created successfully!
ID: ${response.id}
Title: ${response.title}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating task: ${error}`
}
]
};
}
}
);
server.tool(
"update-task",
"Update an existing task in Microsoft Todo. Allows changing any properties of the task including title, due date, importance, etc.",
{
listId: z.string().describe("ID of the task list"),
taskId: z.string().describe("ID of the task to update"),
title: z.string().optional().describe("New title of the task"),
body: z.string().optional().describe("New description or body content of the task"),
dueDateTime: z.string().optional().describe("New due date in ISO format (e.g., 2023-12-31T23:59:59Z)"),
startDateTime: z.string().optional().describe("New start date in ISO format (e.g., 2023-12-31T23:59:59Z)"),
importance: z.enum(["low", "normal", "high"]).optional().describe("New task importance"),
isReminderOn: z.boolean().optional().describe("Whether to enable reminder for this task"),
reminderDateTime: z.string().optional().describe("New reminder date and time in ISO format"),
status: z.enum(["notStarted", "inProgress", "completed", "waitingOnOthers", "deferred"]).optional().describe("New status of the task"),
categories: z.array(z.string()).optional().describe("New categories associated with the task")
},
async ({
listId,
taskId,
title,
body,
dueDateTime,
startDateTime,
importance,
isReminderOn,
reminderDateTime,
status,
categories
}) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const taskBody = {};
if (title !== void 0) {
taskBody.title = title;
}
if (body !== void 0) {
taskBody.body = {
content: body,
contentType: "text"
};
}
if (dueDateTime !== void 0) {
if (dueDateTime === "") {
taskBody.dueDateTime = null;
} else {
taskBody.dueDateTime = {
dateTime: dueDateTime,
timeZone: "UTC"
};
}
}
if (startDateTime !== void 0) {
if (startDateTime === "") {
taskBody.startDateTime = null;
} else {
taskBody.startDateTime = {
dateTime: startDateTime,
timeZone: "UTC"
};
}
}
if (importance !== void 0) {
taskBody.importance = importance;
}
if (isReminderOn !== void 0) {
taskBody.isReminderOn = isReminderOn;
}
if (reminderDateTime !== void 0) {
if (reminderDateTime === "") {
taskBody.reminderDateTime = null;
} else {
taskBody.reminderDateTime = {
dateTime: reminderDateTime,
timeZone: "UTC"
};
}
}
if (status !== void 0) {
taskBody.status = status;
}
if (categories !== void 0) {
taskBody.categories = categories;
}
if (Object.keys(taskBody).length === 0) {
return {
content: [
{
type: "text",
text: "No properties provided for update. Please specify at least one property to change."
}
]
};
}
const response = await makeGraphRequest(
`${MS_GRAPH_BASE}/me/todo/lists/${listId}/tasks/${taskId}`,
token,
"PATCH",
taskBody
);
if (!response) {
return {
content: [
{
type: "text",
text: `Failed to update task with ID: ${taskId} in list: ${listId}`
}
]
};
}
return {
content: [
{
type: "text",
text: `Task updated successfully!
ID: ${response.id}
Title: ${response.title}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error updating task: ${error}`
}
]
};
}
}
);
server.tool(
"delete-task",
"Delete a task from a Microsoft Todo list. This will remove the task and all its checklist items (subtasks).",
{
listId: z.string().describe("ID of the task list"),
taskId: z.string().describe("ID of the task to delete")
},
async ({ listId, taskId }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const url = `${MS_GRAPH_BASE}/me/todo/lists/${listId}/tasks/${taskId}`;
console.error(`Deleting task: ${url}`);
await makeGraphRequest(url, token, "DELETE");
return {
content: [
{
type: "text",
text: `Task with ID: ${taskId} was successfully deleted from list: ${listId}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error deleting task: ${error}`
}
]
};
}
}
);
server.tool(
"get-checklist-items",
"Get checklist items (subtasks) for a specific task. Checklist items are smaller steps or components that belong to a parent task.",
{
listId: z.string().describe("ID of the task list"),
taskId: z.string().describe("ID of the task")
},
async ({ listId, taskId }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const taskResponse = await makeGraphRequest(
`${MS_GRAPH_BASE}/me/todo/lists/${listId}/tasks/${taskId}`,
token
);
const taskTitle = taskResponse ? taskResponse.title : "Unknown Task";
const response = await makeGraphRequest(
`${MS_GRAPH_BASE}/me/todo/lists/${listId}/tasks/${taskId}/checklistItems`,
token
);
if (!response) {
return {
content: [
{
type: "text",
text: `Failed to retrieve checklist items for task: ${taskId}`
}
]
};
}
const items = response.value || [];
if (items.length === 0) {
return {
content: [
{
type: "text",
text: `No checklist items found for task "${taskTitle}" (ID: ${taskId})`
}
]
};
}
const formattedItems = items.map((item) => {
const status = item.isChecked ? "\u2713" : "\u25CB";
let itemInfo = `${status} ${item.displayName} (ID: ${item.id})`;
if (item.createdDateTime) {
const createdDate = new Date(item.createdDateTime).toLocaleString();
itemInfo += `
Created: ${createdDate}`;
}
return itemInfo;
});
return {
content: [
{
type: "text",
text: `Checklist items for task "${taskTitle}" (ID: ${taskId}):
${formattedItems.join("\n\n")}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching checklist items: ${error}`
}
]
};
}
}
);
server.tool(
"create-checklist-item",
"Create a new checklist item (subtask) for a task. Checklist items help break down a task into smaller, manageable steps.",
{
listId: z.string().describe("ID of the task list"),
taskId: z.string().describe("ID of the task"),
displayName: z.string().describe("Text content of the checklist item"),
isChecked: z.boolean().optional().describe("Whether the item is checked off")
},
async ({ listId, taskId, displayName, isChecked }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const requestBody = {
displayName
};
if (isChecked !== void 0) {
requestBody.isChecked = isChecked;
}
const response = await makeGraphRequest(
`${MS_GRAPH_BASE}/me/todo/lists/${listId}/tasks/${taskId}/checklistItems`,
token,
"POST",
requestBody
);
if (!response) {
return {
content: [
{
type: "text",
text: `Failed to create checklist item for task: ${taskId}`
}
]
};
}
return {
content: [
{
type: "text",
text: `Checklist item created successfully!
Content: ${response.displayName}
ID: ${response.id}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error creating checklist item: ${error}`
}
]
};
}
}
);
server.tool(
"update-checklist-item",
"Update an existing checklist item (subtask). Allows changing the text content or completion status of the subtask.",
{
listId: z.string().describe("ID of the task list"),
taskId: z.string().describe("ID of the task"),
checklistItemId: z.string().describe("ID of the checklist item to update"),
displayName: z.string().optional().describe("New text content of the checklist item"),
isChecked: z.boolean().optional().describe("Whether the item is checked off")
},
async ({ listId, taskId, checklistItemId, displayName, isChecked }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const requestBody = {};
if (displayName !== void 0) {
requestBody.displayName = displayName;
}
if (isChecked !== void 0) {
requestBody.isChecked = isChecked;
}
if (Object.keys(requestBody).length === 0) {
return {
content: [
{
type: "text",
text: "No properties provided for update. Please specify either displayName or isChecked."
}
]
};
}
const response = await makeGraphRequest(
`${MS_GRAPH_BASE}/me/todo/lists/${listId}/tasks/${taskId}/checklistItems/${checklistItemId}`,
token,
"PATCH",
requestBody
);
if (!response) {
return {
content: [
{
type: "text",
text: `Failed to update checklist item with ID: ${checklistItemId}`
}
]
};
}
const statusText = response.isChecked ? "Checked" : "Not checked";
return {
content: [
{
type: "text",
text: `Checklist item updated successfully!
Content: ${response.displayName}
Status: ${statusText}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error updating checklist item: ${error}`
}
]
};
}
}
);
server.tool(
"delete-checklist-item",
"Delete a checklist item (subtask) from a task. This removes just the specific subtask, not the parent task.",
{
listId: z.string().describe("ID of the task list"),
taskId: z.string().describe("ID of the task"),
checklistItemId: z.string().describe("ID of the checklist item to delete")
},
async ({ listId, taskId, checklistItemId }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const url = `${MS_GRAPH_BASE}/me/todo/lists/${listId}/tasks/${taskId}/checklistItems/${checklistItemId}`;
console.error(`Deleting checklist item: ${url}`);
await makeGraphRequest(url, token, "DELETE");
return {
content: [
{
type: "text",
text: `Checklist item with ID: ${checklistItemId} was successfully deleted from task: ${taskId}`
}
]
};
} catch (error) {
return {
content: [
{
type: "text",
text: `Error deleting checklist item: ${error}`
}
]
};
}
}
);
server.tool(
"archive-completed-tasks",
"Move completed tasks older than a specified number of days from one list to another (archive) list. Useful for cleaning up active lists while preserving historical tasks.",
{
sourceListId: z.string().describe("ID of the source list to archive tasks from"),
targetListId: z.string().describe("ID of the target archive list"),
olderThanDays: z.number().min(0).default(90).describe("Archive tasks completed more than this many days ago (default: 90)"),
dryRun: z.boolean().optional().default(false).describe("If true, only preview what would be archived without making changes")
},
async ({ sourceListId, targetListId, olderThanDays, dryRun }) => {
try {
const token = await getAccessToken();
if (!token) {
return {
content: [
{
type: "text",
text: "Failed to authenticate with Microsoft API"
}
]
};
}
const cutoffDate = /* @__PURE__ */ new Date();
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
const tasksResponse = await makeGraphRequest(
`${MS_GRAPH_BASE}/me/todo/lists/${sourceListId}/tasks?$filter=status eq 'completed'`,
token
);
if (!tasksResponse || !tasksResponse.value) {
return {
content: [
{
type: "text",
text: "Failed to retrieve tasks from source list"
}
]
};
}
const tasksToArchive = tasksResponse.value.filter((task) => {
var _a;
if (!((_a = task.completedDateTime) == null ? void 0 : _a.dateTime)) return false;
const completedDate = new Date(task.completedDateTime.dateTime);
return completedDate < cutoffDate;
});
if (tasksToArchive.length === 0) {
return {
content: [
{
type: "text",
text: `No completed tasks found older than ${olderThanDays} days.`
}
]
};
}
if (dryRun) {
let preview = `\u{1F4CB} Archive Preview
`;
preview += `Would archive ${tasksToArchive.length} tasks completed before ${cutoffDate.toLocaleDateString()}
`;
tasksToArchive.forEach((task) => {
var _a;
const completedDate = ((_a = task.completedDateTime) == null ? void 0 : _a.dateTime) ? new Date(task.completedDateTime.dateTime).toLocaleDateString() : "Unknown";
preview += `- ${task.title} (completed: ${completedDate})
`;
});
return { content: [{ type: "text"