コンテンツにスキップ

Slack MCP Agent — Custom Tool 設計

v1.6 / 2026-05-06

ツールカタログ、共通スキーマ、ベンダー抽象化の設計を定義する。
Custom Tool Executor は freee HR 3 ツールの本番実行パスである(docs/architecture.md Section 4.4, 8.2 参照)。
MCP Server hr-freee は read-only の freee HR 棚卸し gateway としても利用する。出力系ツール (Google Sheets, Slack DM) は MCP Server common で実行する。
docs/tech-decisions.md ADR-010 (Custom Tool Executor) の詳細化。


1. 設計原則

# 原則 説明
T-1 認証情報は管理プロセス内で完結 OAuth トークンは Backend または freee MCP gateway のサーバープロセスのみが保持。LLM API / Agent コンテキストには渡さない
T-2 ベンダー抽象レイヤーで将来に備える freee 固有型をそのまま返さず、共通スキーマに変換して Agent に提供
T-3 ツールスキーマは JSON Schema Managed Agent が理解できる JSON Schema 形式でツール定義
T-4 MVP は段階的にツール拡張 基本 HR ツールから開始し、Lane B/Lane C の分析系ツールを段階追加
T-5 エラーはテキストで Agent に返す 例外を握りつぶさず、Agent が判断できる形式のエラーメッセージを返す

2. ツールカタログ

2.1 MVP ツール一覧

# ツール名 カテゴリ 実行方式 ベンダー 説明
1 list_employees HR Custom Tool freee 従業員一覧取得
2 list_attendance HR Custom Tool freee 勤怠データ取得
3 calculate_overtime HR Custom Tool freee 残業時間計算
4 write_rows_to_google_sheet Output MCP Server Google スプレッドシート書込
5 send_slack_dm Output MCP Server Slack DM 送信
6 list_work_record_summaries HR Inventory MCP Server (read-only) freee 勤怠サマリ、有給残数、残業内訳の取得可否確認
7 list_employee_group_memberships HR Inventory MCP Server (read-only) freee 所属グループと login_email の取得可否確認
8 list_payroll_statements HR Inventory MCP Server (read-only) freee 給与明細の取得可否確認
9 list_leave_related_work_records HR Inventory MCP Server (read-only) freee 有給・休暇関連の日次勤怠取得可否確認
10 detect_attendance_anomalies HR Analysis Custom Tool freee / SQLite 指定日の打刻欠損・0分勤務・24h超過・休日勤務を検出
11 aggregate_monthly_attendance_diff HR Analysis Custom Tool freee / SQLite 前月/当月の勤怠サマリ差異を検出
12 detect_36_agreement_violations HR Compliance Custom Tool freee / SQLite 36協定違反・超過リスクを検出し監査ログへ記録
13 calculate_prorated_salary Payroll Draft Custom Tool freee / SQLite 入退社月の日割り給与ドラフトを算出(確定は人)
14 detect_payroll_differences Payroll Analysis Custom Tool freee / SQLite 前月比差異を検出し大差異をC候補として抽出
15 generate_payroll_diff_explanations Payroll Explanation Custom Tool SQLite 差異の根拠・前提・不確実性付き説明ドラフトを生成
16 detect_payroll_violation_risks Payroll Compliance Custom Tool SQLite 最低賃金・残業・社保の法令リスク候補を検出

2.2 実行方式の使い分け

判断基準 Custom Tool (Backend 内) MCP Server (Agent 経由)
認証情報を扱う (freee) 採用
個人情報を処理する 採用
書込・更新系 (Sheet/DM) 採用
参照系のみ 両方可 allowlist された read-only tool のみ可
監査要件が高い 採用 (audit_logs 統合)

2.3 freee MCP read-only gateway

Issue #46 の Phase 0 / No.1 では、freee Human Resources API の read endpoint を棚卸しするため、hr-freee MCP Server に allowlist 方式の read-only tool を追加する。MOCK_MODE=true では mock data、MOCK_MODE=false では DB に保存済みの freee OAuth credentials を使って実 API を読む。raw endpoint dispatcher は作らない。

境界条件:

  • OAuth token / client secret は MCP Server の環境変数または Backend 管理 DB に閉じ、Agent のプロンプトや tool input には含めない。
  • tool は GET 相当の read 操作に限定する。PUT / POST / DELETE は対象外。
  • レスポンスは Shared Schema または棚卸し用の安定したフィールド名に正規化する。
  • 実 API のレスポンスサンプルは scripts/freee_hr_inventory.py でメール・氏名・token をマスクして記録する。

