Tiny-Todo-MCP: LLMと相性のよい実装を考えたTODOアプリケーションの設計を考える
TL;DR
- Model Context Protocol (MCP)を活用したTODOアプリ開発。
- LLMの特性を活かした設計アプローチを考慮し、「自然言語での指示」と「AIによる解釈・拡張」を中心に据えたTODO管理システムの開発記録です。
はじめに
「MCPとLLMを使ってTODOアプリを作ったらどうなるだろう?」という疑問からスタートしました。
MCPはAnthropicが提唱した新しいプロトコルで、LLMが外部ツールやAPIを使えるようにするものです。これを活用すれば「やっぱり来週にします」のような自然な言葉でTODOを操作できるかもしれません。実際に試してみた結果を共有します。
MCPとは何か
Model Context Protocol (MCP)は、Anthropic社が提唱した、AI(LLM)が外部システムやAPIと通信するための標準化されたプロトコルです。「AIアプリケーションのUSB-C」と例えられるように、LLMに様々な機能を手軽に追加・削除できるようにするためのインターフェースを提供します。
MCPの詳細については、以前の記事「カスタムMCPサーバーのテンプレートとテスト実装」を参照してください。
LLMと相性のよいTODOアプリの設計
従来のTODOアプリは複雑なデータ構造と精緻なUIに依存しがちです。タイトル、説明、期限、優先度、カテゴリー、タグ、リマインダー...。ユーザーはこれらの要素を適切に入力するためにフォームやカレンダーピッカーなどの複雑なUIを操作する必要があります。
Tiny-Todo-MCPの設計思想は「LLMの強み」を活かすことにあります。具体的には:
- 自然言語による入力: ユーザーは構造化されたフォームではなく、日常会話のような形で情報を入力
- 最小限のデータ構造: データベースにはシンプルなテキストのみを保存
- AIによる情報拡張: 表示時にLLMが内容を解釈し、構造化された形で提示
- コンテキスト情報の活用: 祝日カレンダーなどの補助情報をLLMに提供し判断精度を向上
このアプローチでは、従来の「データはリッチに保存し、必要に応じて表示する」という方向から、「データはシンプルに保存し、表示時にAIの力で豊かにする」という逆方向の設計に転換しています。
システムのデータフロー
以下は、Tiny-Todo-MCPのデータフローを表した図です。シンプルなデータベースからLLMによる解釈・拡張を経て、豊かな表示形式へと変換されていく流れがわかります。
このフローの特徴は、データベース自体はミニマルでありながら、表示時にLLMの力を借りて情報を豊かにしている点にあります。ユーザーの自然な指示はMCPサーバーを通じてデータベースに保存され、表示時にはフォーマットプロンプトと祝日情報などのコンテキスト情報を添えてLLMに解釈させています。
開発過程
メモリー機能からTODO専用アプリへ
最初のプロトタイプでは「メモリー管理」と「TODO管理」の両方を実装していました。しかし、開発を進める中で「やることを単純化したい」という思いが強くなり、メモリー機能を完全に削除してTODO専用アプリへと方向転換しました。
// 削除したファイル
// - memories.ts
// - memoryRepository.ts
// - memoryService.ts
// データベース名も変更
// - 旧: tiny-memory.db
// - 新: tiny-todo.db
データ構造のシンプル化
従来のTODOアプリによくある複雑なデータ構造(タイトル、説明、期限など)を廃止し、単一の「content」フィールドだけにしました。
// 変更前
interface Todo {
id: number;
title: string;
description: string;
due_date: string;
is_completed: boolean;
created_at: string;
}
// 変更後
interface Todo {
id: number;
content: string;
created_at: string;
}
MCPツールの実装
MCPの仕様に従って、3つのシンプルなツールを実装しました。
// update_todoツール
export const update_todo: MCPTool = {
name: "update_todo",
description: "Updates the TODO with new content. If a TODO already exists, it overwrites the latest one. If not, it creates a new one.",
parameters: {
type: "object",
properties: {
content: {
type: "string",
description: "The content of the TODO",
},
},
required: ["content"],
},
handler: async (params, context) => {
try {
const { content } = params;
const todo = await todoService.updateLatestTodo(content);
const formatPrompt = todoService.getTodoFormatPrompt();
const holidayInfo = todoService.getHolidayInfo();
return {
todo,
formatPrompt,
holidayInfo,
};
} catch (error) {
return handleError(error);
}
},
};
特徴的な実装
LLMによるTODOフォーマット
TODOの表示をより使いやすくするため、LLMによるフォーマット機能を組み込みました。MCPを通じてTODOデータを取得すると、以下のようなフォーマットプロンプトも一緒に返されます:
Below is a TODO list written in various formats. Please convert it into a clean, unified Markdown format according to the rules below. Additionally, if the user instructs "I have finished a task, update it," mark the corresponding task as completed. Follow these rules:
1. **Task Status:**
- Each task should be a list item with a checkbox.
- Use `[ ]` for incomplete tasks and `[x]` for completed tasks.
- If the user instructs that a task is finished, update the task status to `[x]`. If a completion date and time are missing, append the current date and time (e.g., `*Completed on:* yyyy/mm/dd (HH:MM)`) or mark as "TBD" as needed.
2. **Task Name & Emojis:**
- Make the task name **bold**.
- Prepend the task name with an appropriate emoji that matches the task's nature (for example, use ✏️ or 📓 for blog posts, 🍰 or 🍳 for cooking, 🏃 or 🏋️ for exercise, etc.).
- Use different emojis based on the task content.
3. **Date and Time Information:**
- If a due date is provided, output it as `*Due:* yyyy/mm/dd (HH:MM)`.
- If a completed date is provided, output it as `*Completed on:* yyyy/mm/dd (HH:MM)`.
- If a date is provided but the time is missing, add `(TBD)` after the date.
- If no date information is available, output `*Due:* TBD`.
4. **Priority:**
- If a task has a specified priority, output it as `*Priority:* High`, `*Priority:* Medium`, or `*Priority:* Low`.
- If no priority is mentioned, output `*Priority:* Normal`.
5. **Notes:**
- If there are any additional notes or details, include them under `*Notes:*`.
このプロンプトにより、LLMが内容を解析して適切な絵文字、優先度、日付情報を追加してフォーマットしてくれます。シンプルな「明日、山に行く」というテキストでも、「🏔️ 山に行く Due: 2025/03/24 (月) Priority: Normal」のように豊かな情報表示に変換されます。
日本の祝日情報
日本の2025年祝日カレンダー情報を追加し、全てのレスポンスに含めるようにしました。これにより、LLMがTODOの内容と祝日との関連性を考慮したフォーマットが可能になります。
const HOLIDAYS_2025 = [
{ date: "2025/01/01", name: "元日 (New Year's Day)", day: "水" },
{ date: "2025/01/13", name: "成人の日 (Coming of Age Day)", day: "月" },
{ date: "2025/02/11", name: "建国記念の日 (National Foundation Day)", day: "火" },
// ...その他の祝日情報
];
export function getHolidayInfo(): HolidayInfo {
return {
year: 2025,
holidays: HOLIDAYS_2025,
};
}
開発で得られた気づき
1. LLMの効果的な活用方法
このプロジェクトを通じて、LLMを活用する上での重要な気づきが得られました。LLMは単に質問に答えるだけでなく、「データの変換・整形・拡張」というパイプラインの一部として組み込むことで大きな価値を発揮します。具体的には:
- 適切なプロンプト設計: 詳細なフォーマット指示を与えることで、一貫性のある高品質な出力を得られる
- コンテキスト活用: 祝日情報など補助データを提供することで、LLMの判断精度が向上する
- ユーザー入力の柔軟な解釈: 自然言語での指示をシステムコマンドに変換できる橋渡し役になる
2. 「データと表示の分離」という新しいパターン
従来のアプリケーション開発では「データはリッチに保存し、必要に応じて表示する」というアプローチが一般的でした。しかしLLMを活用することで「データはミニマルに保存し、表示時にAIで拡張する」という新しいパターンが実現できます。
この方法のメリットは:
- データベース設計の簡素化
- スキーマ変更の柔軟性向上
- 表示ロジックのアプリケーションコードからの分離
- 自然言語によるインターフェースの実現
3. MCPというプロトコルの可能性
MCPはLLMの能力を拡張するための優れた仕組みであり、様々なツールやAPIをLLMに接続する標準的な方法を提供します。今回の実装から得られた知見は:
- シンプルなAPIでも強力な機能拡張が可能
- ユーザーにとっては複雑な処理も自然な会話として体験できる
- 「工具としてのAI」という新しい活用方法の土台となる
実際の使用例
以下は、実際にTiny-Todo-MCPを使用した会話の例です:
ユーザー: zennに記事を書きます。期限は今週末です。
ユーザー: やっぱり来週にします。
ユーザー: 来週は少しペースを落としたいです。アレンジしてください。
この例からわかるように、Tiny-Todo-MCPは自然言語での指示を理解し、適切に対応します。例えば:
- 「やっぱり来週にします」という自然な表現から、期限を自動的に1週間後に設定
- 「ペースを落としたい」という抽象的な要望を解釈し、期限延長と優先度低下という具体的なアクションに変換
従来のTODOアプリでは、日付変更や優先度変更はそれぞれUIの異なる部分での操作が必要ですが、LLMを活用したこのアプローチでは、自然な会話だけで複数の変更を一度に実行できます。
まとめ
Tiny-Todo-MCPの開発を通して、LLMの特性を活かしたアプリケーション設計について考える機会を得ました。このプロジェクトで特に興味深かったのは、「やっぱり来週にします」や「ペースを落としたいです」といった日常会話のような指示でもシステムが適切に対応できる点です。従来のUIでは複数の操作ステップが必要な処理も、自然な言葉だけで完結させられるようになりました。
MCPはまだ発展途上のプロトコルですが、データの保存方法と表示方法を再考するきっかけになりました。シンプルなデータベースとLLMの組み合わせが、予想以上に柔軟なシステムを実現できることは良い発見でした。
このアプローチが全てのアプリケーションに適しているわけではありませんが、特定のユースケースでは大きな価値を発揮する可能性を感じました。
ソースコード
Discussion