AWSLab

DifyのワークフローからPresentonをMCPで呼ぶ【300秒の壁と2つの罠】

スポンサーラベル
DifyのワークフローからPresentonをMCPで呼んでPPTXを返す構成。開始・Presentonで生成(MCPツール)・パスをURLにする(コード)・PPTXを取得(HTTPリクエスト)・終了の5ノードで、2番目は300秒で必ず失敗し、4番目はテキスト判定で弾かれ、最後はダウンロードが403になった AWS

当サイトはアフィリエイト広告を利用しています。

PresentonをAWSに構築するで立てた環境を、REST APIMCPの両方から叩いてきました。次にやりたくなるのは、Dify のワークフローに組み込むことです。

Dify 側には社内文書のナレッジがあり、Presenton 側にはスライドを作る力があります。あいだを MCP でつなげば、「この議事録からスライドを作って」をワークフロー1本で完結させられます。

やってみたところ、生成そのものは最初の1回から成功していました。それなのに、ワークフローとしては3回別々の理由で落ちました。3つとも Presenton ではなく、Dify 側と経路の既定値が原因です。本記事はその切り分けの記録です。

DifyのワークフローからPresentonをMCPで呼んでPPTXを返す構成。開始・Presentonで生成(MCPツール)・パスをURLにする(コード)・PPTXを取得(HTTPリクエスト)・終了の5ノードで、2番目は300秒で必ず失敗し、4番目はテキスト判定で弾かれ、最後はダウンロードが403になった

1. ワークフローの形

作るのは5ノードだけです。

ノード種類役割
開始startテーマと枚数を受け取る
Presentonで生成tool(MCP)generate_presentation を呼ぶ
パスをURLにするcode返ってきた path からURLを組み立てる
PPTXを取得http-request生成物をファイルとして受け取る
終了endfiles を返す

コードノードが要るのは、MCP が返すのがファイル本体ではなくサーバー上のパスだからです。これは REST API のときと同じで、/app_data/exports/…/xxxx.pptx という文字列が返るだけなので、取りに行く工程を自分で足す必要があります。

# MCP ツールの結果は text ではなく json に入る。
# しかも「オブジェクトの配列」なので、先頭を取り出して使う。
BASE = "http://203.0.113.10"

def main(raw) -> dict:
    data = raw
    if isinstance(data, list):
        data = data[0] if data else {}
    if not isinstance(data, dict):
        data = {}
    path = data.get("path") or ""
    return {
        "url": (BASE + path) if path else "",
        "presentation_id": data.get("presentation_id") or "",
    }

ワークフローは画面で組まず、DSL(YAML)を書いてインポートしました。Dify のプロンプト欄は自動入力で壊れるため、画面を機械的に操作するより YAML のほうが速くて確実です。

このとき、MCP ツールノードで2か所つまずきました。どちらもドキュメントに書かれていません。

provider_id に入れるのは UUID ではなくサーバー識別子です。 プラグインのツールノードと同じつもりで UUID を書くと、ノードが解決できません。Dify の tool_manager.pyget_provider(server_identifier=provider_id) で引いているためで、MCP サーバーを登録したときに決めた名前(この環境では presenton)をそのまま書きます。

結果は text ではなく json に入ります。 しかもオブジェクトの配列です。後段のコードノードの入力セレクタは [presenton, json] を指し、配列の先頭を取り出す処理を入れておく必要があります。

2. MCPノードが必ず300秒で失敗する

最初の壁がこれでした。

Failed to invoke tool presenton: ... peer closed connection without sending
complete message body (incomplete chunked read)

処理時間は 300.657秒。何度やっても300秒前後で落ちます。ところが同じ設定のまま165.9秒で成功する回もありました。つまり「生成が300秒を超えた回だけ落ちる」という形です。

2.1 Difyのタイムアウト設定を延ばしても効かない

Dify の MCP サーバー設定には、接続タイムアウトと読み取りタイムアウトの欄があります。読み取り側を900秒にして保存し、DB にも反映されていることを確認しました。

presenton | 60 | 900 | 2026-09-12 01:38:50

それでも300秒で落ちます。 ここで「設定が効いていないのでは」と疑って Dify のソースを追いましたが、値は正しく渡っていました。

# core/mcp/client/streamable_client.py
with create_ssrf_proxy_mcp_http_client(
    headers=transport.request_headers,
    timeout=httpx.Timeout(transport.timeout, read=transport.sse_read_timeout),
) as client:

