@doist/todoist-ai 4.9.4 → 4.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +11 -0
  2. package/dist/index.d.ts +37 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +8 -1
  5. package/dist/mcp-helpers.d.ts +8 -1
  6. package/dist/mcp-helpers.d.ts.map +1 -1
  7. package/dist/mcp-helpers.js +1 -1
  8. package/dist/mcp-server.d.ts.map +1 -1
  9. package/dist/mcp-server.js +5 -0
  10. package/dist/tool-helpers.d.ts +8 -1
  11. package/dist/tool-helpers.d.ts.map +1 -1
  12. package/dist/tool-helpers.js +13 -10
  13. package/dist/tools/__tests__/fetch.test.d.ts +2 -0
  14. package/dist/tools/__tests__/fetch.test.d.ts.map +1 -0
  15. package/dist/tools/__tests__/fetch.test.js +275 -0
  16. package/dist/tools/__tests__/find-completed-tasks.test.js +71 -12
  17. package/dist/tools/__tests__/search.test.d.ts +2 -0
  18. package/dist/tools/__tests__/search.test.d.ts.map +1 -0
  19. package/dist/tools/__tests__/search.test.js +208 -0
  20. package/dist/tools/add-tasks.d.ts.map +1 -1
  21. package/dist/tools/add-tasks.js +8 -2
  22. package/dist/tools/fetch.d.ts +26 -0
  23. package/dist/tools/fetch.d.ts.map +1 -0
  24. package/dist/tools/fetch.js +99 -0
  25. package/dist/tools/find-completed-tasks.d.ts +12 -12
  26. package/dist/tools/find-completed-tasks.d.ts.map +1 -1
  27. package/dist/tools/find-completed-tasks.js +15 -1
  28. package/dist/tools/find-projects.d.ts +2 -2
  29. package/dist/tools/search.d.ts +26 -0
  30. package/dist/tools/search.d.ts.map +1 -0
  31. package/dist/tools/search.js +64 -0
  32. package/dist/tools/update-tasks.d.ts.map +1 -1
  33. package/dist/tools/update-tasks.js +8 -2
  34. package/dist/utils/constants.d.ts +1 -1
  35. package/dist/utils/constants.js +1 -1
  36. package/dist/utils/tool-names.d.ts +2 -0
  37. package/dist/utils/tool-names.d.ts.map +1 -1
  38. package/dist/utils/tool-names.js +3 -0
  39. package/package.json +1 -1
@@ -6,11 +6,25 @@ import { findCompletedTasks } from '../find-completed-tasks.js';
6
6
  const mockTodoistApi = {
7
7
  getCompletedTasksByCompletionDate: jest.fn(),
8
8
  getCompletedTasksByDueDate: jest.fn(),
9
+ getUser: jest.fn(),
9
10
  };
10
11
  const { FIND_COMPLETED_TASKS } = ToolNames;
11
12
  describe(`${FIND_COMPLETED_TASKS} tool`, () => {
12
13
  beforeEach(() => {
13
14
  jest.clearAllMocks();
15
+ // Mock default user with UTC timezone
16
+ mockTodoistApi.getUser.mockResolvedValue({
17
+ id: 'test-user-id',
18
+ fullName: 'Test User',
19
+ email: 'test@example.com',
20
+ tzInfo: {
21
+ timezone: 'UTC',
22
+ gmtString: '+00:00',
23
+ hours: 0,
24
+ minutes: 0,
25
+ isDst: 0,
26
+ },
27
+ });
14
28
  });
15
29
  describe('getting completed tasks by completion date (default)', () => {
16
30
  it('should get completed tasks by completion date', async () => {
@@ -46,8 +60,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
46
60
  labelsOperator: 'or',
47
61
  }, mockTodoistApi);
48
62
  expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
49
- since: '2025-08-10',
50
- until: '2025-08-15',
63
+ since: '2025-08-10T00:00:00.000Z',
64
+ until: '2025-08-15T23:59:59.000Z',
51
65
  limit: 50,
52
66
  });
53
67
  expect(extractTextContent(result)).toMatchSnapshot();
@@ -68,8 +82,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
68
82
  labelsOperator: 'or',
69
83
  }, mockTodoistApi);
