Files
ResolveSpec/pkg/resolvemcp/README.md
Hein 200a03c225 feat(resolvemcp): add SSE server and bunrouter setup functions
* Introduce SSEServer method for creating an SSE server bound to the handler.
* Add SetupBunRouterRoutes function to mount MCP HTTP/SSE endpoints on bunrouter.
* Update README with usage examples for new features.
2026-03-27 13:28:03 +02:00

10 KiB
Raw Permalink Blame History

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)

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

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

Handler Creation

Function Description
NewHandlerWithGORM(db *gorm.DB) *Handler Backed by GORM
NewHandlerWithBun(db *bun.DB) *Handler Backed by Bun
NewHandlerWithDB(db common.Database) *Handler Backed by any common.Database
NewHandler(db common.Database, registry common.ModelRegistry) *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 / SSE Transport

The *server.SSEServer returned by any of the helpers below implements http.Handler, so it works with every Go HTTP framework.

Gorilla Mux

resolvemcp.SetupMuxRoutes(r, handler, "http://localhost:8080")

Registers:

Route Method Description
/mcp/sse GET SSE connection — clients subscribe here
/mcp/message POST JSON-RPC — clients send requests here
/mcp/* any Full SSE server (convenience prefix)

bunrouter

resolvemcp.SetupBunRouterRoutes(router, handler, "http://localhost:8080", "/mcp")

Registers GET /mcp/sse and POST /mcp/message on the provided *bunrouter.Router.

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:

sse := handler.SSEServer("http://localhost:8080", "/mcp")

// Gin
engine.Any("/mcp/*path", gin.WrapH(sse))

// net/http
http.Handle("/mcp/", http.StripPrefix("/mcp", sse))

// Echo
e.Any("/mcp/*", echo.WrapHandler(sse))

Authentication

Add middleware before the MCP routes. The handler itself has no auth layer.


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: 10100).
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... } }

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)