stacklok / stacklok/toolhive

Streamable proxy: Modern MCP message-shape enforcement (deferred from #5830)

Open
#5,842 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement
Dominant language
Go
Stars
2.2k
Forks
300
Avg merge
1d 15h
Merged PRs (30d)
184

Description

Follow-up to #5830 / #5839. The routing half of #5830 (per-request Modern/Legacy classification → stateless routing) shipped in #5839. This issue tracks the message-shape enforcement half, deferred deliberately.

Background

Under the Modern (2026-07-28) revision the HTTP transport narrows what a POST may be. These rules apply to messages that cannot be classified per-request (no _meta, no body, or illegal-by-definition), so they can't ride on the per-request classifier the way routing does — they need a deployment-level signal.

Changes

  • Reject JSON-RPC batches for Modern. Batches don't exist under Modern; the streamable proxy's resolveSessionForBatch / handleBatchRequest path is Legacy-only by construction today.
  • Reject client-sent responses (client POSTing result/error). handleNotificationOrClientResponse accepts them today (Legacy behavior). Must not break backend→client response delivery through the dispatcher (MRTR InputRequiredResult pass-through).
  • GET/DELETE → 405 under Modern (Mcp-Session-Id/Last-Event-ID ignored, not errors). GET is already 405 in this proxy; DELETE currently tears down a session.

The mode signal

Enforcement needs a Legacy/Modern signal that is not driven off the client-controlled MCP-Protocol-Version header (a client must not be able to flip enforcement by spoofing it). Options discussed:

  • Reuse the existing --stateless run flag — the streamable proxy ignores it today; teaching it to enforce Modern shape when set adds no new user-facing surface. Preferred if/when a real consumer needs Modern-only enforcement.
  • A dedicated proxy config field + Option — avoid landing it inert (no wiring = dead config).

Decide the wiring story before building, so enforcement is reachable by a real deployment rather than tests only.

Related finding (from #5839 review)

handleNotificationOrClientResponse refreshes a session's TTL using a client-supplied Mcp-Session-Id that is never classified or ownership-checked — a client sending only notifications/responses can keep an arbitrary session it knows the id of alive. Does not break the #5839 confidentiality invariant (that path never touches waiters), but belongs to this Modern-aware notification/response work.

Also worth folding in

  • The early isSupportedMCPVersion gate (streamable/utils.go, returns true today) is now redundant with the classifier and a latent landmine — reconcile or remove.

Design: RFC THV-0081; arch doc §"Modern message-shape enforcement" (#5808).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with RFC THV-0081 and the architecture section on Modern message-shape enforcement, then inspect the streamable proxy's resolveSessionForBatch, handleBatchRequest, and handleNotificationOrClientResponse paths. Review streamable/utils.go and the existing --stateless wiring before choosing a deployment-level mode signal. Done means Modern rejects batches, client responses, and GET/DELETE as specified without breaking dispatcher pass-through, and the session-TTL issue is addressed.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend-api-design, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.