libs.ts•16 kB
import flow from 'lodash/flow.js'
import merge from 'lodash/merge.js'
import get from 'lodash/get.js'
import {
createErrorObject,
isErrorObject,
NilAnnotatedFunction,
Response,
} from '@node-in-layers/core'
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
import { JsonAble } from 'functional-models'
import { OpenApiFunctionDescription } from './types.js'
export const isNilAnnotatedFunction = (
fn: any
): fn is NilAnnotatedFunction<any, any> => {
if (fn.schema) {
return true
}
return false
}
const _defOf = (schema: any) => (schema?._zod?.def ?? schema?._def) as any
const _unwrap = (schema: any): any => {
const defAny = _defOf(schema)
const inner =
defAny?.innerType || defAny?.type || defAny?.schema || defAny?.wrapped
return inner && (inner._zod || inner._def) ? _unwrap(inner) : schema
}
const _getDescription = (schema: any, defAny: any): string | undefined => {
return (
(schema && schema.description) ||
defAny?.description ||
defAny?.doc?.description
)
}
// c8 ignore start
const _normalizeType = (t: string | undefined) => {
switch (t) {
case 'ZodString':
return 'string'
case 'ZodNumber':
return 'number'
case 'ZodBoolean':
return 'boolean'
case 'ZodLiteral':
return 'literal'
case 'ZodEnum':
return 'enum'
case 'ZodArray':
return 'array'
case 'ZodRecord':
return 'record'
case 'ZodObject':
return 'object'
case 'ZodUnion':
return 'union'
case 'ZodTuple':
return 'tuple'
case 'ZodUndefined':
return 'undefined'
case 'ZodVoid':
return 'undefined'
// c8 ignore next 3: unused mappings in current paths
// case 'ZodUnknown':
// return 'unknown'
// case 'ZodAny':
// return 'any'
default:
return t
}
}
// c8 ignore stop
// c8 ignore start
const _numberSchema = (defAny: any, desc?: string) => {
const checks = Array.isArray(defAny?.checks) ? defAny.checks : []
const baseType = checks.some((c: any) => c?.kind === 'int')
? 'integer'
: 'number'
const extras = checks.reduce((acc: Record<string, any>, c: any) => {
if (c?.kind === 'min') {
return { ...acc, minimum: c.value }
}
if (c?.kind === 'max') {
return { ...acc, maximum: c.value }
}
if (c?.kind === 'multipleOf') {
return { ...acc, multipleOf: c.value }
}
return acc
}, {})
return { type: baseType, ...(desc ? { description: desc } : {}), ...extras }
}
// c8 ignore stop
// c8 ignore start
const _enumValues = (s: any, defAny: any): readonly any[] => {
const zValues = s?._zod?.values
return zValues && typeof zValues.forEach === 'function'
? Array.from(zValues)
: Array.isArray(defAny?.entries)
? defAny.entries
: defAny?.entries && typeof defAny.entries === 'object'
? Object.values(defAny.entries)
: Array.isArray(defAny?.values)
? defAny.values
: defAny?.values && typeof defAny.values === 'object'
? Object.values(defAny.values)
: Array.isArray(defAny?.options)
? defAny.options
: defAny?.options && typeof defAny.options === 'object'
? Object.values(defAny.options)
: []
}
// c8 ignore stop
// c8 ignore start
const _literalSchema = (defAny: any, desc?: string) => {
const literalValue = Array.isArray(defAny?.values)
? defAny.values[0]
: defAny?.value
const t = typeof literalValue
const jsonType =
t === 'string' || t === 'number' || t === 'boolean' ? t : undefined
return {
...(jsonType ? { type: jsonType } : {}),
const: literalValue,
...(desc ? { description: desc } : {}),
}
}
// c8 ignore stop
const _objectFromShape = (shape: Record<string, any>, desc?: string) => {
const entries = Object.entries(shape || {})
const reduced = entries.reduce(
(acc, [key, field]) => {
const fieldDefAny = _defOf(field) || {}
const fieldType = fieldDefAny?.type || fieldDefAny?.typeName
const isOptional =
fieldType === 'optional' ||
fieldType === 'default' ||
fieldType === 'ZodOptional' ||
fieldType === 'ZodDefault'
const nextProps = { ...acc.properties, [key]: zodToJson(field) }
const nextReq = isOptional ? acc.required : acc.required.concat(key)
return { properties: nextProps, required: nextReq }
},
{ properties: {} as Record<string, any>, required: [] as string[] }
)
return {
type: 'object',
properties: reduced.properties,
additionalProperties: false,
...(desc ? { description: desc } : {}),
...(reduced.required.length > 0 ? { required: reduced.required } : {}),
}
}
const _arrayItems = (defAny: any) =>
defAny?.element ?? defAny?.type ?? defAny?.element
const _recordValueType = (defAny: any) => defAny?.valueType ?? defAny?.value
const _zodToJsonHandlers: Record<
string,
(defAny: any, s: any, desc?: string) => Record<string, any>
> = {
string: (_defAny, _s, desc) => ({
type: 'string',
...(desc ? { description: desc } : {}),
}),
number: (defAny, _s, desc) => _numberSchema(defAny, desc),
boolean: (_defAny, _s, desc) => ({
type: 'boolean',
...(desc ? { description: desc } : {}),
}),
literal: (defAny, _s, desc) => _literalSchema(defAny, desc),
enum: (defAny, s, desc) => ({
type: 'string',
enum: _enumValues(s, defAny),
...(desc ? { description: desc } : {}),
}),
array: (defAny, _s, desc) => {
const item = _arrayItems(defAny)
return {
type: 'array',
items: zodToJson(item),
...(desc ? { description: desc } : {}),
}
},
record: (defAny, _s, desc) => {
const valueType = _recordValueType(defAny)
return {
type: 'object',
additionalProperties: zodToJson(valueType),
...(desc ? { description: desc } : {}),
}
},
object: (defAny, _s, desc) => {
const shapeGetter = defAny?.shape
const shape =
typeof shapeGetter === 'function' ? shapeGetter() : shapeGetter || {}
return _objectFromShape(shape, desc)
},
union: (defAny, _s, desc) => {
const options = (defAny?.options ?? []) as readonly any[]
return {
anyOf: options.map(zodToJson),
...(desc ? { description: desc } : {}),
}
},
// undefined -> OpenAPI null
undefined: (_defAny, _s, desc) => ({
type: 'null',
...(desc ? { description: desc } : {}),
}),
// tuple handler intentionally ignored
// c8 ignore next 7
// tuple: (defAny, _s, desc) => {
// const items = _tupleItems(defAny)
// const arr = Array.isArray(items) ? items : Array.from(items as any)
// return {
// type: 'array',
// prefixItems: arr.map(_zodToJson),
// ...(desc ? { description: desc } : {}),
// }
// },
}
export const zodToJson = (schema: any): Record<string, any> => {
// c8 ignore next 2
if (!schema) {
return {}
}
const original = schema
const s = _unwrap(schema)
const defAny = _defOf(s)
const t = _normalizeType(defAny?.type || defAny?.typeName) || 'unknown'
const desc =
_getDescription(original, _defOf(original)) || _getDescription(s, defAny)
const handler = _zodToJsonHandlers[t]
if (handler) {
return handler(defAny, s, desc)
}
// Map void/undefined schemas to OpenAPI null
if (t === 'undefined' || t === 'void') {
return { type: 'null', ...(desc ? { description: desc } : {}) }
}
// c8 ignore next: fallback defensive return
return {}
}
/**
* CrossLayerProps OpenAPI schema (static):
* {
* logging?: {
* ids?: Array<Record<string,string>>
* }
* }
*/
export const crossLayerPropsOpenApi = (): any => ({
type: 'object',
description:
'CrossLayerProps is an optional argument you can send with NIL MCP tool calls to enable end-to-end tracing across layers (features/services) and across multiple tool invocations. It carries correlation ids that the system logs at each hop so you can stitch together a full execution story.',
additionalProperties: true,
properties: {
logging: {
type: 'object',
additionalProperties: true,
properties: {
ids: {
type: 'array',
items: {
type: 'object',
description:
'Each of these are individual objects, that have a key:id pair. Example: "ids": [{"myId":"123"},{"anotherId":"456"}]',
additionalProperties: { type: 'string' },
},
},
},
},
},
})
/**
* NOTE: Unused breadth-first search fallback for tuple/object discovery. Commented out to avoid
* non-functional patterns (loops/mutation) and because current Zod v4 paths cover our use-cases.
*/
/*
const _findZodNodesByType = (
root: any,
typeName: string,
maxDepth = 6
): any[] => {
return []
}
*/
const _firstDefined = <T>(...vals: readonly (T | undefined)[]) =>
vals.find(v => v !== undefined)
const _getFirstArgSchema = (fnSchema: any): any => {
const def = _defOf(fnSchema)
const inputSchema = def?.input
const tupleDef = _firstDefined(_defOf(inputSchema), inputSchema)
const itemsAny = _firstDefined(
tupleDef?.items,
tupleDef?.elements,
tupleDef?.itemsArray
)
// c8 ignore next 3
const items = Array.isArray(itemsAny)
? itemsAny
: itemsAny
? Array.from(itemsAny)
: undefined
return Array.isArray(items) ? items[0] : undefined
}
export const createOpenApiForNonNilAnnotatedFunction = (name: string) => {
return {
name,
input: {
type: 'object',
additionalProperties: true,
properties: {
args: {
type: 'object',
},
crossLayerProps: crossLayerPropsOpenApi(),
},
required: ['args'],
},
output: {
type: 'object',
additionalProperties: true,
},
}
}
export const nilAnnotatedFunctionToOpenApi = (
name: string,
fn: NilAnnotatedFunction<any, any>
): OpenApiFunctionDescription => {
const schema = fn.schema
const def = _defOf(schema)
const returnsSchema = _firstDefined(
def?.output,
def?.returns,
def?.returnType,
def?.result
)
const argsSchema = _getFirstArgSchema(schema)
const argsJson = argsSchema ? zodToJson(argsSchema) : {}
const outputJson = returnsSchema ? zodToJson(returnsSchema) : {}
const inputObject = {
type: 'object',
additionalProperties: false,
properties: {
args: argsJson,
crossLayerProps: crossLayerPropsOpenApi(),
},
required: ['args'],
}
const output =
outputJson?.type === 'object' && outputJson?.properties
? outputJson.properties
: outputJson
const description: string | undefined =
(schema as any).description ?? _defOf(schema)?.description
return {
name,
...(description ? { description } : {}),
input: inputObject,
output,
}
}
export const createMcpResponse = <T extends JsonAble>(
result: T,
opts?: { isError?: boolean }
): CallToolResult => {
const isError = opts?.isError || isErrorObject(result)
return {
...(isError ? { isError: true } : {}),
content: [
{
type: 'text',
text: JSON.stringify(result !== undefined ? result : '""'),
},
],
}
}
export const createDomainNotFoundError = () =>
createErrorObject('DOMAIN_NOT_FOUND', 'Domain not found')
export const createModelNotFoundError = () =>
createErrorObject('MODEL_NOT_FOUND', 'Model not found')
export const createFeatureNotFoundError = () =>
createErrorObject('FEATURE_NOT_FOUND', 'Feature not found')
export const createModelsNotFoundError = () =>
createErrorObject('MODELS_NOT_FOUND', 'Models not found')
export const isDomainHidden =
(hiddenPaths: Set<string>) => (domain: string) => {
return hiddenPaths.has(domain)
}
export const areAllModelsHidden =
(hiddenPaths: Set<string>) => (domain: string) => {
return hiddenPaths.has(`${domain}.cruds`)
}
export const isFeatureHidden =
(hiddenPaths: Set<string>) => (domain: string, featureName: string) => {
return hiddenPaths.has(`${domain}.${featureName}`)
}
export const isModelHidden =
(hiddenPaths: Set<string>) => (domain: string, modelName: string) => {
return hiddenPaths.has(`${domain}.cruds.${modelName}`)
}
const isMcpResponse = (result: any): boolean => {
if (!result) {
return false
}
const data = get(result, 'content[0].type')
if (data === undefined) {
return false
}
return data === 'text'
}
const _formatResponse = (result: Response<any>): CallToolResult => {
if (isMcpResponse(result)) {
return result
}
if (result !== null && result !== undefined) {
if (isErrorObject(result)) {
return createMcpResponse(result, { isError: true })
}
}
return createMcpResponse(result)
}
export const commonMcpExecute =
(func: (...inputs: any[]) => Promise<Response<any>>) =>
(...inputs: any[]) => {
return func(...inputs)
.then(_formatResponse)
.catch(error => {
return _formatResponse(
createErrorObject(
'UNCAUGHT_EXCEPTION',
'An uncaught exception occurred while executing the feature.',
error
)
)
})
}
export const cleanupSearchQuery = (query: any) => {
const ensureHasQuery = (q: any) => merge({ query: [] }, q)
const isPlainObject = (v: any) =>
v !== null && typeof v === 'object' && !Array.isArray(v)
const inferValueType = (
value: any
): 'string' | 'number' | 'boolean' | 'object' | 'date' => {
if (value instanceof Date) return 'date'
const t = typeof value
if (t === 'string') return 'string'
if (t === 'number') return 'number'
if (t === 'boolean') return 'boolean'
return 'object'
}
const normalizeProperty = (token: any) => {
const valueType = token.valueType || inferValueType(token.value)
const equalitySymbol = token.equalitySymbol || '='
const options = token.options || {}
return {
...token,
type: 'property',
valueType,
equalitySymbol,
options,
}
}
const normalizeDatesAfter = (token: any) => {
const valueType = token.valueType || 'date'
const options = token.options || {}
return {
...token,
type: 'datesAfter',
valueType,
options: {
...options,
...(options.equalToAndAfter === undefined
? { equalToAndAfter: false }
: {}),
},
}
}
const normalizeDatesBefore = (token: any) => {
const valueType = token.valueType || 'date'
const options = token.options || {}
return {
...token,
type: 'datesBefore',
valueType,
options: {
...options,
...(options.equalToAndBefore === undefined
? { equalToAndBefore: false }
: {}),
},
}
}
const normalizeToken = (token: any): any => {
if (token === 'AND' || token === 'OR') return token
if (Array.isArray(token)) return token.map(normalizeToken)
if (isPlainObject(token)) {
if (token.type === 'property') return normalizeProperty(token)
if (token.type === 'datesAfter') return normalizeDatesAfter(token)
if (token.type === 'datesBefore') return normalizeDatesBefore(token)
// Unknown object token, return as-is
return token
}
return token
}
const normalizeQueryTokens = (tokens: any): any => {
if (!tokens) return []
if (Array.isArray(tokens)) return tokens.map(normalizeToken)
return normalizeToken(tokens)
}
const addSortDefaults = (q: any) => {
if (!q.sort) return q
const { sort } = q
if (sort && typeof sort === 'object') {
return {
...q,
sort: {
key: sort.key,
order: sort.order || 'asc',
},
}
}
return q
}
const addSearchDefaults = (q: any) => ({
...q,
page: q.page,
take: q.take,
query: normalizeQueryTokens(q.query),
})
return flow([ensureHasQuery, addSortDefaults, addSearchDefaults])(query)
}