package mcphttp import "net/http" const ( // cacheControl is what every response of an MCP endpoint carries. // // private and no-store are middleware.SetPrivateCache's pair, unchanged and // for its reason: an answer here depends entirely on the credential the // request carried and says nothing about it in the URL, and it may be a // PRIVATE repository's data. no-cache would not do — it still permits a cache // to *store* the body and merely revalidate, which is the thing no-store // forbids. // // no-transform is the SDK's own, kept. The streamable transport writes // `no-cache, no-transform` on every response it produces: no-transform // protects the SSE framing from an intermediary that would recompress or // rechunk it, and there is no reason to drop it. Only no-cache is replaced. cacheControl = "private, no-store, no-transform" // vary names what an answer here actually depends on: the unified-login // cookie and the bearer token. Same two names the rest of the instance varies // on, because it is the same statement. vary = "Cookie, Authorization" ) // PrivateCache marks every response an MCP endpoint writes as one no cache may // keep, and states what it depends on. // // It is not middleware.PrivateCache and cannot be. That one sets the headers // before the handler runs, which is right for a router whose handlers do not // touch Cache-Control; the SDK's streamable transport sets Cache-Control itself, // with Set, from inside the handler, so a value written on the way in is // overwritten on the way out and the response leaves with `no-cache, // no-transform` and no Vary at all. The headers are therefore written at the // last moment they still can be: when the status line is committed and every Set // the handler was going to make has been made. // // The result is not a replacement of what the SDK asked for but a narrowing of // it — see cacheControl. func PrivateCache(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(&cacheWriter{ResponseWriter: w}, r) }) } // cacheWriter is the http.ResponseWriter [PrivateCache] hands down: it sets the // two headers when the response is committed, whether that is an explicit // WriteHeader or the implicit one of the first Write. type cacheWriter struct { http.ResponseWriter committed bool } func (w *cacheWriter) WriteHeader(status int) { w.commit() w.ResponseWriter.WriteHeader(status) } func (w *cacheWriter) Write(b []byte) (int, error) { w.commit() return w.ResponseWriter.Write(b) } // Flush commits before flushing so a handler that streams without ever calling // WriteHeader still leaves with the headers. http.NewResponseController prefers a // Flush on the writer it is handed over one reached through Unwrap, so this // method is what it finds — without it the flush would commit the response at the // writer below and the two headers would never be written. func (w *cacheWriter) Flush() { w.commit() //nolint:errcheck // http.Flusher.Flush reports nothing; the controller's error // is only about the writer not supporting flush, which Unwrap guarantees it does. _ = http.NewResponseController(w.ResponseWriter).Flush() } func (w *cacheWriter) commit() { if w.committed { return } w.committed = true w.Header().Set("Cache-Control", cacheControl) w.Header().Set("Vary", vary) } // Unwrap is what http.NewResponseController follows to reach the real writer. // // It is not what keeps flushing working — this type has its own Flush, and the // controller prefers a method on the writer it is handed over one reached by // unwrapping, so the flush path never gets here. That is worth saying because // the comment here used to claim otherwise, and the test named after the claim // passed with the method deleted. // // What does need it is everything else the controller offers: SetWriteDeadline, // SetReadDeadline, Hijack. A long-lived MCP stream is precisely the response // that wants its write deadline pushed out, and without this method that call // is ErrNotSupported. TestUnwrapReachesTheWriterBelow measures that against a // real server, because a recorder supports no deadlines either way. func (w *cacheWriter) Unwrap() http.ResponseWriter { return w.ResponseWriter }