From c2e2c9b87369f8c80f650c1c15773fa18d228f3e Mon Sep 17 00:00:00 2001 From: Hein Date: Tue, 7 Apr 2026 19:52:38 +0200 Subject: [PATCH] feat(transport): add streamable HTTP transport for MCP --- pkg/resolvemcp/README.md | 71 +++++++++++++++++++++++++----------- pkg/resolvemcp/handler.go | 8 ++++ pkg/resolvemcp/resolvemcp.go | 33 +++++++++++++++++ 3 files changed, 91 insertions(+), 21 deletions(-) diff --git a/pkg/resolvemcp/README.md b/pkg/resolvemcp/README.md index 7bb6429..68399c8 100644 --- a/pkg/resolvemcp/README.md +++ b/pkg/resolvemcp/README.md @@ -67,52 +67,81 @@ Each call immediately creates four MCP **tools** and one MCP **resource** for th --- -## HTTP / SSE Transport - -The `*server.SSEServer` returned by any of the helpers below implements `http.Handler`, so it works with every Go HTTP framework. +## HTTP Transports `Config.BasePath` is required and used for all route registration. `Config.BaseURL` is optional — when empty it is detected from each request. -### Gorilla Mux +Two transports are supported: **SSE** (legacy, two-endpoint) and **Streamable HTTP** (recommended, single-endpoint). + +--- + +### SSE Transport + +Two endpoints: `GET {BasePath}/sse` (subscribe) + `POST {BasePath}/message` (send). + +#### Gorilla Mux ```go resolvemcp.SetupMuxRoutes(r, handler) ``` -Registers: - | Route | Method | Description | |---|---|---| | `{BasePath}/sse` | GET | SSE connection — clients subscribe here | | `{BasePath}/message` | POST | JSON-RPC — clients send requests here | -| `{BasePath}/*` | any | Full SSE server (convenience prefix) | -### bunrouter +#### bunrouter ```go resolvemcp.SetupBunRouterRoutes(router, handler) ``` -Registers `GET {BasePath}/sse` and `POST {BasePath}/message` on the provided `*bunrouter.Router`. - -### Gin (or any `http.Handler`-compatible framework) - -Use `handler.SSEServer()` to get an `http.Handler` and wrap it with the framework's adapter: +#### Gin / net/http / Echo ```go sse := handler.SSEServer() -// Gin -engine.Any("/mcp/*path", gin.WrapH(sse)) - -// net/http -http.Handle("/mcp/", sse) - -// Echo -e.Any("/mcp/*", echo.WrapHandler(sse)) +engine.Any("/mcp/*path", gin.WrapH(sse)) // Gin +http.Handle("/mcp/", sse) // net/http +e.Any("/mcp/*", echo.WrapHandler(sse)) // Echo ``` +--- + +### Streamable HTTP Transport + +Single endpoint at `{BasePath}`. Handles POST (client→server) and GET (server→client streaming). Preferred for new integrations. + +#### Gorilla Mux + +```go +resolvemcp.SetupMuxStreamableHTTPRoutes(r, handler) +``` + +Mounts the handler at `{BasePath}` (all methods). + +#### bunrouter + +```go +resolvemcp.SetupBunRouterStreamableHTTPRoutes(router, handler) +``` + +Registers GET, POST, DELETE on `{BasePath}`. + +#### Gin / net/http / Echo + +```go +h := handler.StreamableHTTPServer() +// or: h := resolvemcp.NewStreamableHTTPHandler(handler) + +engine.Any("/mcp", gin.WrapH(h)) // Gin +http.Handle("/mcp", h) // net/http +e.Any("/mcp", echo.WrapHandler(h)) // Echo +``` + +--- + ### Authentication Add middleware before the MCP routes. The handler itself has no auth layer. diff --git a/pkg/resolvemcp/handler.go b/pkg/resolvemcp/handler.go index cf52414..af33862 100644 --- a/pkg/resolvemcp/handler.go +++ b/pkg/resolvemcp/handler.go @@ -69,6 +69,14 @@ func (h *Handler) SSEServer() http.Handler { return &dynamicSSEHandler{h: h} } +// StreamableHTTPServer returns an http.Handler that serves MCP over the streamable HTTP transport. +// Unlike SSE (which requires two endpoints), streamable HTTP uses a single endpoint for all +// client-server communication (POST for requests, GET for server-initiated messages). +// Mount the returned handler at the desired path; the path itself becomes the MCP endpoint. +func (h *Handler) StreamableHTTPServer() http.Handler { + return server.NewStreamableHTTPServer(h.mcpServer) +} + // newSSEServer creates a concrete *server.SSEServer for known baseURL and basePath values. func (h *Handler) newSSEServer(baseURL, basePath string) *server.SSEServer { return server.NewSSEServer( diff --git a/pkg/resolvemcp/resolvemcp.go b/pkg/resolvemcp/resolvemcp.go index 41130f2..534d8e0 100644 --- a/pkg/resolvemcp/resolvemcp.go +++ b/pkg/resolvemcp/resolvemcp.go @@ -98,3 +98,36 @@ func SetupBunRouterRoutes(router *bunrouter.Router, handler *Handler) { func NewSSEServer(handler *Handler) http.Handler { return handler.SSEServer() } + +// SetupMuxStreamableHTTPRoutes mounts the MCP streamable HTTP endpoint on the given Gorilla Mux router. +// The streamable HTTP transport uses a single endpoint (Config.BasePath) for all communication: +// POST for client→server messages, GET for server→client streaming. +// +// Example: +// +// resolvemcp.SetupMuxStreamableHTTPRoutes(r, handler) // mounts at Config.BasePath +func SetupMuxStreamableHTTPRoutes(muxRouter *mux.Router, handler *Handler) { + basePath := handler.config.BasePath + h := handler.StreamableHTTPServer() + muxRouter.PathPrefix(basePath).Handler(http.StripPrefix(basePath, h)) +} + +// SetupBunRouterStreamableHTTPRoutes mounts the MCP streamable HTTP endpoint on a bunrouter router. +// The streamable HTTP transport uses a single endpoint (Config.BasePath). +func SetupBunRouterStreamableHTTPRoutes(router *bunrouter.Router, handler *Handler) { + basePath := handler.config.BasePath + h := handler.StreamableHTTPServer() + router.GET(basePath, bunrouter.HTTPHandler(h)) + router.POST(basePath, bunrouter.HTTPHandler(h)) + router.DELETE(basePath, bunrouter.HTTPHandler(h)) +} + +// NewStreamableHTTPHandler returns an http.Handler that serves MCP over the streamable HTTP transport. +// Mount it at the desired path; that path becomes the MCP endpoint. +// +// h := resolvemcp.NewStreamableHTTPHandler(handler) +// http.Handle("/mcp", h) +// engine.Any("/mcp", gin.WrapH(h)) +func NewStreamableHTTPHandler(handler *Handler) http.Handler { + return handler.StreamableHTTPServer() +}