📉

MCPでやる必要のない業務をSkillsに置き換えてトークンと時間の消費を爆減してみた

に公開

こんにちは。ダイの大冒険エンジョイ勢のbun913と申します。私はSDET(Software Development Engineer in Test)として、QAチームにいる何でも屋さんとして、私より優秀なみなさんが本質的なことに時間を使えるように日夜改善を考えています。

私は弊QAチームでのMCP活用として以下のような記事を書いておりました。

https://zenn.dev/moneyforward/articles/3f94775f9c8784

今回は最近話題の Agent Skills とあらかじめ作っておいたCLIツールを組み合わせて、QAチームが本質的ではない作業にかける時間とトークン消費量を大幅に削減する試みを紹介します。

https://platform.claude.com/docs/ja/agents-and-tools/agent-skills/overview

内容自体はQAチームの取り組みですが、考え方やツールの使い方は他のことに流用できるのではないかと考えています。

先に背景とやったことをざっくり説明

背景: 前回までの取り組み

弊チームでは自動テストに力をいれつつも、手動テストの威力も大事にしています。実際探索的なテストや、「ここ怪しいな」というアプローチでリリースより前にバグを見つけることが少なからずあります。

手動テストに関する(主な)工程をざっくり整理すると、以下のようになります。

  • 1:要求や仕様から「何をテストするか」を整理する(テスト分析)
  • 2:「何をテストするか」から「じゃあ具体的にどうやってテストするか」をテストケースとして整理する(テスト設計)
  • 3:テストケースにしたがってテスト実行

テスト分析やテスト設計に関して、前回では Claude Code + MCP を活用して以下のような改善を行なっていました。

  • テスト分析手法の標準化
    • ゆもつよメソッド」と呼ばれる手法をアレンジして5つのステップでAIに実行させ、分析の質の標準化を目指した
  • 観点を与えたAIにガンガン分析の成果物を出してもらい、QAEがレビューに集中する
    • AIをコパイロット的に使って、本質的な考える作業に力を入れてもらえるようにしました
  • MCPによるテストケースの成果物作成自動化
    • 私たちは Zephyrなどのようなテスト管理ツールを使っているため、ここにテストケースを登録してきます
    • テストの際には Zephyr に登録されたテストケースに対して、テストの結果やコメントを登録していくイメージです

前回の改善

詳細は以下に記載しています。

https://zenn.dev/moneyforward/articles/3f94775f9c8784

問題点と改善点

しかし実際に行ってみると、以下のような問題点が目にみえるようになってきました。

  • 1: AIへのインプットにまだまだ改善の余地がある
    • 静的なインプット(プロンプトなど) + 動的なインプット(コンテキスト:仕様、要求、デザインなど)
  • 2: AIの中間成果物のアウトプットの形式も同様
    • これまでは人間がレビューしやすいように主に mermaid.js や マークダウン形式で出力させていました
  • 3: Zephyrに対してテストケースを保存する際の、トークンと時間の消費が大きい

1~3のすべての改善を行いましたが、1については今後も様子を見ながら改善を行いつつ、良さそうであればまたご紹介したいと思います。

今回は2と3の改善について主にご紹介します。

アウトプットの形式の改善 と Agent Skills の利用

今回は簡単にいうと、アウトプットの形式をyamlのように構造化された、プログラムでも扱いやすい形式にし、Zephyrを操作するCLI + 業務知識などをパックにした Agent Skills を作成する改善を行いました。

cli_improvement

アウトプットの形式を改善

前回までのフェーズではテスト分析(何をテストするか)の工程において、Step1 ~ Step5 と段階を分けてテスト分析を行なっていました。

各ステップにおいて中間成果物をマークダウンとして出力し、最終的にそれらステップを終えてテスト設計という段階でZephyrというテストマネジメントツールにテストケースを格納していきます。

以下のように1つのテストケースが1行ずつ出来上がっていくようなイメージでした。

