Skip to content
Open
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
35 changes: 33 additions & 2 deletions src/Tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,37 @@ export class TaskActions {
return taskList.map((task) => this.formatTask(task)).join("\n");
}

// Normalize date strings to RFC3339 format required by Google Tasks API
private static normalizeDate(dateStr: string | undefined): string | undefined {
if (!dateStr) return undefined;

// Already has timezone (ends with Z or +/-offset)
if (/Z$|[+-]\d{2}:\d{2}$/.test(dateStr)) {
return dateStr;
}

// Date only (YYYY-MM-DD) - add time and Z
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
return `${dateStr}T00:00:00.000Z`;
}

// DateTime without timezone - add Z
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(dateStr)) {
// Remove any milliseconds and add Z
const base = dateStr.replace(/\.\d+$/, '');
return `${base}.000Z`;
}

// Try to parse and convert
const date = new Date(dateStr);
if (!isNaN(date.getTime())) {
return date.toISOString();
}

// Return as-is if we can't parse (let API handle the error)
return dateStr;
}

private static async _list(request: CallToolRequest, tasks: tasks_v1.Tasks) {
const taskListsResponse = await tasks.tasklists.list({
maxResults: MAX_TASK_RESULTS,
Expand Down Expand Up @@ -134,7 +165,7 @@ export class TaskActions {
const task = {
title: taskTitle,
notes: taskNotes,
due: taskDue,
due: this.normalizeDate(taskDue),
};

const taskResponse = await tasks.tasks.insert({
Expand Down Expand Up @@ -176,7 +207,7 @@ export class TaskActions {
title: taskTitle,
notes: taskNotes,
status: taskStatus,
due: taskDue,
due: this.normalizeDate(taskDue),
};

const taskResponse = await tasks.tasks.update({
Expand Down
47 changes: 44 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import fs from "fs";
import { google, tasks_v1 } from "googleapis";
import path from "path";
import { fileURLToPath } from "url";
import { TaskActions, TaskResources } from "./Tasks.js";

const tasks = google.tasks("v1");
Expand Down Expand Up @@ -201,6 +202,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
required: ["id", "uri"],
},
},
{
name: "list-tasklists",
description: "List all task lists in Google Tasks",
inputSchema: {
type: "object",
properties: {},
},
},
],
};
});
Expand Down Expand Up @@ -230,18 +239,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const taskResult = await TaskActions.clear(request, tasks);
return taskResult;
}
if (request.params.name === "list-tasklists") {
const response = await tasks.tasklists.list();
const taskLists = response.data.items || [];
const formatted = taskLists.map((list) =>
`${list.title} (ID: ${list.id})`
).join("\n");
return {
content: [
{
type: "text",
text: taskLists.length > 0
? `Found ${taskLists.length} task lists:\n${formatted}`
: "No task lists found",
},
],
};
}
throw new Error("Tool not found");
});

const credentialsPath = path.join(
path.dirname(new URL(import.meta.url).pathname),
path.dirname(fileURLToPath(import.meta.url)),
"../.gtasks-server-credentials.json",
);

async function authenticateAndSaveCredentials() {
console.log("Launching auth flow…");
const p = path.join(
path.dirname(new URL(import.meta.url).pathname),
path.dirname(fileURLToPath(import.meta.url)),
"../gcp-oauth.keys.json",
);

Expand All @@ -263,8 +289,23 @@ async function loadCredentialsAndRunServer() {
}

const credentials = JSON.parse(fs.readFileSync(credentialsPath, "utf-8"));
const auth = new google.auth.OAuth2();

// Load OAuth app credentials to enable token refresh
// Support GOOGLE_OAUTH_CREDENTIALS env var for shared credential location
const oauthKeysPath = process.env.GOOGLE_OAUTH_CREDENTIALS ||
path.join(path.dirname(credentialsPath), "gcp-oauth.keys.json");
const oauthKeys = JSON.parse(fs.readFileSync(oauthKeysPath, "utf-8"));
const { client_id, client_secret } = oauthKeys.installed;

const auth = new google.auth.OAuth2(client_id, client_secret);
auth.setCredentials(credentials);

// Auto-save refreshed tokens
auth.on('tokens', (tokens) => {
const updated = { ...credentials, ...tokens };
fs.writeFileSync(credentialsPath, JSON.stringify(updated, null, 2));
});

google.options({ auth });

const transport = new StdioServerTransport();
Expand Down