From dbec226cab9a4b8b28f1ab19a86f165ec3fbf5b6 Mon Sep 17 00:00:00 2001 From: basil1015 Date: Mon, 22 Jun 2026 01:24:15 +0900 Subject: [PATCH 1/4] Limit server to read-only tools Comment out write tools (create/update/delete/add for events, memos, comments, and label updates) in the tool registry so the server only exposes listing/getting tools. Tool definitions and API methods are kept intact so the write tools can be restored by uncommenting. Also comment out the now-unused imports to satisfy noUnusedLocals, and update the tool-registration test to expect the read-only tool set. Co-Authored-By: Claude Opus 4.8 --- src/tools/index.ts | 46 +++++++++++++++++++++++--------------------- tests/tools.test.mjs | 14 +++----------- 2 files changed, 27 insertions(+), 33 deletions(-) diff --git a/src/tools/index.ts b/src/tools/index.ts index e9329ec..87eeda2 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -9,25 +9,26 @@ import { createGetCalendarLabelsTool, createGetCalendarMembersTool, createGetCalendarVirtualMembersTool, - createUpdateCalendarLabelsTool, + // createUpdateCalendarLabelsTool, // 쓰기 기능 비활성화 (조회 전용) } from './calendar-metadata-tools.js'; import { - createAddEventCommentTool, - createDeleteEventCommentTool, + // createAddEventCommentTool, // 쓰기 기능 비활성화 (조회 전용) + // createDeleteEventCommentTool, // 쓰기 기능 비활성화 (조회 전용) createListEventCommentsTool, - createUpdateEventCommentTool, + // createUpdateEventCommentTool, // 쓰기 기능 비활성화 (조회 전용) } from './comment-tools.js'; -import { - createCreateEventTool, - createUpdateEventTool, - createDeleteEventTool, -} from './event-crud-tools.js'; +// 쓰기 기능 비활성화 (조회 전용): 이벤트 생성/수정/삭제 import 전체 주석 처리 +// import { +// createCreateEventTool, +// createUpdateEventTool, +// createDeleteEventTool, +// } from './event-crud-tools.js'; import { createGetEventsTool, createGetUpdatedEventsTool } from './event-tools.js'; import { - createCreateMemoTool, - createDeleteMemoTool, + // createCreateMemoTool, // 쓰기 기능 비활성화 (조회 전용) + // createDeleteMemoTool, // 쓰기 기능 비활성화 (조회 전용) createListMemosTool, - createUpdateMemoTool, + // createUpdateMemoTool, // 쓰기 기능 비활성화 (조회 전용) } from './memo-tools.js'; export function registerTools(apiClient: TimeTreeAPIClient) { @@ -35,19 +36,20 @@ export function registerTools(apiClient: TimeTreeAPIClient) { createListCalendarsTool(apiClient), createGetEventsTool(apiClient), createGetUpdatedEventsTool(apiClient), - createCreateEventTool(apiClient), - createUpdateEventTool(apiClient), - createDeleteEventTool(apiClient), + // 쓰기 기능 비활성화 (조회 전용) + // createCreateEventTool(apiClient), + // createUpdateEventTool(apiClient), + // createDeleteEventTool(apiClient), createListMemosTool(apiClient), - createCreateMemoTool(apiClient), - createUpdateMemoTool(apiClient), - createDeleteMemoTool(apiClient), - createAddEventCommentTool(apiClient), + // createCreateMemoTool(apiClient), + // createUpdateMemoTool(apiClient), + // createDeleteMemoTool(apiClient), + // createAddEventCommentTool(apiClient), createListEventCommentsTool(apiClient), - createUpdateEventCommentTool(apiClient), - createDeleteEventCommentTool(apiClient), + // createUpdateEventCommentTool(apiClient), + // createDeleteEventCommentTool(apiClient), createGetCalendarLabelsTool(apiClient), - createUpdateCalendarLabelsTool(apiClient), + // createUpdateCalendarLabelsTool(apiClient), createGetCalendarMembersTool(apiClient), createGetCalendarVirtualMembersTool(apiClient), ]; diff --git a/tests/tools.test.mjs b/tests/tools.test.mjs index 550da8c..f45a9c7 100644 --- a/tests/tools.test.mjs +++ b/tests/tools.test.mjs @@ -31,16 +31,12 @@ function parseToolText(result) { return JSON.parse(result.content[0].text); } -test('registerTools exposes baseline plus Wave 1 tools', () => { +test('registerTools exposes read-only tools (write tools disabled)', () => { const names = registerTools({}).map((tool) => tool.name).sort(); + // Read-only build: write tools (create/update/delete/add) are commented out + // in src/tools/index.ts. Only listing/getting tools are registered. assert.deepEqual(names, [ - 'add_event_comment', - 'create_event', - 'create_memo', - 'delete_event', - 'delete_event_comment', - 'delete_memo', 'get_calendar_labels', 'get_calendar_members', 'get_calendar_virtual_members', @@ -49,10 +45,6 @@ test('registerTools exposes baseline plus Wave 1 tools', () => { 'list_calendars', 'list_event_comments', 'list_memos', - 'update_calendar_labels', - 'update_event', - 'update_event_comment', - 'update_memo', ].sort()); }); From eb78e4f999987bfa5465e7230eeeccf7a1477d51 Mon Sep 17 00:00:00 2001 From: basil1015 Date: Mon, 22 Jun 2026 01:24:23 +0900 Subject: [PATCH 2/4] Allow null calendar_user name from upstream TimeTree may return a null name for calendar users (e.g. deactivated members), which made CalendarUserSchema.parse throw and surfaced as "Failed to fetch calendars" in list_calendars. Make the name field optional and nullable, matching the virtual-user and label schemas. Add a regression test asserting getCalendars tolerates a null member name. Co-Authored-By: Claude Opus 4.8 --- src/types/timetree.ts | 2 +- tests/api-client.test.mjs | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/types/timetree.ts b/src/types/timetree.ts index 5650e45..234eea6 100644 --- a/src/types/timetree.ts +++ b/src/types/timetree.ts @@ -35,7 +35,7 @@ export type EventAttachmentInput = z.infer; export const CalendarUserSchema = z.object({ id: z.number(), user_id: z.number(), - name: z.string(), + name: z.string().optional().nullable(), // upstream may return null (e.g. deactivated users) role: z.number().optional(), // 1 = owner, 0 = member deactivated_at: z.number().nullable().optional(), birth_day: z.number().optional().nullable(), diff --git a/tests/api-client.test.mjs b/tests/api-client.test.mjs index de6cac0..849f79b 100644 --- a/tests/api-client.test.mjs +++ b/tests/api-client.test.mjs @@ -166,3 +166,27 @@ test('event comment methods use activity endpoints and filter comment activities assert.ok(calls.some((call) => call.url === 'https://timetreeapp.com/api/v1/calendar/123/event/evt/activities')); assert.ok(calls.some((call) => call.url === 'https://timetreeapp.com/api/v1/calendar/123/event/evt/activity/comment')); }); + +test('getCalendars tolerates calendar_users with null name (deactivated users)', async () => { + const client = makeClient({ + get: async () => ({ + calendars: [ + { + id: 1, + name: 'Active calendar', + calendar_users: [ + { id: 1, user_id: 1, name: 'Alice', role: 1 }, + // deactivated member: upstream returns null name + { id: 2, user_id: 2, name: null, deactivated_at: 1700000000000 }, + ], + }, + ], + }), + }); + + const calendars = await client.getCalendars(); + + assert.equal(calendars.length, 1); + assert.equal(calendars[0].name, 'Active calendar'); + assert.equal(calendars[0].calendar_users[1].name, null); +}); From 9f057468325b226a6a5f5f22b6f239c9b6db3904 Mon Sep 17 00:00:00 2001 From: basil1015 Date: Sun, 28 Jun 2026 02:44:13 +0900 Subject: [PATCH 3/4] Add end_before filter and date-range params to get_events - Add startAt/endAt optional params to syncEvents and getEventsByCalendar so the TimeTree sync endpoint receives start_at/end_at query params - Add end_before parameter to get_events MCP tool for symmetric date filtering - Apply both client-side filters (start_after, end_before) regardless of server-side support as a guaranteed fallback Co-Authored-By: Claude Sonnet 4.6 --- src/client/api.ts | 20 +++++++++++++------- src/tools/event-tools.ts | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/client/api.ts b/src/client/api.ts index 305fb25..ab41f6c 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -133,13 +133,17 @@ export class TimeTreeAPIClient { private async syncEvents( calendarId: string, since: number = 0, - accumulated: Event[] = [] + accumulated: Event[] = [], + startAt?: number, + endAt?: number, ): Promise { - const url = `${TIMETREE_CONFIG.BASE_URL}${TIMETREE_CONFIG.ENDPOINTS.EVENTS_SYNC( + let url = `${TIMETREE_CONFIG.BASE_URL}${TIMETREE_CONFIG.ENDPOINTS.EVENTS_SYNC( calendarId )}?since=${since}`; + if (startAt !== undefined) url += `&start_at=${startAt}`; + if (endAt !== undefined) url += `&end_at=${endAt}`; - logger.debug('Syncing events', { calendarId, since, accumulated: accumulated.length }); + logger.debug('Syncing events', { calendarId, since, startAt, endAt, accumulated: accumulated.length }); try { const response = await this.rateLimiter.executeWithRetry(async () => { @@ -156,7 +160,7 @@ export class TimeTreeAPIClient { nextSince: validated.since, fetchedSoFar: accumulated.length, }); - return this.syncEvents(calendarId, validated.since, accumulated); + return this.syncEvents(calendarId, validated.since, accumulated, startAt, endAt); } logger.debug('Event sync complete', { @@ -179,13 +183,15 @@ export class TimeTreeAPIClient { /** Get all events from a calendar (handles pagination automatically). */ async getEventsByCalendar( calendarId: string, - since: number = 0 + since: number = 0, + startAt?: number, + endAt?: number, ): Promise { await this.ensureAuthenticated(); - logger.info('Fetching events for calendar', { calendarId, since }); + logger.info('Fetching events for calendar', { calendarId, since, startAt, endAt }); - const events = await this.syncEvents(calendarId, since); + const events = await this.syncEvents(calendarId, since, [], startAt, endAt); logger.info('Events fetched successfully', { calendarId, diff --git a/src/tools/event-tools.ts b/src/tools/event-tools.ts index 0a5c45d..56cff15 100644 --- a/src/tools/event-tools.ts +++ b/src/tools/event-tools.ts @@ -17,6 +17,12 @@ export const GetEventsInputSchema = z.object({ .describe( 'Optional Unix timestamp in milliseconds. Only return events starting after this time.' ), + end_before: z + .number() + .optional() + .describe( + 'Optional Unix timestamp in milliseconds. Only return events starting before this time.' + ), limit: z .number() .optional() @@ -38,7 +44,8 @@ export function createGetEventsTool(apiClient: TimeTreeAPIClient) { return { name: 'get_events', description: - 'Get all events from a specific TimeTree calendar. Automatically handles pagination to fetch all events. ' + + 'Get events from a specific TimeTree calendar. Automatically handles pagination. ' + + 'Use start_after and end_before to filter by date range (both are optional). ' + 'Returns event details including title, start/end times, location, notes, label color, and more. ' + 'Label colors (label_id 1-10): 1=Emerald green, 2=Modern cyan, 3=Deep sky blue, 4=Pastel brown, ' + '5=Midnight black, 6=Apple red, 7=French rose, 8=Coral pink, 9=Bright orange, 10=Soft violet.', @@ -53,7 +60,13 @@ export function createGetEventsTool(apiClient: TimeTreeAPIClient) { type: 'number', description: 'Optional Unix timestamp in milliseconds. Only return events starting after this time. ' + - 'If user provides a date like "2026-02-01", convert it to Unix timestamp (e.g., 1769904000000).', + 'If user provides a date like "2026-02-01", convert it to Unix timestamp (e.g., 1738368000000).', + }, + end_before: { + type: 'number', + description: + 'Optional Unix timestamp in milliseconds. Only return events starting before this time. ' + + 'If user provides a date like "2026-12-31", convert it to Unix timestamp (e.g., 1767139200000).', }, limit: { type: 'number', @@ -65,16 +78,26 @@ export function createGetEventsTool(apiClient: TimeTreeAPIClient) { handler: async (args: unknown) => { try { const input = GetEventsInputSchema.parse(args); - const { calendar_id, start_after, limit } = input; + const { calendar_id, start_after, end_before, limit } = input; - logger.info('Tool: get_events called', { calendar_id, start_after, limit }); + logger.info('Tool: get_events called', { calendar_id, start_after, end_before, limit }); - const events = await apiClient.getEventsByCalendar(calendar_id, 0); + // Pass date range to the API — the server may use start_at/end_at to narrow + // the time window it returns. Client-side filters below act as a guaranteed fallback. + const events = await apiClient.getEventsByCalendar( + calendar_id, + 0, + start_after, + end_before, + ); - // Filter by start_after if provided + // Client-side date filters (always applied regardless of server-side support) let filteredEvents = events; if (start_after) { - filteredEvents = events.filter((event) => event.start_at > start_after); + filteredEvents = filteredEvents.filter((event) => event.start_at > start_after); + } + if (end_before) { + filteredEvents = filteredEvents.filter((event) => event.start_at < end_before); } // Limit results if provided From a8903a6f337c2093c0f470bef5388de02d2c1b67 Mon Sep 17 00:00:00 2001 From: basil1015 Date: Sun, 28 Jun 2026 02:46:01 +0900 Subject: [PATCH 4/4] Bump hono to 4.12.27 to fix high-severity CVE Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index baa5a1a..6f6a0b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1605,9 +1605,9 @@ } }, "node_modules/hono": { - "version": "4.12.23", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", - "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", "license": "MIT", "engines": { "node": ">=16.9.0"