diff --git a/src/Tasks.ts b/src/Tasks.ts index a1cf5f9..e7d98e6 100644 --- a/src/Tasks.ts +++ b/src/Tasks.ts @@ -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, @@ -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({ @@ -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({ diff --git a/src/index.ts b/src/index.ts index 0118656..41c67f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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"); @@ -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: {}, + }, + }, ], }; }); @@ -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", ); @@ -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();