2.4 Lane B E2E seed

Lane B のE2Eでは、開発検証用ダミーテナントに限定して backend/src/scripts/seed_freee_e2e_data.py から日次勤怠 seed を投入できる。これは通常の Custom Tool / MCP Tool ではなく、検証前準備用のCLIである。 対象は work_records/{date} の作成・更新に限定し、実行時に tenant_id, employee_id, month を明示する。 Slack DM / Google Sheets 書き込みとは異なり、dummy tenant での freee write はユーザーの明示許可がある場合のみ行う。

2.5 Lane C payroll draft policy

Lane C の給与系ツールは、最終確定ではなく draft + rationale + assumptions + uncertainty を返す。 calculate_prorated_salarydetect_payroll_differences は決定論的な計算ロジックでドラフトを作成し、 generate_payroll_diff_explanations は差異データに対する説明文ドラフトを返す。 detect_payroll_violation_risks は違反確定ではなく、Cルーティング対象となるリスク候補を返す。

freee payroll_statements が権限不足(403)の場合は、detect_payroll_differencesstatements を直接渡す代替入力を許可し、 E2E ではこの代替経路を使って判定ロジックを検証できる。

Lane C の検証データテンプレートは backend/src/scripts/seed_payroll_lane_c_data.py で生成できる。 このCLIは freee へ書き込まず、detect_payroll_differences / detect_payroll_violation_risks の入力JSONを出力する。


3. Custom Tool Executor アーキテクチャ

3.0 custom_tool_use イベント処理フロー

L4 ポーリングループ(docs/architecture.md Section 4.3)内で agent.custom_tool_use イベントを検知し、Backend 内で処理する。

L4 ポーリングループ
  ├── sessions.events.list()
  │    └── イベント走査
  │         ├── agent.mcp_tool_use        → MCP Server 経由 (common ツール)
  │         └── agent.custom_tool_use     → ★ Backend 内で処理
  ├── agent.custom_tool_use 検知時:
  │    ├── tool_name, tool_input を抽出
  │    ├── handle_custom_tool(tool_name, tool_input, tenant_id, conn)
  │    │    └── Custom Tool Router → freee Adapter → freee API
  │    └── events.send(tool_result) → Managed Agent Session 続行
  └── 次のポーリングへ

Custom Tool Executor が扱うツール: list_employees, list_attendance, calculate_overtime, detect_attendance_anomalies, aggregate_monthly_attendance_diff, detect_36_agreement_violations(全て freee HR / HR analysis 系)。 MCP Server common が扱うツール (write_rows_to_google_sheet, send_slack_dm) は agent.mcp_tool_use として処理され、Backend は介入しない。

3.1 全体フロー

flowchart TD
    A["Managed Agent Session\n(Anthropic Cloud)"] -->|"agent.custom_tool_use event\n(tool_name, tool_input)"| B["Backend L4 ポーリングループ"]
    B -->|"_handle_custom_tool()"| C{"Custom Tool Router"}

    C -->|list_employees| D1["freee Adapter\nGET /hr/api/v1/employees\n→ SharedSchema.Employee[]"]
    C -->|list_attendance| D2["freee Adapter\nGET /hr/api/v1/employees/id/work_records\n→ SharedSchema.Attendance[]"]
    C -->|calculate_overtime| D3["freee Adapter\nlist_attendance 集計\n→ SharedSchema.OvertimeSummary"]

    D1 --> E["freee API Client"]
    D2 --> E
    D3 --> E

    E -->|"OAuth Token Manager\nRate Limiter\nError Handler"| F["freee API"]

    D1 --> G["tool_result (JSON)"]
    D2 --> G
    D3 --> G

    G -->|"events.send(tool_result)"| A

3.2 Router 実装パターン

from typing import Any

TOOL_EXECUTORS: dict[str, Callable] = {
    "list_employees": execute_list_employees,
    "list_attendance": execute_list_attendance,
    "calculate_overtime": execute_calculate_overtime,
}

