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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- `get_event_comment_image` retrieves one image from an event comment, including images posted by free accounts, and returns standard MCP image content over stdio without saving files. Supports JPEG, PNG, GIF, and WebP up to 5 MiB, with a 30-second download timeout, no redirects, and no authentication material sent to the image host.
- `list_event_comments` and comment write results now include an `images` array with image indices, filenames, and dimensions, including comments containing only an image. Image download locations are not exposed.

## [0.6.0] - 2026-10-18

### Added
Expand Down
21 changes: 21 additions & 0 deletions COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Quick reference for using TimeTree MCP Server with AI assistants.
| **delete_memo** | Delete a memo |
| **add_event_comment** | Add an event comment |
| **list_event_comments** | List event comments |
| **get_event_comment_image** | View one image attached to an event comment |
| **update_event_comment** | Update an event comment |
| **delete_event_comment** | Delete an event comment |
| **get_calendar_labels** | Get calendar labels |
Expand Down Expand Up @@ -254,9 +255,29 @@ Comments are visible to everyone in the calendar. TimeTree lets only a comment's
|------|----------------|
| `add_event_comment` | `calendar_id`, `event_uuid`, `content`, optional `silent` |
| `list_event_comments` | `calendar_id`, `event_uuid` |
| `get_event_comment_image` | `calendar_id`, `event_uuid`, `comment_id`, optional `image_index` (default `0`) |
| `update_event_comment` | `calendar_id`, `event_uuid`, `comment_id`, `content` |
| `delete_event_comment` | `calendar_id`, `event_uuid`, `comment_id` |

`list_event_comments` includes image-only comments. Each comment has an `images` array (empty for text-only comments), with `image_index` (starting at `0`), `filename`, `width`, and `height`. Missing metadata is `null`; dimensions may be numbers or strings. Image locations are not returned.

To view an image, pass its `comment_id` and `image_index` to `get_event_comment_image`:

```json
{
"calendar_id": "123",
"event_uuid": "example-event",
"comment_id": "example-comment",
"image_index": 0
}
```

The result contains JSON metadata (`calendar_id`, `event_uuid`, `comment_id`, `image_index`, `filename`, `width`, `height`, `mime_type`, `byte_size`) as text and `structuredContent`, followed by an MCP `image` content block with base64 data. This works over stdio; the client must support image tool results and image understanding. The server holds the image in memory and does not save a local file or return a download URL.

Images are fetched one at a time after checking the event's current comments, without sending session cookies or CSRF tokens to the image host. Downloads time out after 30 seconds, reject redirects, and accept JPEG, PNG, GIF, and WebP up to 5 MiB (before base64 encoding). Other formats and larger images return a tool error; images are not resized or converted. Comment images posted by free accounts are supported. Premium files attached directly to an event and image uploads are not supported.

Example prompt: "Read the image in the comments on my appointment and summarize it."

---

### calendar metadata tools
Expand Down
5 changes: 4 additions & 1 deletion README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ AI アシスタントに次のように聞くだけです。
## できること

