AI

「埋まっているか」だけ知りたいこともあるので、ChimrのMCPが返す情報を4段階にした

  • Chimr
  • MCP
  • Swift
  • macOS
  • プライバシー

Chimr のMCPサーバーが返す予定の情報を、4つの段階から選べるようにしました。設定画面での名前は「プライバシーレベル」です。同じ「予定を返す」でも、「この時間は空いているか」を知りたいときと「誰が来るのか」を知りたいときでは、要る情報がまるで違うからです。いちばん狭い段階では、1件の予定はこれだけになります。

{
  "id": "1F2B9C4E-...",
  "title": "[Event]",
  "startDate": "2025-06-20T10:00:00Z",
  "endDate": "2025-06-20T10:30:00Z",
  "isAllDay": false,
  "isBusy": true,
  "myAttendanceStatus": "Accepted",
  "status": "upcoming"
}
ChimrMeeting Reminder for macOS with built-in MCPA macOS menu bar meeting reminder that counts down to your next event, chimes a minute before, and joins Zoom, Meet, or Teams in one click. Built-in MCP server lets Claude read your day.

空き時間を聞かれて、会議のメモまで返していた

きっかけは、2日前の未明に足した空き時間計算のツールでした。「今日30分まとまって空いている時間はあるか」「次の予定まで何分あるか」に答えるためのものです。

計算そのものは、予定の開始時刻と終了時刻しか読みません。いまの時刻から今日の残りの予定を開始順にたどり、予定と予定のあいだの差を取り、15分以上あれば空き枠として足していく。それだけです。返す枠に入るのも、開始時刻、終了時刻、何分あるか、指定した長さに足りるかどうかの4つで、予定の中身は1つも入りません。その予定が採用面談なのか歯医者なのかは、この計算に関係しません。

例外が1か所ありました。「次の予定まで何分か」だけを聞くモードでは、空き枠に次の予定を丸ごと添えています。ここで予定一覧と同じ関数を呼んでいたので、返っていたのはこれです。

{
  "id": "1F2B9C4E-...",
  "title": "採用面談(2次)",
  "startDate": "2025-06-18T05:00:00Z",
  "endDate": "2025-06-18T06:00:00Z",
  "location": "会議室A",
  "notes": "前回の所感は共有済み。年収レンジの話は最後に。",
  "url": "https://calendar.example.com/event/123",
  "isAllDay": false,
  "calendar": { "title": "Work", "identifier": "work-cal", "color": "#4285F4" },
  "hasVideoConference": true,
  "videoConferenceURL": "https://zoom.us/j/123456789",
  "videoConferenceService": "Zoom",
  "hasAttendees": true,
  "organizer": { "name": "...", "email": "...", "status": "Accepted", "isCurrentUser": false },
  "attendees": [{ "name": "...", "email": "...", "status": "Accepted", "role": "Required", "isCurrentUser": true }],
  "myAttendanceStatus": "Accepted",
  "status": "upcoming"
}

「あと何分空いてる?」への答えに、会議のメモも参加者のメールアドレスもビデオ会議のURLも付いてきます。同じツールのもう一方のモードは、時刻だけで空き枠を返していました。同じ日のうちに、どこまで返すかを選べるようにしています。

最初は3段階

最初に用意したのは、最小限・標準・詳細の3つです。デフォルトは最小限にしました。

段階を分けるときに考えたのは「どこまで隠すか」ではなく「どの問いに答えられれば足りるか」です。

答えたい問いごとに必要な情報と、Chimrの4つの段階。左から厳格・最小限・標準・詳細の順に、右へ行くほど前の段階に情報が足されていく。厳格は予定のID・開始と終了の時刻・終日かどうか・出席ステータス・開始前か進行中か終了済みかを返し、タイトルは[Event]に、isBusyは常にtrueに固定される。最小限でタイトル・場所・カレンダー・参加者数・ビデオ会議の有無が加わり、標準で参加者名・メモ・ビデオ会議のURL・主催者・予定のURLが、詳細でメールアドレスが加わる

