Langflow から Amazon Nova Canvas MCP Server を利用して画像生成

# 全体の構成

Amazon Nova Canvas MCP Server が公開されたので、Langflow の MCP 統合の機能を使って、Langflow 上で画像を生成してみます。Langflow は OSS 版をローカルにインストールして利用します。構成図は以下のようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/25fe5450-8937-49cb-a81e-dda319b83768.png)

Langflow のエージェントに画像生成のリクエストを送ると、MCP Server Component に設定された `uvx` コマンドで Amazon Nove Canvas を実行して画像を生成します。生成された画像ファイルは `uvx` を実行する際に指定した保存先（ローカルのファイルシステム) に保存されます。このままでもいいのですが、エージェントはローカルの画像を読み込むことができないため、画像アップロードするツールを自作して、エージェントにアップロードして表示するようにします。アップロード先は最近便利になった Langflow のファイル管理システムを使います。

https://blog.langflow.org/launch-week-day-2-files-data/

実際のアップロードは[API 経由で行います](https://docs.langflow.org/api-reference-api-examples#filesv2-endpoints)。ローカルファイルを読むMCPサーバーである[Filesystem MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem)が画像対応するとこの辺も楽になりそうです。


# 事前準備

## Langflow のインストール

公式ドキュメントに沿ってインストールしましょう。

https://docs.langflow.org/get-started-installation

Python 3.10 から 3.13、uv か pip を準備して仮想環境を作り、以下を実行してインストールします。

```shell
uv pip install langflow
```

## AWS の設定

ローカルから AWS を操作できるように AWS CLI をインストールして、Credential 等を設定してセットアップします。

### インストール

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-install.html

### セットアップ

https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/getting-started-quickstart.html


# Langflow を使った実装

## Langflow の起動

`uv run langflow run` で langflow を起動すると、いろいろログが出た後に、URL が表示されるのでブラウザでアクセスします。
```shell
uv run langflow run

(いろいろログがでる)

╭─────────────────────────────────────────────────────────────────────────╮
│                                                                         │
│  Welcome to Langflow                                                    │
│                                                                         │
│  🌟 GitHub: Star for updates → https://github.com/langflow-ai/langflow  │
│  💬 Discord: Join for support → https://discord.com/invite/EqksyE2EX9   │
│                                                                         │
│  We collect anonymous usage data to improve Langflow.                   │
│  To opt out, set: DO_NOT_TRACK=true in your environment.                │
│                                                                         │
│  🟢 Open Langflow → http://127.0.0.1:7860                               │
│                                                                         │
╰─────────────────────────────────────────────────────────────────────────╯
```

## フローの実装

### Simple Agent テンプレートの読み込み

Agentのテンプレートを使用して手っ取り早く作ります。まずは Langflow の画面右上の New Flow を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/499f4e45-ea0e-4442-ac4e-47d689d6a654.png)

するとテンプレート選択の画面が出てくるので、Simple Agent を選びます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/2618f98f-8d62-4f5c-9f17-46e7b94f6ae1.png)

テンプレートが読み込まれました。中央にエージェントがあり、Tools の部分に URL と Calculator のコンポーネントがつながっていますが、今回は MCP Server と画像アップロードのツールを使いますので、URLと Calculator は削除して良いです（使用するかどうかはエージェントが判断するのでそのままでもいいです）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/29c81d95-3c0f-4c43-a17a-4f1fad9232fb.png)


### MCP Server コンポーネントの配置と設定

#### MCP Server コンポーネントの配置
コンポーネントの配置は以下の図のとおりです。コンポーネントを左側のメニューから検索し、エージェントにつなぐためにMCP Server を Tool Mode にして、最後に接続します。デフォルトでは、MCP Command に記述されている `uvx mcp-server-fetch` を実行して、これは指定したWebページのコンテンツを取得するようになっています。ここの MCP Command を Amazon Nova Canvas MCP Server の内容に書き換えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/fac74169-1e05-482a-80e2-e9d2365c3d0e.png)


#### MCP Server コンポーネントの設定

Amazon Nova MCP Server の設定は以下のページに記載があります。

https://awslabs.github.io/mcp/servers/nova-canvas-mcp-server/