70
84
  expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
71
- since: '2025-08-01',
72
- until: '2025-08-31',
85
+ since: '2025-08-01T00:00:00.000Z',
86
+ until: '2025-08-31T23:59:59.000Z',
73
87
  projectId: 'specific-project-id',
74
88
  limit: 100,
75
89
  cursor: 'current-cursor',
@@ -111,8 +125,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
111
125
  labelsOperator: 'or',
112
126
  }, mockTodoistApi);
113
127
  expect(mockTodoistApi.getCompletedTasksByDueDate).toHaveBeenCalledWith({
114
- since: '2025-08-10',
115
- until: '2025-08-20',
128
+ since: '2025-08-10T00:00:00.000Z',
129
+ until: '2025-08-20T23:59:59.000Z',
116
130
  limit: 50,
117
131
  });
118
132
  expect(mockTodoistApi.getCompletedTasksByCompletionDate).not.toHaveBeenCalled();
@@ -172,8 +186,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
172
186
  mockMethod.mockResolvedValue(mockResponse);
173
187
  const result = await findCompletedTasks.execute(params, mockTodoistApi);
174
188
  expect(mockMethod).toHaveBeenCalledWith({
175
- since: params.since,
176
- until: params.until,
189
+ since: `${params.since}T00:00:00.000Z`,
190
+ until: `${params.until}T23:59:59.000Z`,
177
191
  limit: params.limit,
178
192
  filterQuery: expectedFilter,
179
193
  filterLang: 'en',
@@ -194,8 +208,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
194
208
  mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue(mockResponse);
195
209
  await findCompletedTasks.execute(params, mockTodoistApi);
196
210
  expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
197
- since: params.since,
198
- until: params.until,
211
+ since: `${params.since}T00:00:00.000Z`,
212
+ until: `${params.until}T23:59:59.000Z`,
199
213
  limit: params.limit,
200
214
  });
201
215
  });
@@ -221,8 +235,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
221
235
  mockTodoistApi.getCompletedTasksByDueDate.mockResolvedValue(mockResponse);
222
236
  const result = await findCompletedTasks.execute(params, mockTodoistApi);
223
237
  expect(mockTodoistApi.getCompletedTasksByDueDate).toHaveBeenCalledWith({
224
- since: params.since,
225
- until: params.until,
238
+ since: `${params.since}T00:00:00.000Z`,
239
+ until: `${params.until}T23:59:59.000Z`,
226
240
  limit: params.limit,
227
241
  projectId: params.projectId,
228
242
  sectionId: params.sectionId,
@@ -233,6 +247,51 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
233
247
  expect(textContent).toMatchSnapshot();
234
248
  });
235
249
  });
