Cannot authorize streamable-http MCP servers when URL path's last segment isn't mcp/sse - SSE-first fallback never triggers (httpx.ReadTimeout not caught)

オープン 初心者向け
#39,301 コメント 1 件 リアクション 1 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
2/5
見積もり時間
1〜3時間
初心者へのやさしさ
74/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
静か
技術スタック
python
領域
api, backend

調査の方向性

api/core/mcp/mcp_client.py の MCPClient._initialize から始め、最後のパスセグメントが mcp または sse ではない streamable-http エンドポイントに対する認証を再現してください。SSE を待機せずに streamable-http の初期化が成功すること、必要な場合には SSE フォールバックが引き続き機能すること、MCPAuthError が別のトランスポートとして再試行されないことを確認してください。

索引モデルが issue の本文から書いたものです。

説明

1.15.0 project#dify

Title: Cannot authorize streamable-http MCP servers when URL path's last segment isn't mcp/sse — SSE-first fallback never triggers (httpx.ReadTimeout not caught)


Dify version: 1.15.0

Cloud or Self Hosted: Self Hosted (Docker)

Steps to reproduce
  1. Run Dify 1.15.0 via Docker (langgenius/dify-api:1.15.0).
  2. Go to Tools → MCP and add an MCP server whose endpoint is a streamable-http server and whose URL path does not end with /mcp or /sse.
    Example: a PandaWiki MCP share endpoint:
    http://<host>:8089/share/v1/mcp/<share_id>
    
    Here the last path segment is the share_id, not mcp.
  3. Confirm the server is a healthy streamable-http MCP server. A direct POST .../initialize returns 200 + Mcp-Session-Id and a valid JSON-RPC initialize response:
    {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{"listChanged":true}},"serverInfo":{"name":"pandawiki-mcp","version":"1.0.0"}}}
    
    A GET (SSE-style) to the same URL hangs and returns nothing.
  4. Click Authorize on the provider.
✔️ Expected Behavior

Dify detects that the server speaks streamable-http, performs the initialize handshake, fetches the tool list, and marks the provider as authorized.

❌ Actual Behavior

Clicking Authorize appears to do nothing in the UI.

  • nginx access log:
    POST /console/api/workspaces/current/tool-provider/mcp/auth  -> 499
    
    (499 = client closed the connection before the backend responded.)
  • api log:
    core/mcp/client/sse_client.py:301  Error connecting to SSE endpoint
    httpcore.ReadTimeout: timed out
    tool_providers.py:1120  Failed to fetch MCP tools after creation
    
  • The provider stays authed=false in tool_mcp_providers. The backend blocks up to sse_read_timeout (default 300s); the browser gives up first → 499 → no feedback in the UI.
Root cause

In api/core/mcp/mcp_client.py, MCPClient._initialize picks the transport by the last segment of the URL path:

method_name = path.rstrip("/").split("/")[-1] if path else ""
if method_name in {"mcp", "sse"}:
    client_factory = connection_methods[method_name]
    self.connect_server(client_factory, method_name)
else:
    try:
        self.connect_server(sse_client, "sse")          # tries SSE first
    except (MCPConnectionError, ValueError):             # cannot catch httpx.ReadTimeout
        self.connect_server(streamablehttp_client, "mcp")

Two problems combine:

  1. The else branch tries SSE first. A streamable-http server does not respond to the SSE GET, so the connect hangs until sse_read_timeout and raises httpx.ReadTimeout.
  2. except (MCPConnectionError, ValueError) does not catch httpx.ReadTimeout (nor httpx.ConnectError), so the fallback to streamablehttp_client never triggers. The timeout propagates and the auth call fails after a long block.

Also note the method docstring says "Initialize the client with fallback to SSE if streamable connection fails" — i.e. streamable-http is supposed to be the default — but the else branch actually tries SSE first, the opposite of the documented intent.

Impact

Any streamable-http MCP server whose URL path's last segment is not exactly mcp or sse (URLs ending with an id / token / share_id, or carrying a query string) cannot be authorized in Dify 1.15.0. This is a broad class of servers, not specific to PandaWiki.

Related: #24297 reported the same 300s symptom in 1.7.2 (attributed to the sse_client thread pool) and was closed as cant-reproduce. In 1.15.0 the 300s has a different, clearly identifiable root cause: the protocol-detection heuristic above + the except (MCPConnectionError, ValueError) that cannot catch httpx.ReadTimeout, so the SSE -> streamable fallback never runs.

Suggested fix

In the else branch, try streamable-http first and fall back to SSE on any connection failure, while letting MCPAuthError (HTTP 401) propagate so it isn't retried as a different transport:

else:
    try:
        self.connect_server(streamablehttp_client, "mcp")
    except MCPAuthError:
        raise
    except Exception:
        logger.debug("MCP connection failed with 'mcp', falling back to 'sse' method.")
        self.connect_server(sse_client, "sse")

Notes:

  • MCPAuthError is a subclass of MCPConnectionError, so it must be caught and re-raised separately before the generic except Exception (otherwise a 401 would be silently retried over SSE).
  • Verified locally: with this change, the PandaWiki server authorizes in a couple of seconds and list_tools returns correctly.
Environment
  • Dify 1.15.0, Self Hosted (Docker).
  • SSRF_PROXY_HTTP_URL / SSRF_PROXY_HTTPS_URL are unset, so the MCP client connects directly (not via the squid SSRF proxy); the issue is unrelated to the deny to_private_networks squid rule.
  • MCP server: PandaWiki, streamable-http, public share endpoint (no auth).
主要言語
TypeScript
スター
157k
フォーク
24.7k
平均マージ
22時間 32分
マージ済み PR(30日)
611

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

langgenius/dify のほかの issue

langgenius/dify の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。