#10

Grok Bot remote MCP: no Mcp-Session-Id reuse (and GET SSE hang)

Openferrraridave-coderopened 10h ago

Context

Using Pinky with Grok Bot (xAI) as a remote MCP client over Streamable HTTP, so the agent can drive an iPhone (iOS 27 — Device Hub path, no WebDriverAgent).

Grok Bot’s custom MCP connection settings only support:

  • an HTTPS MCP endpoint
  • fixed request headers (e.g. Authorization: Bearer <pinky token>)

It cannot run a local launch command on the Mac (pinkyctl mcp), and it cannot capture a response header and reuse it on later requests.

Setup that reaches Pinky:

  1. Pinky.app with Remote agents on (127.0.0.1:7465/mcp)
  2. Cloudflare Tunnel → that port (HTTPS hostname)
  3. Grok Bot → https://…/mcp + Authorization: Bearer $(pinkyctl token)

What Grok Bot sends (observed)

  • POST /mcp with JSON-RPC (initialize, notifications/initialized, tools/call, …)
  • Fixed Authorization: Bearer …
  • Does not send back Mcp-Session-Id from the initialize response on later calls (often effectively a new session per call / after reconnect)
  • Also opens Streamable HTTP GET /mcp for a server→client SSE stream (optional part of the transport)

Pinky currently ties the phone lease to the MCP HTTP session. Without a sticky Mcp-Session-Id, that shows up as:

  1. phone_connect succeeds (screen is returned in that response)
  2. Next phone_tap / phone_launch → This session is not connected to iPhone / invalid_argument (no lease on the new session)

Separately, if something in front of Pinky blocks on the long-lived GET SSE (e.g. a naïve proxy holding a lock while GET hangs upstream), tool POSTs never reach Pinky. History then looks like: phone_connect → long gap → phone_release (partial), no tap step, and the phone never moves. Returning 405 for GET (tool results on POST only) avoided that hang in a local sticky proxy.

Workaround

A small local sticky-session proxy in front of Pinky that:

  • keeps one upstream MCP session to 127.0.0.1:7465
  • injects Mcp-Session-Id on every forwarded request
  • does not forward a blocking GET SSE to Pinky

Tunnel points at the proxy. After that, Grok Bot can open apps/folders on the phone normally.

Ask

You mentioned looking at keeping the phone for clients that don’t replay Mcp-Session-Id — that would remove the need for a proxy for Grok Bot–style clients. Happy to add more captures (raw headers / timing) if useful.

Happy to share the proxy script as a reference only; not proposing it as the long-term fix.

Activity

4
DIYgod10h ago

Thanks for the detailed write-up, the trace of what Grok Bot sends makes this easy to follow.

Since Grok Bot can't send Mcp-Session-Id back, the lease from phone_connect is lost on the next call. We're changing Pinky so a client like this keeps its phone: calls that come without the header but with the same token will be tied to that token's session, without letting any other token reach the phone. We're working on it now and will link the release here when it's out, so your sticky-session proxy won't be needed.

One question on the GET /mcp SSE stream: does it hang until Grok Bot times out, and does that block the POST calls too, or only add a delay?

DIYgod9h ago

This shipped in Pinky 2026.1108.25. Requests without Mcp-Session-Id (other than initialize) now go to a session Pinky keeps for that client, told apart by User-Agent. The phone, the task and any pending approvals stay with it, even if the client initializes again, so you can drop the sticky-session proxy. Clients that do send a session id aren't mixed in, and other agents still see the phone as in use. A GET without a session id now returns 405, so the GET stream no longer gets in the way, and POSTs are handled while a GET stream is open.

One thing for your Cloudflare Tunnel: the Host header has to be localhost:7465 or 127.0.0.1:7465 (cloudflared's originRequest.httpHostHeader), or Pinky returns 403.

One limit: two clients that send no session id and use the same User-Agent count as one. Could you update, try it without the proxy, and let us know here how it goes?

ferrraridave-coder9h ago

On the GET SSE question: in our setup it blocked POSTs, not just delayed them.

Grok Bot opens a long-lived GET /mcp SSE stream that stays open. While that was held open through our sticky proxy (which briefly held a lock across upstream I/O), the next tool POSTs queued behind it — so after a successful phone_connect, phone_tap never ran (long gap → timeout / release), and the phone never moved. Rejecting GET with 405 (no SSE; tool results on POST only) fixed that path for us.

We didn’t separately prove Pinky’s own server serializes GET vs POST without a proxy in the middle — only that with Grok’s GET in the path, taps were blocked until the stream was refused.

Thanks for 2026.1108.25 — the session-by-token / UA behaviour and GET→405 match what we needed. We’ll update, point the tunnel straight at Pinky (with originRequest.httpHostHeader set to 127.0.0.1:7465), drop the sticky proxy, and report back here.

ferrraridave-coder9h ago

Updated to 2026.1108.25 and retested without the sticky-session proxy.

Setup now:

  • Cloudflare Tunnel → http://127.0.0.1:7465 with originRequest.httpHostHeader: 127.0.0.1:7465
  • Same Grok-style client behaviour: fixed Authorization bearer, no Mcp-Session-Id reuse on later calls

Results:

  • GET /mcp without a session → 405 as expected
  • Local and via the public Cloudflare Tunnel: phone_connect → phone_apps → phone_release all PASS with no session header

Sticky proxy is stopped. Thanks — this removes the workaround for Grok Bot–style clients.