resolvemcp

package
v1.0.75 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 7, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

README

resolvemcp

Package resolvemcp exposes registered database models as Model Context Protocol (MCP) tools and resources over HTTP/SSE transport. It mirrors the resolvespec package patterns — same model registration API, same filter/sort/pagination/preload options, same lifecycle hook system.

Quick Start

import (
    "github.com/bitechdev/ResolveSpec/pkg/resolvemcp"
    "github.com/gorilla/mux"
)

// 1. Create a handler
handler := resolvemcp.NewHandlerWithGORM(db, resolvemcp.Config{
    BaseURL: "http://localhost:8080",
})

// 2. Register models
handler.RegisterModel("public", "users", &User{})
handler.RegisterModel("public", "orders", &Order{})

// 3. Mount routes
r := mux.NewRouter()
resolvemcp.SetupMuxRoutes(r, handler)

Config

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

Function Description
NewHandlerWithGORM(db *gorm.DB, cfg Config) *Handler Backed by GORM
NewHandlerWithBun(db *bun.DB, cfg Config) *Handler Backed by Bun
NewHandlerWithDB(db common.Database, cfg Config) *Handler Backed by any common.Database
NewHandler(db common.Database, registry common.ModelRegistry, cfg Config) *Handler Full control over registry

Registering Models

handler.RegisterModel(schema, entity string, model interface{}) error
  • schema — database schema name (e.g. "public"), or empty string for no schema prefix.
  • entity — table/entity name (e.g. "users").
  • model — a pointer to a struct (e.g. &User{}).

Each call immediately creates four MCP tools and one MCP resource for the model.


HTTP Transports

Config.BasePath is required and used for all route registration. Config.BaseURL is optional — when empty it is detected from each request.

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
resolvemcp.SetupMuxRoutes(r, handler)
Route Method Description
{BasePath}/sse GET SSE connection — clients subscribe here
{BasePath}/message POST JSON-RPC — clients send requests here
bunrouter
resolvemcp.SetupBunRouterRoutes(router, handler)
Gin / net/http / Echo
sse := handler.SSEServer()

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
resolvemcp.SetupMuxStreamableHTTPRoutes(r, handler)

Mounts the handler at {BasePath} (all methods).

bunrouter
resolvemcp.SetupBunRouterStreamableHTTPRoutes(router, handler)

Registers GET, POST, DELETE on {BasePath}.

Gin / net/http / Echo
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.


Security

resolvemcp integrates with the security package to provide per-entity access control, row-level security, and column-level security — the same system used by resolvespec and restheadspec.

Wiring security hooks
import "github.com/bitechdev/ResolveSpec/pkg/security"

securityList := security.NewSecurityList(mySecurityProvider)
resolvemcp.RegisterSecurityHooks(handler, securityList)

Call RegisterSecurityHooks once, after creating the handler and before registering models. It installs these controls automatically:

Hook Effect
BeforeHandle Enforces per-entity operation rules (see below)
BeforeRead Loads RLS/CLS rules, then injects a user-scoped WHERE clause
AfterRead Masks/hides columns per column-security rules; writes audit log
BeforeUpdate Blocks update if CanUpdate is false
BeforeDelete Blocks delete if CanDelete is false
Per-entity operation rules

Use RegisterModelWithRules instead of RegisterModel to set access rules at registration time:

import "github.com/bitechdev/ResolveSpec/pkg/modelregistry"

// Read-only entity
handler.RegisterModelWithRules("public", "audit_logs", &AuditLog{}, modelregistry.ModelRules{
    CanRead:   true,
    CanCreate: false,
    CanUpdate: false,
    CanDelete: false,
})

// Public read, authenticated write
handler.RegisterModelWithRules("public", "products", &Product{}, modelregistry.ModelRules{
    CanPublicRead: true,
    CanRead:       true,
    CanCreate:     true,
    CanUpdate:     true,
    CanDelete:     false,
})

To update rules for an already-registered model:

