MCP と OpenAI Agents SDK で LLM と相談しながら Spotify をあやつる

# はじめに

MCP と OpenAI Agents SDK を使って、Spotify API を操作する簡単な AI エージェントを作ってみました。ユーザーのリクエストに応じて、曲を検索したり再生したりできます。

# 目的

- OpenAI Agents SDK の基本的な使い方を試す
- 外部 API (Spotify) と連携するエージェントを作成する
- 自然言語で音楽再生をコントロールする

# 方法

Python で OpenAI Agents SDK を使い、spotify-mcp を経由して Spotify API を操作するエージェントを実装しました。

## 処理フロー

ユーザーが指示を出すと、エージェントがそれを解釈し、必要に応じて Spotify API を呼び出して応答します。Spotify API の操作は `MCPServer` を介して行います。

```mermaid
graph TD
    A[ユーザー: 指示を入力] --> B(Agent: 指示を解釈);
    B --> C{Spotify 操作が必要？};
    C -- Yes --> D(Agent: Spotify API を呼び出し);
    D --> E(Spotify);
    E --> D;
    C -- No --> F(Agent: 応答を生成);
    D --> F;
    F --> G[ユーザーに応答];
    G --> A;
```

## ライブラリ

- `openai-agents`: エージェントの定義と実行フレームワーク
- `python-dotenv`: 環境変数の読み込み
- `asyncio`: 非同期処理

## ソフトウェア構造

- **`main.py`**: メインスクリプト
    - **`DEFAULT_INSTRUCTION`**: エージェントの基本的な指示・役割を定義
    - **`run_agent`**: 個別のユーザーメッセージに対してエージェントを実行する関数
    - **`run`**: 事前定義されたコマンドを実行し、その後ユーザーとの対話ループに入る関数
    - **`main`**: `MCPServer` を起動し、`run` 関数を実行するエントリーポイント

## リポジトリ

詳細なコードやセットアップ方法は、以下のリポジトリを参照してください。

https://github.com/u-masao/wave-explorer

# 結果

ユーザーが自然言語で指示を出すと、エージェントが Spotify で曲を検索し、再生キューに追加するなどの操作を実行できました。

## 実行例

以下は、実際にエージェントを実行した際のコンソール出力例です。

```text
$ uv run python -m src.main
トレース情報: https://platform.openai.com/traces/trace?trace_id=trace_12c909be94074618b9ede3febea1e26d

[実行中] 「The brecker brothers」の曲を3曲おしえて。テーマは「ゴリゴリのソロが聞ける曲」です。
[応答] <assistant spotifysearch="Brecker Brothers" limit=20 />

以下の3曲をおすすめします。いずれもゴリゴリのソロが堪能できます。

1. Some Skunk Funk
   • アルバム: The Brecker Bros. (1975)
   • ポイント: マイケル・ブレッカーの超絶テナー・ソロとランディ・ブレッカーのトランペットが火花を散らす代表曲。

2. Sponge
   • アルバム: Heavy Metal Be-Bop (1978)
   • ポイント: ジャズ・ファンク全開のリフの上で繰り広げられる両兄弟のタイトかつアグレッシブなソロ。

3. Straphangin’
   • アルバム: Detente (1981)
   • ポイント: 複雑なリズム・チェンジに乗って炸裂するソロがクセになる1曲。
   
[応答の詳細]: {'id': 'msg_68083852e74c81918c1d39f81b4ed99b09ba3d65ccc21935', 'content': [{'annotations': [], 'text': '<assistant spotifysearch="Brecker Brothers" limit=20 />\n\n以下の3曲をおすすめします。いずれもゴリゴリのソロが堪能できます。\n\n1. Some Skunk Funk  \n   • アルバム: The Brecker Bros. (1975)  \n   • ポイント: マイケル・ブレッカーの超絶テナー・ソロとランディ・ブレッカーのトランペットが火花を散らす代表曲。\n\n2. Sponge  \n   • アルバム: Heavy Metal Be-Bop (1978)  \n   • ポイント: ジャズ・ファンク全開のリフの上で繰り広げられる両兄弟のタイトかつアグレッシブなソロ。\n\n3. Straphangin’  \n   • アルバム: Detente (1981)  \n   • ポイント: 複雑なリズム・チェンジに乗って炸裂するソロがクセになる1曲。', 'type': 'output_text'}], 'role': 'assistant', 'status': 'completed', 'type': 'message'}
[実行中] Spotify で新しいセッションを作り artist:The brecker brothers の先ほどの3曲を再生して
[INFO] Listing available tools
[INFO] Available tools: ['SpotifyPlayback', 'SpotifySearch', 'SpotifyQueue', 'SpotifyGetInfo']
[INFO] Tool called: SpotifySearch with arguments: {'query': 'Brecker Brothers Some Skunk Funk', 'qtype': 'track', 'limit': 1}
# ... (Spotify API 呼び出しログ) ...
[INFO] Tool called: SpotifyPlayback with arguments: {'action': 'start', 'spotify_uri': 'spotify:track:4cJC5AjLjRUH2qtemrqLbD'}
# ... (Spotify API 呼び出しログ) ...
[INFO] Tool called: SpotifyQueue with arguments: {'action': 'add', 'track_id': '7EU6pph0fUURnHjyozWZB5'}
# ... (Spotify API 呼び出しログ) ...
[INFO] Tool called: SpotifyQueue with arguments: {'action': 'add', 'track_id': '4SUjJZSvVtVYwsFjNzltkM'}
# ... (Spotify API 呼び出しログ) ...
[応答] 新しいセッションを開始し、以下の3曲を再生・キューに追加しました。

1. 再生中：Some Skunk Funk
2. キューに追加：Sponge
3. キューに追加：Straphangin’

お楽しみください！
[応答の詳細]: {'id': 'msg_6808387669f88191b33a1accfbfeee6609ba3d65ccc21935', 'content': [{'annotations': [], 'text': '新しいセッションを開始し、以下の3曲を再生・キューに追加しました。\n\n1. 再生中：Some Skunk Funk  \n2. キューに追加：Sponge  \n3. キューに追加：Straphangin’  \n\nお楽しみください！', 'type': 'output_text'}], 'role': 'assistant', 'status': 'completed', 'type': 'message'}
ここからはあなたのターンです。AIエージェントに指示をだし続けて下さい
何します？> 現在のキューを表示して
[INFO] Listing available tools
[INFO] Available tools: ['SpotifyPlayback', 'SpotifySearch', 'SpotifyQueue', 'SpotifyGetInfo']
[INFO] Tool called: SpotifyQueue with arguments: {'action': 'get'}
# ... (Spotify API 呼び出しログ) ...
[応答] 現在のキュー:

再生中:
1. Some Skunk Funk – The Brecker Brothers

続くキュー:
2. Sponge – The Brecker Brothers
3. Straphangin’ – The Brecker Brothers
# ... (以下省略) ...
何します？>
終了します。またね。
```

