Skip to content

Commit b583fe1

Browse files
authored
feat: Add user-info tool and upgrade Todoist API (#82)
1 parent 065b7a1 commit b583fe1

File tree

9 files changed

+410
-30
lines changed

9 files changed

+410
-30
lines changed

package-lock.json

Lines changed: 42 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"prepare": "husky"
4545
},
4646
"dependencies": {
47-
"@doist/todoist-api-typescript": "5.1.2",
47+
"@doist/todoist-api-typescript": "5.4.0",
4848
"@modelcontextprotocol/sdk": "^1.11.1",
4949
"date-fns": "^4.1.0",
5050
"dotenv": "^16.5.0",

src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { updateComments } from './tools/update-comments.js'
2626
// General tools
2727
import { deleteObject } from './tools/delete-object.js'
2828
import { getOverview } from './tools/get-overview.js'
29+
import { userInfo } from './tools/user-info.js'
2930

3031
const tools = {
3132
// Task management tools
@@ -50,6 +51,7 @@ const tools = {
5051
// General tools
5152
getOverview,
5253
deleteObject,
54+
userInfo,
5355
}
5456

5557
export { tools, getMcpServer }
@@ -77,4 +79,5 @@ export {
7779
// General tools
7880
getOverview,
7981
deleteObject,
82+
userInfo,
8083
}

src/mcp-server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { updateComments } from './tools/update-comments.js'
2828
// General tools
2929
import { deleteObject } from './tools/delete-object.js'
3030
import { getOverview } from './tools/get-overview.js'
31+
import { userInfo } from './tools/user-info.js'
3132

3233
const instructions = `
3334
Tools to help you manage your todoist tasks.
@@ -78,6 +79,7 @@ function getMcpServer({ todoistApiKey, baseUrl }: { todoistApiKey: string; baseU
7879
// General tools
7980
registerTool(getOverview, server, todoist)
8081
registerTool(deleteObject, server, todoist)
82+
registerTool(userInfo, server, todoist)
8183

8284
return server
8385
}

src/tools/__tests__/update-sections.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe(`${UPDATE_SECTIONS} tool`, () => {
3434
isDeleted: false,
3535
isCollapsed: false,
3636
name: 'Updated Section Name',
37+
url: 'https://todoist.com/sections/existing-section-123',
3738
}
3839

3940
mockTodoistApi.updateSection.mockResolvedValue(mockApiResponse)
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import type { CurrentUser, TodoistApi } from '@doist/todoist-api-typescript'
2+
import { jest } from '@jest/globals'
3+
import {
4+
TEST_ERRORS,
5+
extractStructuredContent,
6+
extractTextContent,
7+
} from '../../utils/test-helpers.js'
8+
import { ToolNames } from '../../utils/tool-names.js'
9+
import { userInfo } from '../user-info.js'
10+
11+
// Mock the Todoist API
12+
const mockTodoistApi = {
13+
getUser: jest.fn(),
14+
} as unknown as jest.Mocked<TodoistApi>
15+
16+
const { USER_INFO } = ToolNames
17+
18+
// Helper function to create a mock user with default values that can be overridden
19+
function createMockUser(overrides: Partial<CurrentUser> = {}): CurrentUser {
20+
return {
21+
id: '123',
22+
fullName: 'Test User',
23+
24+
isPremium: true,
25+
completedToday: 12,
26+
dailyGoal: 10,
27+
weeklyGoal: 100,
28+
startDay: 1, // Monday
29+
tzInfo: {
30+
timezone: 'Europe/Madrid',
31+
gmtString: '+02:00',
32+
hours: 2,
33+
minutes: 0,
34+
isDst: 1,
35+
},
36+
lang: 'en',
37+
avatarBig: 'https://example.com/avatar.jpg',
38+
avatarMedium: null,
39+
avatarS640: null,
40+
avatarSmall: null,
41+
karma: 86394.0,
42+
karmaTrend: 'up',
43+
nextWeek: 1,
44+
weekendStartDay: 6,
45+
timeFormat: 0,
46+
dateFormat: 0,
47+
daysOff: [6, 7],
48+
businessAccountId: null,
49+
completedCount: 102920,
50+
inboxProjectId: '6PVw8cMf7m8fWwRp',
51+
startPage: 'overdue',
52+
...overrides,
53+
}
54+
}
55+
56+
describe(`${USER_INFO} tool`, () => {
57+
beforeEach(() => {
58+
jest.clearAllMocks()
59+
})
60+
61+
it('should generate user info with all required fields', async () => {
62+
const mockUser = createMockUser()
63+
64+
mockTodoistApi.getUser.mockResolvedValue(mockUser)
65+
66+
const result = await userInfo.execute({}, mockTodoistApi)
67+
68+
expect(mockTodoistApi.getUser).toHaveBeenCalledWith()
69+
70+
// Test text content contains expected information
71+
const textContent = extractTextContent(result)
72+
expect(textContent).toContain('Test User')
73+
expect(textContent).toContain('[email protected]')
74+
expect(textContent).toContain('Europe/Madrid')
75+
expect(textContent).toContain('Monday (1)')
76+
expect(textContent).toContain('Completed Today:** 12')
77+
expect(textContent).toContain('Plan:** Todoist Pro')
78+
79+
// Test structured content
80+
const structuredContent = extractStructuredContent(result)
81+
expect(structuredContent).toEqual(
82+
expect.objectContaining({
83+
type: 'user_info',
84+
fullName: 'Test User',
85+
86+
timezone: 'Europe/Madrid',
87+
startDay: 1,
88+
startDayName: 'Monday',
89+
completedToday: 12,
90+
dailyGoal: 10,
91+
weeklyGoal: 100,
92+
plan: 'Todoist Pro',
93+
currentLocalTime: expect.any(String),
94+
weekStartDate: expect.any(String),
95+
weekEndDate: expect.any(String),
96+
currentWeekNumber: expect.any(Number),
97+
}),
98+
)
99+
100+
// Verify date formats
101+
expect(structuredContent.weekStartDate).toMatch(/^\d{4}-\d{2}-\d{2}$/)
102+
expect(structuredContent.weekEndDate).toMatch(/^\d{4}-\d{2}-\d{2}$/)
103+
expect(structuredContent.currentLocalTime).toMatch(
104+
/^\d{2}\/\d{2}\/\d{4}, \d{2}:\d{2}:\d{2}$/,
105+
)
106+
})
107+
108+
it('should handle missing timezone info', async () => {
109+
const mockUser = createMockUser({
110+
isPremium: false,
111+
tzInfo: {
112+
timezone: 'UTC',
113+
gmtString: '+00:00',
114+
hours: 0,
115+
minutes: 0,
116+
isDst: 0,
117+
},
118+
})
119+
120+
mockTodoistApi.getUser.mockResolvedValue(mockUser)
121+
122+
const result = await userInfo.execute({}, mockTodoistApi)
123+
124+
const textContent = extractTextContent(result)
125+
expect(textContent).toContain('UTC') // Should default to UTC
126+
expect(textContent).toContain('Monday (1)') // Should default to Monday
127+
expect(textContent).toContain('Plan:** Todoist Free')
128+
129+
const structuredContent = extractStructuredContent(result)
130+
expect(structuredContent.timezone).toBe('UTC')
131+
expect(structuredContent.startDay).toBe(1)
132+
expect(structuredContent.startDayName).toBe('Monday')
133+
expect(structuredContent.plan).toBe('Todoist Free')
134+
})
135+
136+
it('should handle invalid timezone and fallback to UTC', async () => {
137+
const mockUser = createMockUser({
138+
startDay: 2, // Tuesday
139+
tzInfo: {
140+
timezone: 'Invalid/Timezone',
141+
gmtString: '+05:30',
142+
hours: 5,
143+
minutes: 30,
144+
isDst: 0,
145+
},
146+
})
147+
148+
mockTodoistApi.getUser.mockResolvedValue(mockUser)
149+
150+
const result = await userInfo.execute({}, mockTodoistApi)
151+
152+
const textContent = extractTextContent(result)
153+
expect(textContent).toContain('UTC') // Should fallback to UTC
154+
expect(textContent).toContain('Tuesday (2)')
155+
156+
const structuredContent = extractStructuredContent(result)
157+
expect(structuredContent.timezone).toBe('UTC') // Should be UTC, not the invalid timezone
158+
expect(structuredContent.startDay).toBe(2)
159+
expect(structuredContent.startDayName).toBe('Tuesday')
160+
expect(structuredContent.currentLocalTime).toMatch(
161+
/^\d{2}\/\d{2}\/\d{4}, \d{2}:\d{2}:\d{2}$/,
162+
)
163+
})
164+
165+
it('should propagate API errors', async () => {
166+
const apiError = new Error(TEST_ERRORS.API_UNAUTHORIZED)
167+
mockTodoistApi.getUser.mockRejectedValue(apiError)
168+
169+
await expect(userInfo.execute({}, mockTodoistApi)).rejects.toThrow(
170+
TEST_ERRORS.API_UNAUTHORIZED,
171+
)
172+
})
173+
})

0 commit comments

Comments
 (0)