async def handle_custom_tool(
    tool_name: str,
    tool_input: dict[str, Any],
    tenant_id: str,
    conn: aiosqlite.Connection,
) -> str:
    """Custom Tool を実行し、結果を JSON 文字列で返す。"""
    executor = TOOL_EXECUTORS.get(tool_name)
    if executor is None:
        return json.dumps({"error": f"Unknown tool: {tool_name}"})

    try:
        result = await executor(tool_input, tenant_id, conn)
        await insert_audit_log(conn, tenant_id, "system", None,
            f"tool.{tool_name}", "execution", None,
            {"input": tool_input, "output_summary": _summarize(result)})
        return json.dumps(result, ensure_ascii=False)
    except FreeeAPIError as e:
        return json.dumps({"error": str(e), "retryable": e.retryable})

4. ツールスキーマ定義

4.1 list_employees

{
  "name": "list_employees",
  "description": "従業員一覧を取得する。部署やステータスでフィルタ可能。",
  "parameters": {
    "type": "object",
    "properties": {
      "company_id": {
        "type": "string",
        "description": "事業所 ID(省略時はデフォルト事業所)"
      },
      "department": {
        "type": "string",
        "description": "部署名でフィルタ(省略時は全部署)"
      },
      "status": {
        "type": "string",
        "enum": ["active", "retired", "on_leave"],
        "description": "在籍ステータスでフィルタ(省略時は全件)"
      }
    }
  }
}

出力:

{
  "employees": [
    {
      "id": "string",
      "name": "string",
      "department": "string",
      "position": "string",
      "employment_type": "full_time | part_time | contract",
      "email": "string",
      "slack_user_id": "string | null",
      "status": "active | retired | on_leave",
      "hire_date": "YYYY-MM-DD"
    }
  ],
  "total_count": 42
}

4.2 list_attendance

{
  "name": "list_attendance",
  "description": "指定従業員の指定月の勤怠データを取得する。",
  "parameters": {
    "type": "object",
    "properties": {
      "company_id": { "type": "string" },
      "employee_id": { "type": "string", "description": "従業員 ID" },
      "month": { "type": "string", "description": "対象月 (YYYY-MM)" }
    },
    "required": ["employee_id", "month"]
  }
}

出力:

{
  "employee_id": "string",
  "month": "YYYY-MM",
  "records": [
    {
      "date": "YYYY-MM-DD",
      "day_type": "weekday | weekend | holiday",
      "clock_in": "HH:MM",
      "clock_out": "HH:MM",
      "break_minutes": 60,
      "work_minutes": 480,
      "overtime_minutes": 0,
      "is_absent": false,
      "is_paid_leave": false
    }
  ],
  "total_records": 20
}

4.3 calculate_overtime

{
  "name": "calculate_overtime",
  "description": "指定従業員の指定月の残業時間を集計する。list_attendance の結果を基に計算する。",
  "parameters": {
    "type": "object",
    "properties": {
      "company_id": { "type": "string" },
      "employee_id": { "type": "string" },
      "month": { "type": "string", "description": "対象月 (YYYY-MM)" }
    },
    "required": ["employee_id", "month"]
  }
}

出力:

{
  "employee_id": "string",
  "month": "YYYY-MM",
  "total_work_minutes": 9600,
  "total_overtime_minutes": 120,
  "working_days": 20,
  "breakdown": {
    "normal_overtime_minutes": 100,
    "late_night_overtime_minutes": 20,
    "holiday_work_minutes": 0
  }
}

4.4 write_rows_to_google_sheet (MCP Server)

{
  "name": "write_rows_to_google_sheet",
  "description": "Google Sheets にデータ行を書き込む。",
  "parameters": {
    "type": "object",
    "properties": {
      "spreadsheet_id": { "type": "string" },
      "range": { "type": "string", "description": "A1 表記 (例: Sheet1!A1)" },
      "rows": {
        "type": "array",
        "items": { "type": "array" },
        "description": "書き込む行データの配列"
      }
    },
    "required": ["spreadsheet_id", "range", "rows"]
  }
}

4.5 send_slack_dm (MCP Server)

{
  "name": "send_slack_dm",
  "description": "Slack でユーザーに DM を送信する。",
  "parameters": {
    "type": "object",
    "properties": {
      "user_id": { "type": "string", "description": "Slack ユーザー ID" },
      "message": { "type": "string", "description": "メッセージ本文" }
    },
    "required": ["user_id", "message"]
  }
}

5. 共通スキーマ (Shared Schema)

5.1 設計方針

freee API のレスポンスをそのまま Agent に返さず、ベンダー非依存の共通型に変換する。将来 KOT 等の別ベンダーを追加する際に、L1-L3 のプロンプトを変更せずに済む。