MCPClientWithAuthRetrysuper().__init__(server_url, headers, timeout, sse_read_timeout) で素通ししています。Dify 側は最後まで900秒で待つつもりでいました。

2.2 経路上のタイムアウトを全部並べる

そこで、リクエストが通る層をすべて洗いました。

経路上のタイムアウト比較。DifyのMCPプロバイダ設定は900秒、httpxも900秒、Presentonのnginxは1800秒、PresentonのMCPサーバーは600秒と余裕があるのに、squid(ssrf_proxy)のclient_lifetimeだけが300秒で最小値になっている
設定値判定
Dify の MCP プロバイダ設定sse_read_timeout 900秒余裕あり
httpx のタイムアウトread=900余裕あり
squid(ssrf_proxy)client_lifetime 300秒これが効く
Presenton の nginxproxy_read_timeout 1800秒余裕あり
Presenton の MCP サーバーMCP_API_TIMEOUT_SECONDS 600秒余裕あり

Dify の外向き通信は、同梱の SSRF プロキシ(squid)を必ず通ります。 そのプロキシが300秒で切っていました。

証拠はアクセスログに残っていました。%6tr がミリ秒です。

1789180161.324 165600 TCP_MISS/200          POST /mcp  text/event-stream
1789179732.208 300017 TCP_MISS_TIMEDOUT/200 POST /mcp  text/event-stream
1789181870.250 299863 TCP_MISS_TIMEDOUT/200 POST /mcp  text/event-stream

同時刻の cache.log にも、squid 自身の宣言が出ています。

WARNING: Closing client connection due to lifetime timeout

Dify に返っていた peer closed connectionpeer は Presenton ではなく squid でした。

2.3 なぜ .env では直せないのか

ここが厄介な点です。client_lifetimesquid の設定テンプレートにベタ書きされています。

# docker/ssrf_proxy/squid-common.conf.template
request_timeout 2 minutes
read_timeout 2 minutes
client_lifetime 5 minutes

このテンプレートで環境変数が展開されるのは ${COREDUMP_DIR} だけです。タイムアウト系には env のフックが1つもありません。 そのため、Dify の .env をいくら眺めても該当する項目が見つからず、「設定できないはずがない」と思い込んで時間を溶かしました。

直すにはテンプレート自体を書き換えます。

sudo sed -i.orig -e 's/^client_lifetime 5 minutes/client_lifetime 30 minutes/' -e 's/^read_timeout 2 minutes/read_timeout 10 minutes/' /opt/dify/docker/ssrf_proxy/squid-common.conf.template
cd /opt/dify/docker && sudo docker compose up -d --force-recreate ssrf_proxy

read_timeout も一緒に上げているのには理由があります。 client_lifetime は接続全体の寿命ですが、read_timeout(既定2分)は「相手からデータが来ない状態が続いた時間」の上限です。MCP の応答は SSE なので、生成中は無音の区間が生まれます。実測では165秒・199秒の成功例があり、2分以上の無音は起きていない(=keepalive が出ている)と分かりますが、枚数を増やせば間隔は伸びます。片方だけ上げると、次は read_timeout で切られます。

再作成するのは ssrf_proxy 1つだけなので、他のコンテナは止まりません。反映後は300秒を超えても切られなくなりました。

Presenton | tool | succeeded | 199.9秒

3. PPTXが「テキスト」と判定されて弾かれる

次に、生成物を取りに行く HTTP リクエストノードが落ちました。

Text size is too large, max size is 1.00 MB, but current size is 1.24 MB.

サイズ上限の話に見えますが、そうではありません。 注目するのは先頭の「Text」です。

# graphon/nodes/http_request/executor.py
threshold_size = (
    self._http_request_config.max_binary_size
    if executor_response.is_file
    else self._http_request_config.max_text_size
)

ファイルなら max_binary_size(既定10MB)、テキストなら max_text_size(既定1MB)で判定します。Dify は PPTX をテキストだと思っています。

3.1 MIMEの「openxmlformats」に部分一致していた

判定はこうなっています。

def _is_text_based_application_content(self, content_type: str) -> bool:
    if not content_type.startswith("application/"):
        return False
    if any(text_type in content_type for text_type in _TEXT_BASED_APPLICATION_TYPES):
        return True

_TEXT_BASED_APPLICATION_TYPES{json, xml, javascript, x-www-form-urlencoded, yaml, graphql} で、判定は単なる部分一致です。PPTX の MIME を並べてみます。

