feat(resolvemcp): add read-only mode and function allowlist

- Config.ReadOnly disables insert/update/delete/annotation tools, reports only
  select in list_tables/describe_table and tells the agent it cannot write
- Config.AllowFunctionCalls keeps function tools on a read-only server
- Config.AllowedFunctions limits list_functions/call_function to named
  functions (empty allows all); others are reported as unknown
- reflect read-only mode in the usage guide and exported catalogue
This commit is contained in:
Hein
2026-10-07 14:15:14 +02:00
parent 431b674162
commit 4ed9506ad2
7 changed files with 274 additions and 12 deletions
+27
View File
@@ -442,6 +442,33 @@ The text appears in `list_tables` and `describe_table`. The server also sends a
`handler.ExportCatalog(path)` writes the usage guide, tools, limits and every table (columns, types, keys, relations, allowed operations, descriptions) to disk, JSON for a `.json` path and Markdown otherwise. The file is replaced atomically. It lists every table with at least one allowed operation, regardless of caller, so keep it out of public directories. Call it after registering models (for example at startup, or from a `go generate` step). `handler.ExportCatalog(path)` writes the usage guide, tools, limits and every table (columns, types, keys, relations, allowed operations, descriptions) to disk, JSON for a `.json` path and Markdown otherwise. The file is replaced atomically. It lists every table with at least one allowed operation, regardless of caller, so keep it out of public directories. Call it after registering models (for example at startup, or from a `go generate` step).
## Read-only mode
Set `Config.ReadOnly: true` to disable every write:
```go
handler := resolvemcp.NewHandlerWithGORM(db, resolvemcp.Config{ReadOnly: true})
```
- The insert, update, delete and annotation tools are not registered, so the agent never sees them. `list_functions`/`call_function` are off too, because a registered function may change data, unless you set `AllowFunctionCalls` (below).
- `list_tables` and `describe_table` report only `select`; `describe_table` also sets `read_only: true` and lists no writable columns.
- The MCP server instructions (and the exported catalogue) say the server is read-only and tell the agent not to attempt writes.
- A write that reaches a handler anyway is refused with a `forbidden` error ("this server is read-only: writes are disabled").
### Function calls and the allowlist
```go
// Read-only server that may still run two named functions
resolvemcp.Config{
ReadOnly: true,
AllowFunctionCalls: true, // keep list_functions / call_function on a read-only server
AllowedFunctions: []string{"report_totals", "search_customers"},
}
```
- `AllowFunctionCalls` only matters with `ReadOnly`; without it, functions are always available. Set it only for functions that do not change data.
- `AllowedFunctions` works with or without `ReadOnly`. When empty, every registered function is allowed. When set, only the named functions are listed and callable; any other is reported as `unknown function`, so its existence is not revealed. Per-function `Authorize` still applies on top.
## MCP Tools ## MCP Tools
Fixed set, independent of the models. `table` is `schema.entity`. Errors return `{"success":false,"error":{"code","message"}}` with codes `invalid_argument`, `not_found`, `forbidden`, `limit_exceeded`, `internal` (internal details are logged, the client gets a reference id). Fixed set, independent of the models. `table` is `schema.entity`. Errors return `{"success":false,"error":{"code","message"}}` with codes `invalid_argument`, `not_found`, `forbidden`, `limit_exceeded`, `internal` (internal details are logged, the client gets a reference id).
+30 -3
View File
@@ -24,12 +24,35 @@ const usageGuide = `This server exposes database tables through a fixed set of t
5. Use list_functions / call_function for registered functions. 5. Use list_functions / call_function for registered functions.
Read the error message when a call fails: it says which argument was wrong.` Read the error message when a call fails: it says which argument was wrong.`
// readOnlyGuide replaces usageGuide on a read-only server.
const readOnlyGuide = `This server exposes database tables through a fixed set of tools. It is READ-ONLY: you cannot insert, update or delete data or write annotations, and no tool for that exists. Do not attempt a write; tell the user it is not possible through this server.
1. Call list_tables to see the tables you may read and what they hold.
2. Call describe_table for a table before using it: columns, types, primary key, relations (preloadable) and limits.
3. Read with select_table (filters, sort, columns, preloads). Results are paged; use limit/offset or cursors, and include_count only when you need a total.
Read the error message when a call fails: it says which argument was wrong.`
// readOnlyFunctionsGuide is the extra step of a read-only server that still allows functions.
const readOnlyFunctionsGuide = `
4. Use list_functions / call_function for the registered functions. Only call functions that fit a read-only server; the server decides what is allowed.`
// guideFor returns the usage guide for the server mode.
func guideFor(readOnly, functions bool) string {
if !readOnly {
return usageGuide
}
if functions {
return readOnlyGuide + readOnlyFunctionsGuide
}
return readOnlyGuide
}
// Catalog is a snapshot of what the server offers: the usage guide, the tools, the limits // Catalog is a snapshot of what the server offers: the usage guide, the tools, the limits
// and every table with its columns, relations, allowed operations and descriptions. // and every table with its columns, relations, allowed operations and descriptions.
type Catalog struct { type Catalog struct {
GeneratedAt time.Time `json:"generated_at"` GeneratedAt time.Time `json:"generated_at"`
Server string `json:"server"` Server string `json:"server"`
Version string `json:"version"` Version string `json:"version"`
ReadOnly bool `json:"read_only"`
Guide string `json:"guide"` Guide string `json:"guide"`
Limits CatalogLimits `json:"limits"` Limits CatalogLimits `json:"limits"`
Tools []CatalogTool `json:"tools"` Tools []CatalogTool `json:"tools"`
@@ -122,7 +145,8 @@ func (h *Handler) BuildCatalog() Catalog {
GeneratedAt: time.Now().UTC(), GeneratedAt: time.Now().UTC(),
Server: h.name, Server: h.name,
Version: h.version, Version: h.version,
Guide: usageGuide, ReadOnly: h.config.ReadOnly,
Guide: guideFor(h.config.ReadOnly, h.config.AllowFunctionCalls),
Limits: CatalogLimits{ Limits: CatalogLimits{
DefaultLimit: h.config.DefaultLimit, DefaultLimit: h.config.DefaultLimit,
MaxLimit: h.config.MaxLimit, MaxLimit: h.config.MaxLimit,
@@ -143,7 +167,7 @@ func (h *Handler) BuildCatalog() Catalog {
for name, model := range h.registry.GetAllModels() { for name, model := range h.registry.GetAllModels() {
schema, entity, _ := splitTable(name) schema, entity, _ := splitTable(name)
rules := h.modelRules(schema, entity) rules := h.modelRules(schema, entity)
ops := opsFor(rules) ops := h.opsFor(rules)
if len(ops) == 0 { if len(ops) == 0 {
continue continue
} }
@@ -155,7 +179,7 @@ func (h *Handler) BuildCatalog() Catalog {
for mt != nil && (mt.Kind() == reflect.Pointer || mt.Kind() == reflect.Slice) { for mt != nil && (mt.Kind() == reflect.Pointer || mt.Kind() == reflect.Slice) {
mt = mt.Elem() mt = mt.Elem()
} }
if mt != nil && mt.Kind() == reflect.Struct { if !h.config.ReadOnly && mt != nil && mt.Kind() == reflect.Struct {
for k := range reflectionJSONColumns(mt) { for k := range reflectionJSONColumns(mt) {
writable[k] = true writable[k] = true
} }
@@ -234,6 +258,9 @@ func (h *Handler) ExportCatalog(path string) error {
func (c Catalog) Markdown() string { func (c Catalog) Markdown() string {
var sb strings.Builder var sb strings.Builder
fmt.Fprintf(&sb, "# %s API catalogue\n\nGenerated %s.\n\n", c.Server, c.GeneratedAt.Format(time.RFC3339)) fmt.Fprintf(&sb, "# %s API catalogue\n\nGenerated %s.\n\n", c.Server, c.GeneratedAt.Format(time.RFC3339))
if c.ReadOnly {
sb.WriteString("**This server is read-only.**\n\n")
}
sb.WriteString("## How to use\n\n" + c.Guide + "\n\n") sb.WriteString("## How to use\n\n" + c.Guide + "\n\n")
fmt.Fprintf(&sb, "## Limits\n\ndefault limit %d, max limit %d, max offset %d, max batch %d, max preload depth %d, max rows per filter write %d.\n\n", fmt.Fprintf(&sb, "## Limits\n\ndefault limit %d, max limit %d, max offset %d, max batch %d, max preload depth %d, max rows per filter write %d.\n\n",
c.Limits.DefaultLimit, c.Limits.MaxLimit, c.Limits.MaxOffset, c.Limits.MaxBatch, c.Limits.MaxPreloadDepth, c.Limits.MaxWriteRows) c.Limits.DefaultLimit, c.Limits.MaxLimit, c.Limits.MaxOffset, c.Limits.MaxBatch, c.Limits.MaxPreloadDepth, c.Limits.MaxWriteRows)
+13 -1
View File
@@ -108,6 +108,15 @@ func (h *Handler) function(name string) (Function, bool) {
return f, ok return f, ok
} }
// functionAllowed reports whether Config.AllowedFunctions lets the function through.
func (h *Handler) functionAllowed(name string) bool {
if h.allowedFns == nil {
return true
}
_, ok := h.allowedFns[name]
return ok
}
// visibleFunctions returns the functions the caller may call, sorted by name. // visibleFunctions returns the functions the caller may call, sorted by name.
func (h *Handler) visibleFunctions(ctx context.Context) []Function { func (h *Handler) visibleFunctions(ctx context.Context) []Function {
h.functions.mu.RLock() h.functions.mu.RLock()
@@ -119,6 +128,9 @@ func (h *Handler) visibleFunctions(ctx context.Context) []Function {
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
visible := out[:0] visible := out[:0]
for _, f := range out { for _, f := range out {
if !h.functionAllowed(f.Name) {
continue
}
if f.Authorize == nil || f.Authorize(ctx) == nil { if f.Authorize == nil || f.Authorize(ctx) == nil {
visible = append(visible, f) visible = append(visible, f)
} }
@@ -236,7 +248,7 @@ func (h *Handler) executeCall(ctx context.Context, name string, rawArgs map[stri
defer cancel() defer cancel()
f, ok := h.function(name) f, ok := h.function(name)
if !ok { if !ok || !h.functionAllowed(name) {
return nil, invalidArg("unknown function %q", truncate(name)) return nil, invalidArg("unknown function %q", truncate(name))
} }
hookCtx := &HookContext{Context: ctx, Handler: h, Entity: name, Operation: "call_function", Tx: h.db} hookCtx := &HookContext{Context: ctx, Handler: h, Entity: name, Operation: "call_function", Tx: h.db}
+9 -2
View File
@@ -24,6 +24,7 @@ import (
// Handler exposes registered database models as MCP tools and resources. // Handler exposes registered database models as MCP tools and resources.
type Handler struct { type Handler struct {
allowedFns map[string]struct{} // nil: every function is allowed
db common.Database db common.Database
registry common.ModelRegistry registry common.ModelRegistry
hooks *HookRegistry hooks *HookRegistry
@@ -43,14 +44,20 @@ func NewHandler(db common.Database, registry common.ModelRegistry, cfg Config) *
db: db, db: db,
registry: registry, registry: registry,
hooks: NewHookRegistry(), hooks: NewHookRegistry(),
mcpServer: server.NewMCPServer("resolvemcp", "1.0.0", server.WithInstructions(usageGuide)), mcpServer: server.NewMCPServer("resolvemcp", "1.0.0", server.WithInstructions(guideFor(cfg.ReadOnly, cfg.AllowFunctionCalls))),
config: cfg.withDefaults(), config: cfg.withDefaults(),
confirms: newConfirmStore(), confirms: newConfirmStore(),
name: "resolvemcp", name: "resolvemcp",
version: "1.0.0", version: "1.0.0",
} }
if len(cfg.AllowedFunctions) > 0 {
h.allowedFns = make(map[string]struct{}, len(cfg.AllowedFunctions))
for _, n := range cfg.AllowedFunctions {
h.allowedFns[n] = struct{}{}
}
}
registerMetaTools(h) registerMetaTools(h)
if cfg.EnableAnnotations { if cfg.EnableAnnotations && !cfg.ReadOnly {
registerAnnotationTool(h) registerAnnotationTool(h)
} }
return h return h
+27 -6
View File
@@ -56,6 +56,16 @@ func registerMetaTools(h *Handler) {
mcp.WithBoolean("include_count", mcp.Description("Also return the total number of matching rows (slower on large tables).")), mcp.WithBoolean("include_count", mcp.Description("Also return the total number of matching rows (slower on large tables).")),
), h.handleSelect) ), h.handleSelect)
if !h.config.ReadOnly {
registerWriteTools(h, tableArg, idArg, filtersArg, dryRunArg, confirmArg)
}
if !h.config.ReadOnly || h.config.AllowFunctionCalls {
registerFunctionTools(h, readOnly)
}
}
// registerWriteTools adds the tools that change table rows.
func registerWriteTools(h *Handler, tableArg, idArg, filtersArg, dryRunArg, confirmArg mcp.ToolOption) {
h.mcpServer.AddTool(mcp.NewTool("insert_into_table", h.mcpServer.AddTool(mcp.NewTool("insert_into_table",
mcp.WithDescription("Insert one row (object) or several rows (array, one transaction, capped). Unknown or read-only fields are rejected."), mcp.WithDescription("Insert one row (object) or several rows (array, one transaction, capped). Unknown or read-only fields are rejected."),
tableArg, mcp.WithObject("data", mcp.Required(), mcp.Description("A row object or an array of row objects.")), tableArg, mcp.WithObject("data", mcp.Required(), mcp.Description("A row object or an array of row objects.")),
@@ -73,7 +83,10 @@ func registerMetaTools(h *Handler) {
mcp.WithDestructiveHintAnnotation(true), mcp.WithDestructiveHintAnnotation(true),
tableArg, idArg, filtersArg, dryRunArg, confirmArg, tableArg, idArg, filtersArg, dryRunArg, confirmArg,
), h.handleDelete) ), h.handleDelete)
}
// registerFunctionTools adds list_functions and call_function.
func registerFunctionTools(h *Handler, readOnly mcp.ToolOption) {
h.mcpServer.AddTool(mcp.NewTool("list_functions", readOnly, h.mcpServer.AddTool(mcp.NewTool("list_functions", readOnly,
mcp.WithDescription("List the functions you can call with call_function, with their parameters.")), mcp.WithDescription("List the functions you can call with call_function, with their parameters.")),
h.handleListFunctions) h.handleListFunctions)
@@ -111,11 +124,15 @@ func (h *Handler) modelRules(schema, entity string) modelregistry.ModelRules {
return modelregistry.DefaultModelRules() return modelregistry.DefaultModelRules()
} }
func opsFor(r modelregistry.ModelRules) []string { // opsFor lists the operations the rules allow. A read-only server allows select only.
func (h *Handler) opsFor(r modelregistry.ModelRules) []string {
var ops []string var ops []string
if r.CanRead { if r.CanRead {
ops = append(ops, opSelect) ops = append(ops, opSelect)
} }
if h.config.ReadOnly {
return ops
}
if r.CanCreate { if r.CanCreate {
ops = append(ops, opInsert) ops = append(ops, opInsert)
} }
@@ -140,9 +157,12 @@ func (h *Handler) resolveTable(args map[string]any, op string) (schema, entity s
if _, err := h.registry.GetModelByEntity(schema, entity); err != nil { if _, err := h.registry.GetModelByEntity(schema, entity); err != nil {
return "", "", invalidArg("unknown table %q; see list_tables", truncate(table)) return "", "", invalidArg("unknown table %q; see list_tables", truncate(table))
} }
if op != "" && op != opSelect && h.config.ReadOnly {
return "", "", NewClientError(CodeForbidden, "this server is read-only: writes are disabled")
}
if op != "" { if op != "" {
allowed := false allowed := false
for _, o := range opsFor(h.modelRules(schema, entity)) { for _, o := range h.opsFor(h.modelRules(schema, entity)) {
if o == op { if o == op {
allowed = true allowed = true
} }
@@ -163,7 +183,7 @@ func (h *Handler) handleListTables(ctx context.Context, _ mcp.CallToolRequest) (
var tables []table var tables []table
for name := range h.registry.GetAllModels() { for name := range h.registry.GetAllModels() {
schema, entity, _ := splitTable(name) schema, entity, _ := splitTable(name)
if ops := opsFor(h.modelRules(schema, entity)); len(ops) > 0 { if ops := h.opsFor(h.modelRules(schema, entity)); len(ops) > 0 {
tables = append(tables, table{Table: name, Description: h.modelDocs(schema, entity).Description, Operations: ops}) tables = append(tables, table{Table: name, Description: h.modelDocs(schema, entity).Description, Operations: ops})
} }
} }
@@ -181,7 +201,7 @@ func (h *Handler) handleDescribeTable(_ context.Context, req mcp.CallToolRequest
return toolError("describe_table", invalidArg("unknown table")), nil return toolError("describe_table", invalidArg("unknown table")), nil
} }
rules := h.modelRules(schema, entity) rules := h.modelRules(schema, entity)
if len(opsFor(rules)) == 0 { if len(h.opsFor(rules)) == 0 {
return toolError("describe_table", invalidArg("unknown table %q; see list_tables", buildModelName(schema, entity))), nil return toolError("describe_table", invalidArg("unknown table %q; see list_tables", buildModelName(schema, entity))), nil
} }
info := buildModelInfo(schema, entity, model) info := buildModelInfo(schema, entity, model)
@@ -192,7 +212,7 @@ func (h *Handler) handleDescribeTable(_ context.Context, req mcp.CallToolRequest
modelType = modelType.Elem() modelType = modelType.Elem()
} }
writable := map[string]bool{} writable := map[string]bool{}
if modelType != nil && modelType.Kind() == reflect.Struct { if !h.config.ReadOnly && modelType != nil && modelType.Kind() == reflect.Struct {
for jsonKey := range reflection.BuildJSONToDBColumnMap(modelType) { for jsonKey := range reflection.BuildJSONToDBColumnMap(modelType) {
writable[jsonKey] = true writable[jsonKey] = true
} }
@@ -230,7 +250,8 @@ func (h *Handler) handleDescribeTable(_ context.Context, req mcp.CallToolRequest
"columns": cols, "columns": cols,
"relations": info.relationNames, "relations": info.relationNames,
"writable_columns": writableNames, "writable_columns": writableNames,
"operations": opsFor(rules), "operations": h.opsFor(rules),
"read_only": h.config.ReadOnly,
"filter_operators": filterOperators, "filter_operators": filterOperators,
"limits": map[string]any{ "limits": map[string]any{
"default_limit": h.config.DefaultLimit, "default_limit": h.config.DefaultLimit,
+149
View File
@@ -0,0 +1,149 @@
package resolvemcp
import (
"context"
"strings"
"testing"
"github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
)
func newReadOnlyHandler(t *testing.T) *Handler {
t.Helper()
h := NewHandler(database.NewPgSQLAdapter(nil), modelregistry.NewModelRegistry(),
Config{ReadOnly: true, EnableAnnotations: true})
if err := h.RegisterModel("public", "items", &docItem{}); err != nil {
t.Fatal(err)
}
return h
}
func TestReadOnlyToolSet(t *testing.T) {
h := newReadOnlyHandler(t)
tools := h.mcpServer.ListTools()
for _, name := range []string{"list_tables", "describe_table", "select_table"} {
if tools[name] == nil {
t.Errorf("read tool %s missing", name)
}
}
for _, name := range []string{"insert_into_table", "update_table", "delete_from_table", "call_function", "list_functions", annotationToolName} {
if tools[name] != nil {
t.Errorf("tool %s must not be registered on a read-only server", name)
}
}
}
func TestReadOnlyRefusesWritesAndReportsIt(t *testing.T) {
h := newReadOnlyHandler(t)
ctx := context.Background()
args := map[string]any{"table": "public.items", "data": map[string]any{"name": "x"}, "id": 1}
for name, fn := range map[string]func() map[string]any{
"insert": func() map[string]any { r, _ := h.handleInsert(ctx, callReq(args)); return payload(t, r) },
"update": func() map[string]any { r, _ := h.handleUpdate(ctx, callReq(args)); return payload(t, r) },
"delete": func() map[string]any { r, _ := h.handleDelete(ctx, callReq(args)); return payload(t, r) },
} {
e, _ := fn()["error"].(map[string]any)
if e["code"] != CodeForbidden || !strings.Contains(e["message"].(string), "read-only") {
t.Errorf("%s: error = %v", name, e)
}
}
res, _ := h.handleListTables(ctx, callReq(nil))
tb := payload(t, res)["tables"].([]any)[0].(map[string]any)
if ops := tb["operations"].([]any); len(ops) != 1 || ops[0] != opSelect {
t.Errorf("list_tables operations = %v", ops)
}
res, _ = h.handleDescribeTable(ctx, callReq(map[string]any{"table": "public.items"}))
p := payload(t, res)
if p["read_only"] != true {
t.Errorf("describe_table read_only = %v", p["read_only"])
}
if w, _ := p["writable_columns"].([]any); len(w) != 0 {
t.Errorf("writable_columns = %v", w)
}
cat := h.BuildCatalog()
if !cat.ReadOnly || !strings.Contains(cat.Guide, "READ-ONLY") || !strings.Contains(cat.Markdown(), "read-only") {
t.Error("catalogue must say the server is read-only")
}
for _, c := range cat.Tables[0].Columns {
if c.Writable {
t.Errorf("column %s marked writable", c.Name)
}
}
if !strings.Contains(guideFor(true, false), "READ-ONLY") || strings.Contains(guideFor(false, false), "READ-ONLY") {
t.Error("guideFor")
}
}
func newFnHandler(t *testing.T, cfg Config) *Handler {
t.Helper()
h := NewHandler(database.NewPgSQLAdapter(nil), modelregistry.NewModelRegistry(), cfg)
for _, name := range []string{"alpha", "beta"} {
name := name
err := h.RegisterFunction(Function{Name: name, Handler: func(context.Context, common.Database, map[string]any) (any, error) {
return name, nil
}})
if err != nil {
t.Fatal(err)
}
}
return h
}
func TestReadOnlyAllowFunctionCalls(t *testing.T) {
h := newFnHandler(t, Config{ReadOnly: true, AllowFunctionCalls: true})
tools := h.mcpServer.ListTools()
if tools["list_functions"] == nil || tools["call_function"] == nil {
t.Error("function tools must be registered")
}
if tools["insert_into_table"] != nil || tools["update_table"] != nil {
t.Error("write tools must stay off")
}
if g := guideFor(true, true); !strings.Contains(g, "READ-ONLY") || !strings.Contains(g, "call_function") {
t.Error("guide must mention functions")
}
if h.mcpServer.ListTools()["call_function"] == nil {
t.Error("call_function missing")
}
}
func TestAllowedFunctions(t *testing.T) {
ctx := context.Background()
for name, tc := range map[string]struct {
allowed []string
visible []string
}{
"empty allows all": {nil, []string{"alpha", "beta"}},
"only listed": {[]string{"beta"}, []string{"beta"}},
"unknown name": {[]string{"zzz"}, nil},
} {
h := newFnHandler(t, Config{AllowedFunctions: tc.allowed})
var got []string
for _, f := range h.visibleFunctions(ctx) {
got = append(got, f.Name)
}
if strings.Join(got, ",") != strings.Join(tc.visible, ",") {
t.Errorf("%s: visible = %v, want %v", name, got, tc.visible)
}
for _, fn := range []string{"alpha", "beta"} {
listed := false
for _, v := range tc.visible {
listed = listed || v == fn
}
if h.functionAllowed(fn) != listed {
t.Errorf("%s: functionAllowed(%s) = %v, want %v", name, fn, !listed, listed)
}
if !listed {
// refused before any database work, and indistinguishable from a missing function
if _, err := h.executeCall(ctx, fn, nil); err == nil || !strings.Contains(err.Error(), "unknown function") {
t.Errorf("%s: %s must be reported unknown, err=%v", name, fn, err)
}
}
}
}
}
+19
View File
@@ -51,6 +51,25 @@ type Config struct {
// host, with at most 32 distinct base URLs cached; prefer setting BaseURL. // host, with at most 32 distinct base URLs cached; prefer setting BaseURL.
AllowedHosts []string AllowedHosts []string
// ReadOnly disables every write. The insert, update, delete and annotation tools are not
// registered, list_tables and describe_table report only the select operation (no
// writable columns), a write attempted anyway is refused with a "forbidden" error, and
// the server instructions tell the agent it cannot write. list_functions/call_function
// are also off, because a registered function may change data, unless AllowFunctionCalls
// is set.
ReadOnly bool
// AllowFunctionCalls keeps list_functions and call_function available on a ReadOnly
// server. Only set it for functions that do not change data; pair it with
// AllowedFunctions to name them. It has no effect when ReadOnly is false (functions are
// always available then).
AllowFunctionCalls bool
// AllowedFunctions restricts list_functions and call_function to the named functions.
// Empty allows every registered function. A function outside the list is reported as
// unknown, so its existence is not revealed.
AllowedFunctions []string
// EnableAnnotations registers the resolvespec_annotate tool. Off by default: annotations // EnableAnnotations registers the resolvespec_annotate tool. Off by default: annotations
// are free text that agents read back, so enabling the tool opens a write channel into // are free text that agents read back, so enabling the tool opens a write channel into
// agent-visible text. When on, every call runs the BeforeHandle hooks (operation // agent-visible text. When on, every call runs the BeforeHandle hooks (operation