📅

Google Calendarの色毎に時間を集計するMCPサーバーを作った話

に公開

はじめに

こんにちは!皆さんは自分の活動をふりかえるためにどのようにしていますか?
私はGoogleカレンダーに学習した時間などを記録しています。そして、カレンダーを色ごとにカテゴライズしています。例えば、「青はキャリアの検討」「緑はポッドキャスト収録/準備」みたいに管理しています。
日々の活動をふりかえるために「Googleカレンダー色ごと・時間ごとに集計できる仕組みが欲しい」と思いました。Google Workspace の有料プランなら実現できるんですが、個人の無料のGoogleカレンダーだとそれができないんですよね~

というわけで、その仕組みを実現するMCPサーバーを自作してみました!
Githubリポジトリ: https://github.com/talasago/gcal-color-time-tracker-mcp
もし同じような課題を抱えている方は使ってみてください~!良かったらスターもいただけると嬉しいです⭐

実装でClaude Codeを使ったり、設計の部分ではクリーンアーキテクチャを意識したりと、小さな工夫や発見がいろいろありました。この記事は、その開発の記録と、やってみて気づいたことをまとめたものです。

なぜ作ったのか?

個人の活動を振り返りたかった

上記にも書いたように、色別時間集計の仕組みが欲しかったのですが、具体的には以下のような課題がありました。
Googleカレンダーで色分けして「青はキャリアの検討」「緑はポッドキャスト収録/準備」みたいに管理しているんですが、「今週は何にどのくらい時間を使えのか?」を知るには手動で計算するしかありませんでした。これが結構面倒で...

Google Workspaceの有料プランなら簡単にできるんですが、この目的のために個人で月額払うほどでもないし、「だったら自分で作っちゃえ~」と思いやってみました。
面倒な計算をせずに、生成AIのチャットを通して1分もかからずに集計できるようになったのが良かったです

せっかくだから新しい技術も試したかった

どうせ作るなら、触ったことのない新しい技術にもチャレンジしてみようと思いました、

  • MCPサーバーを自作してみたい:最近話題のMCP(Model Context Protocol)を実際に自作してみて、生成AIのチャットから自作したMCPサーバーの機能が使える経験が欲しい(ちょっと流行に乗り遅れた感もありますがw)
  • Claude Codeを使用して開発してみたい:AIエージェントの開発ツールである、Claude Codeを使った開発がどんな感じか試してみたかった

使用した技術要素

主要技術スタック

  • 言語: Ruby
  • API: Google Calendar API
  • プロトコル: MCP(Model Context Protocol)
  • 開発ツール: Claude Code
  • MCP SDK: 公式Ruby SDK(mcp gem)

アーキテクチャ

  • クリーンアーキテクチャ: 依存関係を内側の層へのみ向ける設計
    • infrastracture層: 外部APIやファイルとの接続
    • Interface Adapters層: MCPサーバーとしてのインターフェース
    • application層: アプリケーションのビジネスロジック
    • domain層: ドメインモデルやエンティティ

認証・セキュリティ

  • OAuth 2.0 OOB(帯域外)フロー: ローカルウェブサーバーなしのCLI認証
  • シングルファイルトークン保存: データベース依存なしのローカルファイル管理

処理の流れ

工夫した点

段階的な開発アプローチ

MCPサーバーを作成したことが無く、実現可能かどうか個人的に不確実性が高いと感じたため、以下のアプローチを採用しました:

  1. PoC優先: まずはMCPサーバー経由でカレンダーの色毎に集計するまでを最優先。アーキテクチャやテストコードは後回し。
  2. 動作確認後のテスト追加: 動くものができてからテストコードを作成
  3. 段階的にクリーンアーキテクチャを適用するためのリアーキテクチャ: 動作を担保してからアーキテクチャの改善

特定のクラスをシングルトンで実装

今回の開発では、以下の3つのクラスにシングルトンパターンを適用しました:

  • LoggerManager - ログ管理
  • ConfigurationService - 設定管理
  • TokenRepository - 認証トークン管理

これらのクラスは、アプリケーション全体で唯一であるべき責務を持っているため、シングルトンパターンを採用しました。
シングルトンにすることで解決できた問題としては以下の通りです。

リソースの一元管理

  • LoggerManagerでは、複数のインスタンスが同じログファイルに同時アクセスすることによるファイル競合を防止
  • メモリ使用量の最適化とファイルハンドルの重複作成回避

設定の統一管理

  • ConfigurationServiceでは、環境変数の検証を一度だけ実行し、アプリケーション全体で一貫した設定値を提供

共有状態の安全な管理

  • TokenRepositoryでは、全てのサービスが同一の認証トークン状態を参照し、トークン更新の即座な反映を実現

ただし、この選択には後述するテスト面での課題が発生してしまいました。具体的にはテスト間の暗黙的な依存関係が発生して、全体実行と個別実行で結果が異なるような、Flakyな状態になってしまいました。
状態が引き継がれること、具体的には環境変数の状態が引き継がれてしまったことでテストが不安定になってしまいました。

身をもってシングルトンパターンの課題を実感する良い機会になりました。今回はシングルトンパターンを使ってみましたが、デメリットが多いデザインパターンとも理解しているので、シングルトンパターン以外の選択肢を選ぶ時は、根拠を持って選択できるようにしてきます。

Claude Codeのセッション管理とコンテキスト圧縮

一つのセッションで会話が長くなってしまうと、コンテキストが肥大化してしまい、Claude CodeのProプランの上限にすぐ到達してしまうという問題がありました。具体的には、リアーキテクチャの方針とその実装が当てはまります。
この問題に対して、以下のアプローチをとることで、問題を軽減することができました。

  1. リアーキテクチャ方法の検討し、それをドキュメントに出力。合わせてphase毎の実装も記載
  2. セッション分離: 各フェーズを各セッションで実装

コンテキストに関する部分は他に良い案がありそうな気もしますが、あまり調べてないので、自前で工夫してみました。

テスト戦略の模索

当初は各ファイル・クラス単位でモックを多用していました。ただ、モックするばかりだとテストの信頼性に懸念が生じるため、途中から以下の方針に変更しました:

  • クリーンアーキテクチャの内側のレイヤーのクラスを使う場合: モック化しない(実際の振る舞いをテスト)
  • クリーンアーキテクチャの外側のレイヤーのクラスを使う場合: 積極的にモック化(外部依存を排除)

ただし、この方針が正しいかどうかはまだ確信を持てていません。今回のプロジェクトを通じてこういうアイデアが良いのではないかと仮説を立てた段階で、今後より多くのプロジェクトで検証していく必要があると感じています。

学びになった点

Claude Codeを使った開発経験

ペアプログラミング的な体験

Claude Codeを使った開発は、まさにペアプログラミングのような体験でした。

  • 自分: ナビゲーター役(方向性の決定、レビュー)
  • Claude Code: ドライバー役(実装)

1行程度の指示で、エージェントとして計画から実装、修正まで一連の流れを実行してくれる点が新鮮で印象的でした!
そして、AIエージェントを使ったコーディングのメリットとしては、以下を感じました。

  • 迅速な実装
  • 技術的な壁打ち相手として優秀、例えばアーキテクチャパターンの提案

生成されたコード(主にテストコード)の品質課題と対策

Claude Codeでの開発を通じて、一部内部品質が悪いコードになりがちだなと感じました。
具体的には、生成されたRSpecのテストコードには以下のような問題が頻発しました。

  • プロダクションコード: 体感約10%の確率で変数名・メソッド名に違和感
  • テストコード: 体感約60%の確率で以下の問題が発生
    • 同じテストを別contextで重複実装
    • 不適切なcontext名
    • 無理やりモック化してテストを通す
    • parameterizedテストを使わない冗長な実装

具体的な問題のイメージ

# 悪い例:同じテストの重複実装
describe 'TimeAnalysisService' do
  context 'when events exist' do
    it 'should calculate total time' do
      # テスト実装
    end
  end
  
  context 'when valid events are provided' do  # contextが重複している
    it 'should return correct time calculation' do
      # 同じ内容のテスト
    end
  end
end

# 悪い例:parameterizedを使わない冗長なテスト
describe 'color filtering' do
  it 'should filter red events' do
    # 色固有のテスト
  end
  
  it 'should filter blue events' do
    # ほぼ同じ内容で色だけ違う
  end
  
  it 'should filter green events' do
    # さらに同じ内容で色だけ違う
  end
end

この問題に対して、プロジェクトのCLAUDE.mdに以下の様なRSpecの記法ルールとテストコードのガイドラインを追加しました。

タイトル追加したテストコードガイドライン
#### RSpecの記法について
- `describe``context``it`ブロックを使用してテストを構造化する
- `it`ブロックにはshouldを使用して期待すべき動作を記述する
- `context`を使用してテストの前提条件を記述する
  - テスト内でif文を使用しない(代わりに`context`を使用して条件分岐を表現する) 
    - if文による分岐はテストの理解と保守を困難にするため
  - 同じインデントで`context`が5つ以上存在する場合、`rspec-parameterized `を使用してパラメータ駆動テストとする
  - 1つの`describe`に複数の前回条件がある場合は、`context`を使用して分ける。
    - 一方で、1つの`describe`に1つの`context`は冗長化するため不要
- `let`の遅延評価を使うことで、適切にコード量を減らす
- `subject`を使用して、わかりやすくSUT(System Under Test)を表す
  - `subject`はAAAパターンの`Act`部分に相当できる
  - `subject` は 各`describe`の下にメソッドごとに定義する