250
+ describe('timezone handling', () => {
251
+ it('should convert user timezone to UTC correctly (Europe/Madrid)', async () => {
252
+ // Mock user with Madrid timezone
253
+ mockTodoistApi.getUser.mockResolvedValue({
254
+ id: 'test-user-id',
255
+ fullName: 'Test User',
256
+ email: 'test@example.com',
257
+ tzInfo: {
258
+ timezone: 'Europe/Madrid',
259
+ gmtString: '+02:00',
260
+ hours: 2,
261
+ minutes: 0,
262
+ isDst: 0,
263
+ },
264
+ });
265
+ const mockCompletedTasks = [
266
+ createMockTask({
267
+ id: '8485093750',
268
+ content: 'Task completed in Madrid timezone',
269
+ completedAt: '2025-10-11T15:30:00Z',
270
+ }),
271
+ ];
272
+ mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue({
273
+ items: mockCompletedTasks,
274
+ nextCursor: null,
275
+ });
276
+ const result = await findCompletedTasks.execute({
277
+ getBy: 'completion',
278
+ limit: 50,
279
+ since: '2025-10-11',
280
+ until: '2025-10-11',
281
+ labels: [],
282
+ labelsOperator: 'or',
283
+ }, mockTodoistApi);
284
+ // Should convert Madrid local time to UTC
285
+ // 2025-10-11 00:00:00 +02:00 = 2025-10-10 22:00:00 UTC
286
+ // 2025-10-11 23:59:59 +02:00 = 2025-10-11 21:59:59 UTC
287
+ expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
288
+ since: '2025-10-10T22:00:00.000Z',
289
+ until: '2025-10-11T21:59:59.000Z',
290
+ limit: 50,
291
+ });
292
+ expect(extractTextContent(result)).toMatchSnapshot();
293
+ });
294
+ });
236
295
  describe('error handling', () => {
237
296
  it('should propagate completion date API errors', async () => {
238
297
  const apiError = new Error('API Error: Invalid date range');
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=search.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/search.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,208 @@
1
+ import { jest } from '@jest/globals';
2
+ import { getTasksByFilter } from '../../tool-helpers.js';
3
+ import { createMappedTask, createMockApiResponse, createMockProject, TEST_IDS, } from '../../utils/test-helpers.js';
4
+ import { ToolNames } from '../../utils/tool-names.js';
5
+ import { search } from '../search.js';
6
+ jest.mock('../../tool-helpers', () => {
7
+ const actual = jest.requireActual('../../tool-helpers');
8
+ return {
9
+ getTasksByFilter: jest.fn(),
10
+ buildTodoistUrl: actual.buildTodoistUrl,
11
+ };
12
+ });
13
+ const { SEARCH } = ToolNames;
14
+ const mockGetTasksByFilter = getTasksByFilter;
15
+ // Mock the Todoist API
16
+ const mockTodoistApi = {
17
+ getProjects: jest.fn(),
18
+ };
19
+ describe(`${SEARCH} tool`, () => {
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ });
23
+ describe('searching tasks and projects', () => {
24
+ it('should search both tasks and projects and return combined results', async () => {
25
+ const mockTasks = [
26
+ createMappedTask({
27
+ id: TEST_IDS.TASK_1,
28
+ content: 'Important meeting task',
29
+ }),
30
+ createMappedTask({
31
+ id: TEST_IDS.TASK_2,
32
+ content: 'Another important item',
33
+ }),
34
+ ];
35
+ const mockProjects = [
36
+ createMockProject({
37
+ id: TEST_IDS.PROJECT_WORK,
38
+ name: 'Important Work Project',
39
+ }),
40
+ createMockProject({
41
+ id: TEST_IDS.PROJECT_TEST,
42
+ name: 'Test Project',
43
+ }),
44
+ ];
45
+ mockGetTasksByFilter.mockResolvedValue({ tasks: mockTasks, nextCursor: null });
46
+ mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
47
+ const result = await search.execute({ query: 'important' }, mockTodoistApi);
48
+ // Verify both API calls were made
49
+ expect(mockGetTasksByFilter).toHaveBeenCalledWith({
50
+ client: mockTodoistApi,
51
+ query: 'search: important',
52
+ limit: 100, // TASKS_MAX
53
+ cursor: undefined,
54
+ });
55
+ expect(mockTodoistApi.getProjects).toHaveBeenCalledWith({
56
+ limit: 100, // PROJECTS_MAX
57
+ });
58
+ // Verify result structure
59
+ expect(result.content).toHaveLength(1);
60
+ expect(result.content[0]?.type).toBe('text');
61
+ // Parse the JSON response
62
+ const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
63
+ expect(jsonResponse).toHaveProperty('results');
64
+ expect(jsonResponse.results).toHaveLength(3); // 2 tasks + 1 project matching "important"
65
+ // Verify task results
66
+ expect(jsonResponse.results[0]).toEqual({
67
+ id: `task:${TEST_IDS.TASK_1}`,
68
+ title: 'Important meeting task',
69
+ url: `https://app.todoist.com/app/task/${TEST_IDS.TASK_1}`,
70
+ });
71
+ expect(jsonResponse.results[1]).toEqual({
72
+ id: `task:${TEST_IDS.TASK_2}`,
73
+ title: 'Another important item',
74
+ url: `https://app.todoist.com/app/task/${TEST_IDS.TASK_2}`,
75
+ });
76
+ // Verify project result (only "Important Work Project" matches)
77
+ expect(jsonResponse.results[2]).toEqual({
78
+ id: `project:${TEST_IDS.PROJECT_WORK}`,
79
+ title: 'Important Work Project',
80
+ url: `https://app.todoist.com/app/project/${TEST_IDS.PROJECT_WORK}`,
81
+ });
82
+ });
83
+ it('should return only matching tasks when no projects match', async () => {
84
+ const mockTasks = [
85
+ createMappedTask({
86
+ id: TEST_IDS.TASK_1,
87
+ content: 'Unique task content',
88
+ }),
89
+ ];
90
+ const mockProjects = [
91
+ createMockProject({
92
+ id: TEST_IDS.PROJECT_WORK,
93
+ name: 'Work Project',
94
+ }),
95
+ ];
96
+ mockGetTasksByFilter.mockResolvedValue({ tasks: mockTasks, nextCursor: null });
97
+ mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
98
+ const result = await search.execute({ query: 'unique' }, mockTodoistApi);
99
+ const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
100
+ expect(jsonResponse.results).toHaveLength(1);
101
+ expect(jsonResponse.results[0].id).toBe(`task:${TEST_IDS.TASK_1}`);
102
+ });
103
+ it('should return only matching projects when no tasks match', async () => {
104
+ const mockProjects = [
105
+ createMockProject({
106
+ id: TEST_IDS.PROJECT_WORK,
107
+ name: 'Special Project Name',
108
+ }),
109
+ createMockProject({
110
+ id: TEST_IDS.PROJECT_TEST,
111
+ name: 'Another Project',
112
+ }),
113
+ ];
114
+ mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
115
+ mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
116
+ const result = await search.execute({ query: 'special' }, mockTodoistApi);
117
+ const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
118
+ expect(jsonResponse.results).toHaveLength(1);
119
+ expect(jsonResponse.results[0]).toEqual({
120
+ id: `project:${TEST_IDS.PROJECT_WORK}`,
121
+ title: 'Special Project Name',
122
+ url: `https://app.todoist.com/app/project/${TEST_IDS.PROJECT_WORK}`,
123
+ });
124
+ });
125
+ it('should return empty results when nothing matches', async () => {
126
+ mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
127
+ mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse([]));
128
+ const result = await search.execute({ query: 'nonexistent' }, mockTodoistApi);
129
+ const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
130
+ expect(jsonResponse.results).toHaveLength(0);
131
+ });
132
+ it('should perform case-insensitive project filtering', async () => {
133
+ const mockProjects = [
134
+ createMockProject({
135
+ id: TEST_IDS.PROJECT_WORK,
136
+ name: 'Important Work',
137
+ }),
138
+ ];
139
+ mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
140
+ mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
141
+ const result = await search.execute({ query: 'IMPORTANT' }, mockTodoistApi);
142
+ const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
143
+ expect(jsonResponse.results).toHaveLength(1);
144
+ expect(jsonResponse.results[0].title).toBe('Important Work');
145
+ });
146
+ it('should handle partial matches in project names', async () => {
147
+ const mockProjects = [
148
+ createMockProject({ id: 'project-1', name: 'Development Tasks' }),
149
+ createMockProject({ id: 'project-2', name: 'Developer Resources' }),
150
+ createMockProject({ id: 'project-3', name: 'Marketing' }),
151
+ ];
152
+ mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
153
+ mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
154
+ const result = await search.execute({ query: 'develop' }, mockTodoistApi);
155
+ const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
156
+ expect(jsonResponse.results).toHaveLength(2);
157
+ expect(jsonResponse.results[0].title).toBe('Development Tasks');
158
+ expect(jsonResponse.results[1].title).toBe('Developer Resources');
159
+ });
160
+ });
161
+ describe('error handling', () => {
162
+ it('should return error response for task search failure', async () => {
163
+ mockGetTasksByFilter.mockRejectedValue(new Error('Task search failed'));
164
+ mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse([]));
165
+ const result = await search.execute({ query: 'test' }, mockTodoistApi);
166
+ expect(result.isError).toBe(true);
167
+ expect(result.content[0]?.text).toBe('Task search failed');
168
+ });
169
+ it('should return error response for project search failure', async () => {
170
+ mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
171
+ mockTodoistApi.getProjects.mockRejectedValue(new Error('Project search failed'));
172
+ const result = await search.execute({ query: 'test' }, mockTodoistApi);
173
+ expect(result.isError).toBe(true);
174
+ expect(result.content[0]?.text).toBe('Project search failed');
175
+ });
176
+ });
177
+ describe('OpenAI MCP spec compliance', () => {
178
+ it('should return exactly one content item with type "text"', async () => {
179
+ mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
180
+ mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse([]));
181
+ const result = await search.execute({ query: 'test' }, mockTodoistApi);
182
+ expect(result.content).toHaveLength(1);
183
+ expect(result.content[0]?.type).toBe('text');
184
+ });
185
+ it('should return valid JSON string in text field', async () => {
186
+ mockGetTasksByFilter.mockResolvedValue({ tasks: [], nextCursor: null });
187
+ mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse([]));
188
+ const result = await search.execute({ query: 'test' }, mockTodoistApi);
189
+ expect(() => JSON.parse(result.content[0]?.text ?? '{}')).not.toThrow();
190
+ });
191
+ it('should include required fields (id, title, url) in each result', async () => {
192
+ const mockTasks = [createMappedTask({ id: TEST_IDS.TASK_1, content: 'Test' })];
193
+ const mockProjects = [createMockProject({ id: TEST_IDS.PROJECT_WORK, name: 'Test' })];
194
+ mockGetTasksByFilter.mockResolvedValue({ tasks: mockTasks, nextCursor: null });
195
+ mockTodoistApi.getProjects.mockResolvedValue(createMockApiResponse(mockProjects));
196
+ const result = await search.execute({ query: 'test' }, mockTodoistApi);
197
+ const jsonResponse = JSON.parse(result.content[0]?.text ?? '{}');
198
+ for (const item of jsonResponse.results) {
199
+ expect(item).toHaveProperty('id');
200
+ expect(item).toHaveProperty('title');
201
+ expect(item).toHaveProperty('url');
202
+ expect(typeof item.id).toBe('string');
203
+ expect(typeof item.title).toBe('string');
204
+ expect(typeof item.url).toBe('string');
205
+ }
206
+ });
207
+ });
208
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"add-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/add-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAClF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AA2CvB,QAAA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuB4B,CAAA;AAgI1C,OAAO,EAAE,QAAQ,EAAE,CAAA"}
1
+ {"version":3,"file":"add-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/add-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAqB,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAClF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAqDvB,QAAA,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuB4B,CAAA;AAgI1C,OAAO,EAAE,QAAQ,EAAE,CAAA"}
@@ -7,8 +7,14 @@ import { convertPriorityToNumber, PrioritySchema } from '../utils/priorities.js'
7
7
  import { generateTaskNextSteps, getDateString, summarizeTaskOperation, } from '../utils/response-builders.js';
