-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Bug
FlowMCP.prepareServerTool() crashes with _interface.min is not a function when schema parameters have default() or optional() before min()/max()/length()/regex() in the options array.
Affected: callListTools, run, getZodInterfaces — everything that builds Zod schemas.
Not affected: fetch, getAllTests — these don't build Zod schemas.
Root Cause
File: src/task/Interface.mjs — #insertOptions() (line 105)
Options are applied in the order defined in the schema:
options: ["default(20)", "min(1)", "max(100)"]
Chain:
z.number()→ZodNumber(has.min(),.max(),.default()).default(20)→ZodDefault(wrapper — has NO.min()).min(1)→ CRASH_interface.min is not a function
Zod's architecture: .default() and .optional() return wrapper objects (ZodDefault, ZodOptional) that don't have constraint methods (.min(), .max(), .length(), .regex()). Constraints must be applied BEFORE wrappers.
Affected Schemas in flowmcp-schemas
| Schema | Parameter | Broken Options |
|---|---|---|
aave/aave.mjs:17 |
first (getReserves) |
["default(20)", "min(1)", "max(100)"] |
aave/aave.mjs:32 |
first (getUserData) |
["default(10)", "min(1)", "max(50)"] |
web3-career/job-listings.mjs:21 |
limit |
["optional()", "min(1)", "max(100)", "default(50)"] |
Suggested Fix
Sort options before applying: constraints first, wrappers (optional, default) last.
static #insertOptions( { _interface, options } ) {
const wrapperPrefixes = [ 'optional', 'default' ]
const sortedOptions = [ ...options ]
.sort( ( a, b ) => {
const aIsWrapper = wrapperPrefixes
.some( ( prefix ) => a.startsWith( prefix ) )
const bIsWrapper = wrapperPrefixes
.some( ( prefix ) => b.startsWith( prefix ) )
if( aIsWrapper && !bIsWrapper ) { return 1 }
if( !aIsWrapper && bIsWrapper ) { return -1 }
return 0
} )
_interface = sortedOptions
.reduce( ( acc, option ) => {
_interface = Interface
.#insertOption( { _interface, option } )
return _interface
}, _interface )
return _interface
}Effect: ["default(20)", "min(1)", "max(100)"] becomes ["min(1)", "max(100)", "default(20)"]
Additional Bug in #insertOptions
The current reduce has a logic error — acc is never properly updated:
// CURRENT (buggy — acc always returns the ORIGINAL)
_interface = options
.reduce( ( acc, option ) => {
_interface = Interface.#insertOption( { _interface, option } )
return acc // ← always returns original, not updated value
}, _interface )
// FIX — return _interface instead of acc
return _interface // ← return the updated valueWorks by accident due to closure mutation, but is logically incorrect.
Reproduction
# In flowmcp-cli with aave schema in a group:
flowmcp call list-tools
# Shows: error_getReserves_aave with "Error: _interface.min is not a function"
# But: flowmcp test works fine for same routes (no Zod involved in fetch)