application/vnd.openxmlformats-officedocument.presentationml.presentation
                     ^^^

openxmlformats の中の “xml” に引っかかります。 docx も xlsx も同じ MIME 体系なので、Office の新形式はすべてテキスト扱いになります。

さらに厄介なのが次のコードです。

# graphon/nodes/http_request/node.py
is_file = response.is_file
if not is_file:
    return ArrayFileSegment(value=[])

1MB 未満のファイルなら、エラーも出さずに files が空のまま成功します。 実際、これに気づく前は「正常に終わったのに何も入っていない」という状態を見ていました。サイズ上限を緩めても解決しません。 エラーが消えて、空のまま成功するようになるだけです。

3.2 なぜnginx側で直したのか

判定関数の先頭を見ると、逃げ道があります。

if self._has_file_content_disposition():
    return True          # attachment か filename= があれば即ファイル扱い

部分一致判定より前に Content-Disposition を見ています。 つまり、サーバー側がこのヘッダーを返せば、is_file は問答無用で True になります。

直し方は2つありました。

  • Dify 側entities.py の判定を直す
  • Presenton 側:nginx に Content-Disposition: attachment を足す

後者を選びました。理由は3つです。Dify を書き換えると次のアップグレードで消えること.venv の中のライブラリを直すことになり管理対象として扱いづらいこと、そして「生成物をダウンロードさせる場所に Content-Disposition を付ける」のは、そもそも正しい振る舞いだからです。回避策というより、足りていなかった設定を足す形になります。

Presenton の nginx は、生成物の配信にこれを付けていませんでした。

location /app_data/exports/ {
  auth_request /_auth_check;
  alias /app_data/exports/;
  expires 1y;
  add_header Cache-Control "private, max-age=31536000";
}

ここに1行足します。

sudo docker exec presenton sh -c "sed -i '/location \/app_data\/exports\/ {/a add_header Content-Disposition \"attachment\";' /etc/nginx/nginx.conf && nginx -t && nginx -s reload"

nginx -ttest is successful を返せば反映されています。これで HTTP リクエストノードが成功し、files に PPTX が1件入るようになりました。

注意点が1つあります。 この設定はコンテナのイメージの中にあるため、コンテナを作り直すと消えます。 後述のとおり起動スクリプト側に入れておかないと、次に docker rm した時点で同じ場所で詰まります。

4. ダウンロードが403になる

ここまでで files に PPTX(962KB)が入りました。ところが、ダウンロードボタンを押すとブラウザが「サイトでファイルを取得できませんでした」と出します。

nginx のログを見ると 403 です。

GET /files/tools/aaba559a-….pptx?timestamp=1789188525&nonce=611d41…&sign=VX1hNS…
  403 63

これは署名URLの有効期限切れでした。

# core/tools/signature.py
current_time = int(time.time())
return current_time - int(timestamp) <= dify_config.FILES_ACCESS_TIMEOUT

FILES_ACCESS_TIMEOUT の既定は 300秒です。Dify が返すファイルのURLは、URL自体に timestampsign を埋め込んだ署名付きのもので、発行から5分で無効になります。

これが MCP と相性の悪い理由は、生成に数分かかることです。実行を投げて席を外し、戻ってきてダウンロードを押すと、その頃にはもう期限が切れています。生成に3分かかる処理の結果を、残り2分で取りに行かないといけない計算になります。

延ばすなら .env の1行です。

cd /opt/dify/docker && sudo sed -i 's/^FILES_ACCESS_TIMEOUT=.*/FILES_ACCESS_TIMEOUT=86400/' .env
cd /opt/dify/docker && sudo docker compose up -d api worker

この値を読むのはバックエンドだけなので、作り直すのは apiworker の2つで足ります。

ただし、延ばす前に置き場所を確認してください。 署名URLは認証を経ずに叩けます。24時間にするということは、チャットのログや履歴に残ったURLが翌日まで有効だということです。インターネットから到達できる場所に置いているなら、この値は短いままにしておくべきです。 閉域で動かす前提なら実害はありませんが、「後で本番に移す」ときに見落としやすい設定です。

ここまでの3つを直したあと、同じワークフローを通しで実行しました。開始から終了まで全ノードが成功し、PPTX がファイルとして返ってきます(この回は862KB)。テーマを1行渡すだけで PowerPoint が手元に落ちてくる状態になりました。

