AIエージェントを構築する Strands Agents SDK を使って MCP ホストを動かす

## はじめに

AWS が 2025 年 5 月 16 日に発表した [Strands Agents SDK](https://strandsagents.com/latest/) を使って MCP ホストアプリケーションを動かしてみました。元ネタは、みのるんさんのこの記事です。

https://qiita.com/minorun365/items/428ca505a8dd40136b5d

この記事にある GitHub のコードをもとにちょっといじってみたので、その感触を紹介します。本記事では、AWS Bedrock の Claude モデルを活用して [AWS Documentation MCP サーバー](https://awslabs.github.io/mcp/servers/aws-documentation-mcp-server/) を使って質問の回答を得ます。そして、その内容を Slack MCP サーバーを使って Slack チャンネルに送信してみます。

## 参考情報

https://aws.amazon.com/jp/blogs/aws/introducing-strands-agents-open-source-ai-agent-framework-for-developers/

https://strandsagents.com/latest/

https://qiita.com/minorun365/items/428ca505a8dd40136b5d

https://qiita.com/revsystem/items/19b057686c9012801fc5

## Strands Agents SDK とは

Strands Agents SDK とは、数行のコードで AI エージェントの実装を実現する SDK です。[AWS ブログ](https://aws.amazon.com/jp/blogs/aws/introducing-strands-agents-open-source-ai-agent-framework-for-developers/) に以下のように紹介されています。

> Strands Agents は、わずか数行のコードで AI エージェントを構築・実行するモデル駆動型アプローチを採用したオープンソース SDK です。Strands は、シンプルなエージェントのユースケースから複雑なものまで、そしてローカル開発から本番環境でのデプロイまで対応します。

「わずか数行」とあるとおり、4 行で質問に答える AI エージェントが実装できました。

```python:sample.py
from strands import Agent
from strands.models import BedrockModel

agent = Agent(BedrockModel())
agent("こんにちは。あなたは誰ですか？")
```

```shell
$ python ./sample.py

こんにちは！私は AI アシスタントです。質問に答えたり、情報提供をしたり、会話をしたりするためにここにいます。何かお手伝いできることはありますか？
```

モデル名やリージョンを指定していませんが、省略すると us-west-2 の Anthropic Claude 3.7 Sonnet を実行するようです。省略せずに書くと、このような感じでしょうか。

```python:sample.py
from strands import Agent
from strands.models import BedrockModel

bedrock_model = BedrockModel(
    model_id="us.anthropic.claude-3-7-sonnet-20250219-v1:0",
    region_name='us-west-2',
)

agent = Agent(model=bedrock_model)

agent = Agent(BedrockModel())
agent("こんにちは。あなたは誰ですか？")
```

## 前提条件

- WSL2
- Python 3.12
- 以下の記事を参考に、Slack MCP Server を使って自分の Slack チャンネルにメッセージを送信できるようにしておく

https://qiita.com/revsystem/items/b7904fc53a72ef3ba02a

## 環境構築

### node.js のインストール

Slack MCP サーバーを動かすために node.js が必要です。

```shell
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt-get install -y nodejs
```

### Python モジュールのインストール

requirements.txt を作成して、以下のモジュールをインストールします。

```python:requirements.txt
uv
streamlit
strands-agents
strands-agents-builder
strands-agents-tools
```

```shell
$ pip install -r requirements.txt
```

### コードの作成

[みのるんさんのリポジトリ](https://github.com/minorun365/strands-mcp-agent) を参考に、以下のコードを作成しました。みのるんさんのアプリケーションとの違いは、このようなものです。

- Bedrock モデルの選択を追加
- 最大トークン数の設定を追加
- Temperature の設定を追加
- リージョンの選択を追加
- Slack チャンネル名の入力を追加
- 回答のフォーマットを指定するシステムプロンプトを追加
- 簡易プログレスバーを表示

```python:strands-mcp-agent.py
import asyncio
import os
import streamlit as st
from strands import Agent
from strands.models import BedrockModel
from strands.tools.mcp import MCPClient
from mcp import stdio_client, StdioServerParameters

# ページ設定（最初に実行する必要がある）
st.set_page_config(
    page_title="Strands MCPエージェント",
    page_icon="☁️",
    menu_items={'About': "Strands Agents SDKで作ったMCPホストアプリです。"}
)

# 環境変数のチェック
required_aws_env_vars = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_DEFAULT_REGION"]
required_slack_env_vars = ["SLACK_BOT_TOKEN", "SLACK_TEAM_ID"]

missing_aws_vars = [var for var in required_aws_env_vars if not os.environ.get(var)]
missing_slack_vars = [var for var in required_slack_env_vars if not os.environ.get(var)]

if missing_aws_vars or missing_slack_vars:
    st.error(f"以下の環境変数が設定されていません。: {', '.join(missing_aws_vars + missing_slack_vars)}")
    st.info("以下のコマンドで環境変数を設定してください。")
    for var in missing_aws_vars:
        st.code(f'export {var}="your_value_here"')
    for var in missing_slack_vars:
        st.code(f'export {var}="your_value_here"')
    st.stop()
else:
    st.success("AWS, Slackの認証情報が正常に読み込まれました。")


# メインエリア
st.title("Strands MCPエージェント")
st.markdown("👈 サイドバーで好きなMCPサーバーを設定して、[Strands Agents SDK](https://aws.amazon.com/jp/blogs/news/introducing-strands-agents-an-open-source-ai-agents-sdk/) を動かしてみよう！")
question = st.text_area("質問を入力", "Bedrockでマルチエージェントは作れる？", height=100)

# サイドバー
with st.sidebar:
    package_manager = st.selectbox("パッケージマネージャー", ["uvx", "npx"])
    mcp_args = st.text_input(f"MCPサーバーのパッケージ名（{package_manager}用）", "awslabs.aws-documentation-mcp-server@latest")

    # 指定されたモデルのみをセレクトボックスで選択できるようにする
    bedrock_models = [
        # Claude モデル
        "us.anthropic.claude-sonnet-4-20250514-v1:0",  # Claude Sonnet 4
        "us.anthropic.claude-opus-4-20250514-v1:0",    # Claude Opus 4
        "us.anthropic.claude-3-7-sonnet-20250219-v1:0", # Claude 3.7 Sonnet

        "apac.anthropic.claude-sonnet-4-20250514-v1:0",  # Claude Sonnet 4
        "apac.anthropic.claude-opus-4-20250514-v1:0",    # Claude Opus 4
        "apac.anthropic.claude-3-7-sonnet-20250219-v1:0", # Claude 3.7 Sonnet
        # Amazon Nova モデル
        "amazon.nova-premier-v1:0",
        "amazon.nova-pro-v1:0",
        "amazon.nova-lite-v1:0",
        "amazon.nova-micro-v1:0"
    ]

    model_id = st.selectbox("Bedrockのモデル", bedrock_models)
    temperature = st.slider("Temperature", min_value=0.0, max_value=1.0, step=0.1)

    # max_tokensを入力欄で設定できるようにする
    max_tokens = st.number_input("最大トークン数", min_value=100, max_value=10000, value=2000, step=100)

    # リージョンをセレクトボックスで選択できるようにする
    regions = [
        "us-east-1",  # バージニア北部
        "us-west-2",  # オレゴン
        "ap-northeast-1"  # 東京
    ]
    region = st.selectbox("AWSリージョン", regions)

    slack_channel_name = st.text_input("Slackチャンネル名", "#general")
    st.text("")
    st.markdown("このアプリの作り方 [https://qiita.com/minorun365/items/dd05a3e4938482ac6055](https://qiita.com/minorun365/items/dd05a3e4938482ac6055)")


def create_mcp_client(mcp_args, package_manager):
    """MCPクライアントを作成"""
    # npxの場合は-yフラグを追加
    if package_manager == "npx":
        args = ["-y", mcp_args]
    else:
        args = [mcp_args]

    return MCPClient(lambda: stdio_client(
        StdioServerParameters(command=package_manager, args=args)
    ))


def create_slack_mcp_client():
    """Slack MCPクライアントを作成"""
    # Slack環境変数を設定
    slack_env = {
        "SLACK_BOT_TOKEN": os.environ.get("SLACK_BOT_TOKEN", ""),
        "SLACK_TEAM_ID": os.environ.get("SLACK_TEAM_ID", ""),
    }

    return MCPClient(lambda: stdio_client(
        StdioServerParameters(
            command="npx",
            args=["-y", "@modelcontextprotocol/server-slack"],
            env=slack_env
        )
    ))


def create_agent(clients, model_id, region, temperature, max_tokens):
    """エージェントを作成"""
    all_tools = []
    for client in clients:
        all_tools.extend(client.list_tools_sync())

    SYSTEM_PROMPT = """
あなたはユーザーの質問を理解し、最適なMCPサーバーを選択して回答するアシスタントです。

最適なMCPサーバーを選択し、それが提供するツールをひとつもしくは複数利用して、情報を収集してください。
情報を収集後、情報を適切な粒度に整理し、Markdown形式で回答を作成してください。
適切な粒度とは、見出しや段落、箇条書き、コードブロックなどを使って情報の概要がおおよそ理解できる程度であることを示します。
詳細な情報が引用元のURLに記載されていることを示すため、回答の末尾に引用元のURLを記載してください。回答および引用元のURLは以下のフォーマットに従います。

<回答のフォーマット>
# 見出し
## 見出し
### 見出し
- 箇条書き
- 箇条書き
- 箇条書き\
## 参考情報
- [引用元のURL](https://example.com)
- [引用元のURL](https://example.com)
</回答のフォーマット>

回答内容は、画面に出力するとともにユーザーが指定するSlackチャンネルにも同じ内容をポストしてください。これは、ユーザーが回答内容を確認するために訳に立ちます。
ユーザーが入力したチャンネル名をもとに、Slack MCPサーバーが提供するSlackのチャンネル一覧を取得するツール(slack_list_channels)を利用してchannel_idを取得してください。
もし、Slackチャンネル名が空の場合は、Slackへの投稿は行わないでください。

アシスタントのキャラクター:
- 質問に対する情報の検索や回答の整理に全力で取り組む。
- 絵文字: ほどほどに使う。

"""

    return Agent(
        model=BedrockModel(
            model_id=model_id,
            region_name=region,
            max_tokens=max_tokens,
            temperature=temperature
        ),
        system_prompt=SYSTEM_PROMPT,
        tools=all_tools
    )


def extract_tool_info(chunk):
    """チャンクからツール情報を抽出"""
    event = chunk.get('event', {})
    if 'contentBlockStart' in event:
        tool_use = event['contentBlockStart'].get('start', {}).get('toolUse', {})
        return tool_use.get('toolUseId'), tool_use.get('name')
    return None, None


def extract_text(chunk):
    """チャンクからテキストを抽出"""
    if text := chunk.get('data'):
        return text
    elif delta := chunk.get('delta', {}).get('text'):
        return delta
    return ""


async def stream_response(agent, question, container):
    """レスポンスをストリーミング表示"""
    text_holder = container.empty()
    buffer = ""
    shown_tools = set()

    async for chunk in agent.stream_async(question):
        if isinstance(chunk, dict):
            # ツール実行を検出して表示
            tool_id, tool_name = extract_tool_info(chunk)
            if tool_id and tool_name and tool_id not in shown_tools:
                shown_tools.add(tool_id)
                if buffer:
                    text_holder.markdown(buffer)
                    buffer = ""
                container.info(f"🔧 **{tool_name}** ツールを実行中...")
                text_holder = container.empty()

            # テキストを抽出して表示
            if text := extract_text(chunk):
                buffer += text
                text_holder.markdown(buffer + "▌")

    # 最終表示
    if buffer:
        text_holder.markdown(buffer)


# ボタンを押したら生成開始
if st.button("質問する"):
    main_client = create_mcp_client(mcp_args, package_manager)
    slack_client = create_slack_mcp_client()

    query = f"{question} ポスト先のSlackチャンネルは {slack_channel_name}です。"

    progress_bar = st.progress(0)
    status_text = st.empty()
    status_text.text("🔄 MCPクライアントを初期化中...")

    with st.spinner("回答を生成中…"):
        with main_client, slack_client:
            progress_bar.progress(25)
            status_text.text("🔄 エージェントを作成中...")

            agent = create_agent([main_client, slack_client], model_id, region, temperature, max_tokens)
            container = st.container()

            progress_bar.progress(50)
            status_text.text("🔄 回答を生成中... (この処理には時間がかかる場合があります)")

            # 非同期実行
            loop = asyncio.new_event_loop()

            progress_bar.progress(75)
            loop.run_until_complete(stream_response(agent, query, container))

            progress_bar.progress(100)
            status_text.text("✅ 回答生成完了")

            loop.close()
```

## 設定

### 環境変数の設定

Slack MCPサーバーを動かすために、以下の環境変数を設定します。`SLACK_BOT_TOKEN`と`SLACK_TEAM_ID`の取得方法は､以下の記事を参考にしてください。

https://qiita.com/revsystem/items/b7904fc53a72ef3ba02a

```shell
export SLACK_BOT_TOKEN=xoxb-your-bot-token
export SLACK_TEAM_ID=T0000000000
```

AWSの認証情報を設定します。

```shell
export AWS_ACCESS_KEY_ID=your-access-key-id
export AWS_SECRET_ACCESS_KEY=your-secret-access-key
export AWS_DEFAULT_REGION=us-east-1
```

### アプリケーションの実行

以下のコマンドでアプリケーションを実行します。

```shell
$ streamlit run strands-mcp-agent.py
```

環境変数が設定されていない場合は、以下のようなエラーが表示されます。

![環境変数が設定されていない場合のエラー画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/a3f44d54-224b-4e43-883b-83fe1c4ac1b9.png)

無事にアプリケーションが起動すると、以下の画面が表示されます。

![アプリケーションのメイン画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/7e60d14f-9169-4bf2-b3d8-b4b52c2d3555.png)

## 動作確認

モデルやリージョンの選択、Temperature や最大トークン数の設定、Slack チャンネル名の入力を行って、質問を実行します。Slack チャンネル名の入力を省略した場合は、回答生成のみ実行します。

### 質問の実行

質問を実行すると、以下のような画面が表示されます。

![質問の実行](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/0667f25c-ee18-4b67-a0a9-774a53a7b14d.png)

### Slack チャンネルへの投稿

このように、slack_list_channels ツールを使って Slack チャンネルの一覧を取得し、指定したチャンネルのチャンネル ID を取得して、そのチャンネルにメッセージを投稿します。

![Slack チャンネルへの投稿](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/72d68174-e73c-4a23-9507-ea0330e1eac0.png)

指定したチャンネルにメッセージが投稿されました。

![Slack チャンネルのメッセージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61809/526b6d87-f834-4a29-abbd-7db50e57c1b5.png)

## まとめ

Strands Agents SDK を使って、AWS Bedrock の Claude モデルを活用して [AWS Documentation MCP サーバー](https://awslabs.github.io/mcp/servers/aws-documentation-mcp-server/) を使って質問の回答を得ました。そして、その内容を Slack MCP サーバーを使って Slack チャンネルに送信してみました。

このように、Strands Agents SDK を使うと、MCP ホストを容易に作成することができます。そして、複数の MCP サーバーを組み合わせたエージェントを作成することもできます。