handler.SetModelRules("public", "users", modelregistry.ModelRules{
    CanRead:   true,
    CanCreate: true,
    CanUpdate: true,
    CanDelete: false,
})

RegisterModel (no rules) registers with all-allowed defaults (CanRead/Create/Update/Delete = true).

ModelRules fields
Field Default Description
CanPublicRead false Allow unauthenticated reads
CanPublicCreate false Allow unauthenticated creates
CanPublicUpdate false Allow unauthenticated updates
CanPublicDelete false Allow unauthenticated deletes
CanRead true Allow authenticated reads
CanCreate true Allow authenticated creates
CanUpdate true Allow authenticated updates
CanDelete true Allow authenticated deletes
SecurityDisabled false Skip all security checks for this model

MCP Tools

Tool Naming
{operation}_{schema}_{entity}    // e.g. read_public_users
{operation}_{entity}             // e.g. read_users  (when schema is empty)

Operations: read, create, update, delete.

Read Tool — read_{schema}_{entity}

Fetch one or many records.

Argument Type Description
id string Primary key value. Omit to return multiple records.
limit number Max records per page (recommended: 10–100).
offset number Records to skip (offset-based pagination).
cursor_forward string PK of the last record on the current page (next-page cursor).
cursor_backward string PK of the first record on the current page (prev-page cursor).
columns array Column names to include. Omit for all columns.
omit_columns array Column names to exclude.
filters array Filter objects (see Filtering).
sort array Sort objects (see Sorting).
preloads array Relation preload objects (see Preloading).

Response:

{
  "success": true,
  "data": [...],
  "metadata": {
    "total": 100,
    "filtered": 100,
    "count": 10,
    "limit": 10,
    "offset": 0
  }
}
Create Tool — create_{schema}_{entity}

Insert one or more records.

Argument Type Description
data object | array Single object or array of objects to insert.

Array input runs inside a single transaction — all succeed or all fail.

Response:

{ "success": true, "data": { ... } }
Update Tool — update_{schema}_{entity}

Partially update an existing record. Only non-null, non-empty fields in data are applied; existing values are preserved for omitted fields.

Argument Type Description
id string Primary key of the record. Can also be included inside data.
data object (required) Fields to update.

Response:

{ "success": true, "data": { ...merged record... } }
Delete Tool — delete_{schema}_{entity}

Delete a record by primary key. Irreversible.

Argument Type Description
id string (required) Primary key of the record to delete.

Response:

{ "success": true, "data": { ...deleted record... } }
Annotation Tool — resolvespec_annotate

Store or retrieve freeform annotation records for any tool, model, or entity. Registered automatically on every handler.

Argument Type Description
tool_name string (required) Key to annotate — an MCP tool name (e.g. read_public_users), a model name (e.g. public.users), or any other identifier.
annotations object Annotation data to persist. Omit to retrieve existing annotations instead.

Set annotations (calls resolvespec_set_annotation(tool_name, annotations)):

{ "tool_name": "read_public_users", "annotations": { "description": "Returns active users", "owner": "platform-team" } }

Response:

{ "success": true, "tool_name": "read_public_users", "action": "set" }

Get annotations (calls resolvespec_get_annotation(tool_name)):

{ "tool_name": "read_public_users" }

Response:

{ "success": true, "tool_name": "read_public_users", "action": "get", "annotations": { ... } }

Resource — {schema}.{entity}

Each model is also registered as an MCP resource with URI schema.entity (or just entity when schema is empty). Reading the resource returns up to 100 records as application/json.


Filtering

Pass an array of filter objects to the filters argument:

[
  { "column": "status", "operator": "=", "value": "active" },
  { "column": "age", "operator": ">", "value": 18, "logic_operator": "AND" },
  { "column": "role", "operator": "in", "value": ["admin", "editor"], "logic_operator": "OR" }
]
Supported Operators
Operator Aliases Description
= eq Equal
!= neq, <> Not equal
> gt Greater than
>= gte Greater than or equal
< lt Less than
<= lte Less than or equal
like SQL LIKE (case-sensitive)
ilike SQL ILIKE (case-insensitive)
in Value in list
is_null Column IS NULL
is_not_null Column IS NOT NULL
Logic Operators
  • "logic_operator": "AND" (default) — filter is AND-chained with the previous condition.
  • "logic_operator": "OR" — filter is OR-grouped with the previous condition.