5. LLMをBedrockに寄せる

検証の途中で、Presenton 側に設定していた Anthropic API のクレジットが尽きました。

llmai.shared.errors.LLMError: 400: Your credit balance is too low to access
the Anthropic API.

アウトラインまでは作れて、スライド生成の途中で400になります。その結果 MCP が path を返さず、コードノードの出力が {} になり、HTTP ノードが url is required で落ちる、という連鎖でした。MCP 経由だと、LLM 側の課金エラーが3ノード先で別の顔をして出てきます。 原因を追うときは Presenton 本体のログを見るのが早いです。

そこで Bedrock に切り替えました。PresentonをAWSに構築するで「Bedrock はインスタンスロールで使えない」と書きましたが、今の版では設定画面に「Bedrock API key」の欄が増えています。 ロールは相変わらず使えず、認証情報を明示する必要があるのは変わりません。

# servers/fastapi/utils/llm_config.py
if not kwargs["api_key"] and not (
    kwargs["aws_access_key_id"] and kwargs["aws_secret_access_key"]
):
    raise HTTPException(status_code=400, detail=(
        "Bedrock auth is incomplete. Set BEDROCK_API_KEY, or "
        "set BEDROCK_AWS_ACCESS_KEY_ID and BEDROCK_AWS_SECRET_ACCESS_KEY."))

profile_name を受け取る口はあるのに、この判定を先に通れません。 boto3 の既定の資格情報チェーンに落ちる前に400で弾く実装なので、EC2 のインスタンスロールでは動かせません。Bedrock の API キーを発行して貼るのが現実的です。

なお設定画面で保存した内容は環境変数より優先されます。 コンテナの環境変数が LLM=bedrock でも、管理画面で Anthropic を選んでいればそちらが使われます。設定は /app_data/userConfig.json に入っていて、ここを見ると「今どちらで動いているか」がすぐ分かります。

6. CloudFormationに入れておく

ここまでの修正は、どれも手で直したものです。このままだと、スタックを作り直した次の人(未来の自分を含む)が同じ3か所で詰まります。直した理由ごとテンプレートに残しておきます。

Dify 側のテンプレートには、docker compose up -d の前に3行足しました。

          # MCP のツール呼び出しは、1本の HTTP リクエストが数分続く。
          # ところが同梱の squid(ssrf_proxy) は client_lifetime 5分・read_timeout 2分で、
          # どちらも環境変数から変えられない(テンプレートに直接書かれている)。
          # 直さないと、生成が5分を超えた時点でプロキシ側が接続を切る。
          sed -i 's/^client_lifetime 5 minutes/client_lifetime 30 minutes/' ssrf_proxy/squid-common.conf.template
          sed -i 's/^read_timeout 2 minutes/read_timeout 10 minutes/' ssrf_proxy/squid-common.conf.template

          # 生成物のダウンロードURLの有効期限。既定の300秒だと、
          # 数分かかる生成の結果を後から取りに行けない
          sed -i 's/^FILES_ACCESS_TIMEOUT=.*/FILES_ACCESS_TIMEOUT=86400/' .env

Presenton 側は、UserData ではなく起動スクリプトのほうに入れるのがポイントです。nginx の設定はイメージの中にあるので、docker run のたびに消えます。UserData は初回起動時にしか走らないため、そこに書くとコンテナを作り直した瞬間に失われます。

          # Presenton の nginx は生成物に Content-Disposition を付けない。
          # 付いていないと Dify の HTTP リクエストノードが PPTX をテキストとみなし、
          # 1MB のテキスト上限で弾く。MIME の
          # application/vnd.openxmlformats-officedocument… に含まれる "xml" へ
          # 部分一致してしまうためで、ファイル側の問題ではない。
          # この設定はイメージの中にあり、コンテナを作り直すと消える。
          # そのため起動するたびにここで入れ直す。
          for i in $(seq 1 60); do
            docker exec presenton test -f /etc/nginx/nginx.conf 2>/dev/null && break
            sleep 5
          done
          docker exec presenton sh -c 'grep -q Content-Disposition /etc/nginx/nginx.conf || sed -i "/location \/app_data\/exports\/ {/a add_header Content-Disposition \"attachment\";" /etc/nginx/nginx.conf; nginx -t && nginx -s reload'

grep -q を挟んでいるのは、2回流しても二重に挿入されないようにするためです。run.sh は再起動のたびに実行されるので、冪等にしておかないと add_header が積み上がります。

