From 09723c8ed97ca408515eb88f4dfb5d9f1ef592e3 Mon Sep 17 00:00:00 2001 From: giveen Date: Wed, 14 Jan 2026 16:53:22 -0700 Subject: [PATCH] mcp: wait/attempt discovery of POST messages endpoint before POST to avoid 405 --- pentestagent/mcp/transport.py | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pentestagent/mcp/transport.py b/pentestagent/mcp/transport.py index 1232a8e..5839ed6 100644 --- a/pentestagent/mcp/transport.py +++ b/pentestagent/mcp/transport.py @@ -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