| 190 | } |
| 191 | |
| 192 | func (h *SSEHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
| 193 | // DNS rebinding protection: auto-enabled for localhost servers. |
| 194 | // See: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices#local-mcp-server-compromise |
| 195 | if !h.opts.DisableLocalhostProtection && disablelocalhostprotection != "1" { |
| 196 | if localAddr, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr); ok && localAddr != nil { |
| 197 | if util.IsLoopback(localAddr.String()) && !util.IsLoopback(req.Host) { |
| 198 | http.Error(w, fmt.Sprintf("Forbidden: invalid Host header %q", req.Host), http.StatusForbidden) |
| 199 | return |
| 200 | } |
| 201 | } |
| 202 | } |
| 203 | |
| 204 | // Validate 'Content-Type' header. |
| 205 | if disablecontenttypecheck != "1" && req.Method == http.MethodPost { |
| 206 | mediaType, _, err := mime.ParseMediaType(req.Header.Get("Content-Type")) |
| 207 | if err != nil || mediaType != "application/json" { |
| 208 | http.Error(w, "Content-Type must be 'application/json'", http.StatusUnsupportedMediaType) |
| 209 | return |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | sessionID := req.URL.Query().Get("sessionid") |
| 214 | |
| 215 | // For POST requests, the message body is a message to send to a session. |
| 216 | if req.Method == http.MethodPost { |
| 217 | // Look up the session. |
| 218 | if sessionID == "" { |
| 219 | http.Error(w, "sessionid must be provided", http.StatusBadRequest) |
| 220 | return |
| 221 | } |
| 222 | h.mu.Lock() |
| 223 | session := h.sessions[sessionID] |
| 224 | h.mu.Unlock() |
| 225 | if session == nil { |
| 226 | http.Error(w, "session not found", http.StatusNotFound) |
| 227 | return |
| 228 | } |
| 229 | |
| 230 | session.ServeHTTP(w, req) |
| 231 | return |
| 232 | } |
| 233 | |
| 234 | if req.Method != http.MethodGet { |
| 235 | w.Header().Set("Allow", "GET, POST") |
| 236 | http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed) |
| 237 | return |
| 238 | } |
| 239 | |
| 240 | // GET requests create a new session, and serve messages over SSE. |
| 241 | |
| 242 | // TODO: it's not entirely documented whether we should check Accept here. |
| 243 | // Let's again be lax and assume the client will accept SSE. |
| 244 | |
| 245 | w.Header().Set("Content-Type", "text/event-stream") |
| 246 | w.Header().Set("Cache-Control", "no-cache") |
| 247 | w.Header().Set("Connection", "keep-alive") |
| 248 | |
| 249 | sessionID = rand.Text() |