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
77 changes: 77 additions & 0 deletions MCP_AUTO_RECOVERY_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Codex MCP Auto-Recovery Setup

PC再起動後も、CodexがMCP接続時に `cloud-run-logging-mcp` を自動起動・復旧するための手順です。
他プロジェクトへ横展開できるように、共通化前提でまとめています。

## 前提

- Codexは `~/.codex/config.toml` の `mcp_servers` を参照してMCPを起動する
- 再起動後に接続が切れる主な原因は以下
- Docker daemon 未起動
- MCPイメージ未build
- 鍵ファイルパス不整合

## 1. ラッパースクリプトを作成

各プロジェクトに `scripts/start_cloud_run_logging_mcp.sh` を作成します。

```bash
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MCP_REPO_DIR="${MCP_REPO_DIR:-${SCRIPT_DIR}/../../cloud-run-logging-mcp}"
MCP_IMAGE="${MCP_IMAGE:-cloud-run-logging-mcp:local}"
MCP_KEY_PATH="${MCP_KEY_PATH:-/absolute/path/to/sa.json}"

docker info >/dev/null 2>&1 || { [[ "$(uname)" == "Darwin" ]] && open -ga Docker || true; }
for i in {1..90}; do docker info >/dev/null 2>&1 && break; sleep 2; done
docker info >/dev/null 2>&1

docker image inspect "${MCP_IMAGE}" >/dev/null 2>&1 || docker build -t "${MCP_IMAGE}" "${MCP_REPO_DIR}"
[[ -f "${MCP_KEY_PATH}" ]]

exec docker run --rm -i \
-e GCP_SA_JSON_PATH=/secrets/key.json \
-v "${MCP_KEY_PATH}:/secrets/key.json:ro" \
"${MCP_IMAGE}"
```

## 2. 実行権限を付与

```bash
chmod +x scripts/start_cloud_run_logging_mcp.sh
```

## 3. `~/.codex/config.toml` を設定

`mcp_servers.cloud-run-logging-mcp` をラッパー呼び出しに変更します。

```toml
personality = "pragmatic"

[mcp_servers.cloud-run-logging-mcp]
command = "/bin/zsh"
args = ["-lc", "/ABSOLUTE/PATH/TO/PROJECT/scripts/start_cloud_run_logging_mcp.sh"]
enabled = true
```

## 4. プロジェクト差分を環境変数で吸収

必要に応じて以下を上書きします。

- `MCP_KEY_PATH`
- `MCP_REPO_DIR`
- `MCP_IMAGE`

## 5. 動作確認

1. Dockerを停止した状態でCodexを起動
2. MCPツール(Cloud Runログ取得)を実行
3. 自動でDocker起動待ち・MCP起動され、ログ取得できることを確認

## 運用メモ

- 複数プロジェクトで同一MCPを使う場合:
- スクリプトを `~/.codex/bin/` などに1本化し、各プロジェクトは同じスクリプトを参照
- `config.toml` は相対パスより絶対パス運用が安定
Binary file modified scripts/assets/rich_menu_default.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions scripts/capture_rich_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python3
"""Capture rich_menu_preview.html as a 2500x1686 PNG using Playwright."""
import sys
from pathlib import Path
from playwright.sync_api import sync_playwright

HTML_PATH = Path(__file__).parent / "rich_menu_preview.html"
OUTPUT = Path(__file__).parent / "assets" / "rich_menu_default.png"


def main():
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 2500, "height": 1686})
page.goto(f"file://{HTML_PATH.resolve()}")
# Google Fontsの読み込みを待つ
page.wait_for_load_state("networkidle")
page.screenshot(path=str(OUTPUT), full_page=False)
browser.close()
print(f"Saved: {OUTPUT}")


if __name__ == "__main__":
main()
131 changes: 131 additions & 0 deletions scripts/rich_menu_preview.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=2500">
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@500;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,1,0');

* { margin: 0; padding: 0; box-sizing: border-box; }

body {
width: 2500px;
height: 1686px;
overflow: hidden;
background: #ebebf0;
font-family: 'Noto Sans JP', sans-serif;
}

.grid {
display: grid;
grid-template-columns: 833px 833px 834px;
grid-template-rows: 843px 843px;
width: 2500px;
height: 1686px;
}