flowchart LR
    A["freee API Response"] --> B["freee Adapter\n(変換ロジック)"]
    B --> C["Shared Schema\n(Agent に返す)"]
    D["KOT API Response"] -.-> E["KOT Adapter\n(Post-MVP)"]
    E -.-> C

5.2 型定義

from dataclasses import dataclass
from datetime import date


@dataclass(frozen=True)
class Employee:
    """ベンダー共通の従業員型。"""
    id: str
    name: str
    department: str
    position: str
    employment_type: str   # full_time | part_time | contract
    email: str
    slack_user_id: str | None
    status: str            # active | retired | on_leave
    hire_date: date


@dataclass(frozen=True)
class AttendanceRecord:
    """ベンダー共通の勤怠レコード型。"""
    date: date
    day_type: str           # weekday | weekend | holiday
    clock_in: str | None    # HH:MM
    clock_out: str | None   # HH:MM
    break_minutes: int
    work_minutes: int
    overtime_minutes: int
    is_absent: bool
    is_paid_leave: bool


@dataclass(frozen=True)
class OvertimeSummary:
    """ベンダー共通の残業集計型。"""
    employee_id: str
    month: str              # YYYY-MM
    total_work_minutes: int
    total_overtime_minutes: int
    working_days: int
    normal_overtime_minutes: int
    late_night_overtime_minutes: int
    holiday_work_minutes: int


@dataclass(frozen=True)
class WorkRecordSummary:
    """勤怠サマリと有給残数の棚卸し型。"""
    employee_id: str
    month: str
    num_paid_holidays_left: float
    total_overtime_except_normal_work_mins: int
    total_overtime_work_mins: int
    normal_work_mins: int


@dataclass(frozen=True)
class EmployeeGroupMembership:
    """人物マスタ突合用の所属グループ型。"""
    employee_id: str
    group_id: str
    group_name: str
    login_email: str


@dataclass(frozen=True)
class PayrollStatement:
    """給与明細の代表フィールド型。"""
    employee_id: str
    year: int
    month: int
    gross_pay: int
    net_pay: int
    paid_at: str


@dataclass(frozen=True)
class LeaveRelatedWorkRecord:
    """有給・休暇関連の日次勤怠棚卸し型。"""
    employee_id: str
    date: str
    paid_holiday: float
    special_holiday: float
    is_absence: bool

5.3 freee アダプタ

class FreeeAdapter:
    """freee API レスポンスを共通スキーマに変換する。"""

    @staticmethod
    def to_employee(raw: dict) -> Employee:
        return Employee(
            id=str(raw["id"]),
            name=f"{raw['last_name']} {raw['first_name']}",
            department=raw.get("department", {}).get("name", ""),
            position=raw.get("position", ""),
            employment_type=_map_employment_type(raw.get("employment_type", "")),
            status=_map_status(raw.get("employment_status", "")),
            hire_date=date.fromisoformat(raw.get("hire_date", "2000-01-01")),
        )

    @staticmethod
    def to_attendance_record(raw: dict) -> AttendanceRecord:
        return AttendanceRecord(
            date=date.fromisoformat(raw["date"]),
            day_type=_map_day_type(raw.get("day_pattern", "")),
            clock_in=raw.get("clock_in_at"),
            clock_out=raw.get("clock_out_at"),
            break_minutes=raw.get("break_minutes", 0),
            work_minutes=raw.get("normal_work_minutes", 0),
            overtime_minutes=raw.get("overtime_work_minutes", 0),
            is_absent=raw.get("is_absence", False),
            is_paid_leave=raw.get("is_paid_holiday", False),
        )


def _map_employment_type(freee_type: str) -> str:
    mapping = {"regular": "full_time", "part_time": "part_time",
               "contract": "contract", "temporary": "contract"}
    return mapping.get(freee_type, "full_time")

def _map_status(freee_status: str) -> str:
    mapping = {"employed": "active", "retired": "retired",
               "on_leave": "on_leave"}
    return mapping.get(freee_status, "active")

def _map_day_type(freee_pattern: str) -> str:
    mapping = {"normal_day": "weekday", "prescribed_holiday": "weekend",
               "legal_holiday": "holiday"}
    return mapping.get(freee_pattern, "weekday")

5.4 Post-MVP: KOT アダプタの追加例