これによると以下のような json を利用すればよいようです。
```json
{
  "mcpServers": {
    "awslabs.nova-canvas-mcp-server": {
      "command": "uvx",
      "args": ["awslabs.nova-canvas-mcp-server@latest"],
      "env": {
        "AWS_PROFILE": "your-aws-profile",
        "AWS_REGION": "us-east-1",
        "FASTMCP_LOG_LEVEL": "ERROR"
      },
      "disabled": false,
      "autoApprove": []
    }
  }
}
````

この JSON のところからコマンドに相当する部分、`"awslabs.nova-canvas-mcp-server"`のところを抜き出して1行にしたものが MCP Command になります。例えば以下のようなコマンドになります。これをコンポーネントの MCP Command のところにいれます。**いれたら一度 Tool Mode を OFF にして、もう一度 ON にしてください。** 自動的に MCP Server の内容が読み込まれ、Amazon Nova Canvas が画像生成ツールであることがエージェントに認識されます。

```shell
ENV AWS_PROFILE=default AWS_REGION=us-east-1 FASTMCP_LOG_LEVEL=ERROR uvx awslabs.nova-canvas-mcp-server@latest
```

AWS_PROFILE は `~/.aws/credentials`を開くと以下のようになっていると思うので、[]で囲まれている部分の名前をいれると良いです。

```md
[default]
aws_access_key_id=<AWS_ACCESSS_KEY>
aws_secret_access_key=<AWS_SECRET_ACCESS_KEY>
aws_session_token=<Session_Token>
```

### エージェントの LLM の設定

エージェントの LLM はデフォルトでは、OpenAIがセットされていますが、Amazon Nova Canvas にあわせて Amazon Bedrock も使えます。その場合は、上記のアクセスキーなどを以下の箇所に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/5211d562-5be6-4795-bd00-f117b599c52b.png)

Model ID は `anthropic.claude-3-5-sonnet-20240620-v1:0` であればそれなりに動きます。比較的新しいモデルは Langflow で表示されなかったり、inference profile に対応していなかったりして使えませんでした。

Agent Instructions については以下の点を考慮して作成しました。

- 書き込み可能なローカルパスを伝えてあげないと、書き込み不可なところに画像を出力しようとしてエラーになる。以下の **(書き込み可能なローカルパス)** は適当に置き換えてください。
- エージェントがローカルファイルにアクセスできないことを考慮して、後述する画像アップロードツールを使って必ずアップロードすることを伝える。もし伝えていないとローカルパスで表示しようとしてうまく表示されない。

作成した Agent Instructions
```
入力された指示に従ってテキストや画像を生成し、それらを表示可能なマークダウンを出力します。
生成した画像は (書き込み可能なローカルパス）に保存し、保存した画像をImageUploader-upload_imageを使って必ずアップロードしてください。
マークダウンから画像をリンクするときは、ImageUploader-upload_imageのレスポンスに含まれるアップロードした画像へのリンク先URLを使用してください。
```

### 画像アップロードツールの作成

Langflow のファイル管理システムにファイルをアップロードして、http://127.0.0.1:7860/ でアクセスできるようにするツールを自作します。エージェントはローカルファイルを読むことはできませんが http へのアクセスは可能です。

ツールを自作するためには、左下の New Custom Component をクリックしてコンポーネントを配置し、コードエディタを開いて後述のpythonコードを貼り付けます。保存してから Tool Mode を ON にしてエージェントに繋げば完了です。この Python コードは、Langflow API を使って Langflow にファイルアップロードする実装ですが、アップロード先は Amazon S3 とかでもいいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/7fce2176-2aea-41cd-80fe-0a623a185c32.png)


```python
from langflow.custom import Component
from langflow.io import MessageTextInput, Output
from langflow.schema import Data
import requests
import os

class ImageUploadComponent(Component):
    display_name = "Image File Uploader"
    name = "ImageUploader"
    description = "Reads an image file, uploads it to a specified API endpoint, and returns the file URL"
    inputs = [
        MessageTextInput(name="file_path", display_name="Image File Path", value="", tool_mode=True)
    ]
    outputs = [
        Output(display_name="File URL", name="file_url", method="upload_image")
    ]

    def upload_image(self) -> Data:
        file_path = self.file_path
        api_url = "http://127.0.0.1:7860/api/v2/files"
        headers = {"accept": "application/json"}

        try:
            # ファイルの存在を検証
            if not os.path.exists(file_path):
                return Data(value={"error": f"File {file_path} does not exist"})

            # ファイルをmultipart/form-data形式で準備
            with open(file_path, "rb") as file:
                files = {"file": (os.path.basename(file_path), file, "image/png")}
                # APIにPOSTリクエストを送信
                response = requests.post(api_url, headers=headers, files=files)

            # レスポンスを処理
            if response.status_code != 201:
                return Data(value={"error": f"API returned {response.status_code}: {response.text}"})

            # JSONレスポンスから"path"を抽出
            response_json = response.json()
            file_path_in_response = response_json.get("path")
            if not file_path_in_response:
                return Data(value={"error": "Path not found in API response"})

            # パスからファイル名部分（最後の部分）を抽出し、拡張子を削除
            file_name = os.path.basename(file_path_in_response)
            file_name_without_ext = os.path.splitext(file_name)[0]

            # URLを構築
            file_url = f"{api_url}/{file_name_without_ext}"

            return Data(value=file_url)

        except Exception as e:
            return Data(value={"error": f"Failed to upload file: {str(e)}"})

```

# Langflow の実行

Langflow の画面右上から Playground を開いて打ち込んでみます。

### 猫を主人公にした小説と画像
```text
猫を主人公とした小説を作成して、最後にその小説の内容を表す画像を作成し、
小説のテキストとあわせて表示してください。
```

出力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/e107a573-6fac-4ad5-9529-68c50a06fa98.png)


### ギター好きの女子高生の小説と画像

```text
ギターを愛する孤独な少女がひょんなことから女子高生バンドを
組むことになり、下北沢を舞台として活躍する小説をアニメ調の
画像とともに生成してください
```

出力
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/279199/134cafc0-4734-49d9-b4d7-2d63cec7319b.png)

「ギターができるなら、ベースもきっとすぐに覚えられるはず！」と言われ強引にベースをやらされるギター少女の話...

# 感想

- Langflow から簡単に Amazon Nova Canvas MCP Server につなげました。
- 画像の取り扱いが一番悩みました。アップロードする代わりに Base64で画像を埋め込もうとしましたが、エージェントに返したときにトークンサイズが大きくなってエラーになりました。やはりどこかに保存してパスでやり取りするのが良さそうです。
- Amazon Nova Canvas の画像のクオリティは結構高い気がします。チャットのインプットでどういう画像を生成してほしいか指示すると良さそうです。