📝 はじめに
Cursorでプロジェクトのスケジューリングを行う際、日付を切り分けて作業を進めていると、AIアシスタントが日付を認識できず、翌日に作業を行っても「同じ日の続き」として認識されてしまう問題が発生していました。本稿では、この問題を解決するために実装したMCP(Model Context Protocol)日時サーバーについて、実装の背景、課題、解決方法、そして改善効果について説明します。
🔍 従来の仕様と問題点
発生していた問題
プロジェクトのスケジューリングをCursorで行う際、以下のような課題がありました:
-
日付認識の不足
- AIアシスタントが会話開始時に現在の日時を認識できない
- 「今日」「昨日」「明日」などの自然な表現を理解できない
- 日付を意識した作業管理ができない
-
作業日の誤認識
- 翌日に作業を行っても「同じ作業日の続き」として認識される
- スケジュールドキュメントと照らし合わせながら作業する際に混乱が生じる
- 日付ベースのタスク管理が困難
-
スケジュール管理との非連携
- Obsidianなどで管理しているスケジュールmdファイルと日付を照らし合わせることができない
- 「今日のタスクは?」「昨日の続きから」などの自然な質問に対応できない
問題が発生するシナリオ
シナリオ1: 翌日の作業開始時
ユーザー: 「今日のタスクを確認したい」
AI: (前日のタスクを表示してしまう、または日付が分からない)
シナリオ2: 作業日の継続判断
ユーザー: 「昨日の続きから始めよう」
AI: (同じ作業日として認識してしまう)
シナリオ3: スケジュールドキュメントとの照らし合わせ
ユーザー: 「この作業は何日かかっている?」
AI: (日付を認識できないため、正確に回答できない)
従来の実装
従来は、User Rulesに以下のルールを設定していました:
Always run .cursor\getDate.js to know what time it is at first.
しかし、この実装には以下の課題がありました:
- 実行タイミングの制御が難しい: 会話開始時に自動的に実行されるが、AIが日時情報をどのように活用するかが不明確
- 動的な日時取得ができない: 会話中に最新の日時情報を取得することができない
- 日付比較機能がない: 前回作業日との差分を計算できない
- スケジュールドキュメントとの連携がない: 外部ドキュメントとの連携機能がない
-
外部ファイルへの依存:
getDate.jsへの依存により、ファイルパスの解決問題が発生する可能性がある
💡 解決アプローチ: MCPサーバーの実装
MCP(Model Context Protocol)とは
MCPは、AIアシスタントと外部システムを連携するためのプロトコルです。Cursorでは、MCPサーバーを実装することで、AIアシスタントにカスタムツールやリソースを提供できます。
解決アプローチの選定
以下の3つの方法を検討しました:
| 方式 | メリット | デメリット |
|---|---|---|
| User Rules + getDate.js | シンプル、即座に実行可能 | 毎回明示的に実行が必要、自動反映されない |
| MCPサーバー | ツールとして統合、自動利用可能、リソースとして常時参照可能 | 設定が必要、サーバー実装が必要 |
結論: MCPサーバーを実装することで、より柔軟で強力な日時認識機能を実現できると判断しました。
MCPサーバーの利点
- 自動的なツール利用: AIアシスタントが自動的に日時情報を取得できる
- リソースとしての提供: 常に最新の日時情報をリソースとして提供できる
- 拡張性: 将来的にスケジュールドキュメント連携などの機能を追加しやすい
- 自然な会話: ツールとして統合されているため、自然な会話で日時情報を活用できる
🛠️ 実装内容
1. アーキテクチャ設計
MCPサーバー (datetime-mcp-server.js)
↓
日時情報をAIに提供
↓
自然な会話での日付認識
MCPサーバーは自己完結型の設計となっており、外部ファイルへの依存がありません。
2. MCPサーバーの実装
2.1 ファイル構成
.cursor/
├── datetime-mcp-server.js # MCPサーバーのメインファイル
├── test-datetime-server.js # テストスクリプト
└── MCP_SETUP.md # セットアップガイド
2.2 主要機能の実装
① 日時フォーマット機能
/**
* 現在日時を 'YYYY-MM-DD HH:mm:ss' で返すユーティリティ
* @param {Date} [baseDate=new Date()] - 任意の Date を受け取ってフォーマットも可能
* @returns {string}
*/
function formatDateTime(baseDate = new Date()) {
const pad = (n) => String(n).padStart(2, '0');
return [
baseDate.getFullYear(),
pad(baseDate.getMonth() + 1),
pad(baseDate.getDate()),
].join('-') + ' ' + [
pad(baseDate.getHours()),
pad(baseDate.getMinutes()),
pad(baseDate.getSeconds()),
].join(':');
}
② 現在日時の取得機能
function getCurrentDateTime(params = {}) {
const { timezone = 'Asia/Tokyo', format = 'custom' } = params;
const now = new Date();
const localDateTime = formatDateTime(now);
const isoDateTime = now.toISOString();
// 日付のみ取得(YYYY-MM-DD)
const dateOnly = localDateTime.split(' ')[0];
// 時間のみ取得(HH:mm:ss)
const timeOnly = localDateTime.split(' ')[1];
// 曜日取得
const days = ['日', '月', '火', '水', '木', '金', '土'];
const dayOfWeek = days[now.getDay()];
return {
datetime: format === 'iso' ? isoDateTime : localDateTime,
date: dateOnly,
time: timeOnly,
dayOfWeek: dayOfWeek,
timezone: timezone,
timestamp: now.getTime(),
year: now.getFullYear(),
month: String(now.getMonth() + 1).padStart(2, '0'),
day: String(now.getDate()).padStart(2, '0'),
hour: String(now.getHours()).padStart(2, '0'),
minute: String(now.getMinutes()).padStart(2, '0'),
second: String(now.getSeconds()).padStart(2, '0')
};
}
③ 日付比較機能
function calculateDateDifference(date1, date2 = null) {
const d1 = new Date(date1);
const d2 = date2 ? new Date(date2) : new Date();
// 時刻を00:00:00に設定して日付のみで比較
d1.setHours(0, 0, 0, 0);
d2.setHours(0, 0, 0, 0);
const diffTime = d2.getTime() - d1.getTime();
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
return {
date1: date1,
date2: date2 || formatDateTime(new Date()).split(' ')[0],
differenceDays: diffDays,
isSameDay: diffDays === 0,
isPast: diffDays < 0,
isFuture: diffDays > 0,
description: diffDays === 0 ? '同じ日' :
diffDays === 1 ? '1日後' :
diffDays === -1 ? '1日前' :
diffDays > 0 ? `${Math.abs(diffDays)}日後` :
`${Math.abs(diffDays)}日前`
};
}
2.3 提供するツール
MCPサーバーは以下の2つのツールを提供します:
① get_current_datetime
現在の日時を取得するツールです。
-
パラメータ:
-
timezone(オプション): タイムゾーン(デフォルト: Asia/Tokyo) -
format(オプション): フォーマット形式(iso/custom、デフォルト: custom)
-
-
返り値:
{ "datetime": "2025-12-03 09:57:02", "date": "2025-12-03", "time": "09:57:02", "dayOfWeek": "水", "timezone": "Asia/Tokyo", "timestamp": 1701571022000, "year": 2025, "month": "12", "day": "03", "hour": "09", "minute": "57", "second": "02" }
② calculate_date_difference
2つの日付の差分を計算するツールです。
-
パラメータ:
-
date1(必須): 比較する日付1 (YYYY-MM-DD形式) -
date2(オプション): 比較する日付2 (YYYY-MM-DD形式、省略時は今日)
-
-
返り値:
{ "date1": "2025-12-02", "date2": "2025-12-03", "differenceDays": 1, "isSameDay": false, "isPast": false, "isFuture": false, "description": "1日後" }
2.4 JSON-RPC通信の実装
MCPサーバーは、標準的なJSON-RPC 2.0プロトコルに従って実装されています。
① リクエスト処理
function handleRequest(request) {
const { method, params, id } = request;
// 通知(idがないメッセージ)の処理
if (id === undefined || id === null) {
// 通知は応答を返さない
if (method === 'notifications/initialized') {
// クライアントからの初期化通知は無視(正常)
return;
}
// その他の通知も無視
return;
}
switch (method) {
case 'initialize':
handleInitialize(id, params);
break;
case 'tools/list':
handleToolsList(id);
break;
case 'tools/call':
handleToolCall(id, params);
break;
case 'resources/list':
handleResourcesList(id);
break;
case 'resources/read':
handleResourceRead(id, params);
break;
default:
// idがあるリクエストに対してのみエラーレスポンスを返す
sendError(id, -32601, 'Method not found', `Unknown method: ${method}`);
}
}
② メッセージ送信(NDJSON形式)
CursorはNDJSON形式(改行区切りのJSON)を期待しているため、Content-Lengthヘッダーは使用せず、改行区切りのJSONのみで送信します。
function sendResponse(response) {
const jsonText = JSON.stringify(response);
// NDJSON形式で送信(改行区切りのJSON)
process.stdout.write(jsonText + '\n');
}
function sendNotification(method, params) {
const notification = {
jsonrpc: '2.0',
method: method,
params: params || {}
};
// NDJSON形式で送信
const jsonText = JSON.stringify(notification);
process.stdout.write(jsonText + '\n');
}
③ メッセージ受信(両形式対応)
受信時は、Content-Length形式とNDJSON形式の両方に対応しています。
function processMessages() {
while (buffer.length > 0) {
// Content-Length形式を試す
const contentLengthMatch = buffer.match(/^Content-Length:\s*(\d+)\r?\n\r?\n/);
if (contentLengthMatch) {
// Content-Length形式の処理
// ...
}
// NDJSON形式(改行区切り)を試す
const newlineIndex = buffer.indexOf('\n');
if (newlineIndex !== -1) {
// NDJSON形式の処理
// ...
}
}
}
④ 自己完結型の実装
MCPサーバーは外部ファイルに依存せず、formatDateTime関数を内部に直接実装しています。これにより、以下の利点があります:
-
依存関係の削減: 外部ファイル(
getDate.js)への依存がなくなり、シンプルな構成に - 確実な動作: ファイルパスの解決問題が発生しない
- 保守性の向上: すべての機能が1つのファイルに集約され、管理が容易
3. User Rulesの拡張
既存のUser Rulesに、日時認識に関する説明を追加しました:
## 日時認識とスケジュール管理
### 日時の自動認識
- **常に現在の日時を把握する**: 会話開始時や作業開始時に、現在の日時を自動的に取得・認識してください。
- **自然な会話での日付認識**: ユーザーが「今日のタスク」「昨日の続き」「新しい作業日」などと話した場合、自動的に日付を判断してください。
- **スケジュールドキュメントとの連携**: プロジェクトのスケジュール管理用mdファイルと照らし合わせて、日付を正確に認識してください。
### 日時取得の方法
1. **MCPサーバー**: `datetime-mcp-server.js` が利用可能な場合、`get_current_datetime` ツールを使用して日時を取得(推奨)
2. **日付比較**: `calculate_date_difference` ツールを使用して、前回作業日との差分を計算
4. テストスクリプトの実装
実装が正しく動作することを確認するため、テストスクリプトを作成しました:
// テスト1: 現在日時の取得
const now = getCurrentDateTime();
console.log('日時:', now.datetime);
console.log('日付:', now.date);
console.log('曜日:', now.dayOfWeek);
// テスト2: 日付の差分計算
const diff = calculateDateDifference('2025-12-02');
console.log('差分:', diff.differenceDays, '日');
console.log('説明:', diff.description);
テスト結果: すべてのテストが成功しました ✅
🐛 実装中に発生したエラーと解決方法
エラー1: 通知(Notification)の誤処理
エラー内容:
Client error for command [
{
"code": "invalid_union",
"issues": [
{
"code": "invalid_type",
"expected": "string",
"received": "undefined",
"path": ["id"],
"message": "Required"
}
]
}
]
原因:
-
notifications/initializedは通知(idがないメッセージ)なのに、リクエストとして処理していた - 通知に対してエラーレスポンスを返そうとしていたが、
idがnullのためCursorのバリデーションエラーが発生
解決方法:
通知(idがないメッセージ)を正しく処理するように修正しました。
function handleRequest(request) {
const { method, params, id } = request;
// 通知(idがないメッセージ)の処理
if (id === undefined || id === null) {
// 通知は応答を返さない
if (method === 'notifications/initialized') {
return; // 正常な通知なので無視
}
return;
}
// idがあるリクエストのみ処理
// ...
}
エラー2: Content-Length形式とNDJSON形式の混在
エラー内容:
Client error for command Unexpected token 'C', "Content-Length: 220" is not valid JSON
Client error for command Unexpected end of JSON input
原因:
- CursorはNDJSON形式(改行区切りのJSON)を期待している
- しかし、実装ではContent-Lengthヘッダー形式で送信していた
- CursorがContent-LengthヘッダーをJSONとして解析しようとしてエラーが発生
解決方法:
送信形式をNDJSON形式のみに統一しました。
// 修正前(Content-Length形式)
function sendResponse(response) {
const jsonText = JSON.stringify(response);
const contentLength = Buffer.byteLength(jsonText, 'utf8');
process.stdout.write(`Content-Length: ${contentLength}\r\n\r\n`);
process.stdout.write(jsonText);
}
// 修正後(NDJSON形式)
function sendResponse(response) {
const jsonText = JSON.stringify(response);
// NDJSON形式で送信(改行区切りのJSON)
process.stdout.write(jsonText + '\n');
}
注意点:
- 受信時はContent-Length形式とNDJSON形式の両方に対応(Cursorがどちらで送信しても対応可能)
- 送信時はNDJSON形式のみを使用(Cursorが期待する形式)
エラー3: パス解決の問題と依存関係の削除
エラー内容:
Error: Cannot find module './getDate.js'
原因:
- 相対パス
require('./getDate.js')が、Cursorの実行環境で正しく解決されない場合がある - 特にWindows環境や、異なるディレクトリから実行された場合に問題が発生
- 外部ファイルへの依存により、設定が複雑になる
解決方法:
getDate.jsへの依存を完全に削除し、formatDateTime関数をMCPサーバー内に直接実装しました。
// 修正前(外部ファイルへの依存)
const { formatDateTime } = require('./getDate.js');
// 修正後(自己完結型)
/**
* 現在日時を 'YYYY-MM-DD HH:mm:ss' で返すユーティリティ
*/
function formatDateTime(baseDate = new Date()) {
const pad = (n) => String(n).padStart(2, '0');
return [
baseDate.getFullYear(),
pad(baseDate.getMonth() + 1),
pad(baseDate.getDate()),
].join('-') + ' ' + [
pad(baseDate.getHours()),
pad(baseDate.getMinutes()),
pad(baseDate.getSeconds()),
].join(':');
}
これにより、MCPサーバーは外部ファイルに依存せず、単体で動作するようになりました。
エラー解決のまとめ
| エラー | 原因 | 解決方法 | 影響 |
|---|---|---|---|
| 通知の誤処理 | 通知をリクエストとして処理 | 通知を正しく識別して無視 | バリデーションエラー解消 |
| 形式の混在 | Content-Length形式で送信 | NDJSON形式のみで送信 | JSONパースエラー解消 |
| パス解決 | 外部ファイルへの依存 | 関数を内部に直接実装 | モジュール読み込みエラー解消、依存関係削減 |
これらの修正により、MCPサーバーは正常に動作するようになりました。
📊 改善効果
期待される効果
1. 自動日時認識
- ✅ 会話開始時に自動的に現在日時を取得
- ✅ 「今日」「昨日」「明日」などの自然な表現を理解
- ✅ 日付に基づいた正確なタスク表示
2. 作業日の自動判定
- ✅ 前回作業日との差分を自動計算
- ✅ 「新しい作業日」か「同じ作業日の続き」かを自動判定
- ✅ スケジュールドキュメントとの照らし合わせが容易に
3. スケジュール管理との連携
- ✅ スケジュールmdファイルと照らし合わせながら作業
- ✅ 日付に基づいた正確なタスク表示
- ✅ 作業履歴の自動記録(将来拡張)
実測データ
動作確認を行った結果、以下のような自然な会話が可能になりました:
会話例1: 今日のタスク確認
ユーザー: 「今日のタスクは何ですか?」
AI: 今日は2025-12-03(水曜日)です。今日のタスクを確認します...
会話例2: 昨日の続きから作業
ユーザー: 「昨日の続きから始めましょう」
AI: 昨日(2025-12-02)の作業内容を確認します...
会話例3: 作業期間の確認
ユーザー: 「この作業は何日かかっていますか?」
AI: 作業開始日と現在日を比較して、経過日数を計算します...
実装による影響範囲
| 機能 | 修正前 | 修正後 |
|---|---|---|
| 日時認識 | 手動実行が必要 | 自動的に認識 |
| 作業日判定 | できない | 自動判定可能 |
| 自然な会話 | 限定的 | 完全対応 |
| スケジュール連携 | できない | 可能(将来拡張予定) |
🎯 技術的な背景
MCP(Model Context Protocol)の仕組み
MCPは、AIアシスタントと外部システムを連携するための標準プロトコルです。Cursorでは、MCPサーバーを実装することで、以下のような機能を提供できます:
- ツール(Tools): AIアシスタントが呼び出せる関数群
- リソース(Resources): AIアシスタントが参照できるデータ
JSON-RPC通信の実装
MCPサーバーは、標準的なJSON-RPC 2.0プロトコルに従って実装されています。stdin/stdoutを介してJSON-RPCメッセージを送受信します。
重要な実装ポイント:
-
NDJSON形式での送信: CursorはNDJSON形式(改行区切りのJSON)を期待しているため、Content-Lengthヘッダーは使用せず、改行区切りのJSONのみで送信します。
-
通知の処理: JSON-RPC 2.0では、通知(
idがないメッセージ)は応答を返しません。notifications/initializedなどの通知を正しく処理する必要があります。 -
両形式対応の受信: 受信時は、Content-Length形式とNDJSON形式の両方に対応しています(Cursorがどちらで送信しても対応可能)。
リクエスト例:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_current_datetime",
"arguments": {
"format": "custom"
}
}
}
レスポンス例(NDJSON形式):
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"datetime\":\"2025-12-03 11:07:04\",\"date\":\"2025-12-03\",\"time\":\"11:07:04\",\"dayOfWeek\":\"水\"}"}]}}
通知例:
{
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
}
(通知はidを持たず、応答も返さない)
自己完結型の設計
MCPサーバーは外部ファイルに依存しない自己完結型の設計となっています:
- 単一ファイル構成: すべての機能が1つのファイルに集約され、管理が容易
- 依存関係なし: 外部ファイルへの依存がないため、設定がシンプル
- 確実な動作: ファイルパスの解決問題が発生しない
- 移植性: ファイルをコピーするだけで、どこでも動作する
拡張性の設計
将来的な拡張を考慮して、以下のような設計にしています:
- スケジュールドキュメント連携: Obsidianのスケジュールmdファイルから日付を自動抽出
- 作業履歴の記録: 前回作業日を自動記録・比較
- タイムゾーン自動検出: システムのタイムゾーンを自動検出
- 複数タイムゾーン対応: グローバルチームでの作業に対応
🚀 セットアップ方法
1. MCPサーバーの設定
Cursorの設定で、MCPサーバーを有効化します:
{
"mcpServers": {
"datetime-server": {
"command": "node",
"args": [".cursor/datetime-mcp-server.js"],
"cwd": "${workspaceFolder}"
}
}
}
詳細なセットアップ手順は、.cursor/MCP_SETUP.md を参照してください。
2. 動作確認
以下のコマンドで、MCPサーバーが正常に動作するか確認できます:
cd .cursor
node test-datetime-server.js
3. Cursorでの確認
- Cursorを完全に再起動してMCPサーバーを読み込む
- Settings → Tools & MCP → datetime-server でエラーがないか確認
- AIアシスタントに「今日は何日?」「現在の時刻は?」と質問
- 現在の日時が正しく回答されることを確認
4. トラブルシューティング
問題1: MCPサーバーがエラーを表示する
症状: Settings → Tools & MCP → datetime-server で「Error - Show Output」と表示される
確認事項:
- Node.jsが正しくインストールされているか確認
-
.cursor/datetime-mcp-server.jsが存在するか確認 - 「Show Output」をクリックしてエラーメッセージの詳細を確認
よくある原因と解決方法:
-
パスエラー:
cwdパラメータが${workspaceFolder}に設定されているか確認 -
通知の誤処理: 通知(
idがないメッセージ)を正しく処理しているか確認 - JSON形式エラー: NDJSON形式(改行区切りのJSON)で送信しているか確認
問題2: ツールが認識されない
症状: AIアシスタントが日時を取得できない
確認事項:
- Cursorを完全に再起動
- Settings → Tools & MCP → datetime-server で「2 tools, 0 prompts, and 1 resources」と表示されているか確認
- エラーログに「Found 2 tools, 0 prompts, and 1 resources」と表示されているか確認
問題3: JSONパースエラーが発生する
症状: エラーログに「Unexpected token 'C'」や「is not valid JSON」と表示される
原因: Content-Length形式で送信している可能性
解決方法: NDJSON形式(改行区切りのJSON)のみで送信するように実装されているか確認
📚 参考資料
まとめ
- 従来: 日付認識ができず、翌日に作業を行っても「同じ日の続き」として認識される
- 改修後: MCPサーバーを実装することで、自動的な日時認識と自然な会話での日付理解が可能に
- 効果: スケジュール管理との連携が容易になり、日付ベースの作業管理が実現
MCPサーバーを実装することで、Cursorでのプロジェクトスケジューリングがより効率的になり、AIアシスタントが日付を正確に認識し、自然な会話で作業を進めることができるようになりました。
実装のポイント
- NDJSON形式の使用: CursorはNDJSON形式(改行区切りのJSON)を期待しているため、送信時はこの形式を使用
-
通知の正しい処理: JSON-RPC 2.0の通知(
idがないメッセージ)を正しく識別して処理 - 自己完結型の設計: 外部ファイルへの依存を削除し、すべての機能を1つのファイルに集約
- シンプルな構成: 依存関係がないため、設定が簡単で確実に動作する
今後の拡張
- スケジュールドキュメント連携: Obsidianのスケジュールmdファイルから日付を自動抽出
- 作業履歴の自動記録: 前回作業日を自動記録・比較
- タイムゾーン自動検出: システムのタイムゾーンを自動検出
- 複数タイムゾーン対応: グローバルチームでの作業に対応
実装中に発生したエラーとその解決方法を記録することで、同様の問題に直面した開発者の参考になれば幸いです。