Google Tasks MCP Server
by zcaceres
- src
import {
CallToolRequest,
CallToolResult,
ListResourcesRequest,
ReadResourceRequest,
} from "@modelcontextprotocol/sdk/types.js";
import { GaxiosResponse } from "gaxios";
import { tasks_v1 } from "googleapis";
const MAX_TASK_RESULTS = 100;
export class TaskResources {
static async read(request: ReadResourceRequest, tasks: tasks_v1.Tasks) {
const taskId = request.params.uri.replace("gtasks:///", "");
const taskListsResponse: GaxiosResponse<tasks_v1.Schema$TaskLists> =
await tasks.tasklists.list({
maxResults: MAX_TASK_RESULTS,
});
const taskLists = taskListsResponse.data.items || [];
let task: tasks_v1.Schema$Task | null = null;
for (const taskList of taskLists) {
if (taskList.id) {
try {
const taskResponse: GaxiosResponse<tasks_v1.Schema$Task> =
await tasks.tasks.get({
tasklist: taskList.id,
task: taskId,
});
task = taskResponse.data;
break;
} catch (error) {
// Task not found in this list, continue to the next one
}
}
}
if (!task) {
throw new Error("Task not found");
}
return task;
}
static async list(
request: ListResourcesRequest,
tasks: tasks_v1.Tasks,
): Promise<[tasks_v1.Schema$Task[], string | null]> {
const pageSize = 10;
const params: any = {
maxResults: pageSize,
};
if (request.params?.cursor) {
params.pageToken = request.params.cursor;
}
const taskListsResponse = await tasks.tasklists.list({
maxResults: MAX_TASK_RESULTS,
});
const taskLists = taskListsResponse.data.items || [];
let allTasks: tasks_v1.Schema$Task[] = [];
let nextPageToken = null;
for (const taskList of taskLists) {
const tasksResponse = await tasks.tasks.list({
tasklist: taskList.id,
...params,
});
const taskItems = tasksResponse.data.items || [];
allTasks = allTasks.concat(taskItems);
if (tasksResponse.data.nextPageToken) {
nextPageToken = tasksResponse.data.nextPageToken;
}
}
return [allTasks, nextPageToken];
}
}
export class TaskActions {
private static formatTask(task: tasks_v1.Schema$Task) {
return `${task.title}\n (Due: ${task.due || "Not set"}) - Notes: ${task.notes} - ID: ${task.id} - Status: ${task.status} - URI: ${task.selfLink} - Hidden: ${task.hidden} - Parent: ${task.parent} - Deleted?: ${task.deleted} - Completed Date: ${task.completed} - Position: ${task.position} - Updated Date: ${task.updated} - ETag: ${task.etag} - Links: ${task.links} - Kind: ${task.kind}}`;
}
private static formatTaskList(taskList: tasks_v1.Schema$Task[]) {
return taskList.map((task) => this.formatTask(task)).join("\n");
}
private static async _list(request: CallToolRequest, tasks: tasks_v1.Tasks) {
const taskListsResponse = await tasks.tasklists.list({
maxResults: MAX_TASK_RESULTS,
});
const taskLists = taskListsResponse.data.items || [];
let allTasks: tasks_v1.Schema$Task[] = [];
for (const taskList of taskLists) {
if (taskList.id) {
try {
const tasksResponse = await tasks.tasks.list({
tasklist: taskList.id,
maxResults: MAX_TASK_RESULTS,
});
const items = tasksResponse.data.items || [];
allTasks = allTasks.concat(items);
} catch (error) {
console.error(`Error fetching tasks for list ${taskList.id}:`, error);
}
}
}
return allTasks;
}
static async create(request: CallToolRequest, tasks: tasks_v1.Tasks) {
const taskListId =
(request.params.arguments?.taskListId as string) || "@default";
const taskTitle = request.params.arguments?.title as string;
const taskNotes = request.params.arguments?.notes as string;
const taskStatus = request.params.arguments?.status as string;
const taskDue = request.params.arguments?.due as string;
if (!taskTitle) {
throw new Error("Task title is required");
}
const task = {
title: taskTitle,
notes: taskNotes,
due: taskDue,
};
const taskResponse = await tasks.tasks.insert({
tasklist: taskListId,
requestBody: task,
});
return {
content: [
{
type: "text",
text: `Task created: ${taskResponse.data.title}`,
},
],
isError: false,
};
}
static async update(request: CallToolRequest, tasks: tasks_v1.Tasks) {
const taskListId =
(request.params.arguments?.taskListId as string) || "@default";
const taskUri = request.params.arguments?.uri as string;
const taskId = request.params.arguments?.id as string;
const taskTitle = request.params.arguments?.title as string;
const taskNotes = request.params.arguments?.notes as string;
const taskStatus = request.params.arguments?.status as string;
const taskDue = request.params.arguments?.due as string;
if (!taskUri) {
throw new Error("Task URI is required");
}
if (!taskId) {
throw new Error("Task ID is required");
}
const task = {
id: taskId,
title: taskTitle,
notes: taskNotes,
status: taskStatus,
due: taskDue,
};
const taskResponse = await tasks.tasks.update({
tasklist: taskListId,
task: taskUri,
requestBody: task,
});
return {
content: [
{
type: "text",
text: `Task updated: ${taskResponse.data.title}`,
},
],
isError: false,
};
}
static async list(request: CallToolRequest, tasks: tasks_v1.Tasks) {
const allTasks = await this._list(request, tasks);
const taskList = this.formatTaskList(allTasks);
return {
content: [
{
type: "text",
text: `Found ${allTasks.length} tasks:\n${taskList}`,
},
],
isError: false,
};
}
static async delete(request: CallToolRequest, tasks: tasks_v1.Tasks) {
const taskListId =
(request.params.arguments?.taskListId as string) || "@default";
const taskId = request.params.arguments?.id as string;
if (!taskId) {
throw new Error("Task URI is required");
}
await tasks.tasks.delete({
tasklist: taskListId,
task: taskId,
});
return {
content: [
{
type: "text",
text: `Task ${taskId} deleted`,
},
],
isError: false,
};
}
static async search(request: CallToolRequest, tasks: tasks_v1.Tasks) {
const userQuery = request.params.arguments?.query as string;
const allTasks = await this._list(request, tasks);
const filteredItems = allTasks.filter(
(task) =>
task.title?.toLowerCase().includes(userQuery.toLowerCase()) ||
task.notes?.toLowerCase().includes(userQuery.toLowerCase()),
);
const taskList = this.formatTaskList(filteredItems);
return {
content: [
{
type: "text",
text: `Found ${allTasks.length} tasks:\n${taskList}`,
},
],
isError: false,
};
}
static async clear(request: CallToolRequest, tasks: tasks_v1.Tasks) {
const taskListId =
(request.params.arguments?.taskListId as string) || "@default";
await tasks.tasks.clear({
tasklist: taskListId,
});
return {
content: [
{
type: "text",
text: `Tasks from tasklist ${taskListId} cleared`,
},
],
isError: false,
};
}
}