- **カレンダーを読む。** 日付、キーワード、ラベルで予定を絞り込み、繰り返し予定は実際の日付に展開します。
- **コメントの画像を読む。** 無料会員が投稿した画像も含め、予定のコメントに添付された画像を AI クライアントに渡せます。
- **頼めば変更する。** 予定、メモ、コメントの作成、更新、コピー、削除と、ラベル名や色の変更ができます。繰り返し予定は1回分だけ、またはその回以降をまとめて変更・削除できます。
- **バーチャルメンバーを管理する。** 子どもやペットなど TimeTree アカウントのないメンバーを追加、変更、削除できます。
- **変更を把握する。** ある時点以降に変わった予定と、最近だれが何を変えたかを確認できます。
Expand Down Expand Up @@ -118,13 +119,15 @@ GUI クライアントが `node` を見つけられない場合は、`command -v
| 予定 | `get_events`, `get_updated_events`, `create_event`, `update_event`, `delete_event`, `copy_event` |
| 繰り返し予定 | `update_event_occurrence`, `delete_event_occurrence` |
| メモ | `list_memos`, `create_memo`, `update_memo`, `delete_memo` |
| コメント | `list_event_comments`, `add_event_comment`, `update_event_comment`, `delete_event_comment` |
| コメント | `list_event_comments`, `get_event_comment_image`, `add_event_comment`, `update_event_comment`, `delete_event_comment` |
| ラベルとメンバー | `get_calendar_labels`, `update_calendar_labels`, `get_calendar_members`, `get_calendar_virtual_members`, `create_virtual_member`, `update_virtual_member`, `delete_virtual_member` |
| その他 | `get_holidays`, `get_recent_activity` |

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

`list_event_comments` は添付画像の情報も返します。`get_event_comment_image` で1枚ずつ取得し、stdio 経由で MCP 標準の画像データとして返します。クライアント側がツールの画像応答に対応している必要があります。JPEG・PNG・GIF・WebP、最大 5 MiB に対応し、画像はメモリにだけ保持してディスクには保存しません。予定に直接添付されたプレミアムのファイルと画像のアップロードは対象外です。

パラメータと使用例: [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 @@ -36,6 +36,7 @@ AI 어시스턴트에게 이렇게 물어보면 됩니다.
## 할 수 있는 일

- **캘린더를 읽습니다.** 날짜, 키워드, 라벨로 일정을 거르고, 반복 일정은 실제 날짜로 펼쳐서 보여줍니다.
- **댓글 이미지를 읽습니다.** 무료 회원이 올린 이미지를 포함해 일정 댓글에 첨부된 이미지를 AI 클라이언트에 전달합니다.
- **요청하면 바꿉니다.** 일정, 메모, 댓글을 만들고 고치고 복사하고 지우며, 라벨 이름과 색도 바꿉니다. 반복 일정은 한 회차만, 또는 그 회차부터 이후 전부를 고치거나 지울 수 있습니다.
- **가상 멤버를 관리합니다.** 아이나 반려동물처럼 TimeTree 계정이 없는 멤버를 추가하고 고치고 지웁니다.
- **변경 사항을 알려줍니다.** 특정 시점 이후 바뀐 일정과 최근에 누가 무엇을 바꿨는지 확인합니다.
Expand Down Expand Up @@ -118,13 +119,15 @@ GUI 클라이언트가 `node`를 찾지 못하면 `command -v node`로 나온
| 일정 | `get_events`, `get_updated_events`, `create_event`, `update_event`, `delete_event`, `copy_event` |
| 반복 일정 | `update_event_occurrence`, `delete_event_occurrence` |
| 메모 | `list_memos`, `create_memo`, `update_memo`, `delete_memo` |
| 댓글 | `list_event_comments`, `add_event_comment`, `update_event_comment`, `delete_event_comment` |
| 댓글 | `list_event_comments`, `get_event_comment_image`, `add_event_comment`, `update_event_comment`, `delete_event_comment` |
| 라벨과 멤버 | `get_calendar_labels`, `update_calendar_labels`, `get_calendar_members`, `get_calendar_virtual_members`, `create_virtual_member`, `update_virtual_member`, `delete_virtual_member` |
| 기타 | `get_holidays`, `get_recent_activity` |

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

`list_event_comments`는 첨부 이미지 정보도 반환합니다. `get_event_comment_image`로 한 장씩 가져와 stdio를 통해 MCP 표준 이미지 데이터로 전달합니다. 클라이언트가 도구의 이미지 응답을 지원해야 합니다. JPEG·PNG·GIF·WebP, 최대 5 MiB를 지원하며 이미지는 메모리에만 두고 디스크에 저장하지 않습니다. 일정에 직접 첨부된 프리미엄 파일과 이미지 업로드는 지원하지 않습니다.

파라미터와 사용 예시: [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 @@ -36,6 +36,7 @@ Ask your AI assistant things like:
## What it does

- **Reads your calendars.** Events with date, keyword, and label filters. Recurring events are expanded into their actual dates.
- **Reads comment images.** Pass images attached to event comments to your AI client, including images posted by free accounts.
- **Changes them when you ask.** Create, update, copy, and delete events, memos, and comments, and rename or recolor labels. Change or delete a single occurrence of a recurring event, or it and the ones after it.
- **Manages virtual members.** Add, rename, or remove members without a TimeTree account, such as children or pets.
- **Keeps you up to date.** See what changed since a given time, and who made recent changes.
Expand Down Expand Up @@ -118,7 +119,7 @@ Details: [docs/UPDATING.md](docs/UPDATING.md). Changes: [CHANGELOG.md](CHANGELOG
| Events | `get_events`, `get_updated_events`, `create_event`, `update_event`, `delete_event`, `copy_event` |
| Recurring events | `update_event_occurrence`, `delete_event_occurrence` |
| Memos | `list_memos`, `create_memo`, `update_memo`, `delete_memo` |
| Comments | `list_event_comments`, `add_event_comment`, `update_event_comment`, `delete_event_comment` |
| Comments | `list_event_comments`, `get_event_comment_image`, `add_event_comment`, `update_event_comment`, `delete_event_comment` |
| Labels and members | `get_calendar_labels`, `update_calendar_labels`, `get_calendar_members`, `get_calendar_virtual_members`, `create_virtual_member`, `update_virtual_member`, `delete_virtual_member` |
| Other | `get_holidays`, `get_recent_activity` |

Expand All @@ -127,6 +128,8 @@ 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.

`list_event_comments` includes image metadata. Use `get_event_comment_image` to retrieve one image as standard MCP image content over stdio; the client must support image tool results. JPEG, PNG, GIF, and WebP up to 5 MiB are supported. Images stay in memory and are not saved to disk. Premium files attached directly to an event and image uploads are not supported.

## 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
4 changes: 3 additions & 1 deletion docs/FEATURE_CANDIDATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ TimeTree web app features that this server does not expose as tools yet, with wh

How these were observed: reading the web app's JavaScript bundle (its generated API client and the in-app AI assistant's tool definitions) and calling the endpoints from a signed-in browser session against a test calendar with made-up data, then deleting the test data. Paths are relative to `https://timetreeapp.com/api`. Behavior can change without notice.

