mcp: wait/attempt discovery of POST messages endpoint before POST to avoid 405

This commit is contained in:
giveen
2026-01-14 16:53:22 -07:00
parent 1476b1e117
commit 09723c8ed9

View File

@@ -229,6 +229,15 @@ class SSETransport(MCPTransport):
if not self.session:
raise RuntimeError("Transport not connected")
# Ensure we have a POST endpoint. If discovery hasn't completed yet,
# try a quick synchronous discovery attempt before posting so we don't
# accidentally POST to the SSE listen endpoint which returns 405.
if not self._post_url:
try:
await self._discover_post_url(timeout=2.0)
except Exception:
pass
post_target = self._post_url or self.url
try:
@@ -258,6 +267,44 @@ class SSETransport(MCPTransport):
except Exception as e:
raise RuntimeError(f"SSE request failed: {e}") from e
async def _discover_post_url(self, timeout: float = 2.0) -> None:
"""Attempt a short GET to the SSE endpoint to find the advertised POST URL.
This is a fallback used when the background listener hasn't yet
extracted the `endpoint` event. It reads a few lines with a short
timeout and sets `self._post_url` if found.
"""
if not self.session:
return
try:
async with self.session.get(self.url, timeout=timeout) as resp:
if resp.status != 200:
return
# Read up to a few lines looking for `data:`
for _ in range(20):
line = await resp.content.readline()
if not line:
break
try:
text = line.decode(errors="ignore").strip()
except Exception:
continue
if text.startswith("data:"):
endpoint = text.split("data:", 1)[1].strip()
from urllib.parse import urlparse
p = urlparse(self.url)
if endpoint.startswith("http"):
self._post_url = endpoint
elif endpoint.startswith("/"):
self._post_url = f"{p.scheme}://{p.netloc}{endpoint}"
else:
self._post_url = f"{p.scheme}://{p.netloc}/{endpoint.lstrip('/')}"
return
except Exception:
return
async def disconnect(self):
"""Close the HTTP session."""
# Cancel listener and close SSE response