.cell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 40px;
border-right: 1.5px solid #d8d8e0;
border-bottom: 1.5px solid #d8d8e0;
position: relative;
overflow: hidden;
background: #ffffff;
}

/* 右端・下端はボーダーなし */
.cell:nth-child(3),
.cell:nth-child(6) { border-right: none; }
.cell:nth-child(4),
.cell:nth-child(5),
.cell:nth-child(6) { border-bottom: none; }

/* セル内のサークル背景 */
.icon-wrap {
width: 260px;
height: 260px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}

.cell-1 .icon-wrap { background: #eef0ff; }
.cell-2 .icon-wrap { background: #e0f7fa; }
.cell-3 .icon-wrap { background: #f3eeff; }
.cell-4 .icon-wrap { background: #e0f2f1; }
.cell-5 .icon-wrap { background: #e8f5e9; }
.cell-6 .icon-wrap { background: #e1f5fe; }

.material-symbols-rounded {
font-size: 120px;
font-variation-settings: 'FILL' 1, 'wght' 400, 'GRAD' 0, 'opsz' 48;
}

.cell-1 .material-symbols-rounded { color: #5c6bc0; }
.cell-2 .material-symbols-rounded { color: #0097a7; }
.cell-3 .material-symbols-rounded { color: #7e57c2; }
.cell-4 .material-symbols-rounded { color: #00897b; }
.cell-5 .material-symbols-rounded { color: #43a047; }
.cell-6 .material-symbols-rounded { color: #0288d1; }

.label {
font-size: 68px;
font-weight: 700;
letter-spacing: 0.04em;
color: #2d2d3e;
}
</style>
</head>
<body>
<div class="grid">
<!-- 一覧 -->
<div class="cell cell-1">
<div class="icon-wrap">
<span class="material-symbols-rounded">format_list_bulleted</span>
</div>
<div class="label">一覧</div>
</div>
<!-- 登録 -->
<div class="cell cell-2">
<div class="icon-wrap">
<span class="material-symbols-rounded">add_circle</span>
</div>
<div class="label">登録</div>
</div>
<!-- 使い方 -->
<div class="cell cell-3">
<div class="icon-wrap">
<span class="material-symbols-rounded">help</span>
</div>
<div class="label">使い方</div>
</div>
<!-- Web一覧 -->
<div class="cell cell-4">
<div class="icon-wrap">
<span class="material-symbols-rounded">language</span>
</div>
<div class="label">Web一覧</div>
</div>
<!-- 連携 -->
<div class="cell cell-5">
<div class="icon-wrap">
<span class="material-symbols-rounded">calendar_month</span>
</div>
<div class="label">カレンダー</div>
</div>
<!-- 習慣タスク -->
<div class="cell cell-6">
<div class="icon-wrap">
<span class="material-symbols-rounded">task_alt</span>
</div>
<div class="label">習慣タスク</div>
</div>
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion scripts/setup_line_rich_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def _build_rich_menu(name: str, chat_bar_text: str, server_url: str) -> RichMenu
),
RichMenuArea(
bounds=RichMenuBounds(x=833, y=843, width=833, height=843),
action=MessageAction(label="連携", text="アカウント連携"),
action=URIAction(label="カレンダー", uri=habit_url),
),
RichMenuArea(
bounds=RichMenuBounds(x=1666, y=843, width=834, height=843),
Expand Down
8 changes: 8 additions & 0 deletions src/Domains/Entities/HabitTask.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Optional


@dataclass()
Expand All @@ -12,6 +13,8 @@ class HabitTask:
is_active: bool
created_at: datetime
updated_at: datetime
notify_day_of_week: Optional[int] # 0=月〜6=日(weekly用)
notify_day_of_month: Optional[int] # 1〜31(monthly用)

def __init__(
self,
Expand All @@ -23,6 +26,9 @@ def __init__(
is_active: bool = True,
created_at: datetime = datetime.now(),
updated_at: datetime = datetime.now(),
notify_day_of_week: Optional[int] = None,
notify_day_of_month: Optional[int] = None,
**kwargs,
):
self._id = _id
self.owner_id = owner_id
Expand All @@ -32,3 +38,5 @@ def __init__(
self.is_active = is_active
self.created_at = created_at
self.updated_at = updated_at
self.notify_day_of_week = notify_day_of_week
self.notify_day_of_month = notify_day_of_month
8 changes: 5 additions & 3 deletions src/Domains/Entities/Stock.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Optional

STOCK_STATUS = ['disabled', 'active', 'archived']

Expand All @@ -11,7 +12,7 @@ class Stock:
owner_id: str
expiry_date: datetime
status: int
notify_enabled: bool
notify_days_before: Optional[int]
created_at: datetime
updated_at: datetime

Expand All @@ -22,15 +23,16 @@ def __init__(
owner_id: str = None,
expiry_date: datetime = None,
status: int = 0,
notify_enabled: bool = False,
notify_days_before: Optional[int] = None,
created_at: datetime = datetime.now(),
updated_at: datetime = datetime.now(),
**kwargs,
):
self._id = _id
self.item_name = item_name
self.owner_id = owner_id
self.expiry_date = expiry_date
self.status = status
self.notify_enabled = notify_enabled
self.notify_days_before = notify_days_before
self.created_at = created_at
self.updated_at = updated_at
1 change: 0 additions & 1 deletion src/UseCases/Line/AddStockUseCase.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ def execute(self) -> None:
owner_id=self._line_request_service.req_line_user_id,
expiry_date=expiry_date,
status=1,
notify_enabled=False,
)
self._stock_repository.create(new_stock)

Expand Down
50 changes: 27 additions & 23 deletions src/UseCases/Line/CheckExpiredStockUseCase.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from datetime import datetime, timezone

from src import config
from src.Domains.IRepositories.IStockRepository import IStockRepository
from src.Domains.IRepositories.IWebUserRepository import IWebUserRepository
from src.UseCases.Interface.ILineResponseService import ILineResponseService
from src.UseCases.Interface.IUseCase import IUseCase
from src.line_rich_messages import add_stock_web_link_button


def _should_notify(stock, days_until_expire: int) -> bool:
"""notify_days_beforeに基づいて通知すべきかを判定"""
if stock.notify_days_before is None: # 常に通知
return True
return days_until_expire <= stock.notify_days_before


class CheckExpiredStockUseCase(IUseCase):
Expand Down Expand Up @@ -52,41 +57,40 @@ def execute(self) -> None:
"status": 1,
}
)
near_due_stocks = []
notify_on_items = []
notify_stocks = []
for stock in stocks:
if stock.notify_enabled:
notify_on_items.append(stock.item_name)

if stock.expiry_date is None:
continue

days_until_expire = (stock.expiry_date.date() - today).days
if days_until_expire < 0 or days_until_expire > 3:

if not _should_notify(stock, days_until_expire):
continue

if days_until_expire == 0:
if days_until_expire < 0:
label = f"{abs(days_until_expire)}日超過"
icon = "🔴"
elif days_until_expire == 0:
label = "今日まで"
icon = "🟠"
elif days_until_expire == 1:
label = "明日まで"
icon = "🟠"
elif days_until_expire <= 3:
label = f"残り{days_until_expire}日"
icon = "🟠"
elif days_until_expire <= 7:
label = f"残り{days_until_expire}日"
icon = "🟡"
else:
label = f"あと{days_until_expire}日"
near_due_stocks.append(f"{stock.item_name}: {label}")
label = f"残り{days_until_expire}日"
icon = "🟢"
notify_stocks.append(f"{icon} {stock.item_name}({label})")

if len(near_due_stocks) == 0:
if not notify_stocks:
continue

self._line_response_service.add_message(
"期限が3日以内のもの:\n" + "\n".join(near_due_stocks)
)
add_stock_web_link_button(
line_response_service=self._line_response_service,
server_url=config.SERVER_URL,
f"📋 期限通知({len(notify_stocks)}件)\n" + "\n".join(notify_stocks)
)
if len(notify_on_items) == 0:
self._line_response_service.add_message("通知ONのアイテム: なし")
else:
self._line_response_service.add_message(
"通知ONのアイテム:\n" + "\n".join(notify_on_items)
)
self._line_response_service.push(to=schedule.line_user_id)
Loading