mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-04-05 15:36:15 +00:00
feat(resolvemcp): enhance handler with configuration support
* Introduce Config struct for BaseURL and BasePath settings * Update handler creation functions to accept configuration * Modify SSEServer to use dynamic base URL detection * Adjust route setup functions to utilize BasePath from config
This commit is contained in:
@@ -11,7 +11,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 1. Create a handler
|
// 1. Create a handler
|
||||||
handler := resolvemcp.NewHandlerWithGORM(db)
|
handler := resolvemcp.NewHandlerWithGORM(db, resolvemcp.Config{
|
||||||
|
BaseURL: "http://localhost:8080",
|
||||||
|
})
|
||||||
|
|
||||||
// 2. Register models
|
// 2. Register models
|
||||||
handler.RegisterModel("public", "users", &User{})
|
handler.RegisterModel("public", "users", &User{})
|
||||||
@@ -19,19 +21,35 @@ handler.RegisterModel("public", "orders", &Order{})
|
|||||||
|
|
||||||
// 3. Mount routes
|
// 3. Mount routes
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
resolvemcp.SetupMuxRoutes(r, handler, "http://localhost:8080")
|
resolvemcp.SetupMuxRoutes(r, handler)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Config struct {
|
||||||
|
// BaseURL is the public-facing base URL of the server (e.g. "http://localhost:8080").
|
||||||
|
// Sent to MCP clients during the SSE handshake so they know where to POST messages.
|
||||||
|
// If empty, it is detected from each incoming request using the Host header and
|
||||||
|
// TLS state (X-Forwarded-Proto is honoured for reverse-proxy deployments).
|
||||||
|
BaseURL string
|
||||||
|
|
||||||
|
// BasePath is the URL path prefix where MCP endpoints are mounted (e.g. "/mcp").
|
||||||
|
// Required.
|
||||||
|
BasePath string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Handler Creation
|
## Handler Creation
|
||||||
|
|
||||||
| Function | Description |
|
| Function | Description |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `NewHandlerWithGORM(db *gorm.DB) *Handler` | Backed by GORM |
|
| `NewHandlerWithGORM(db *gorm.DB, cfg Config) *Handler` | Backed by GORM |
|
||||||
| `NewHandlerWithBun(db *bun.DB) *Handler` | Backed by Bun |
|
| `NewHandlerWithBun(db *bun.DB, cfg Config) *Handler` | Backed by Bun |
|
||||||
| `NewHandlerWithDB(db common.Database) *Handler` | Backed by any `common.Database` |
|
| `NewHandlerWithDB(db common.Database, cfg Config) *Handler` | Backed by any `common.Database` |
|
||||||
| `NewHandler(db common.Database, registry common.ModelRegistry) *Handler` | Full control over registry |
|
| `NewHandler(db common.Database, registry common.ModelRegistry, cfg Config) *Handler` | Full control over registry |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,40 +71,43 @@ Each call immediately creates four MCP **tools** and one MCP **resource** for th
|
|||||||
|
|
||||||
The `*server.SSEServer` returned by any of the helpers below implements `http.Handler`, so it works with every Go HTTP framework.
|
The `*server.SSEServer` returned by any of the helpers below implements `http.Handler`, so it works with every Go HTTP framework.
|
||||||
|
|
||||||
|
`Config.BasePath` is required and used for all route registration.
|
||||||
|
`Config.BaseURL` is optional — when empty it is detected from each request.
|
||||||
|
|
||||||
### Gorilla Mux
|
### Gorilla Mux
|
||||||
|
|
||||||
```go
|
```go
|
||||||
resolvemcp.SetupMuxRoutes(r, handler, "http://localhost:8080")
|
resolvemcp.SetupMuxRoutes(r, handler)
|
||||||
```
|
```
|
||||||
|
|
||||||
Registers:
|
Registers:
|
||||||
|
|
||||||
| Route | Method | Description |
|
| Route | Method | Description |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/mcp/sse` | GET | SSE connection — clients subscribe here |
|
| `{BasePath}/sse` | GET | SSE connection — clients subscribe here |
|
||||||
| `/mcp/message` | POST | JSON-RPC — clients send requests here |
|
| `{BasePath}/message` | POST | JSON-RPC — clients send requests here |
|
||||||
| `/mcp/*` | any | Full SSE server (convenience prefix) |
|
| `{BasePath}/*` | any | Full SSE server (convenience prefix) |
|
||||||
|
|
||||||
### bunrouter
|
### bunrouter
|
||||||
|
|
||||||
```go
|
```go
|
||||||
resolvemcp.SetupBunRouterRoutes(router, handler, "http://localhost:8080", "/mcp")
|
resolvemcp.SetupBunRouterRoutes(router, handler)
|
||||||
```
|
```
|
||||||
|
|
||||||
Registers `GET /mcp/sse` and `POST /mcp/message` on the provided `*bunrouter.Router`.
|
Registers `GET {BasePath}/sse` and `POST {BasePath}/message` on the provided `*bunrouter.Router`.
|
||||||
|
|
||||||
### Gin (or any `http.Handler`-compatible framework)
|
### Gin (or any `http.Handler`-compatible framework)
|
||||||
|
|
||||||
Use `handler.SSEServer` to get a pre-bound `*server.SSEServer` and wrap it with the framework's adapter:
|
Use `handler.SSEServer()` to get an `http.Handler` and wrap it with the framework's adapter:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
sse := handler.SSEServer("http://localhost:8080", "/mcp")
|
sse := handler.SSEServer()
|
||||||
|
|
||||||
// Gin
|
// Gin
|
||||||
engine.Any("/mcp/*path", gin.WrapH(sse))
|
engine.Any("/mcp/*path", gin.WrapH(sse))
|
||||||
|
|
||||||
// net/http
|
// net/http
|
||||||
http.Handle("/mcp/", http.StripPrefix("/mcp", sse))
|
http.Handle("/mcp/", sse)
|
||||||
|
|
||||||
// Echo
|
// Echo
|
||||||
e.Any("/mcp/*", echo.WrapHandler(sse))
|
e.Any("/mcp/*", echo.WrapHandler(sse))
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/mark3labs/mcp-go/server"
|
"github.com/mark3labs/mcp-go/server"
|
||||||
|
|
||||||
@@ -21,17 +23,19 @@ type Handler struct {
|
|||||||
registry common.ModelRegistry
|
registry common.ModelRegistry
|
||||||
hooks *HookRegistry
|
hooks *HookRegistry
|
||||||
mcpServer *server.MCPServer
|
mcpServer *server.MCPServer
|
||||||
|
config Config
|
||||||
name string
|
name string
|
||||||
version string
|
version string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a Handler with the given database and model registry.
|
// NewHandler creates a Handler with the given database, model registry, and config.
|
||||||
func NewHandler(db common.Database, registry common.ModelRegistry) *Handler {
|
func NewHandler(db common.Database, registry common.ModelRegistry, cfg Config) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
db: db,
|
db: db,
|
||||||
registry: registry,
|
registry: registry,
|
||||||
hooks: NewHookRegistry(),
|
hooks: NewHookRegistry(),
|
||||||
mcpServer: server.NewMCPServer("resolvemcp", "1.0.0"),
|
mcpServer: server.NewMCPServer("resolvemcp", "1.0.0"),
|
||||||
|
config: cfg,
|
||||||
name: "resolvemcp",
|
name: "resolvemcp",
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
}
|
}
|
||||||
@@ -52,12 +56,18 @@ func (h *Handler) MCPServer() *server.MCPServer {
|
|||||||
return h.mcpServer
|
return h.mcpServer
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSEServer creates an *server.SSEServer bound to this handler.
|
// SSEServer returns an http.Handler that serves MCP over SSE.
|
||||||
// Use it to mount MCP on any HTTP framework that accepts http.Handler.
|
// Config.BasePath must be set. Config.BaseURL is used when set; if empty it is
|
||||||
//
|
// detected automatically from each incoming request.
|
||||||
// sse := handler.SSEServer("http://localhost:8080", "/mcp")
|
func (h *Handler) SSEServer() http.Handler {
|
||||||
// ginEngine.Any("/mcp/*path", gin.WrapH(sse))
|
if h.config.BaseURL != "" {
|
||||||
func (h *Handler) SSEServer(baseURL, basePath string) *server.SSEServer {
|
return h.newSSEServer(h.config.BaseURL, h.config.BasePath)
|
||||||
|
}
|
||||||
|
return &dynamicSSEHandler{h: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSSEServer creates a concrete *server.SSEServer for known baseURL and basePath values.
|
||||||
|
func (h *Handler) newSSEServer(baseURL, basePath string) *server.SSEServer {
|
||||||
return server.NewSSEServer(
|
return server.NewSSEServer(
|
||||||
h.mcpServer,
|
h.mcpServer,
|
||||||
server.WithBaseURL(baseURL),
|
server.WithBaseURL(baseURL),
|
||||||
@@ -65,6 +75,44 @@ func (h *Handler) SSEServer(baseURL, basePath string) *server.SSEServer {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dynamicSSEHandler detects BaseURL from each request and delegates to a cached
|
||||||
|
// *server.SSEServer per detected baseURL. Used when Config.BaseURL is empty.
|
||||||
|
type dynamicSSEHandler struct {
|
||||||
|
h *Handler
|
||||||
|
mu sync.Mutex
|
||||||
|
pool map[string]*server.SSEServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *dynamicSSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
baseURL := requestBaseURL(r)
|
||||||
|
|
||||||
|
d.mu.Lock()
|
||||||
|
if d.pool == nil {
|
||||||
|
d.pool = make(map[string]*server.SSEServer)
|
||||||
|
}
|
||||||
|
s, ok := d.pool[baseURL]
|
||||||
|
if !ok {
|
||||||
|
s = d.h.newSSEServer(baseURL, d.h.config.BasePath)
|
||||||
|
d.pool[baseURL] = s
|
||||||
|
}
|
||||||
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
s.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestBaseURL builds the base URL from an incoming request.
|
||||||
|
// It honours the X-Forwarded-Proto header for deployments behind a proxy.
|
||||||
|
func requestBaseURL(r *http.Request) string {
|
||||||
|
scheme := "http"
|
||||||
|
if r.TLS != nil {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
|
||||||
|
scheme = proto
|
||||||
|
}
|
||||||
|
return scheme + "://" + r.Host
|
||||||
|
}
|
||||||
|
|
||||||
// RegisterModel registers a model and immediately exposes it as MCP tools and a resource.
|
// RegisterModel registers a model and immediately exposes it as MCP tools and a resource.
|
||||||
func (h *Handler) RegisterModel(schema, entity string, model interface{}) error {
|
func (h *Handler) RegisterModel(schema, entity string, model interface{}) error {
|
||||||
fullName := buildModelName(schema, entity)
|
fullName := buildModelName(schema, entity)
|
||||||
|
|||||||
@@ -8,18 +8,17 @@
|
|||||||
//
|
//
|
||||||
// Usage:
|
// Usage:
|
||||||
//
|
//
|
||||||
// handler := resolvemcp.NewHandlerWithGORM(db)
|
// handler := resolvemcp.NewHandlerWithGORM(db, resolvemcp.Config{BaseURL: "http://localhost:8080"})
|
||||||
// handler.RegisterModel("public", "users", &User{})
|
// handler.RegisterModel("public", "users", &User{})
|
||||||
//
|
//
|
||||||
// r := mux.NewRouter()
|
// r := mux.NewRouter()
|
||||||
// resolvemcp.SetupMuxRoutes(r, handler, "http://localhost:8080")
|
// resolvemcp.SetupMuxRoutes(r, handler)
|
||||||
package resolvemcp
|
package resolvemcp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/mark3labs/mcp-go/server"
|
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
bunrouter "github.com/uptrace/bunrouter"
|
bunrouter "github.com/uptrace/bunrouter"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -29,72 +28,73 @@ import (
|
|||||||
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
|
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config holds configuration for the resolvemcp handler.
|
||||||
|
type Config struct {
|
||||||
|
// BaseURL is the public-facing base URL of the server (e.g. "http://localhost:8080").
|
||||||
|
// It is sent to MCP clients during the SSE handshake so they know where to POST messages.
|
||||||
|
BaseURL string
|
||||||
|
|
||||||
|
// BasePath is the URL path prefix where the MCP endpoints are mounted (e.g. "/mcp").
|
||||||
|
// If empty, the path is detected from each incoming request automatically.
|
||||||
|
BasePath string
|
||||||
|
}
|
||||||
|
|
||||||
// NewHandlerWithGORM creates a Handler backed by a GORM database connection.
|
// NewHandlerWithGORM creates a Handler backed by a GORM database connection.
|
||||||
func NewHandlerWithGORM(db *gorm.DB) *Handler {
|
func NewHandlerWithGORM(db *gorm.DB, cfg Config) *Handler {
|
||||||
return NewHandler(database.NewGormAdapter(db), modelregistry.NewModelRegistry())
|
return NewHandler(database.NewGormAdapter(db), modelregistry.NewModelRegistry(), cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandlerWithBun creates a Handler backed by a Bun database connection.
|
// NewHandlerWithBun creates a Handler backed by a Bun database connection.
|
||||||
func NewHandlerWithBun(db *bun.DB) *Handler {
|
func NewHandlerWithBun(db *bun.DB, cfg Config) *Handler {
|
||||||
return NewHandler(database.NewBunAdapter(db), modelregistry.NewModelRegistry())
|
return NewHandler(database.NewBunAdapter(db), modelregistry.NewModelRegistry(), cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandlerWithDB creates a Handler using an existing common.Database and a new registry.
|
// NewHandlerWithDB creates a Handler using an existing common.Database and a new registry.
|
||||||
func NewHandlerWithDB(db common.Database) *Handler {
|
func NewHandlerWithDB(db common.Database, cfg Config) *Handler {
|
||||||
return NewHandler(db, modelregistry.NewModelRegistry())
|
return NewHandler(db, modelregistry.NewModelRegistry(), cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupMuxRoutes mounts the MCP HTTP/SSE endpoints on the given Gorilla Mux router.
|
// SetupMuxRoutes mounts the MCP HTTP/SSE endpoints on the given Gorilla Mux router
|
||||||
//
|
// using the base path from Config.BasePath (falls back to "/mcp" if empty).
|
||||||
// baseURL is the public-facing base URL of the server (e.g. "http://localhost:8080").
|
|
||||||
// It is sent to MCP clients during the SSE handshake so they know where to POST messages.
|
|
||||||
//
|
//
|
||||||
// Two routes are registered:
|
// Two routes are registered:
|
||||||
// - GET /mcp/sse — SSE connection endpoint (client subscribes here)
|
// - GET {basePath}/sse — SSE connection endpoint (client subscribes here)
|
||||||
// - POST /mcp/message — JSON-RPC message endpoint (client sends requests here)
|
// - POST {basePath}/message — JSON-RPC message endpoint (client sends requests here)
|
||||||
//
|
//
|
||||||
// To protect these routes with authentication, wrap the mux router or apply middleware
|
// To protect these routes with authentication, wrap the mux router or apply middleware
|
||||||
// before calling SetupMuxRoutes.
|
// before calling SetupMuxRoutes.
|
||||||
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler, baseURL string) {
|
func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler) {
|
||||||
sseServer := server.NewSSEServer(
|
basePath := handler.config.BasePath
|
||||||
handler.mcpServer,
|
h := handler.SSEServer()
|
||||||
server.WithBaseURL(baseURL),
|
|
||||||
server.WithBasePath("/mcp"),
|
|
||||||
)
|
|
||||||
|
|
||||||
muxRouter.Handle("/mcp/sse", sseServer.SSEHandler()).Methods("GET", "OPTIONS")
|
muxRouter.Handle(basePath+"/sse", h).Methods("GET", "OPTIONS")
|
||||||
muxRouter.Handle("/mcp/message", sseServer.MessageHandler()).Methods("POST", "OPTIONS")
|
muxRouter.Handle(basePath+"/message", h).Methods("POST", "OPTIONS")
|
||||||
|
|
||||||
// Convenience: also expose the full SSE server at /mcp for clients that
|
// Convenience: also expose the full SSE server at basePath for clients that
|
||||||
// use ServeHTTP directly (e.g. net/http default mux).
|
// use ServeHTTP directly (e.g. net/http default mux).
|
||||||
muxRouter.PathPrefix("/mcp").Handler(http.StripPrefix("/mcp", sseServer))
|
muxRouter.PathPrefix(basePath).Handler(http.StripPrefix(basePath, h))
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSSEServer creates an *server.SSEServer that can be mounted manually,
|
// SetupBunRouterRoutes mounts the MCP HTTP/SSE endpoints on a bunrouter router
|
||||||
// useful when integrating with non-Mux routers or adding extra middleware.
|
// using the base path from Config.BasePath.
|
||||||
//
|
//
|
||||||
// sseServer := resolvemcp.NewSSEServer(handler, "http://localhost:8080", "/mcp")
|
// Two routes are registered:
|
||||||
// http.Handle("/mcp/", http.StripPrefix("/mcp", sseServer))
|
|
||||||
func NewSSEServer(handler *Handler, baseURL, basePath string) *server.SSEServer {
|
|
||||||
return server.NewSSEServer(
|
|
||||||
handler.mcpServer,
|
|
||||||
server.WithBaseURL(baseURL),
|
|
||||||
server.WithBasePath(basePath),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetupBunRouterRoutes mounts the MCP HTTP/SSE endpoints on a bunrouter router.
|
|
||||||
//
|
|
||||||
// Two routes are registered under the given basePath prefix:
|
|
||||||
// - GET {basePath}/sse — SSE connection endpoint
|
// - GET {basePath}/sse — SSE connection endpoint
|
||||||
// - POST {basePath}/message — JSON-RPC message endpoint
|
// - POST {basePath}/message — JSON-RPC message endpoint
|
||||||
func SetupBunRouterRoutes(router *bunrouter.Router, handler *Handler, baseURL, basePath string) {
|
func SetupBunRouterRoutes(router *bunrouter.Router, handler *Handler) {
|
||||||
sseServer := server.NewSSEServer(
|
basePath := handler.config.BasePath
|
||||||
handler.mcpServer,
|
h := handler.SSEServer()
|
||||||
server.WithBaseURL(baseURL),
|
|
||||||
server.WithBasePath(basePath),
|
|
||||||
)
|
|
||||||
|
|
||||||
router.GET(basePath+"/sse", bunrouter.HTTPHandler(sseServer.SSEHandler()))
|
router.GET(basePath+"/sse", bunrouter.HTTPHandler(h))
|
||||||
router.POST(basePath+"/message", bunrouter.HTTPHandler(sseServer.MessageHandler()))
|
router.POST(basePath+"/message", bunrouter.HTTPHandler(h))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSSEServer returns an http.Handler that serves MCP over SSE.
|
||||||
|
// If Config.BasePath is set it is used directly; otherwise the base path is
|
||||||
|
// detected from each incoming request (by stripping the "/sse" or "/message" suffix).
|
||||||
|
//
|
||||||
|
// h := resolvemcp.NewSSEServer(handler)
|
||||||
|
// http.Handle("/api/mcp/", h)
|
||||||
|
func NewSSEServer(handler *Handler) http.Handler {
|
||||||
|
return handler.SSEServer()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user