docs: explain why GET /api/mcp deliberately answers 405 in stateless mode
This commit is contained in:
+4
-1
@@ -199,7 +199,10 @@ func runSchemaSetup() {
|
||||
|
||||
// apiVersion mirrors the @version annotation on the swagger block above,
|
||||
// which is the only version string this server already establishes — there is
|
||||
// no separate runtime build-version constant to reuse instead.
|
||||
// no separate runtime build-version constant to reuse instead. Nothing ties
|
||||
// the two together mechanically, so change them in the same commit: this is
|
||||
// the value mcp.SetVersion reports to MCP clients, and it must keep agreeing
|
||||
// with "// @version" above or the two will read as two different servers.
|
||||
const apiVersion = "1.0"
|
||||
|
||||
func serve() {
|
||||
|
||||
@@ -126,6 +126,15 @@ func RegisterRoutes(r *gin.Engine) {
|
||||
// activity and RequireScopes all apply from where it lives rather than
|
||||
// because someone remembered. The route-level scope is a floor: one route
|
||||
// serves many tools, so per-tool scopes are enforced inside the handler.
|
||||
//
|
||||
// GET is registered deliberately even though the transport runs stateless
|
||||
// and therefore never serves it usefully: mcp.Handler's underlying SDK
|
||||
// handler answers every GET with a hardcoded 405, because a stateless
|
||||
// server has no session to open the server-to-client SSE stream against.
|
||||
// That 405 is the protocol-correct response for an MCP server that offers
|
||||
// no SSE leg — an unregistered GET would 404 instead, which a client reads
|
||||
// as "no MCP endpoint here at all" rather than "this one is POST-only".
|
||||
// This route is not a working GET; it exists solely to produce that 405.
|
||||
mcpGroup := apiGroup.Group("/mcp", RequireFeature(license.FeatureMCP))
|
||||
mcpGroup.POST("", mcp.Handler())
|
||||
mcpGroup.GET("", mcp.Handler())
|
||||
|
||||
@@ -25,6 +25,15 @@ func callerFromContext(c *gin.Context) Caller {
|
||||
// Handler serves the MCP endpoint. It is stateless: no session resumption, each
|
||||
// request self-contained, which is what lets it sit behind ordinary request
|
||||
// middleware with no special casing.
|
||||
//
|
||||
// Stateless mode also means this handler is POST-only in practice: the SDK's
|
||||
// StreamableHTTPHandler hardcodes a 405 for GET whenever Stateless is true,
|
||||
// because a stateless server has no session to open the server-to-client SSE
|
||||
// stream against. The GET route is still registered deliberately (see
|
||||
// handlers.go) so a client probing for the endpoint sees a protocol-correct
|
||||
// 405 rather than gin's 404 — the MCP spec expects exactly that response from
|
||||
// a server that does not offer the GET/SSE leg. Nothing here should route GET
|
||||
// requests differently or try to make them do anything else.
|
||||
func Handler() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
caller := callerFromContext(c)
|
||||
|
||||
Reference in New Issue
Block a user