コンテナの起動直後は nginx の設定ファイルがまだ無いことがあるため、待つループを入れています。

7. 同期で呼ぶ設計そのものの限界

client_lifetime を30分にすれば当面は動きます。ただし、これは検証環境だから許される回避です。

client_lifetime は squid の安全弁で、本来は「いつまでも掴んだままの接続」を切るためのものです。そして生成時間は実測で 108秒・165秒・199秒とばらつきました。枚数を増やせば伸びますし、モデルを変えても変わります。「同期で待つ」設計である限り、上限をどこに置いても超える日が来ます。

正しい形は非同期です。Presenton の MCP には3つのツールがあり、うち2つが非同期用でした。

generate_presentation                同期で生成する
generate_presentation_async          非同期で生成する
get_presentation_generation_status   非同期の進捗を見る

generate_presentation_async を投げて、get_presentation_generation_status をループノードで叩く形にすれば、1リクエストあたり数秒で終わります。プロキシのタイムアウトも、署名URLの期限も、どちらも関係なくなります。

今回は先に同期で通して詰まりどころを洗い出しましたが、運用に載せるなら非同期に組み替えるべきだと考えています。ここは次回に回します。

8. まとめ

  • MCPノードが300秒で必ず失敗するのは、Dify同梱のsquidが切っているから。 client_lifetime 5 minutes がテンプレートにベタ書きで、環境変数から変えられない。画面のタイムアウト設定を延ばしても効かない
  • 経路上のタイムアウトは全部並べて比べる。最小値を持っている層が実際の上限になる
  • DifyはPPTXをテキストと判定する。 MIME の openxmlformats に含まれる “xml” へ部分一致するためで、Office の新形式は全滅する
  • 1MB未満なら、エラーも出さずに files が空のまま成功する。 サイズ上限を緩めても直らない
  • 回避は Content-Disposition: attachment。判定関数はこれを部分一致より先に見る。Dify本体ではなく配信側に足すほうが、アップグレードで消えない
  • ダウンロードの403は署名URLの期限切れ(既定300秒)。生成に数分かかる用途とは噛み合わない。延ばすなら、URLが認証なしで叩ける点を踏まえて置き場所から考える
  • Presenton の Bedrock はインスタンスロールを使えない。 今の版は API キー欄が増えたが、明示的な認証情報が要るのは変わらない
  • 手で直した設定は理由ごとテンプレートへ。nginx の設定はイメージの中にあるので、UserDataではなく起動スクリプトに入れる

3つとも Presenton の問題ではありませんでした。自作でないもの同士をつなぐときは、両端ではなく「あいだ」を疑うという、当たり前のことを改めて確かめる作業になりました。とくに Dify の SSRF プロキシは、存在を意識していないと最後まで候補に挙がりません。

AWSを効率的に学習する方法

私が効率的にAWSを学習するために実施した方法は以下の通りです。
 ①最初に書籍(ハンズオンができる)を購入、座学でAWSの基礎を学習
 ②AWS資格試験を取得ための学習

※私の場合は①と②を合わせて2か月でソリューションアーキテクトを取得できました。

①AWS基礎学習

最初に購入した書籍は「Amazon Web Services 基礎からのネットワーク&サーバー構築」です。

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂4版

この本では、AWSの基本サービスを利用したハンズオンを通じて、AWSの基礎を学習することができます。

また、タイトル通りAWSのネットワークやインフラに関しても網羅しているため、もともとインフラ系の技術者ではない人たちにとっても分かりやすい内容だと思います。

とりあえずAWS上にサーバーを設定して開発を行うための準備までするには最良の一冊です。

Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂4版

②AWS資格取得

AWSの基礎をある程度学習することができたら、次はAWS資格を取得しましょう。まずはAWSソリューションアーキテクトを目指しましょう。

資格勉強のための問題集をひたすら解きながら、AWSの知識を積み重ねて習得していきましょう!

苦行ではありますがこの問題集を3周ほどすればAWS用語や構成に関しても習得できているはずです。

AWS認定ソリューションアーキテクト-アソシエイト問題集

独学だと手が止まってしまう場合は、ササエル のようなインフラエンジニア向けのオンラインスクールを使う手もあります。ネットワークやサーバーの分野に絞ったカリキュラムなので、業務でインフラを触る人が学び直すのにも使えます

コメント

タイトルとURLをコピーしました