Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Added `create_calendar` without member invitations, requiring an explicit name and purpose. Uses existing rate limiting and authentication recovery without retrying timeouts or server errors.
- Calendar creation distinguishes invalid fields and authentication failures without forwarding upstream details.

### Fixed
- Calendar creation treats invalid success responses as uncertain outcomes instead of invalid user input, and reports persistent TimeTree session or CSRF rejections as authentication failures.
- Authentication recovery no longer retries HTTP 5xx responses, even when their bodies contain session or CSRF error codes.

### Security
- Updated locked MCP SDK and proxy-addr dependencies to resolve high and critical audit findings.

## [0.4.0] - 2026-10-03

### Added
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Important context:

The server currently exposes tools for:

- Listing calendars.
- Listing and creating calendars (creation does not invite members).
- Reading calendar events and updated events.
- Creating, updating, and deleting events.
- Managing event comments.
Expand Down
14 changes: 14 additions & 0 deletions COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,3 +291,17 @@ Events can be color-coded with `label_id` 1-10:
- **All-day events**: TimeTree uses inclusive end dates. A Feb 15-16 event sets `end_at` to Feb 16 00:00, not Feb 17.
- **Timezones**: Default is UTC. Specify timezone for accurate local times.
- **Write operations** (create/update/delete) require a CSRF token, which is managed automatically.

### Create a calendar

Call `create_calendar` with both `name` (1–20 characters) and an explicit `purpose`:

```json
{"name": "Project calendar", "purpose": "work"}
```