Consecutive OR filters are grouped into a single (cond1 OR cond2 OR ...) clause.


Sorting

[
  { "column": "created_at", "direction": "desc" },
  { "column": "name", "direction": "asc" }
]

Pagination

Offset-Based
{ "limit": 20, "offset": 40 }
Cursor-Based

Cursor pagination uses a SQL EXISTS subquery for stable, efficient paging. Always pair with a sort argument.

// Next page: pass the PK of the last record on the current page
{ "cursor_forward": "42", "limit": 20, "sort": [{"column": "id", "direction": "asc"}] }

// Previous page: pass the PK of the first record on the current page
{ "cursor_backward": "23", "limit": 20, "sort": [{"column": "id", "direction": "asc"}] }

Preloading Relations

[
  { "relation": "Profile" },
  { "relation": "Orders" }
]

Available relations are listed in each tool's description. Only relations defined on the model struct are valid.


Hook System

Hooks let you intercept and modify CRUD operations at well-defined lifecycle points.

Hook Types
Constant Fires
BeforeHandle After model resolution, before operation dispatch (all CRUD)
BeforeRead / AfterRead Around read queries
BeforeCreate / AfterCreate Around insert
BeforeUpdate / AfterUpdate Around update
BeforeDelete / AfterDelete Around delete
Registering Hooks
handler.Hooks().Register(resolvemcp.BeforeCreate, func(ctx *resolvemcp.HookContext) error {
    // Inject a timestamp before insert
    if data, ok := ctx.Data.(map[string]interface{}); ok {
        data["created_at"] = time.Now()
    }
    return nil
})

// Register the same hook for multiple events
handler.Hooks().RegisterMultiple(
    []resolvemcp.HookType{resolvemcp.BeforeCreate, resolvemcp.BeforeUpdate},
    auditHook,
)
HookContext Fields
Field Type Description
Context context.Context Request context
Handler *Handler The resolvemcp handler
Schema string Database schema name
Entity string Entity/table name
Model interface{} Registered model instance
Options common.RequestOptions Parsed request options (read operations)
Operation string "read", "create", "update", or "delete"
ID string Primary key from request (read/update/delete)
Data interface{} Input data (create/update — modifiable)
Result interface{} Output data (set by After hooks)
Error error Operation error, if any
Query common.SelectQuery Live query object (available in BeforeRead)
Tx common.Database Database/transaction handle
Abort bool Set to true to abort the operation
AbortMessage string Error message returned when aborting
AbortCode int Optional status code for the abort
Aborting an Operation
handler.Hooks().Register(resolvemcp.BeforeDelete, func(ctx *resolvemcp.HookContext) error {
    ctx.Abort = true
    ctx.AbortMessage = "deletion is disabled"
    return nil
})
Managing Hooks
registry := handler.Hooks()
registry.HasHooks(resolvemcp.BeforeCreate)   // bool
registry.Clear(resolvemcp.BeforeCreate)      // remove hooks for one type
registry.ClearAll()                          // remove all hooks

Context Helpers

Request metadata is threaded through context.Context during handler execution. Hooks and custom tools can read it:

schema    := resolvemcp.GetSchema(ctx)
entity    := resolvemcp.GetEntity(ctx)
tableName := resolvemcp.GetTableName(ctx)
model     := resolvemcp.GetModel(ctx)
modelPtr  := resolvemcp.GetModelPtr(ctx)

You can also set values manually (e.g. in middleware):

ctx = resolvemcp.WithSchema(ctx, "tenant_a")

Adding Custom MCP Tools

Access the underlying *server.MCPServer to register additional tools:

mcpServer := handler.MCPServer()
mcpServer.AddTool(myTool, myHandler)

Table Name Resolution