Comment image reading uses the web app's `attachment.images` entries (`object_key`, `filename`, `width`, `height`) and its fixed `https://attachments.timetreeapp.com/` prefix. This read path was checked against the public JavaScript bundle, covered with mock tests, and verified end to end in Codex over stdio: authenticated comment reads, JPEG retrieval as MCP image content, and reading text from the returned image.

## Waiting for a decision

These work, but each needs a decision before it becomes a tool.
Expand All @@ -15,7 +17,7 @@ These work, but each needs a decision before it becomes a tool.
| Join or leave an event | Add or remove yourself as an attendee | `POST` / `DELETE /v1/calendar/{cid}/event/{eid}/attendees` (no body) | Verified | `update_event` with `attendees` already covers this; a separate tool is only a convenience. |
| Rename a calendar | Change a calendar's name (and badge image, and whether album photos keep their location) | `PUT /v1/calendar/{cid}` with `{"name": "..."}` | Verified (name) | Changes the name for every member of a shared calendar. `{"media_content_attach_location": null}` returned `-403`; badge needs an image upload first (see below). |
| Calendar album | List, upload, and delete photos in a calendar's album | List: `GET /v1/calendars/{cid}/media_contents`. Upload: `POST .../media_contents/presigned_urls` with `{"files": [{"filename", "taken_at", "location_lat", "location_lon", "request_id"}]}`, then a form POST of the file to the returned S3 `presigned_post`, then poll `GET .../media_contents/{uuid}/status` until `complete`. Delete: `DELETE .../media_contents` with `{"media_content_uuids": [...]}` | Verified end to end | Upload needs a local file path as tool input, and there is a monthly upload limit (error `-607`). Listing alone could ship first. |
| Image in a comment | Attach a photo to an event comment | `POST /v1/attachments` as multipart with `type=calendar`, `id={cid}`, `files[]=<image>` returns `{"attachments": [{"object_key", "filename", "width", "height"}]}`; then `POST /v1/calendar/{cid}/event/{eid}/activity` with `{"id", "attachment": {"images": <attachments>}, "silent": true}` | Verified | Same local-file input question as the album. |
| Upload an image in a comment | Attach a photo to an event comment | `POST /v1/attachments` as multipart with `type=calendar`, `id={cid}`, `files[]=<image>` returns `{"attachments": [{"object_key", "filename", "width", "height"}]}`; then `POST /v1/calendar/{cid}/event/{eid}/activity` with `{"id", "attachment": {"images": <attachments>}, "silent": true}` | Verified | Same local-file input question as the album. Reading existing comment images is supported by `list_event_comments` and `get_event_comment_image`. |
| Batch create, update, delete | Several events in one call | No dedicated endpoint; the web app's assistant loops over the single-event endpoints | Not built | A wrong call affects many events at once; needs confirmation behavior. |
| Find free time | Free slots for one or more members in a range | No endpoint; computed from events | Not built | Needs a definition of working hours and of which events block time. |