class KOTAdapter:
    """KOT API レスポンスを共通スキーマに変換する。"""

    @staticmethod
    def to_employee(raw: dict) -> Employee:
        return Employee(
            id=raw["employeeKey"],
            name=raw["fullName"],
            department=raw.get("divisionName", ""),
            # ... KOT 固有のマッピング
        )

ツール実行時のベンダー切替:

async def execute_list_employees(tool_input: dict, tenant_id: str, conn) -> dict:
    vendor = await get_tenant_vendor(conn, tenant_id)  # "freee" | "kot"

    if vendor == "freee":
        raw = await freee_client.list_employees(tool_input)
        employees = [FreeeAdapter.to_employee(e) for e in raw]
    elif vendor == "kot":
        raw = await kot_client.list_employees(tool_input)
        employees = [KOTAdapter.to_employee(e) for e in raw]

    return {
        "employees": [asdict(e) for e in employees],
        "total_count": len(employees),
    }

6. freee API Client

6.1 エンドポイントマッピング

Custom Tool freee API メソッド パス
list_employees 従業員一覧 GET /hr/api/v1/companies/{company_id}/employees
list_attendance 勤怠データ GET /hr/api/v1/employees/{employee_id}/work_record_summaries/{year}/{month}?work_records=true
calculate_overtime (Backend 集計) - list_attendance 結果を計算
list_work_record_summaries 勤怠サマリ GET /hr/api/v1/employees/{employee_id}/work_record_summaries/{year}/{month}
list_employee_group_memberships 所属グループ GET /hr/api/v1/employee_group_memberships
list_payroll_statements 給与明細 GET /hr/api/v1/salaries/employee_payroll_statements
list_leave_related_work_records 休暇関連 (日次勤怠) GET /hr/api/v1/employees/{employee_id}/work_records/{date}

ベース URL は https://api.freee.co.jpbackend/src/tools/freee_client.py 参照)。MOCK_MODE=true のときは freee_adapter.py がモックデータを返すため外部通信は発生しない。

6.1.1 Phase 0 / No.1 データ棚卸し

対象 必要フィールド 後続 Phase 利用 分類 代替策 / 備考
employees id, display_name, entry_date, retire_date Phase 0 / No.2 人物マスタ 取得可能 entry_date を入社日、retire_date の有無を在籍ステータス判定に利用する
work_record_summaries num_paid_holidays_left, total_overtime_except_normal_work_mins, total_overtime_work_mins, normal_work_mins Phase ½ 勤怠・有給残数 取得可能 work_records=true で日次勤怠も同時取得できる
employee_group_memberships id, login_email, group_memberships Phase 0 / No.2 人物マスタ突合 代替あり endpoint は取得可能。検証データでは login_email が null、group_memberships が空のため Slack email / Google Sheet 人物マスタで補完する
payroll_statements employee_id, gross_pay, net_pay, paid_at Phase 3 / No.13, Phase 4 / No.16 不足あり 現 freee アプリ権限では 403。給与明細スコープ追加または CSV 手動取込が必要
leave_related_work_records paid_holiday, special_holiday, is_absence Phase ½ 有給申請・休暇確認 取得可能 専用申請 endpoint ではなく日次勤怠から有給・休暇実績を確認する

実 API の取得結果は token / 氏名 / email をマスクしたうえで docs/custom-tools.md のこの表へ反映する。取得できなかったフィールドは 不足あり、別経路で運用可能なものは 代替あり に分類する。2026-04-29 の検証では、給与明細のみ権限不足で 403 となった。

2026-04-29 レスポンスサンプル要約

scripts/freee_hr_inventory.py で token / 氏名 / email をマスクして確認した結果。