Supported purposes: `family`, `private`, `lover`, `work`, `friend`, `work_schedule`, `lesson`, `school_event`, `circle`, `hobby`, `other`.
Creation does not invite members. Requests use rate limiting and retry HTTP 429 rejections or rejected authentication through the shared client. Timeouts and server errors are not retried.
Invalid input returns field-level reasons without input values. Authentication errors return `Authentication failed`.
Persistent TimeTree session or CSRF rejections are reported as authentication failures after one sign-in recovery attempt.
After an uncertain creation failure (such as a timeout or an unexpected success response), check existing calendars before retrying. Response validation failures are not reported as invalid input. HTTP 5xx responses are never retried, even if their bodies contain authentication error codes.
5 changes: 4 additions & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,16 @@ GUI クライアントが `node` を見つけられない場合は、`command -v

| 分類 | ツール |
|---|---|
| カレンダー | `list_calendars` |
| カレンダー | `list_calendars`, `create_calendar` |
| 予定 | `get_events`, `get_updated_events`, `create_event`, `update_event`, `delete_event` |
| メモ | `list_memos`, `create_memo`, `update_memo`, `delete_memo` |
| コメント | `list_event_comments`, `add_event_comment`, `update_event_comment`, `delete_event_comment` |
| ラベルとメンバー | `get_calendar_labels`, `update_calendar_labels`, `get_calendar_members`, `get_calendar_virtual_members` |
| その他 | `get_holidays`, `get_recent_activity` |

`create_calendar` は名前(1〜20文字)と用途の明示を必須とします。メンバー招待は行わず、タイムアウト・サーバーエラーでは再試行しません。
作成後の応答を検証できない場合は、再実行前に既存のカレンダーを確認してください。

パラメータと使用例: [COMMANDS.md](COMMANDS.md)

## プライバシーとセキュリティ
Expand Down
5 changes: 4 additions & 1 deletion README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,16 @@ GUI 클라이언트가 `node`를 찾지 못하면 `command -v node`로 나온

| 영역 | 툴 |
|---|---|
| 캘린더 | `list_calendars` |
| 캘린더 | `list_calendars`, `create_calendar` |
| 일정 | `get_events`, `get_updated_events`, `create_event`, `update_event`, `delete_event` |
| 메모 | `list_memos`, `create_memo`, `update_memo`, `delete_memo` |
| 댓글 | `list_event_comments`, `add_event_comment`, `update_event_comment`, `delete_event_comment` |
| 라벨과 멤버 | `get_calendar_labels`, `update_calendar_labels`, `get_calendar_members`, `get_calendar_virtual_members` |
| 기타 | `get_holidays`, `get_recent_activity` |

`create_calendar`에는 이름(1~20자)과 명시적인 용도가 필수입니다. 멤버를 초대하지 않으며 시간 초과나 서버 오류는 재시도하지 않습니다.
생성 응답을 검증할 수 없으면 다시 시도하기 전에 기존 캘린더를 확인하세요.

파라미터와 사용 예시: [COMMANDS.md](COMMANDS.md)

## 개인정보와 보안
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ Details: [docs/UPDATING.md](docs/UPDATING.md). Changes: [CHANGELOG.md](CHANGELOG

| Area | Tools |
|---|---|
| Calendars | `list_calendars` |
| Calendars | `list_calendars`, `create_calendar` |
| Events | `get_events`, `get_updated_events`, `create_event`, `update_event`, `delete_event` |
| Memos | `list_memos`, `create_memo`, `update_memo`, `delete_memo` |
| Comments | `list_event_comments`, `add_event_comment`, `update_event_comment`, `delete_event_comment` |
Expand All @@ -122,6 +122,9 @@ Details: [docs/UPDATING.md](docs/UPDATING.md). Changes: [CHANGELOG.md](CHANGELOG

Parameters and examples: [COMMANDS.md](COMMANDS.md)

`create_calendar` requires a name (1–20 characters) and an explicit purpose. It does not invite members. Timeouts and server errors are not retried.
If the creation response cannot be validated, check existing calendars before trying again.

## Privacy and security

- Your email and password are stored only in your MCP client configuration, or in Claude Desktop's extension settings. They are sent only to TimeTree.
Expand Down
1 change: 1 addition & 0 deletions mcpb/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
},
"tools": [
{ "name": "list_calendars", "description": "List your TimeTree calendars." },
{ "name": "create_calendar", "description": "Create a calendar with an explicit name and purpose, without inviting members." },
{ "name": "get_events", "description": "Read events with date, keyword, and label filters; expands recurring events." },
{ "name": "get_updated_events", "description": "Find events changed after a point in time." },
{ "name": "create_event", "description": "Create an event." },
Expand Down
16 changes: 10 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 33 additions & 2 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { randomUUID } from 'crypto';
import { TIMETREE_CONFIG } from '../config/config.js';
import { RateLimiter } from '../utils/rate-limiter.js';
import { logger } from '../utils/logger.js';
import { TimeTreeAuthManager } from './auth.js';
import { AuthenticationError, TimeTreeAuthManager } from './auth.js';
import type {
Calendar,
CalendarLabel,
Expand Down Expand Up @@ -36,6 +36,7 @@ import type {
} from '../types/timetree.js';
import {
CalendarLabelsResponseSchema,
CalendarSchema,
CalendarUsersResponseSchema,
CalendarVirtualUsersResponseSchema,
CalendarsResponseSchema,
Expand Down Expand Up @@ -129,7 +130,10 @@ function isRequestShapeRejected(error: unknown): boolean {

/** Signing in again fetches a fresh session and CSRF token, so either rejection is recoverable. */
function needsReauthentication(error: unknown): boolean {
if (getStatusCode(error) === 401) return true;
const statusCode = getStatusCode(error);
// A server error leaves write outcomes uncertain, even when its body contains an auth code.
if (statusCode !== undefined && statusCode >= 500) return false;
if (statusCode === 401) return true;
const code = getTimeTreeErrorCode(error);
return code === TIMETREE_ERROR.SESSION_REJECTED || code === TIMETREE_ERROR.CSRF_REJECTED;
}
Expand Down Expand Up @@ -258,6 +262,33 @@ export class TimeTreeAPIClient {
}
}

/** Create a calendar without invitations. Rate limited; timeouts and server errors are not retried. */
async createCalendar(name: string, purpose: string): Promise<Calendar> {
if (!name.trim() || name.trim().length > 20) {
throw new TimeTreeAPIError('Calendar name must contain 1 to 20 characters', 400);
}
await this.ensureAuthenticated();
// Precondition check: writes require a CSRF token before sending the request.
this.authManager.getCsrfToken();
try {
const response = await this.send(() =>
this.authManager.getHttpClient().post<{ calendar: unknown }>(
`${TIMETREE_CONFIG.V2_BASE_URL}/calendars`,
{ name: name.trim(), purpose }, undefined, true
)
);
return CalendarSchema.parse(response.calendar);
} catch (error) {
if (getStatusCode(error) === 403) {
throw new TimeTreeAPIError('CSRF token missing or invalid - re-authentication required', 403);
}
if (needsReauthentication(error)) {
throw new AuthenticationError('Session or CSRF token rejected after re-authentication', getStatusCode(error));
}
throw error;
}
}

/**
* Fetch every page of an event feed. TimeTree splits large responses into chunks: while
* `chunk` is true, the returned `since` is the cursor for the next page.
Expand Down
59 changes: 59 additions & 0 deletions src/tools/calendar-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { z } from 'zod';
import type { TimeTreeAPIClient } from '../client/api.js';
import { AuthenticationError } from '../client/auth.js';
import { logger } from '../utils/logger.js';

export const ListCalendarsInputSchema = z.object({});
Expand Down Expand Up @@ -80,3 +81,61 @@ export function createListCalendarsTool(apiClient: TimeTreeAPIClient) {
},
};
}

export const CreateCalendarInputSchema = z.object({
name: z.string().trim().min(1).max(20),
purpose: z.enum(['family', 'private', 'lover', 'work', 'friend', 'work_schedule',
'lesson', 'school_event', 'circle', 'hobby', 'other']),
}).strict();

export function createCreateCalendarTool(apiClient: TimeTreeAPIClient) {
return {
name: 'create_calendar',
description: 'Create a calendar owned by the authenticated user. Does not invite members or change existing calendars. Purpose is required. Requests are rate-limited; timeouts and server errors are not retried.',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 20 },
purpose: { type: 'string', enum: ['family', 'private', 'lover', 'work', 'friend',
'work_schedule', 'lesson', 'school_event', 'circle', 'hobby', 'other'] },
},
required: ['name', 'purpose'],
additionalProperties: false,
},
handler: async (args: unknown) => {
const parsed = CreateCalendarInputSchema.safeParse(args);
if (!parsed.success) {
const issues = parsed.error.issues.map((issue) => ({
field: issue.path[0] === 'name' || issue.path[0] === 'purpose' ? issue.path[0] : 'input',
reason: issue.code === 'too_small' ? 'Must contain at least 1 character'
: issue.code === 'too_big' ? 'Must contain at most 20 characters'
: issue.code === 'invalid_enum_value' ? 'Choose a supported calendar purpose'
: issue.code === 'unrecognized_keys' ? 'Only name and purpose are allowed'
: 'Required field with a supported type and value',
}));
return { content: [{ type: 'text', text: JSON.stringify({
error: 'Invalid calendar input', issues,
}) }], isError: true };
}
// Response validation can fail after the calendar was created; keep it out of input errors.
try {
const calendar = await apiClient.createCalendar(parsed.data.name, parsed.data.purpose);
return { content: [{ type: 'text', text: JSON.stringify({
id: String(calendar.id), name: calendar.name, alias_code: calendar.alias_code ?? null,
}) }] };
} catch (error) {
const statusCode = error && typeof error === 'object' && 'statusCode' in error
? error.statusCode : undefined;
if (error instanceof AuthenticationError || statusCode === 401 || statusCode === 403) {
return { content: [{ type: 'text', text: JSON.stringify({
error: 'Authentication failed',
}) }], isError: true };
}
logger.error('Tool: create_calendar failed', { error });
return { content: [{ type: 'text', text: JSON.stringify({
error: 'Calendar creation failed. Check existing calendars before retrying.',
}) }], isError: true };
}
},
};
}
3 changes: 2 additions & 1 deletion src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import type { TimeTreeAPIClient } from '../client/api.js';
import { createListCalendarsTool } from './calendar-tools.js';
import { createCreateCalendarTool, createListCalendarsTool } from './calendar-tools.js';
import {
createGetCalendarLabelsTool,
createGetCalendarMembersTool,
Expand Down Expand Up @@ -35,6 +35,7 @@ import {
export function registerTools(apiClient: TimeTreeAPIClient) {
return [
createListCalendarsTool(apiClient),
createCreateCalendarTool(apiClient),
createGetEventsTool(apiClient),
createGetUpdatedEventsTool(apiClient),
createCreateEventTool(apiClient),
Expand Down
Loading
Loading