8
8
  import { ToolNames } from '../utils/tool-names.js';
9
9
  const TaskSchema = z.object({
10
- content: z.string().min(1).describe('The content of the task to create.'),
11
- description: z.string().optional().describe('The description of the task.'),
10
+ content: z
11
+ .string()
12
+ .min(1)
13
+ .describe('The task name/title. Should be concise and actionable (e.g., "Review PR #123", "Call dentist"). For longer content, use the description field instead. Supports Markdown.'),
14
+ description: z
15
+ .string()
16
+ .optional()
17
+ .describe('Additional details, notes, or context for the task. Use this for longer content rather than putting it in the task name. Supports Markdown.'),
12
18
  priority: PrioritySchema.optional().describe('The priority of the task: p1 (highest), p2 (high), p3 (medium), p4 (lowest/default).'),
13
19
  dueString: z.string().optional().describe('The due date for the task, in natural language.'),
14
20
  duration: z
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ type FetchToolOutput = {
3
+ content: {
4
+ type: 'text';
5
+ text: string;
6
+ }[];
7
+ isError?: boolean;
8
+ };
9
+ /**
10
+ * OpenAI MCP fetch tool - retrieves the full contents of a task or project by ID.
11
+ *
12
+ * This tool follows the OpenAI MCP fetch tool specification:
13
+ * @see https://platform.openai.com/docs/mcp#fetch-tool
14
+ */
15
+ declare const fetch: {
16
+ name: "fetch";
17
+ description: string;
18
+ parameters: {
19
+ id: z.ZodString;
20
+ };
21
+ execute(args: {
22
+ id: string;
23
+ }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<FetchToolOutput>;
24
+ };
25
+ export { fetch };
26
+ //# sourceMappingURL=fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/tools/fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAuBvB,KAAK,eAAe,GAAG;IACnB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzC,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,QAAA,MAAM,KAAK;;;;;;;;oEAKsB,OAAO,CAAC,eAAe,CAAC;CAsFf,CAAA;AAE1C,OAAO,EAAE,KAAK,EAAE,CAAA"}
@@ -0,0 +1,99 @@
1
+ import { z } from 'zod';
2
+ import { getErrorOutput } from '../mcp-helpers.js';
3
+ import { buildTodoistUrl, mapProject, mapTask } from '../tool-helpers.js';
4
+ import { ToolNames } from '../utils/tool-names.js';
5
+ const ArgsSchema = {
6
+ id: z
7
+ .string()
8
+ .min(1)
9
+ .describe('A unique identifier for the document in the format "task:{id}" or "project:{id}".'),
10
+ };
11
+ /**
12
+ * OpenAI MCP fetch tool - retrieves the full contents of a task or project by ID.
13
+ *
14
+ * This tool follows the OpenAI MCP fetch tool specification:
15
+ * @see https://platform.openai.com/docs/mcp#fetch-tool
16
+ */
17
+ const fetch = {
18
+ name: ToolNames.FETCH,
19
+ description: 'Fetch the full contents of a task or project by its ID. The ID should be in the format "task:{id}" or "project:{id}".',
20
+ parameters: ArgsSchema,
21
+ async execute(args, client) {
22
+ try {
23
+ const { id } = args;
24
+ // Parse the composite ID
25
+ const [type, objectId] = id.split(':', 2);
26
+ if (!objectId || (type !== 'task' && type !== 'project')) {
27
+ throw new Error('Invalid ID format. Expected "task:{id}" or "project:{id}". Example: "task:8485093748" or "project:6cfCcrrCFg2xP94Q"');
28
+ }
29
+ let result;
30
+ if (type === 'task') {
31
+ // Fetch task
32
+ const task = await client.getTask(objectId);
33
+ const mappedTask = mapTask(task);
34
+ // Build text content
35
+ const textParts = [mappedTask.content];
36
+ if (mappedTask.description) {
37
+ textParts.push(`\n\nDescription: ${mappedTask.description}`);
38
+ }
39
+ if (mappedTask.dueDate) {
40
+ textParts.push(`\nDue: ${mappedTask.dueDate}`);
41
+ }
42
+ if (mappedTask.labels.length > 0) {
43
+ textParts.push(`\nLabels: ${mappedTask.labels.join(', ')}`);
44
+ }
45
+ result = {
46
+ id: `task:${mappedTask.id}`,
47
+ title: mappedTask.content,
48
+ text: textParts.join(''),
49
+ url: buildTodoistUrl('task', mappedTask.id),
50
+ metadata: {
51
+ priority: mappedTask.priority,
52
+ projectId: mappedTask.projectId,
53
+ sectionId: mappedTask.sectionId,
54
+ parentId: mappedTask.parentId,
55
+ recurring: mappedTask.recurring,
56
+ duration: mappedTask.duration,
57
+ responsibleUid: mappedTask.responsibleUid,
58
+ assignedByUid: mappedTask.assignedByUid,
59
+ },
60
+ };
61
+ }
62
+ else {
63
+ // Fetch project
64
+ const project = await client.getProject(objectId);
65
+ const mappedProject = mapProject(project);
66
+ // Build text content
67
+ const textParts = [mappedProject.name];
68
+ if (mappedProject.isShared) {
69
+ textParts.push('\n\nShared project');
70
+ }
71
+ if (mappedProject.isFavorite) {
72
+ textParts.push('\nFavorite: Yes');
73
+ }
74
+ result = {
75
+ id: `project:${mappedProject.id}`,
76
+ title: mappedProject.name,
77
+ text: textParts.join(''),
78
+ url: buildTodoistUrl('project', mappedProject.id),
79
+ metadata: {
80
+ color: mappedProject.color,
81
+ isFavorite: mappedProject.isFavorite,
82
+ isShared: mappedProject.isShared,
83
+ parentId: mappedProject.parentId,
84
+ inboxProject: mappedProject.inboxProject,
85
+ viewStyle: mappedProject.viewStyle,
86
+ },
87
+ };
88
+ }
89
+ // Return as JSON-encoded string in a text content item (OpenAI MCP spec)
90
+ const jsonText = JSON.stringify(result);
91
+ return { content: [{ type: 'text', text: jsonText }] };
92
+ }
93
+ catch (error) {
94
+ const message = error instanceof Error ? error.message : 'An unknown error occurred';
95
+ return getErrorOutput(message);
96
+ }
97
+ },
98
+ };
99
+ export { fetch };
@@ -16,17 +16,17 @@ declare const findCompletedTasks: {
16
16
  cursor: z.ZodOptional<z.ZodString>;
17
17
  };
18
18
  execute(args: {
19
- limit: number;
20
- getBy: "due" | "completion";
19
+ getBy: "completion" | "due";
21
20
  since: string;
22
21
  until: string;
23
- projectId?: string | undefined;
24
- parentId?: string | undefined;
22
+ limit: number;
23
+ labels?: string[] | undefined;
24
+ labelsOperator?: "and" | "or" | undefined;
25
25
  workspaceId?: string | undefined;
26
+ projectId?: string | undefined;
26
27
  sectionId?: string | undefined;
27
- labels?: string[] | undefined;
28
+ parentId?: string | undefined;
28
29
  cursor?: string | undefined;
29
- labelsOperator?: "and" | "or" | undefined;
30
30
  }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
31
31
  content: {
32
32
  type: "text";
@@ -52,17 +52,17 @@ declare const findCompletedTasks: {
52
52
  totalCount: number;
53
53
  hasMore: boolean;
54
54
  appliedFilters: {
55
- limit: number;
56
- getBy: "due" | "completion";
55
+ getBy: "completion" | "due";
57
56
  since: string;
58
57
  until: string;
59
- projectId?: string | undefined;
60
- parentId?: string | undefined;
58
+ limit: number;
59
+ labels?: string[] | undefined;
60
+ labelsOperator?: "and" | "or" | undefined;
61
61
  workspaceId?: string | undefined;
62
+ projectId?: string | undefined;
62
63
  sectionId?: string | undefined;
63
- labels?: string[] | undefined;
64
+ parentId?: string | undefined;
64
65
  cursor?: string | undefined;
65
- labelsOperator?: "and" | "or" | undefined;
66
66
  };
67
67
  };
68
68
  } | {
@@ -1 +1 @@
1
- {"version":3,"file":"find-completed-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/find-completed-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiDvB,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoCkB,CAAA;AAmE1C,OAAO,EAAE,kBAAkB,EAAE,CAAA"}
1
+ {"version":3,"file":"find-completed-tasks.d.ts","sourceRoot":"","sources":["../../src/tools/find-completed-tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiDvB,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsDkB,CAAA;AAmE1C,OAAO,EAAE,kBAAkB,EAAE,CAAA"}
@@ -43,15 +43,29 @@ const findCompletedTasks = {
43
43
  description: 'Get completed tasks.',
44
44
  parameters: ArgsSchema,
45
45
  async execute(args, client) {
46
- const { getBy, labels, labelsOperator, ...rest } = args;
46
+ const { getBy, labels, labelsOperator, since, until, ...rest } = args;
47
47
  const labelsFilter = generateLabelsFilter(labels, labelsOperator);
48
+ // Get user timezone to convert local dates to UTC
49
+ const user = await client.getUser();
50
+ const userGmtOffset = user.tzInfo?.gmtString || '+00:00';
51
+ // Convert user's local date to UTC timestamps
52
+ // This ensures we capture the entire day from the user's perspective
53
+ const sinceWithOffset = `${since}T00:00:00${userGmtOffset}`;
54
+ const untilWithOffset = `${until}T23:59:59${userGmtOffset}`;
55
+ // Parse and convert to UTC
56
+ const sinceDateTime = new Date(sinceWithOffset).toISOString();
57
+ const untilDateTime = new Date(untilWithOffset).toISOString();
48
58
  const { items, nextCursor } = getBy === 'completion'
49
59
  ? await client.getCompletedTasksByCompletionDate({
50
60
  ...rest,
61
+ since: sinceDateTime,
62
+ until: untilDateTime,
51
63
  ...(labelsFilter ? { filterQuery: labelsFilter, filterLang: 'en' } : {}),
52
64
  })
53
65
  : await client.getCompletedTasksByDueDate({
54
66
  ...rest,
67
+ since: sinceDateTime,
68
+ until: untilDateTime,
55
69
  ...(labelsFilter ? { filterQuery: labelsFilter, filterLang: 'en' } : {}),
56
70
  });
57
71
  const mappedTasks = items.map(mapTask);
@@ -9,8 +9,8 @@ declare const findProjects: {
9
9
  };
10
10
  execute(args: {
11
11
  limit: number;
12
- cursor?: string | undefined;
13
12
  search?: string | undefined;
13
+ cursor?: string | undefined;
14
14
  }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<{
15
15
  content: {
16
16
  type: "text";
@@ -32,8 +32,8 @@ declare const findProjects: {
32
32
  hasMore: boolean;
33
33
  appliedFilters: {
34
34
  limit: number;
35
- cursor?: string | undefined;
36
35
  search?: string | undefined;
36
+ cursor?: string | undefined;
37
37
  };
38
38
  };
39
39
  } | {
@@ -0,0 +1,26 @@
1
+ import { z } from 'zod';
2
+ type SearchToolOutput = {
3
+ content: {
4
+ type: 'text';
5
+ text: string;
6
+ }[];
7
+ isError?: boolean;
8
+ };
9
+ /**
10
+ * OpenAI MCP search tool - returns a list of relevant search results from Todoist.
11
+ *
12
+ * This tool follows the OpenAI MCP search tool specification:
13
+ * @see https://platform.openai.com/docs/mcp#search-tool
14
+ */
15
+ declare const search: {
16
+ name: "search";
17
+ description: string;
18
+ parameters: {
19
+ query: z.ZodString;
20
+ };
21
+ execute(args: {
22
+ query: string;
23
+ }, client: import("@doist/todoist-api-typescript").TodoistApi): Promise<SearchToolOutput>;
24
+ };
25
+ export { search };
26
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/tools/search.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAiBvB,KAAK,gBAAgB,GAAG;IACpB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzC,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,QAAA,MAAM,MAAM;;;;;;;;oEAKqB,OAAO,CAAC,gBAAgB,CAAC;CAmDhB,CAAA;AAE1C,OAAO,EAAE,MAAM,EAAE,CAAA"}