対象 ステータス マスク済みサンプル要約
employees 200 id, num, display_name, entry_date, retire_date, email, payroll_calculation, closing_day, pay_day, month_of_pay_day を確認。display_name***MASKED***email は null
work_record_summaries 200 num_paid_holidays_left, total_overtime_except_normal_work_mins, total_work_mins, work_records[].date, work_records[].paid_holiday, work_records[].special_holiday, work_records[].is_absence を確認
employee_group_memberships 200 employee_group_memberships[].id, display_name, entry_date, retire_date, login_email, group_memberships を確認。ただし検証データでは login_email が null、group_memberships が空
payroll_statements 403 {\"error\":\"access_denied\",\"message\":\"このアプリケーションにはアクセス権限がないエンドポイントです\"}。給与明細用の権限追加が必要
leave_related_work_records 200 date, day_pattern, paid_holiday, paid_holidays, special_holiday, special_holiday_setting_id, is_absence, total_overtime_work_mins を確認

6.2 OAuth Token Manager

class FreeeTokenManager:
    """freee OAuth トークンの管理。自動リフレッシュ対応。"""

    async def get_access_token(self, tenant_id: str, conn) -> str:
        integration = await get_integration(conn, tenant_id, "freee")
        creds = decrypt_credentials(integration.credentials)

        if self._is_expired(creds):
            creds = await self._refresh(creds)
            await update_integration_credentials(conn, integration.id, creds)

        return creds["access_token"]

    async def _refresh(self, creds: dict) -> dict:
        resp = await httpx.post("https://accounts.secure.freee.co.jp/public_api/token", data={
            "grant_type": "refresh_token",
            "refresh_token": creds["refresh_token"],
            "client_id": settings.freee_client_id,
            "client_secret": settings.freee_client_secret,
        })
        resp.raise_for_status()
        return resp.json()

6.3 エラーハンドリング

HTTP ステータス 対処 リトライ
401 トークンリフレッシュ → リトライ 1 回
429 Retry-After ヘッダに従い待機 最大 3 回
5xx Exponential backoff 最大 3 回
4xx (その他) エラーテキストを Agent に返す しない
class FreeeAPIError(Exception):
    def __init__(self, message: str, status_code: int, retryable: bool = False):
        super().__init__(message)
        self.status_code = status_code
        self.retryable = retryable

7. L2/L3 用ツールカタログ

7.1 L2 用ツールカタログ (自然言語)

L2 で Agent に「使えるツール」を伝えるためのテキスト。L2 の user message に含める。

利用可能なツール:
- list_employees: 従業員一覧を取得(部署・ステータスでフィルタ可能)
- list_attendance: 指定従業員の指定月の勤怠データを取得
- calculate_overtime: 指定従業員の指定月の残業時間を計算
- list_work_record_summaries: 勤怠サマリ・有給残数・残業内訳の取得可否を確認
- list_employee_group_memberships: 所属グループと login_email を取得
- list_payroll_statements: 給与明細の代表フィールドを取得
- list_leave_related_work_records: 有給・休暇関連の日次勤怠を取得
- detect_attendance_anomalies: 指定日の打刻欠損・0分勤務・24h超過・休日勤務を検出
- aggregate_monthly_attendance_diff: 前月/当月の勤怠サマリ差異を検出
- detect_36_agreement_violations: 36協定違反・超過リスクを検出
- write_rows_to_google_sheet: Google Sheets にデータを書き込み
- send_slack_dm: Slack で DM を送信

7.2 L3 用ツールスキーマ (JSON)

L3 で Agent がステップ計画を作成する際に参照する JSON Schema。Section 4 のスキーマをまとめて渡す。

DEFAULT_TOOL_SCHEMAS = json.dumps([
    # Section 4.1 ~ 4.5 のスキーマを配列で提供
], ensure_ascii=False, indent=2)

8. Post-MVP 拡張

項目 MVP Post-MVP
ベンダー freee のみ KOT, SmartHR 等のアダプタ追加
ツール数 5 給与計算、社会保険、年末調整等
Policy Engine 全ツール自動承認 tool_permissions テーブルで allow/deny/requires_confirmation
動的カタログ ハードコード テナント設定から有効ツールを動的生成
Shared Schema dataclass Pydantic v2 でバリデーション強化
MCP ハイブリッド 2 パターン並行 Custom Tool + MCP の判定を Policy Engine が自動化

変更履歴

日付 バージョン 変更内容
2026-04-18 v1.0 初版作成
2026-04-19 v1.1 Custom Tool Executor が freee 本番パスであることを明確化。custom_tool_use イベント処理フローを追記 (issue #11)
2026-04-20 v1.2 freee API パスを /hr/api/v1/ に修正、イベント名を agent.custom_tool_use に統一(実装整合)
2026-04-29 v1.3 read-only freee MCP gateway と Phase 0 / No.1 データ棚卸し対象を追加 (issue #46)
2026-05-05 v1.4 Lane B E2E 用 freee work_records seed CLI を追加
2026-05-05 v1.5 Lane B analysis tools を Custom Tool Executor / L3 catalog に追加
2026-05-06 v1.6 Lane C payroll tools(#60/#61/#58/#62)と draft-first 運用方針を追加