The handler resolves table names in priority order:

  1. TableNameProvider interface — TableName() string (can return "schema.table")
  2. SchemaProvider interface — SchemaName() string (combined with entity name)
  3. Fallback: schema.entity (or schema_entity for SQLite)

Documentation

Overview

Package resolvemcp exposes registered database models as Model Context Protocol (MCP) tools and resources over HTTP/SSE transport.

It mirrors the resolvespec package patterns:

  • Same model registration API
  • Same filter, sort, cursor pagination, preload options
  • Same lifecycle hook system

Usage:

handler := resolvemcp.NewHandlerWithGORM(db, resolvemcp.Config{BaseURL: "http://localhost:8080"})
handler.RegisterModel("public", "users", &User{})

r := mux.NewRouter()
resolvemcp.SetupMuxRoutes(r, handler)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetEntity

func GetEntity(ctx context.Context) string

func GetModel

func GetModel(ctx context.Context) interface{}

func GetModelPtr

func GetModelPtr(ctx context.Context) interface{}

func GetSchema

func GetSchema(ctx context.Context) string

func GetTableName

func GetTableName(ctx context.Context) string

func NewSSEServer

func NewSSEServer(handler *Handler) http.Handler

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 NewStreamableHTTPHandler added in v1.0.75

func NewStreamableHTTPHandler(handler *Handler) http.Handler

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 RegisterSecurityHooks added in v1.0.74

func RegisterSecurityHooks(handler *Handler, securityList *security.SecurityList)

RegisterSecurityHooks wires the security package's access-control layer into the resolvemcp handler. Call it once after creating the handler, before registering models.

The following controls are applied:

  • Per-entity operation rules (CanRead, CanCreate, CanUpdate, CanDelete, CanPublic*) stored via RegisterModelWithRules / SetModelRules.
  • Row-level security: WHERE clause injected per user from the SecurityList provider.
  • Column-level security: sensitive columns masked/hidden in read results.
  • Audit logging after each read.

func SetupBunRouterRoutes added in v1.0.69

func SetupBunRouterRoutes(router *bunrouter.Router, handler *Handler)

SetupBunRouterRoutes mounts the MCP HTTP/SSE endpoints on a bunrouter router using the base path from Config.BasePath.

Two routes are registered:

  • GET {basePath}/sse — SSE connection endpoint
  • POST {basePath}/message — JSON-RPC message endpoint

func SetupBunRouterStreamableHTTPRoutes added in v1.0.75

func SetupBunRouterStreamableHTTPRoutes(router *bunrouter.Router, handler *Handler)

SetupBunRouterStreamableHTTPRoutes mounts the MCP streamable HTTP endpoint on a bunrouter router. The streamable HTTP transport uses a single endpoint (Config.BasePath).

func SetupMuxRoutes

func SetupMuxRoutes(muxRouter *mux.Router, handler *Handler)

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).

Two routes are registered:

  • GET {basePath}/sse — SSE connection endpoint (client subscribes 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 before calling SetupMuxRoutes.

func SetupMuxStreamableHTTPRoutes added in v1.0.75

func SetupMuxStreamableHTTPRoutes(muxRouter *mux.Router, handler *Handler)

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 WithEntity

func WithEntity(ctx context.Context, entity string) context.Context

func WithModel

func WithModel(ctx context.Context, model interface{}) context.Context

func WithModelPtr

func WithModelPtr(ctx context.Context, modelPtr interface{}) context.Context

func WithSchema

func WithSchema(ctx context.Context, schema string) context.Context

func WithTableName

func WithTableName(ctx context.Context, tableName string) context.Context

Types

type Config added in v1.0.70

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
}

Config holds configuration for the resolvemcp handler.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

Handler exposes registered database models as MCP tools and resources.

func NewHandler

func NewHandler(db common.Database, registry common.ModelRegistry, cfg Config) *Handler

NewHandler creates a Handler with the given database, model registry, and config.

func NewHandlerWithBun

func NewHandlerWithBun(db *bun.DB, cfg Config) *Handler

NewHandlerWithBun creates a Handler backed by a Bun database connection.