# 考察

- **OpenAI Agents SDK**: `Agent` と `Runner` を使うことで、比較的簡単にエージェントの定義と実行ができました。非同期処理 (`asyncio`) との組み合わせもスムーズです。
- **Spotify 連携**: `MCPServer` (今回は `MCPServerStdio` を使用) を介して外部プロセスとして Spotify API ラッパーを起動し、ツールとして連携させる方法が有効でした。これにより、認証情報などをエージェント本体から分離できます。
- **プロンプト**: `DEFAULT_INSTRUCTION` でエージェントの役割や API の使い方を指示することで、LLM が適切にツール (Spotify API) を呼び出すことができました。特に、検索クエリを短くする指示は有効でした。
- **改善点**:
    - エラーハンドリングをより丁寧にする。
    - より複雑な対話（複数のターンにわたる指示など）に対応する。
    - Gradio などで GUI を作成する。

# まとめ

OpenAI Agents SDK を利用して、Spotify を操作する簡単な AI エージェントを作成しました。SDK の基本的な使い方や、`MCPServer` を介した外部 API との連携方法を学ぶことができました。プロンプトでエージェントの振る舞いを制御する面白さも体験できました。

やってることはシンプルですが、普通に面白いのでぜひ実際に試して頂きたいです。「コレができるなら、もしかしてアレもできちゃう？」みたいな連想が無限に続く可能性がありますのでご注意下さい。

最後で読んで頂きありがとうございました！

# 備考

## メインのコード抜粋

### エージェントの基本指示

エージェントの役割と Spotify API の使い方を指示します。

```python
DEFAULT_INSTRUCTION = """
    あなたは出版された音楽に関するエキスパートです。
    ユーザーのリクエストにシンプルな回答を与えて。
    必要に応じて Spotify API を操作して。
    SpotifySearch は文字列一致の傾向が強いです。
    そのため、クエリ文字列を短くして一覧を取得し、
    LLMが最適なものを選んで。
"""
```

### エージェントの実行部分

ユーザーからのメッセージを受け取り、エージェントを実行して応答を返します。

```python
async def run_agent(
    history: Dict[str, Any],
    message: str,
    mcp_servers: List[MCPServer],
    model_name: str = "o4-mini",
):
    # エージェントを定義
    agent = Agent(
        name="再生音楽のエキスパート",
        instructions=textwrap.dedent(DEFAULT_INSTRUCTION),
        mcp_servers=mcp_servers,
        model=model_name,
    )

    # メッセージを作成
    history.append({"role": "user", "content": message})

    # エージェントを実行
    result = await Runner.run(starting_agent=agent, input=history)

    # 結果を処理
    print(f"[応答] {result.final_output}")
    return result.to_input_list()
```

### メイン処理

Spotify API を操作するための `MCPServer` を起動し、対話ループを開始します。

```python
async def main():
    """メイン処理"""
    # MCPServer (Spotify API ラッパー) の起動設定
    params = {
        "command": "uv",
        "args": [
            "--directory",
            os.getenv("MCP_SPOTIFY_PATH"),
            "run",
            "spotify-mcp",
        ],
        "env": {
            "SPOTIFY_CLIENT_ID": os.getenv("SPOTIFY_CLIENT_ID"),
            "SPOTIFY_CLIENT_SECRET": os.getenv("SPOTIFY_CLIENT_SECRET"),
            "SPOTIPY_REDIRECT_URI": os.getenv("SPOTIPY_REDIRECT_URI"),
        },
    }
    async with MCPServerStdio(name="spotify mcp server", params=params) as server:
        trace_id = gen_trace_id()
        with trace(workflow_name="wave explorer", trace_id=trace_id):
            print(
                "トレース情報: https://platform.openai.com/traces/trace"
                f"?trace_id={trace_id}\n"
            )
            # 事前定義コマンド実行 & 対話ループ開始
            await run(server)

if __name__ == "__main__":
    asyncio.run(main())
```