図の左端にある厳格は、このときにはまだありません。3段階でいちばん狭かったのは最小限で、タイトルと時刻と場所は返っていました。

段階はトグル5つのプリセットにした

4つの段階のどれかを選んで終わり、にはしていません。段階と別に、個別のトグルを5つ置いています。参加者名、参加者のメールアドレス、ビデオ会議のURL、メモ、主催者です。

段階を選ぶと、この5つがその段階の組み合わせにまとめて切り替わります。標準を選べば参加者名とビデオ会議のURLとメモと主催者が入り、メールアドレスだけが外れます。詳細を選ぶと5つとも入ります。そこから1つだけ外す、という操作ができます。

メールアドレスのトグルは、参加者名のトグルがONのあいだだけ操作できます。最小限を選んだときは、この5つのトグルごと画面から消しています。

厳格を足して、タイトルまで伏せた

3段階で足りない場面が残りました。時間帯が埋まっているかどうかだけを知りたいのに、最小限でもタイトルと場所が返ります。カレンダーのタイトルは、それ単体で誰と会うのか何の案件なのかを伝えてしまうことがあります。

2日後、段階の手前に厳格を足しました。ドキュメントには「絶対的なプライバシーが必要なユーザー向け」と書いていますが、実際にやっているのは、冒頭の8つのキー以外を辞書から消すことだけです。5つのトグルは、最小限と同じように画面から消します。

厳格で落とすものは、値を空文字やマスクに置き換えるのではなく、キーごと消しています。locationnotesattendeesorganizercalendarvideoConferenceURLhasVideoConferencehasAttendees は、辞書に存在しません。厳格以外の段階では、場所の指定がない予定でも location は空文字で返ります。キーが無いことと、空で返ることは別のことです。

例外がタイトルで、ここだけは消さずに "[Event]" という固定文字列を入れています。

空き時間計算に添える次の予定も、この辞書を通ります。厳格にすると、時刻しか読んでいない計算に付くのも、冒頭の8つのキーだけになります。

段階で絞る処理を1か所にまとめても、そこを通らない経路が残っていれば意味がありません。JSONとは別に、予定から直接組み立てた説明文が3か所ありました。

期間を指定して取得するツールでは、応答に添える説明文を変えました。厳格のときは期間内の件数と予定のある日数だけを書き、ビデオ会議の件数、終日予定の件数、カレンダー名の一覧は落とします。カレンダー別の内訳も空にします。予定を落としても、カレンダー名の一覧が残っていれば、どんなカレンダーを使っているかは伝わります。

厳格と合わせて、予定を一覧で返すツールの応答からも箇条書きの要約を消しました。以前はJSONとは別に、予定のタイトルと参加者数を箇条書きにした説明文を添えていました。この箇条書きは予定の値を直接読んでいたので、段階による絞り込みを通っていません。JSONだけを返すように変えています。

ビデオ会議を開くツールの応答文も同じでした。event.title を直接埋めていたので、厳格でもタイトルが説明文に出ていました。いまは同じ辞書を経由するので、厳格では Successfully opened video meeting for event: [Event] になります。

厳格の手前で、最小限にも出席ステータスを足しています。それまでは、辞退した予定と出席する予定が同じ内容で返っていました。

返るものを設定画面で見せる

段階の説明文だけでは、実際に何が出ていくのかは分かりません。設定画面に、いま選んでいる段階でMCPが返すJSONをそのまま表示する欄を置きました。

この欄はプレビュー用の整形を持っていません。サンプルの予定を1件その場で組み立て、MCPが実際に使うのと同じ関数へ通し、返ってきた辞書を整形して出しているだけです。トグルを1つ切り替えるとキーが増減するのが、その場で見えます。

説明用に別の文言を書くと、実装が変わったときにそちらだけ古くなります。実物を通していれば、実装が変わった分だけ表示も変わります。

段階を作れるデータと、作れないデータ