func NewHandlerWithDB

func NewHandlerWithDB(db common.Database, cfg Config) *Handler

NewHandlerWithDB creates a Handler using an existing common.Database and a new registry.

func NewHandlerWithGORM

func NewHandlerWithGORM(db *gorm.DB, cfg Config) *Handler

NewHandlerWithGORM creates a Handler backed by a GORM database connection.

func (*Handler) GetDatabase

func (h *Handler) GetDatabase() common.Database

GetDatabase returns the underlying database.

func (*Handler) Hooks

func (h *Handler) Hooks() *HookRegistry

Hooks returns the hook registry.

func (*Handler) MCPServer

func (h *Handler) MCPServer() *server.MCPServer

MCPServer returns the underlying MCP server, e.g. to add custom tools.

func (*Handler) RegisterModel

func (h *Handler) RegisterModel(schema, entity string, model interface{}) error

RegisterModel registers a model and immediately exposes it as MCP tools and a resource.

func (*Handler) RegisterModelWithRules added in v1.0.74

func (h *Handler) RegisterModelWithRules(schema, entity string, model interface{}, rules modelregistry.ModelRules) error

RegisterModelWithRules registers a model and sets per-entity operation rules (CanRead, CanCreate, CanUpdate, CanDelete, CanPublic*, SecurityDisabled). Requires RegisterSecurityHooks to have been called for the rules to be enforced.

func (*Handler) SSEServer added in v1.0.69

func (h *Handler) SSEServer() http.Handler

SSEServer returns an http.Handler that serves MCP over SSE. Config.BasePath must be set. Config.BaseURL is used when set; if empty it is detected automatically from each incoming request.

func (*Handler) SetModelRules added in v1.0.74

func (h *Handler) SetModelRules(schema, entity string, rules modelregistry.ModelRules) error

SetModelRules updates the operation rules for an already-registered model. Requires RegisterSecurityHooks to have been called for the rules to be enforced.

func (*Handler) StreamableHTTPServer added in v1.0.75

func (h *Handler) StreamableHTTPServer() http.Handler

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.

type HookContext

type HookContext struct {
	Context      context.Context
	Handler      *Handler
	Schema       string
	Entity       string
	Model        interface{}
	Options      common.RequestOptions
	Operation    string
	ID           string
	Data         interface{}
	Result       interface{}
	Error        error
	Query        common.SelectQuery
	Abort        bool
	AbortMessage string
	AbortCode    int
	Tx           common.Database
}

HookContext contains all the data available to a hook

type HookFunc

type HookFunc func(*HookContext) error

HookFunc is the signature for hook functions

type HookRegistry

type HookRegistry struct {
	// contains filtered or unexported fields
}

HookRegistry manages all registered hooks

func NewHookRegistry

func NewHookRegistry() *HookRegistry

func (*HookRegistry) Clear

func (r *HookRegistry) Clear(hookType HookType)

func (*HookRegistry) ClearAll

func (r *HookRegistry) ClearAll()

func (*HookRegistry) Execute

func (r *HookRegistry) Execute(hookType HookType, ctx *HookContext) error

func (*HookRegistry) HasHooks

func (r *HookRegistry) HasHooks(hookType HookType) bool

func (*HookRegistry) Register

func (r *HookRegistry) Register(hookType HookType, hook HookFunc)

func (*HookRegistry) RegisterMultiple

func (r *HookRegistry) RegisterMultiple(hookTypes []HookType, hook HookFunc)

type HookType

type HookType string

HookType defines the type of hook to execute

const (
	// BeforeHandle fires after model resolution, before operation dispatch.
	BeforeHandle HookType = "before_handle"

	BeforeRead HookType = "before_read"
	AfterRead  HookType = "after_read"

	BeforeCreate HookType = "before_create"
	AfterCreate  HookType = "after_create"

	BeforeUpdate HookType = "before_update"
	AfterUpdate  HookType = "after_update"

	BeforeDelete HookType = "before_delete"
	AfterDelete  HookType = "after_delete"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL