internal/mcpgateway/resumedsession.go
Rama main · réplica de lectura del repositorio original.
package mcpgateway
import (
"fmt"
"mime"
"net/http"
"slices"
"strings"
"sync"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// resumedSession hosts a session this replica rebuilt from the shared store.
// The SDK handler only serves sessions it created, so this serves the
// session's HTTP exchanges itself, through the session's own transport, with
// the handler's request checks and idle timeout.
type resumedSession struct {
session *mcp.ServerSession
transport *mcp.StreamableServerTransport
mu sync.Mutex
// posts counts in-flight POSTs, which pause the idle timer like the SDK
// handler's; timer is nil once stopped.
posts int
timer *time.Timer
}
func newResumedSession(session *mcp.ServerSession, transport *mcp.StreamableServerTransport) *resumedSession {
host := &resumedSession{session: session, transport: transport}
host.timer = time.AfterFunc(sessionIdleTimeout, host.close)
return host
}
// ServeHTTP mirrors the SDK handler's stateful GET, POST, and DELETE. Its
// loopback Host check is left out: originGuard has already run on every bind
// address.
func (h *resumedSession) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Before the method switch, as in the SDK handler: a DELETE with an
// unsupported version is rejected, not run.
if rejectUnsupportedProtocolVersion(w, r) {
return
}
switch r.Method {
case http.MethodGet:
if _, streamOK := streamableAccepts(r.Header.Values("Accept")); !streamOK {
http.Error(w, "Accept must contain 'text/event-stream' for GET requests", http.StatusBadRequest)
return
}
case http.MethodPost:
if mediaType, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")); mediaType != "application/json" {
http.Error(w, "Content-Type must be 'application/json'", http.StatusUnsupportedMediaType)
return
}
if jsonOK, streamOK := streamableAccepts(r.Header.Values("Accept")); !jsonOK || !streamOK {
http.Error(w, "Accept must contain both 'application/json' and 'text/event-stream'", http.StatusBadRequest)
return
}
case http.MethodDelete:
h.close()
w.WriteHeader(http.StatusNoContent)
return
default:
w.Header().Set("Allow", "GET, POST, DELETE")
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
if r.Body != nil {
r.Body = http.MaxBytesReader(w, r.Body, mcp.DefaultMaxRequestBodyBytes)
}
if r.Method == http.MethodPost {
h.startPOST()
defer h.endPOST()
}
h.transport.ServeHTTP(w, r)
}
func (h *resumedSession) startPOST() {
h.mu.Lock()
defer h.mu.Unlock()
if h.timer != nil && h.posts == 0 {
h.timer.Stop()
}
h.posts++
}
func (h *resumedSession) endPOST() {
h.mu.Lock()
defer h.mu.Unlock()
h.posts--
if h.timer != nil && h.posts == 0 {
h.timer.Reset(sessionIdleTimeout)
}
}
func (h *resumedSession) stopTimer() {
h.mu.Lock()
defer h.mu.Unlock()
if h.timer != nil {
h.timer.Stop()
h.timer = nil
}
}
// close ends the session; it waits for in-flight requests to drain.
func (h *resumedSession) close() {
h.stopTimer()
_ = h.session.Close()
}
// sessionProtocolVersions are the protocol versions every session server
// negotiates, and the ones rejectUnsupportedProtocolVersion accepts.
var sessionProtocolVersions = mcp.SupportedProtocolVersions()
// perRequestProtocolVersion is the first protocol version that carries its
// version on every request (SEP-2575). The SDK handler passes a version from
// it on that it does not list to its own handling instead of rejecting it.
const perRequestProtocolVersion = "2026-07-28"
// rejectUnsupportedProtocolVersion answers 400 to a request whose
// Mcp-Protocol-Version the session servers do not negotiate, matching the SDK
// handler's check. It runs before a request touches its session, so a
// rejected DELETE leaves the session open.
func rejectUnsupportedProtocolVersion(w http.ResponseWriter, r *http.Request) bool {
version := r.Header.Get("Mcp-Protocol-Version")
if version == "" || slices.Contains(sessionProtocolVersions, version) || version >= perRequestProtocolVersion {
return false
}
http.Error(w, fmt.Sprintf("Bad Request: Unsupported protocol version (supported versions: %s)", strings.Join(sessionProtocolVersions, ",")), http.StatusBadRequest)
return true
}
// streamableAccepts reports whether Accept admits JSON and an event stream,
// matching the SDK handler's check.
func streamableAccepts(values []string) (jsonOK, streamOK bool) {
for _, value := range values {
for token := range strings.SplitSeq(value, ",") {
base, _, _ := strings.Cut(token, ";")
switch strings.ToLower(strings.TrimSpace(base)) {
case "application/json", "application/*":
jsonOK = true
case "text/event-stream", "text/*":
streamOK = true
case "*/*":
jsonOK, streamOK = true, true
}
}
}
return jsonOK, streamOK
}