この分け方が成立したのは、予定というデータが「時間帯」と「中身」に分かれるからです。空き時間の計算が実際に時刻しか読んでいなかったことが、その裏づけになっています。中身を捨てても答えられる問いが、段階を作る前から存在していました。

予定と違って、タスクには「中身を見ずに答えられる問い」が思いつきません。タイトルもメモもこれまでのやり取りも見て初めて次の行動が決まるので、削った先に問いが残りません。情報を減らした段階を用意しても、結局いちばん詳しい段階しか使われないので、分けた意味がなくなります。

段階を用意するかどうかは、扱っているデータに「情報を減らしても答えられる問い」があるかどうかで決まります。カレンダーにはそれがあって、「今日は何がある?」と「この時間、空いてる?」は別の問いです。無いデータに段階を作っても、選択肢が増えるだけです。

仕様は中身まで決めていない

MCPの仕様には、この種の段階についての規定がありません。ツールの仕様が求めているのは、サーバーの一般的な責務までです。

Servers MUST:

  • Validate all tool inputs
  • Implement proper access controls
  • Rate limit tool invocations
  • Sanitize tool outputs

Tools - Model Context Protocol

「出力をサニタイズする」「アクセス制御を実装する」までが仕様で、カレンダーの予定のどのフィールドを落とすかは書かれていません。そこはドメインごとに違うので、段階の数も、名前も、どこで切るかも、サーバーの実装で決めることになります。

3段階を入れたあとで、公開ドキュメントの表現も直しました。書いてあったのはこれです。

AIアシスタントが明示的に要求しない限り、カレンダーデータはデバイスから送信されません。すべてのMCP通信はMac上でローカルに行われます。

直したあとに、こう足しています。

MCPクライアントとChimr間の通信はMac上でローカルに行われます。ただし、使用するMCPクライアントによっては、取得されたカレンダーデータ(プライバシー設定でフィルタリング済み)が処理のために外部サーバーに送信される場合があります。

MCPクライアントとChimrのあいだがローカル通信であることと、受け取ったクライアントがそのデータをどこへ送るかは、別の話です。

段階の設定が効くのは、Chimrがクライアントへ渡すところまでです。そこから先はクライアントの作り次第で、段階で決められるのはこの境目の手前までです。

MCP連携そのものの説明はドキュメントにまとめています。

残っている制限

段階はアプリ全体に1つしかありません。ツールごとにも、接続しているクライアントごとにも分けられません。厳格にすると、空き時間の計算も今日の予定の一覧も同じ厳格で返ります。用途によって必要な情報が違うという話から始めておきながら、その用途を切り替えるのは、使う人が設定画面で行うことになっています。

呼び出し元から段階を指定する口もありません。ツールの引数に厳格を指定して1回だけ狭める、という使い方はできません。

デフォルトは最小限のままです。厳格を足したあとも初期値を変えていないので、設定画面を開かずに繋いだクライアントには、タイトルと場所が返ります。

期間指定の応答には、説明文とは別に件数をまとめたオブジェクトが入っています。こちらは段階で分岐していないので、厳格でもビデオ会議の件数と終日予定の件数が返ります。説明文から消した2つが、隣に残ったままです。

厳格でも id と時刻は返ります。同じ予定は何度取得しても同じ id なので、クライアントで増減と時刻の変更を貯めていけば、何曜日の何時に予定が入りやすいかは中身を見なくても分かります。厳格が落としているのは中身だけで、時間帯そのものは落としていません。

厳格の isBusy は、予定の内容を見ずに true を入れています。時間帯が埋まっているかどうかを答えるための段階なので、ここは実際の値を引くべき箇所です。

ブログChimrのMCPセットアップを手で書かせないために --mcp-install と --mcp-config を足したChimr を Claude Code や Claude Desktop の MCP サーバーとして使うときの設定を、ユーザーに手で書いてもらう前提から、Chimr 自身が `claude mcp add` コマンドや JSON を吐く前提に切り替えました。`--mcp-install` `--mcp-config` `--mcp` の3つの CLI フラグの実装と、bundle path を `CommandLine.arguments[0]` から取る判断をまとめます。