#### RSpecによらない一般的なテストコードのルールについて
- また、AAA(Arrange-Act-Assert)パターンに従って構造化する
  - **Arrange**: テストの前提条件を設定
  - **Act**: テスト対象のコードを実行 
  - **Assert**: 結果を検証
- テストは実装の詳細ではなく、公開されている振る舞いをテストする
- 外部の依存はテストダブルを使用する
- 各テストは独立して実行可能で、他のテストに依存しない
- テストは明確な目的を持ち、何をテストしているかを明示する
- テストは実行順序に依存しない
- テストは実装の変更に対して堅牢であるべきで、実装の詳細に依存しない
  - つまりprivateメソッドを直接テストしない
- テストは読みやすく、理解しやすいコードであるべき
- テストは失敗することが期待される場合、明示的に失敗するように記述する
- C0、C1カバレッジの目安は80%
  - 100%にすることを目指すが、実装の複雑さを増すために無理に達成しないため

なお、このCLAUDE.mdは別の方の記事を参考にインスパイアして作成しました。
https://zenn.dev/moneyforward/articles/718cc51a5e8118#開発時のルールファイル例

また、他の方法として、プロンプトではテストコードを生成した後に各テストの目的と必要性をClaude Codeに説明させたり、「ものすごく考えて批判的にレビューして」と指示したりして、解決しました。

この経験を通して、AIで生成されたコードが 「設計思想に基づいているか」「現実に即しているか」を判断する力 が重要だなと感じました。
時には変なコードを生成することもあるので、コードを鵜呑みにせず、自分で考えて判断する力が必要だと改めて実感しました。日々の学びを積み重ねていきます。

アーキテクチャ・設計の学び

クリーンアーキテクチャの適用

最近学んだクリーンアーキテクチャを実際に適用してみました。それによって、以下の課題を解決することができました。

  • 問題: 初期のPoCのコード上で、APIクライアントクラスにビジネスロジックが混在
  • 解決: アーキテクチャを適切にすることでビジネスロジックレイヤーを明確に分離

他にもクリーンアーキテクチャ自体が初めてだったので、他にもメリットがあると思うのですが、まだ理解が浅いので、今後も学びを深めて言語化できるようにしていきたいと思います。

リアーキテクチャの難しさ

リアーキテクチャ、設計改善を行う際の課題、つらさを経験できました。
単体テストだけではリグレッションを完全に防げないことは理解していました。そしてリグレッションを防ぐためには 単体テストの考え方/使い方にも書いてある通りE2Eテストが重要であることも理解していました。
とはいえ、全てのテストが自動化できず(例えばGoogle API呼び出す部分など)、手動テストに頼らざるを得ない部分もありました。手動テストはどうしても抜け漏れが発生しがちです。

段階的にリアーキテクチャをしていたのですが、リアーキテクチャ時に意図しない動作変更が発生してしまいました。
途中から、振る舞いを完璧に変えずに、内部の品質を改善していくのは難しいなと、身をもって経験できたことが学びです。

MCP通信フロー

MCPサーバーとの通信フローを理解できました。

  1. initializeの呼び出し
  2. tools/listでツール一覧を取得
  3. バージョンチェックによる互換性確認

苦労したこと

Google Calendar APIの問題

開発中に発見したAPI側の問題があります。
色がピーコック(青色)とブルーベリー(青紫色)が同じcolor_id:9で返ってくるので、同じ色扱いになってしまいます。本当はピーコック(青色)はcolor_idは7を期待しているのですが、APIレスポンスで正常に返却されません。
こちら側で出来ることが無いので、早く治ることを祈ってます。。。

おわりに

正直なところ、カレンダーの色毎の時間集計であればMCPサーバーよりもGASの方が楽に実現できるかもしれません...
また、Google カレンダーを操作するMCPのOSSプロジェクトも存在するので、そもそもそちらに実装した方が良かったかもしれません。

ただ、MCPサーバーを作って経験してみたかったこと、そしてRubyで実装したかったため今回自作しました。あと、AIチャットに統合できるので便利だと思います!(活用例は全く浮かんでないですが...w)
今回の開発を通じて、MCPサーバーの実装方法からClaude Code を使った開発フロー、そしてRubyでのクリーンアーキテクチャ実装まで、技術的経験を得ることができました。アーキテクチャを綺麗になった時は特に達成感をおぼえました!
そして、Claude Codeは一定の課題はあるものの、開発において強力な相棒となることを実感しました。

設計面やアーキテクチャについて、今後も学んで経験を積んでいって、より良いソフトウェアを作れるように、「内部品質からプロダクト価値を向上するエンジニア」になっていきます!

Discussion