| 機能カテゴリ | 機能項目 | テストカテゴリ | 仕様項目 | ステップ | 期待結果 | テストパラメータ |
|-------------|---------|--------------|---------|---------|---------|----------------|
| 商品検索 | キーワード検索 | Display | デフォルト値 | 1. 商品検索画面を開く<br>2. 検索ボックスのデフォルト状態を確認 | 1. 商品検索画面に遷移(https://example.com)<br>2. プレースホルダー「商品名を入力」が表示されている | - |
| 商品検索 | キーワード検索 | Screen Input, Button Click | 検索実行後の遷移 | 1. キーワードを入力<br>2. 「検索」をクリック | 1. 入力したキーワードが保持される<br>2. 検索結果一覧が表示される(参照: https://example.com) | - |

これらを1行ずつMCPで読み取りながら、素早く確認しつつあらかじめ用意してOSSとして公開してたMCPサーバーを活用して、Zephyrというテストマネジメントツールにテストケースを格納していました。

https://zenn.dev/moneyforward/articles/9fda378312370e

しかし、以下の点でマークダウン形式ですべてを管理するのには若干課題を感じていました。

  • csvなどと比べて図などを活用できて良いが、レビュー時にプレビューを使わないと確認が手間
    • mermaid.jsなどを活用する成果物の時だけマークダウン形式を使えば十分
  • (体感値ですが)途中で「しまった。これらテストケースに全部参考のURLを貼り忘れた」などとなった時のAIの修正に時間がかかる
  • マークダウンをプログラマティックに処理しようとすると、手間がかかる。全てMCPでAIに任せて処理をする流れを作ってしまっていた

そこで以下のようなイメージで、各中間成果物を可能な限りyamlで構造化して管理するようにしてみました。最終的にはZephyrにテストケースやフォルダの構造をそのまま再現した形で作成します。

test_analysis:
  - category: "商品検索"
    items:
      - name: "キーワード検索"
        test_categories:
          - type: "Display"
            specs:
              - title: "デフォルト値"
                steps:
                  - "1. 商品検索画面を開く"
                  - "2. 検索ボックスのデフォルト状態を確認"
                expected_results:
                  - "1. 商品検索画面に遷移 (https://example.com)"
                  - "2. プレースホルダー「商品名を入力」が表示されている"
                parameters: null

          - type: "Screen Input, Button Click"
            specs:
              - title: "検索実行後の遷移"
                steps:
                  - "1. キーワードを入力"
                  - "2. 「検索」をクリック"
                expected_results:
                  - "1. 入力したキーワードが保持される"
                  - "2. 検索結果一覧が表示される (参照: https://example.com)"
                parameters: null

これにより、以下の嬉しいポイントがすぐにわかりました。

  • Claude Code などを活用してAIとやり取りする際にプレビューを見なくても変更がはっきりと見やすい
  • jsonやcsvと比べると人間も見やすく、tomlと比べるとフォルダなどを含んだヒエラルキーが誰でもわかる・扱える
  • yaml にしたことでプログラムで一括処理がしやすくなった

プログラムの処理のしやすさなどはやる前からわかりそうなものです(私は開発者でもあるので)が、正直なところ「私が作ったOSSのMCPサーバーを使って改善するぞ」という意識があって、そこまで考えずに目にみえる改善をやり始めてしまったのだと思います。これは大いなる反省です。

しかし、そのおかげでAIに成果物を出力させる際は、どのようにしてその成果物を使うのか、誰がチェックするのかまで考えて出力形式を考えておくべきだと学べました。

Agent Skills の活用

さて、ここまでの改善でyamlという形式で成果物が出力されるようになったので、プログラムによる処理がしやすくなりました。

そこでこれまでMCPでZephyrにテストケースやフォルダなどの作成をしていた部分を、より「プログラムに任せられるところは任せる」かつ「何度も車輪の再発明をしないで良いように共有する」ことを目指すようにしました。

そこで以下の素晴らしいブログを参考に、次のことを実践してみることにしました

  • 人間でもAIでも、どの環境でも使える道具を用意しておき
  • Agent Skills という形でエージェントに使い方を教え
  • 足りない部分をプログラムで補っていろんな場面で柔軟に利用できるようにしよう

https://dev.classmethod.jp/articles/agent-skills-2025-standardized-overview/

まず年末年始の空いた時間で、あらかじめbunでZephyrを操作するためのCLIツールを作成していました。

https://zenn.dev/moneyforward/articles/ca87c065173a46

そこで以下2点のスキルを作成しました。

テスト設計の業務に特化したSkills

私が Skills に価値を感じているところは、AIにプログラムファイルなどを含めて「君はこういうことのために、こういうことができるんだ」ということを保持して、それを皆さんに共有できる点です。

このような形で Skills の指示を書いて、pythonプログラムと一緒に格納することで、プログラマティックにZephyrにテストケースを一括アップロードできるスキルを作ってみました。

なお、私は Claude Code に限らず Cursor などのルールやスキルの記載を一箇所で管理することができるrulesyncというOSSを活用しているため、若干記法が特殊ですが、大枠は変わりませんのでご了承ください。
(また一部のみを抜粋しています)

---
name: bulk-upload-to-zephyr
description: Bulk upload test cases from YAML to Zephyr Scale using Python script
targets: ["claudecode"]
claudecode:
  allowed-tools:
    - "Read"
    - "Write"
    - "Bash"
---

# Zephyrへの一括アップロード: YAMLからテストケースをバッチアップロード

**目的**: Pythonスクリプトを使用してYAMLファイルからZephyr Scaleへテストケースを一括アップロードし、ID追跡による冪等性を維持します。

## 初期セットアップ

### Step 1: 親フォルダIDをユーザーに確認

アップロードを開始する前に、ユーザーに確認してください:
- 「テストケースをどのフォルダの下に作成しますか?」
- 「ZephyrのフォルダIDを入力してください(またはEnterキーでサンプルプロジェクトのデフォルト値: 11111を使用)」

**一般的な親フォルダ**:
- Sample Project: 11111
- その他のプロジェクト: ユーザーがIDを提供

**フォルダIDを見つけるには**:
```bash
zephyr folder list --folder-type TEST_CASE
\```

### Step 2: YAMLファイルを確認

処理対象のYAMLファイルをユーザーに表示:
- すべての `artifacts/testcases/testcases_*.yaml` ファイルを一覧表示
- 処理前にユーザーに確認を求める

## 依存関係

- `zephyr` CLIがインストールおよび設定済みであること
- テストケースYAMLファイルが `artifacts/testcases/testcases_*.yaml` に存在すること
- ユーザーから親フォルダIDを取得済みであること

## 使用方法

bulk_upload.pyを使用してYAMLファイルを解析し、Zephyrに保存できます。

### 基本的なアップロード

```bash
# すべてのtestcases YAMLファイルを処理
python scripts/upload_to_zephyr.py artifacts/testcases/*.yaml
\```

### アップロード後のID確認

YAMLファイルで追加されたフィールドを確認:
```yaml
sections:
  - name: 商品一覧画面
    folder_id: 11112  # ← 作成後に追加
    folders:
      - name: テストケース
        folder_id: 11113  # ← 作成後に追加
        test_cases:
          - title: Test title
            case_id: ABC-T123  # ← 作成後に追加
\```

## YAMLからZephyrへのフィールドマッピング

### テストケースフィールド

| YAMLフィールド | Zephyr APIフィールド | 備考 |
|------------|------------------|-------|
| `title` | `name` | テストケース名 |
| `objective` | `objective` | テストの目的/説明 |
| `pre_condition` | `precondition` | 前提条件 |
| `steps[].step` | `testSteps[].inline.description` | ステップの説明 |
| `steps[].expected_result` | `testSteps[].inline.expectedResult` | 期待される結果 |
| `steps[].test_data` | `testSteps[].inline.testData` | このステップのテストデータ |
| `notes` | マッピングなし | 内部ドキュメント専用 |
| `function_category` | マッピングなし | テスト分析のトレーサビリティ用 |
| `function_item` | マッピングなし | テスト分析のトレーサビリティ用 |
| `test_category` | マッピングなし | テスト分析のトレーサビリティ用 |

### フォルダフィールド

| YAMLフィールド | Zephyr APIフィールド | 備考 |
|------------|------------------|-------|
| `name` | `name` | フォルダ名 |
| `folder_id` | `id` | 作成後に追加 |
| `notes` | マッピングなし | 内部ドキュメント専用 |

### 利用可能なカスタムフィールド(ABCプロジェクト)

サンプルテストケース構造から取得:
- `automation_id`: 自動化テストID
- `Automation Status`: 自動化ステータス
- `Test Level`: テストレベル分類
- `Automation Candidate`: 自動化に適しているかどうか
- `Test Type`: テストの種類

## リファレンス

適切な構造を持つ完全なテストケースYAMLの例:
- `projects/sample_project/artifacts/testcases/testcases_01_login.yaml`
- `projects/sample_project/artifacts/testcases/testcases_04_verification.yaml`
元ファイル(英語)
---
name: bulk-upload-to-zephyr
description: Bulk upload test cases from YAML to Zephyr Scale using Python script
targets: ["claudecode"]
claudecode:
  allowed-tools:
    - "Read"
    - "Write"
    - "Bash"
---

# Bulk Upload to Zephyr: Batch Upload Test Cases from YAML

**Purpose**: Batch upload test cases from YAML files to Zephyr Scale using Python script, maintaining idempotency through ID tracking.

## Initial Setup

### Step 1: Ask User for Parent Folder ID

Before starting upload, ask user:
- "Which folder should I create test cases under?"
- "Please provide the Zephyr folder ID (or press Enter for sample project default value: 11111)"

**Common parent folders**:
- Sample Project: 11111
- Other projects: User provides ID

**To find folder ID**:
```bash
zephyr folder list --folder-type TEST_CASE
\```

### Step 2: Confirm YAML Files

Show user which YAML files will be processed:
- List all `artifacts/testcases/testcases_*.yaml` files
- Ask user to confirm before proceeding

## Dependencies

- `zephyr` CLI installed and configured
- Test case YAML files in `artifacts/testcases/testcases_*.yaml`
- Parent folder ID from user

## Usage

You can use bulk_upload.py to parse yaml file and save them into Zephyr.

### Basic Upload

```bash
# Process all testcases YAML files
python scripts/upload_to_zephyr.py artifacts/testcases/*.yaml
\```

### Verify IDs After Upload

Check YAML files for added fields:
```yaml
sections:
  - name: 商品一覧画面
    folder_id: 11112  # ← Added after creation
    folders:
      - name: テストケース
        folder_id: 11113  # ← Added after creation
        test_cases:
          - title: Test title
            case_id: ABC-T123  # ← Added after creation
\```

## YAML to Zephyr Field Mapping

### Test Case Fields

| YAML Field | Zephyr API Field | Notes |
|------------|------------------|-------|
| `title` | `name` | Test case name |
| `objective` | `objective` | Test purpose/description |
| `pre_condition` | `precondition` | Prerequisites |
| `steps[].step` | `testSteps[].inline.description` | Step description |
| `steps[].expected_result` | `testSteps[].inline.expectedResult` | Expected outcome |
| `steps[].test_data` | `testSteps[].inline.testData` | Test data for this step |
| `notes` | Not mapped | Internal documentation only |
| `function_category` | Not mapped | For test analysis traceability |
| `function_item` | Not mapped | For test analysis traceability |
| `test_category` | Not mapped | For test analysis traceability |

### Folder Fields

| YAML Field | Zephyr API Field | Notes |
|------------|------------------|-------|
| `name` | `name` | Folder name |
| `folder_id` | `id` | Added after creation |
| `notes` | Not mapped | Internal documentation only |

### Available Custom Fields (ABC Project)

Retrieved from sample test case structure:
- `automation_id`: Automation test ID
- `Automation Status`: Automation status
- `Test Level`: Test level classification
- `Automation Candidate`: Whether suitable for automation
- `Test Type`: Type of test

## Reference

Complete test case YAML examples with proper structure:
- `projects/sample_project/artifacts/testcases/testcases_01_login.yaml`
- `projects/sample_project/artifacts/testcases/testcases_04_verification.yaml`

またこのようなPythonファイルを一緒に配置しておきます。これらのスクリプトはAIに全て作ってもらい、動作のチェックだけしております。(このようなスクリプトをサクッと作っていただきやすくなりました)

#!/usr/bin/env python3
"""Upload test cases from YAML to Zephyr Scale"""

import yaml
import subprocess
import json
import sys

def create_folder(name, parent_id, project_key="ABC"):
    """Create folder in Zephyr and return folder ID"""
    cmd = ["zephyr", "folder", "create",
           "--project-key", project_key,
           "--name", name,
           "--folder-type", "TEST_CASE",
           "--parent-id", str(parent_id)]

    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error creating folder '{name}': {result.stderr}")
        return None

    folder_data = json.loads(result.stdout)
    print(f"✓ Created folder: {name} (ID: {folder_data['id']})")
    return folder_data["id"]

def create_testcase(test_case, folder_id):
    """Create test case in Zephyr and return case key"""
    cmd = ["zephyr", "testcase", "create",
           "--name", test_case["title"],
           "--objective", test_case["objective"],
           "--precondition", test_case["pre_condition"],
           "--folder-id", str(folder_id)]

    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Error creating test case '{test_case['title']}': {result.stderr}")
        return None

    case_data = json.loads(result.stdout)
    print(f"✓ Created test case: {test_case['title']} ({case_data['key']})")
    return case_data["key"]

def process_yaml(yaml_file, parent_folder_id):
    """Process YAML file and upload to Zephyr"""
    print(f"\n📁 Processing: {yaml_file}")

    with open(yaml_file, 'r', encoding='utf-8') as f:
        data = yaml.safe_load(f)

    modified = False

    for section in data["sections"]:
        # Create section folder if no folder_id
        if "folder_id" not in section:
            folder_id = create_folder(section["name"], parent_folder_id)
            if folder_id:
                section["folder_id"] = folder_id
                modified = True
        else:
            folder_id = section["folder_id"]
            print(f"○ Section already exists: {section['name']} (ID: {folder_id})")

        # Process test cases directly in section (no folders)
        for test_case in section.get("test_cases", []):
            if "case_id" not in test_case:
                case_key = create_testcase(test_case, folder_id)
                if case_key:
                    test_case["case_id"] = case_key
                    modified = True

                    if "steps" in test_case and test_case["steps"]:
                        create_teststeps(case_key, test_case["steps"])
            else:
                print(f"○ Test case already exists: {test_case['title']} ({test_case['case_id']})")

        # Process nested folders
        if "folders" in section:
            process_folders(section["folders"], folder_id, data, yaml_file)

        # Save progress
        if modified:
            with open(yaml_file, 'w', encoding='utf-8') as f:
                yaml.dump(data, f, allow_unicode=True, sort_keys=False)
            modified = False

    print(f"✅ Completed: {yaml_file}\n")

if __name__ == "__main__":

    parent_folder_id = int(sys.argv[1])
    yaml_files = sys.argv[2:]

    print(f"🚀 Starting upload to Zephyr (Parent Folder ID: {parent_folder_id})")

    for yaml_file in yaml_files:
        try:
            process_yaml(yaml_file, parent_folder_id)
        except Exception as e:
            print(f"❌ Error processing {yaml_file}: {e}")
            print("You can re-run this script to continue from where it stopped")
            sys.exit(1)

    print("🎉 All files processed successfully!")

テストケースの一括アップロードに関してはこのスキルを使うことで半自動化され、処理の大半をプログラムが負ってくれるようになったので、ものの数分で数百件以上のテストケースを一気に格納できるようになり時間とトークンの大幅な削減に成功しました。

MCPに全てを任せると数十分以上かかりますし、何より「一回出力した成果物を元に」また「同じような構造のものをZephyrに入れ直す」という数十万トークンを使ってしまうような処理をさせていたので、大きな改善です。

また、Skills として切り出していることで目的やできることを理解したAIエージェントが、多少の間違いやフォーマットのおかしい点などを直してくれるのも「プログラムだけ」「AIだけ」に比べた時の利点として感じています。

最近ではMCPでもオンデマンドに必要な時に必要なツールを探して使う機能も考案されているようですが、 Agent Skills はツールも必要な時に読み込むため無駄なコンテキスト消費が少ない点も魅力だと理解していますので、うまく使っていきたいですね。

MCPを置き換える汎用的に使えるSkills

また、せっかくCLIを作っておいたので、MCPに置き換えてもいいように基本的な Zephyr 操作のための知恵も Skill として与え、こちらもチームに共有しています。

これにより柔軟なZephyrの操作が可能になり、最悪ちょっとした用事なら自然言語でAIにそのままやってもらえるようにしました。このような使い方ならMCPでも十分かとも思いましたが、せっかくなので依存先はCLIだけにしようと思った次第です。

---
name: zephyr
description: Zephyr Scale CLI operations - Create folders, test cases, manage test resources individually
targets: ["claudecode"]
claudecode:
  allowed-tools:
    - "Bash"
    - "Read"
---
# Zephyr: Zephyr Scale CLI Operations

**Purpose**: Execute individual Zephyr Scale operations using the `zephyr` CLI.

## Use Cases

- Create a single folder
- Create a single test case
- List folders or test cases
- Get folder/test case details
- Update test case metadata
- Create/update test steps
- Manage test cycles, plans, executions
- Query Zephyr resources

## Zephyr CLI Commands Reference

### Folder Management

**Create folder**:
```bash
zephyr folder create \
  --project-key ABC \
  --name "Folder Name" \
  --folder-type TEST_CASE \
  --parent-id 12345  # optional, omit for root folder
\```
Returns JSON: `{"id": 123456, "name": "Folder Name", ...}`

**Get folder**:
```bash
zephyr folder get 123456
\```

**List folders**:
```bash
zephyr folder list --project-key ABC --folder-type TEST_CASE
\```

### Test Case Management

**Create test case with inline steps** (RECOMMENDED):
```bash
zephyr testcase create \
  --name "Test case title" \
  --objective "Test purpose" \
  --precondition "Prerequisites" \
  --folder-id 123456 \
  --step "Step 1 description|Expected result 1" \
  --step "Step 2 description|Expected result 2" \
  --step "Step 3 description|Expected result 3"
\```
Returns JSON: `{"key": "ABC-T123", ...}`

**Important**: The `--step` option creates test steps inline during test case creation. This is the RECOMMENDED approach for initial creation.

**Get test case**:
```bash
zephyr testcase get ABC-T123
\```

**Update test case** (metadata only, does NOT update steps):
```bash
zephyr testcase update ABC-T123 \
  --name "Updated title" \
  --objective "Updated objective" \
  --precondition "Updated precondition"
\```

**List test cases**:
```bash
zephyr testcase list --project-key ABC --folder-id 123456
\```

(省略)

## Notes

- All commands return JSON output
- Use `jq` for JSON parsing if needed: `zephyr folder get 123 | jq '.name'`
- For bulk operations, use `/bulk-upload-to-zephyr` skill instead
- Error handling: Check `$?` exit code after each command

これにより、私個人としてもOSSを2つも管理せずに済みますし、人間が自分でCLIを使うこともできるので、このCLIは本当に作っておいて良かったなと思いました。

もはや一般の知識としてAIが当然のように使いこなしてくる gh コマンドのように zephyr コマンドも使いこなしてくれます。とても素晴らしい。

まとめ

  • マークダウンとMCPに依存していたQAチームの業務を改善しました
  • 改善点
    • マークダウンだけでなく、yamlなどの構造化されたファイル形式を取り入れた
    • Agent Skills + CLI + pythonスクリプトで、構造化されたyamlをプログラムで半自動化で処理できるようにした
      • MCP + AI ゴリ押しよりもトークンと時間の消費が圧倒的に改善されました

以上、とあるQAチームのAI活用の第2弾についてご紹介しました。

今回で「AIにしてもらいたい仕事」と「機械的に処理してもらうべき仕事」、そしてそのための「CLI(道具)を配る前準備」の大事さを改めて認識しました。
(わかっていたつもりで、MCPサーバーにばかり目がいっていました)

また改善点が見つかったらご紹介したいと思います。
以上、ご覧いただきありがとうございました。

GitHubで編集を提案
Money Forward Developers

Discussion