Skip to content

Commit b608d32

Browse files
feat: Add support for filtering by label (#71)
# Pull Request ## Short description A suggestion from @amix was that we should be able to filter task acquisition by labels. This adds support to the three main task tools such that labels can be provided, and it can be set whether a task should have any of the labels or all of them. ## PR Checklist Feel free to leave unchecked or remove the lines that are not applicable. - [ ] Added tests for bugs / new features - [ ] Updated docs (README, etc.) - [ ] New tools added to `getMcpServer` AND exported in `src/index.ts`. <!-- _Note:_ versioning is handled by [release-please](https://github.com/googleapis/release-please) action, based on the PR title. -->
1 parent 7dd56c0 commit b608d32

10 files changed

+786
-28
lines changed

src/tools/__tests__/__snapshots__/find-completed-tasks.test.ts.snap

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,39 @@ Next:
2626
- Use find-tasks-by-date for active tasks or get-overview for current productivity.
2727
- Recurring tasks will automatically create new instances."
2828
`;
29+
30+
exports[`find-completed-tasks tool label filtering should combine other filters with label filters 1`] = `
31+
"Completed tasks (by due date): 1 (limit 25).
32+
Filter: due date: 2025-08-01 to 2025-08-31; project: test-project-id; section: test-section-id; labels: @important.
33+
Preview:
34+
Important completed task • P1 • id=8485093748
35+
Next:
36+
- Use find-tasks-by-date for active tasks or get-overview for current productivity."
37+
`;
38+
39+
exports[`find-completed-tasks tool label filtering should filter completed tasks by labels: multiple labels with AND operator 1`] = `
40+
"Completed tasks (by due date): 1 (limit 50).
41+
Filter: due date: 2025-08-01 to 2025-08-31; labels: @work & @urgent.
42+
Preview:
43+
Completed task with label • P1 • id=8485093748
44+
Next:
45+
- Use find-tasks-by-date for active tasks or get-overview for current productivity."
46+
`;
47+
48+
exports[`find-completed-tasks tool label filtering should filter completed tasks by labels: multiple labels with OR operator 1`] = `
49+
"Completed tasks (by completed date): 1 (limit 25).
50+
Filter: completed date: 2025-08-10 to 2025-08-20; labels: @personal | @shopping.
51+
Preview:
52+
Completed task with label • P1 • id=8485093748
53+
Next:
54+
- Use find-tasks-by-date for active tasks or get-overview for current productivity."
55+
`;
56+
57+
exports[`find-completed-tasks tool label filtering should filter completed tasks by labels: single label with OR operator 1`] = `
58+
"Completed tasks (by completed date): 1 (limit 50).
59+
Filter: completed date: 2025-08-01 to 2025-08-31; labels: @work.
60+
Preview:
61+
Completed task with label • P1 • id=8485093748
62+
Next:
63+
- Use find-tasks-by-date for active tasks or get-overview for current productivity."
64+
`;

src/tools/__tests__/__snapshots__/find-tasks-by-date.test.ts.snap

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ Filter: today + 6 more days.
66
No results. Expand date range with larger 'daysCount'; Check 'overdue' for past-due items."
77
`;
88

9+
exports[`find-tasks-by-date tool label filtering should combine date filters with label filters 1`] = `
10+
"Tasks for 2025-08-15: 1 (limit 25).
11+
Filter: 2025-08-15; labels: @important.
12+
Preview:
13+
Important task for specific date • due 2025-08-15 • P1 • id=8485093748
14+
Next:
15+
- Use update-tasks to modify priorities or due dates
16+
- Use complete-tasks to mark finished tasks
17+
- Focus on overdue items first to get back on track"
18+
`;
19+
920
exports[`find-tasks-by-date tool listing overdue tasks should handle overdue tasks ignoring daysCount 1`] = `
1021
"Overdue tasks: 0 (limit 50).
1122
Filter: overdue tasks only.

src/tools/__tests__/__snapshots__/find-tasks.test.ts.snap

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ Preview:
3737
Regular future task • due 2025-08-25 • P1 • id=8485093748
3838
Next:
3939
- Use update-tasks to modify priorities or due dates
40-
- Use complete-tasks to mark finished tasks"
40+
- Use complete-tasks to mark finished tasks
41+
- Focus on overdue items first to get back on track"
4142
`;
4243

4344
exports[`find-tasks tool next steps logic should provide helpful suggestions for empty search results 1`] = `

src/tools/__tests__/find-completed-tasks.test.ts

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,14 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
4545
})
4646

4747
const result = await findCompletedTasks.execute(
48-
{ getBy: 'completion', limit: 50, since: '2025-08-10', until: '2025-08-15' },
48+
{
49+
getBy: 'completion',
50+
limit: 50,
51+
since: '2025-08-10',
52+
until: '2025-08-15',
53+
labels: [],
54+
labelsOperator: 'or' as const,
55+
},
4956
mockTodoistApi,
5057
)
5158

@@ -72,6 +79,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
7279
until: '2025-08-31',
7380
projectId: 'specific-project-id',
7481
cursor: 'current-cursor',
82+
labels: [],
83+
labelsOperator: 'or' as const,
7584
},
7685
mockTodoistApi,
7786
)
@@ -121,6 +130,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
121130
limit: 50,
122131
since: '2025-08-10',
123132
until: '2025-08-20',
133+
labels: [],
134+
labelsOperator: 'or' as const,
124135
},
125136
mockTodoistApi,
126137
)
@@ -136,6 +147,141 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
136147
})
137148
})
138149

150+
describe('label filtering', () => {
151+
it.each([
152+
{
153+
name: 'single label with OR operator',
154+
params: {
155+
getBy: 'completion' as const,
156+
since: '2025-08-01',
157+
until: '2025-08-31',
158+
limit: 50,
159+
labels: ['work'],
160+
},
161+
expectedMethod: 'getCompletedTasksByCompletionDate',
162+
expectedFilter: '(@work)',
163+
},
164+
{
165+
name: 'multiple labels with AND operator',
166+
params: {
167+
getBy: 'due' as const,
168+
since: '2025-08-01',
169+
until: '2025-08-31',
170+
limit: 50,
171+
labels: ['work', 'urgent'],
172+
labelsOperator: 'and' as const,
173+
},
174+
expectedMethod: 'getCompletedTasksByDueDate',
175+
expectedFilter: '(@work & @urgent)',
176+
},
177+
{
178+
name: 'multiple labels with OR operator',
179+
params: {
180+
getBy: 'completion' as const,
181+
since: '2025-08-10',
182+
until: '2025-08-20',
183+
limit: 25,
184+
labels: ['personal', 'shopping'],
185+
},
186+
expectedMethod: 'getCompletedTasksByCompletionDate',
187+
expectedFilter: '(@personal | @shopping)',
188+
},
189+
])(
190+
'should filter completed tasks by labels: $name',
191+
async ({ params, expectedMethod, expectedFilter }) => {
192+
const mockCompletedTasks = [
193+
createMockTask({
194+
id: '8485093748',
195+
content: 'Completed task with label',
196+
labels: params.labels,
197+
completedAt: '2024-01-01T00:00:00Z',
198+
}),
199+
]
200+
201+
const mockResponse = { items: mockCompletedTasks, nextCursor: null }
202+
const mockMethod = mockTodoistApi[
203+
expectedMethod as keyof typeof mockTodoistApi
204+
] as jest.MockedFunction<
205+
(...args: never[]) => Promise<{ items: unknown[]; nextCursor: string | null }>
206+
>
207+
mockMethod.mockResolvedValue(mockResponse)
208+
209+
const result = await findCompletedTasks.execute(params, mockTodoistApi)
210+
211+
expect(mockMethod).toHaveBeenCalledWith({
212+
since: params.since,
213+
until: params.until,
214+
limit: params.limit,
215+
filterQuery: expectedFilter,
216+
filterLang: 'en',
217+
})
218+
219+
const textContent = extractTextContent(result)
220+
expect(textContent).toMatchSnapshot()
221+
},
222+
)
223+
224+
it('should handle empty labels array', async () => {
225+
const params = {
226+
getBy: 'completion' as const,
227+
since: '2025-08-01',
228+
until: '2025-08-31',
229+
limit: 50,
230+
labels: [],
231+
labelsOperator: 'or' as const,
232+
}
233+
234+
const mockResponse = { items: [], nextCursor: null }
235+
mockTodoistApi.getCompletedTasksByCompletionDate.mockResolvedValue(mockResponse)
236+
237+
await findCompletedTasks.execute(params, mockTodoistApi)
238+
239+
expect(mockTodoistApi.getCompletedTasksByCompletionDate).toHaveBeenCalledWith({
240+
since: params.since,
241+
until: params.until,
242+
limit: params.limit,
243+
})
244+
})
245+
246+
it('should combine other filters with label filters', async () => {
247+
const params = {
248+
getBy: 'due' as const,
249+
since: '2025-08-01',
250+
until: '2025-08-31',
251+
limit: 25,
252+
projectId: 'test-project-id',
253+
sectionId: 'test-section-id',
254+
labels: ['important'],
255+
labelsOperator: 'or' as const,
256+
}
257+
258+
const mockTasks = [
259+
createMockTask({
260+
content: 'Important completed task',
261+
labels: ['important'],
262+
completedAt: '2024-01-01T00:00:00Z',
263+
}),
264+
]
265+
const mockResponse = { items: mockTasks, nextCursor: null }
266+
mockTodoistApi.getCompletedTasksByDueDate.mockResolvedValue(mockResponse)
267+
268+
const result = await findCompletedTasks.execute(params, mockTodoistApi)
269+
270+
expect(mockTodoistApi.getCompletedTasksByDueDate).toHaveBeenCalledWith({
271+
since: params.since,
272+
until: params.until,
273+
limit: params.limit,
274+
projectId: params.projectId,
275+
sectionId: params.sectionId,
276+
filterQuery: '(@important)',
277+
filterLang: 'en',
278+
})
279+
280+
const textContent = extractTextContent(result)
281+
expect(textContent).toMatchSnapshot()
282+
})
283+
})
284+
139285
describe('error handling', () => {
140286
it('should propagate completion date API errors', async () => {
141287
const apiError = new Error('API Error: Invalid date range')
@@ -144,7 +290,14 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
144290
await expect(
145291
findCompletedTasks.execute(
146292
// invalid date range
147-
{ getBy: 'completion', limit: 50, since: '2025-08-31', until: '2025-08-01' },
293+
{
294+
getBy: 'completion',
295+
limit: 50,
296+
since: '2025-08-31',
297+
until: '2025-08-01',
298+
labels: [],
299+
labelsOperator: 'or' as const,
300+
},
148301
mockTodoistApi,
149302
),
150303
).rejects.toThrow('API Error: Invalid date range')
@@ -162,6 +315,8 @@ describe(`${FIND_COMPLETED_TASKS} tool`, () => {
162315
since: '2025-08-01',
163316
until: '2025-08-31',
164317
projectId: 'non-existent-project',
318+
labels: [],
319+
labelsOperator: 'or' as const,
165320
},
166321
mockTodoistApi,
167322
),

0 commit comments

Comments
 (0)