Expand Down
3 changes: 2 additions & 1 deletion mcpb/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@
{ "name": "create_memo", "description": "Create a memo." },
{ "name": "update_memo", "description": "Change a memo." },
{ "name": "delete_memo", "description": "Delete a memo." },
{ "name": "list_event_comments", "description": "List comments on an event." },
{ "name": "list_event_comments", "description": "List comments and their attached image metadata." },
{ "name": "get_event_comment_image", "description": "View an image attached to an event comment." },
{ "name": "add_event_comment", "description": "Comment on an event." },
{ "name": "update_event_comment", "description": "Edit a comment." },
{ "name": "delete_event_comment", "description": "Delete a comment." },
Expand Down
15 changes: 15 additions & 0 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +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 { downloadCommentImage } from '../utils/comment-image.js';
import { AuthenticationError, TimeTreeAuthManager } from './auth.js';
import type {
Calendar,
Expand Down Expand Up @@ -1122,6 +1123,20 @@ export class TimeTreeAPIClient {
}
}

/** Resolve an image through the event's current comments before downloading it. */
async getEventCommentImage(calendarId: string, eventUuid: string, commentId: string, imageIndex: number) {
if (!Number.isSafeInteger(imageIndex) || imageIndex < 0) {
throw new TimeTreeAPIError('image_index must be a non-negative integer.', 400);
}
const comments = await this.listEventComments(calendarId, eventUuid);
const comment = comments.find((candidate) => candidate.id === commentId);
if (!comment) throw new TimeTreeAPIError('Comment not found.', 404);
const image = comment.attachment?.images?.[imageIndex];
if (!image) throw new TimeTreeAPIError('Image not found. Check images in list_event_comments.', 404);
const downloaded = await this.rateLimiter.executeWithRetry(() => downloadCommentImage(image.object_key));
return { image, ...downloaded };
}

async updateEventComment(
calendarId: string,
eventUuid: string,
Expand Down
1 change: 1 addition & 0 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const TIMETREE_CONFIG = {
// Base URLs for TimeTree APIs.
BASE_URL: 'https://timetreeapp.com/api/v1',
V2_BASE_URL: 'https://timetreeapp.com/api/v2',
ATTACHMENT_BASE_URL: 'https://attachments.timetreeapp.com/',

// API Endpoints.
ENDPOINTS: {
Expand Down
Loading
Loading