@pikku/inspector 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/OPTIMIZATION-PLAN.md +195 -0
  3. package/dist/add/add-ai-agent.d.ts +2 -0
  4. package/dist/add/add-ai-agent.js +314 -0
  5. package/dist/add/add-channel.js +69 -61
  6. package/dist/add/add-cli.js +36 -18
  7. package/dist/add/add-file-with-factory.js +2 -0
  8. package/dist/add/add-functions.js +250 -75
  9. package/dist/add/add-http-route.d.ts +19 -10
  10. package/dist/add/add-http-route.js +152 -66
  11. package/dist/add/add-http-routes.d.ts +5 -0
  12. package/dist/add/add-http-routes.js +159 -0
  13. package/dist/add/add-keyed-wiring.d.ts +12 -0
  14. package/dist/add/add-keyed-wiring.js +97 -0
  15. package/dist/add/add-mcp-prompt.js +14 -9
  16. package/dist/add/add-mcp-resource.js +14 -9
  17. package/dist/add/add-middleware.d.ts +1 -4
  18. package/dist/add/add-middleware.js +364 -79
  19. package/dist/add/add-permission.d.ts +1 -1
  20. package/dist/add/add-permission.js +152 -40
  21. package/dist/add/add-queue-worker.js +18 -12
  22. package/dist/add/add-rpc-invocations.js +14 -0
  23. package/dist/add/add-schedule.js +11 -5
  24. package/dist/add/add-secret.d.ts +3 -0
  25. package/dist/add/add-secret.js +82 -0
  26. package/dist/add/add-trigger.d.ts +2 -0
  27. package/dist/add/add-trigger.js +87 -0
  28. package/dist/add/add-variable.d.ts +1 -0
  29. package/dist/add/add-variable.js +8 -0
  30. package/dist/add/add-workflow-graph.d.ts +3 -2
  31. package/dist/add/add-workflow-graph.js +143 -406
  32. package/dist/add/add-workflow.js +6 -4
  33. package/dist/error-codes.d.ts +14 -1
  34. package/dist/error-codes.js +19 -1
  35. package/dist/index.d.ts +9 -8
  36. package/dist/index.js +5 -4
  37. package/dist/inspector.d.ts +1 -1
  38. package/dist/inspector.js +91 -14
  39. package/dist/schema-generator.d.ts +1 -0
  40. package/dist/schema-generator.js +1 -0
  41. package/dist/types-map.js +10 -1
  42. package/dist/types.d.ts +163 -39
  43. package/dist/utils/compute-required-schemas.d.ts +4 -0
  44. package/dist/utils/compute-required-schemas.js +41 -0
  45. package/dist/utils/contract-hashes.d.ts +35 -0
  46. package/dist/utils/contract-hashes.js +202 -0
  47. package/dist/utils/custom-types-generator.d.ts +9 -0
  48. package/dist/utils/custom-types-generator.js +71 -0
  49. package/dist/utils/detect-schema-vendor.d.ts +22 -0
  50. package/dist/utils/detect-schema-vendor.js +76 -0
  51. package/dist/utils/ensure-function-metadata.d.ts +5 -2
  52. package/dist/utils/ensure-function-metadata.js +220 -6
  53. package/dist/utils/extract-function-name.d.ts +5 -16
  54. package/dist/utils/extract-function-name.js +86 -291
  55. package/dist/utils/extract-services.d.ts +2 -1
  56. package/dist/utils/extract-services.js +25 -1
  57. package/dist/utils/filter-inspector-state.js +107 -23
  58. package/dist/utils/get-property-value.d.ts +6 -1
  59. package/dist/utils/get-property-value.js +28 -3
  60. package/dist/utils/hash.d.ts +2 -0
  61. package/dist/utils/hash.js +23 -0
  62. package/dist/utils/middleware.d.ts +7 -30
  63. package/dist/utils/middleware.js +80 -66
  64. package/dist/utils/permissions.d.ts +2 -2
  65. package/dist/utils/permissions.js +10 -10
  66. package/dist/utils/post-process.d.ts +9 -10
  67. package/dist/utils/post-process.js +231 -24
  68. package/dist/utils/resolve-external-package.d.ts +12 -0
  69. package/dist/utils/resolve-external-package.js +34 -0
  70. package/dist/utils/resolve-function-types.d.ts +6 -0
  71. package/dist/utils/resolve-function-types.js +29 -0
  72. package/dist/utils/resolve-identifier.d.ts +10 -0
  73. package/dist/utils/resolve-identifier.js +36 -0
  74. package/dist/utils/resolve-versions.d.ts +2 -0
  75. package/dist/utils/resolve-versions.js +78 -0
  76. package/dist/utils/schema-generator.d.ts +9 -0
  77. package/dist/utils/schema-generator.js +209 -0
  78. package/dist/utils/serialize-inspector-state.d.ts +59 -22
  79. package/dist/utils/serialize-inspector-state.js +92 -20
  80. package/dist/utils/serialize-mcp-json.d.ts +2 -0
  81. package/dist/utils/serialize-mcp-json.js +99 -0
  82. package/dist/utils/serialize-middleware-groups-meta.d.ts +12 -0
  83. package/dist/utils/serialize-middleware-groups-meta.js +28 -0
  84. package/dist/utils/serialize-openapi-json.d.ts +85 -0
  85. package/dist/utils/serialize-openapi-json.js +151 -0
  86. package/dist/utils/serialize-permissions-groups-meta.d.ts +6 -0
  87. package/dist/utils/serialize-permissions-groups-meta.js +31 -0
  88. package/dist/utils/workflow/dsl/deserialize-dsl-workflow.js +34 -102
  89. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +23 -4
  90. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +12 -10
  91. package/dist/utils/workflow/graph/finalize-workflow-wires.d.ts +3 -0
  92. package/dist/utils/workflow/graph/finalize-workflow-wires.js +276 -0
  93. package/dist/utils/workflow/graph/finalize-workflows.d.ts +2 -0
  94. package/dist/utils/workflow/graph/finalize-workflows.js +75 -0
  95. package/dist/utils/workflow/graph/index.d.ts +2 -0
  96. package/dist/utils/workflow/graph/index.js +2 -0
  97. package/dist/utils/workflow/graph/serialize-workflow-graph.d.ts +0 -8
  98. package/dist/utils/workflow/graph/serialize-workflow-graph.js +1 -3
  99. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +53 -79
  100. package/dist/utils/workflow/graph/workflow-graph.types.js +1 -1
  101. package/dist/visit.js +11 -6
  102. package/package.json +14 -4
  103. package/src/add/add-ai-agent.ts +468 -0
  104. package/src/add/add-channel.ts +82 -79
  105. package/src/add/add-cli.ts +49 -20
  106. package/src/add/add-file-with-factory.ts +2 -0
  107. package/src/add/add-functions.ts +330 -86
  108. package/src/add/add-http-route.ts +245 -88
  109. package/src/add/add-http-routes.ts +228 -0
  110. package/src/add/add-keyed-wiring.ts +151 -0
  111. package/src/add/add-mcp-prompt.ts +26 -15
  112. package/src/add/add-mcp-resource.ts +27 -15
  113. package/src/add/add-middleware.ts +482 -80
  114. package/src/add/add-permission.ts +199 -40
  115. package/src/add/add-queue-worker.ts +24 -19
  116. package/src/add/add-rpc-invocations.ts +17 -0
  117. package/src/add/add-schedule.ts +16 -11
  118. package/src/add/add-secret.ts +140 -0
  119. package/src/add/add-trigger.ts +154 -0
  120. package/src/add/add-variable.ts +9 -0
  121. package/src/add/add-workflow-graph.ts +180 -522
  122. package/src/add/add-workflow.ts +5 -4
  123. package/src/error-codes.ts +24 -1
  124. package/src/index.ts +22 -13
  125. package/src/inspector.ts +129 -17
  126. package/src/schema-generator.ts +1 -0
  127. package/src/types-map.ts +12 -1
  128. package/src/types.ts +175 -58
  129. package/src/utils/compute-required-schemas.ts +49 -0
  130. package/src/utils/contract-hashes.test.ts +528 -0
  131. package/src/utils/contract-hashes.ts +290 -0
  132. package/src/utils/custom-types-generator.ts +88 -0
  133. package/src/utils/detect-schema-vendor.ts +90 -0
  134. package/src/utils/ensure-function-metadata.ts +324 -7
  135. package/src/utils/extract-function-name.ts +101 -351
  136. package/src/utils/extract-services.ts +35 -2
  137. package/src/utils/filter-inspector-state.test.ts +34 -20
  138. package/src/utils/filter-inspector-state.ts +140 -31
  139. package/src/utils/get-property-value.ts +42 -4
  140. package/src/utils/hash.ts +26 -0
  141. package/src/utils/middleware.test.ts +204 -0
  142. package/src/utils/middleware.ts +129 -67
  143. package/src/utils/permissions.test.ts +35 -12
  144. package/src/utils/permissions.ts +10 -10
  145. package/src/utils/post-process.ts +283 -43
  146. package/src/utils/resolve-external-package.ts +42 -0
  147. package/src/utils/resolve-function-types.ts +42 -0
  148. package/src/utils/resolve-identifier.ts +46 -0
  149. package/src/utils/resolve-versions.test.ts +249 -0
  150. package/src/utils/resolve-versions.ts +105 -0
  151. package/src/utils/schema-generator.ts +329 -0
  152. package/src/utils/serialize-inspector-state.ts +163 -40
  153. package/src/utils/serialize-mcp-json.ts +145 -0
  154. package/src/utils/serialize-middleware-groups-meta.ts +33 -0
  155. package/src/utils/serialize-openapi-json.ts +277 -0
  156. package/src/utils/serialize-permissions-groups-meta.ts +35 -0
  157. package/src/utils/test-data/inspector-state.json +69 -66
  158. package/src/utils/workflow/dsl/deserialize-dsl-workflow.ts +43 -119
  159. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +24 -4
  160. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +17 -10
  161. package/src/utils/workflow/graph/finalize-workflow-wires.ts +310 -0
  162. package/src/utils/workflow/graph/finalize-workflows.ts +100 -0
  163. package/src/utils/workflow/graph/index.ts +5 -0
  164. package/src/utils/workflow/graph/serialize-workflow-graph.ts +1 -8
  165. package/src/utils/workflow/graph/workflow-graph.types.ts +29 -78
  166. package/src/visit.ts +12 -6
  167. package/tsconfig.tsbuildinfo +1 -1
  168. package/dist/add/add-forge-credential.d.ts +0 -8
  169. package/dist/add/add-forge-credential.js +0 -77
  170. package/dist/add/add-forge-node.d.ts +0 -7
  171. package/dist/add/add-forge-node.js +0 -77
  172. package/dist/add/add-mcp-tool.d.ts +0 -2
  173. package/dist/add/add-mcp-tool.js +0 -81
  174. package/dist/utils/extract-service-metadata.d.ts +0 -19
  175. package/dist/utils/extract-service-metadata.js +0 -244
  176. package/dist/utils/write-service-metadata.d.ts +0 -13
  177. package/dist/utils/write-service-metadata.js +0 -37
  178. package/src/add/add-forge-credential.ts +0 -119
  179. package/src/add/add-forge-node.ts +0 -132
  180. package/src/add/add-mcp-tool.ts +0 -141
  181. package/src/utils/extract-service-metadata.ts +0 -353
  182. package/src/utils/write-service-metadata.ts +0 -51
@@ -1,16 +1,23 @@
1
1
  import * as ts from 'typescript'
2
- import { AddWiring } from '../types.js'
2
+ import { AddWiring, SchemaRef } from '../types.js'
3
+ import { detectSchemaVendorOrError } from '../utils/detect-schema-vendor.js'
3
4
  import { TypesMap } from '../types-map.js'
4
- import { extractFunctionName } from '../utils/extract-function-name.js'
5
+ import {
6
+ extractFunctionName,
7
+ funcIdToTypeName,
8
+ } from '../utils/extract-function-name.js'
5
9
  import { extractFunctionNode } from '../utils/extract-function-node.js'
6
- import { FunctionServicesMeta } from '@pikku/core'
10
+ import { extractUsedWires } from '../utils/extract-services.js'
11
+ import { FunctionServicesMeta, formatVersionedId } from '@pikku/core'
7
12
  import {
8
13
  getPropertyValue,
9
14
  getCommonWireMetaData,
10
15
  } from '../utils/get-property-value.js'
11
16
  import { resolveMiddleware } from '../utils/middleware.js'
12
17
  import { resolvePermissions } from '../utils/permissions.js'
18
+ import { extractWireNames } from '../utils/post-process.js'
13
19
  import { ErrorCode } from '../error-codes.js'
20
+ import type { NodeType } from '@pikku/core/node'
14
21
 
15
22
  const isValidVariableName = (name: string) => {
16
23
  const regex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
@@ -22,7 +29,8 @@ const nullifyTypes = (type: string | null) => {
22
29
  type === 'void' ||
23
30
  type === 'undefined' ||
24
31
  type === 'unknown' ||
25
- type === 'any'
32
+ type === 'any' ||
33
+ type === 'null'
26
34
  ) {
27
35
  return null
28
36
  }
@@ -186,8 +194,8 @@ const getNamesAndTypes = (
186
194
  return { names: [], types: [] }
187
195
  }
188
196
 
189
- // 1) Handle an explicit void (or undefined) type up front
190
- if (type.flags & ts.TypeFlags.VoidLike) {
197
+ // 1) Handle an explicit void (or undefined or null) type up front
198
+ if (type.flags & (ts.TypeFlags.VoidLike | ts.TypeFlags.Null)) {
191
199
  return {
192
200
  names: [],
193
201
  types: [],
@@ -205,8 +213,7 @@ const getNamesAndTypes = (
205
213
  const firstName = rawNames[0]
206
214
  if (rawNames.length > 1 || (firstName && !isValidVariableName(firstName))) {
207
215
  const aliasType = rawNames.join(' | ')
208
- const aliasName =
209
- funcName.charAt(0).toUpperCase() + funcName.slice(1) + direction
216
+ const aliasName = funcIdToTypeName(funcName) + direction
210
217
 
211
218
  // record the alias in your TypesMap
212
219
  const references = rawTypes
@@ -252,7 +259,6 @@ const isPrimitiveType = (type: ts.Type): boolean => {
252
259
  ts.TypeFlags.Void |
253
260
  ts.TypeFlags.Undefined |
254
261
  ts.TypeFlags.Null |
255
- ts.TypeFlags.Any |
256
262
  ts.TypeFlags.Unknown |
257
263
  ts.TypeFlags.VoidLike
258
264
 
@@ -286,7 +292,13 @@ function unwrapPromise(checker: ts.TypeChecker, type: ts.Type): ts.Type {
286
292
  * Inspect pikkuFunc calls, extract input/output and first-arg destructuring,
287
293
  * then push into state.functions.meta.
288
294
  */
289
- export const addFunctions: AddWiring = (logger, node, checker, state) => {
295
+ export const addFunctions: AddWiring = (
296
+ logger,
297
+ node,
298
+ checker,
299
+ state,
300
+ options
301
+ ) => {
290
302
  if (!ts.isCallExpression(node)) return
291
303
 
292
304
  const { expression, arguments: args, typeArguments } = node
@@ -309,8 +321,15 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
309
321
 
310
322
  if (args.length === 0) return
311
323
 
312
- const { pikkuFuncName, name, explicitName, exportedName } =
313
- extractFunctionName(node, checker, state.rootDir)
324
+ let { pikkuFuncId, name, explicitName, exportedName } = extractFunctionName(
325
+ node,
326
+ checker,
327
+ state.rootDir
328
+ )
329
+
330
+ if (!pikkuFuncId || pikkuFuncId.startsWith('__temp_')) {
331
+ return
332
+ }
314
333
 
315
334
  let title: string | undefined
316
335
  let tags: string[] | undefined
@@ -318,8 +337,15 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
318
337
  let description: string | undefined
319
338
  let errors: string[] | undefined
320
339
  let expose: boolean | undefined
321
- let internal: boolean | undefined
340
+ let remote: boolean | undefined
341
+ let mcp: boolean | undefined
342
+ let requiresApproval: boolean | undefined
343
+ let version: number | undefined
322
344
  let objectNode: ts.ObjectLiteralExpression | undefined
345
+ let nodeDisplayName: string | null = null
346
+ let nodeCategory: string | null = null
347
+ let nodeType: NodeType | null = null
348
+ let nodeErrorOutput: boolean | null = null
323
349
 
324
350
  // Extract the function node using shared utility
325
351
  const firstArg = args[0]!
@@ -329,22 +355,24 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
329
355
  isDirectFunction,
330
356
  } = extractFunctionNode(firstArg, checker)
331
357
 
332
- // Variables to hold zod schema references if provided
333
- let inputZodSchemaRef: { variableName: string; sourceFile: string } | null =
334
- null
335
- let outputZodSchemaRef: { variableName: string; sourceFile: string } | null =
336
- null
358
+ // Variables to hold schema references if provided
359
+ let inputSchemaRef: SchemaRef | null = null
360
+ let outputSchemaRef: SchemaRef | null = null
337
361
 
338
- // Helper to resolve schema identifier to its actual source file
339
- const resolveSchemaSourceFile = (
340
- identifier: ts.Identifier
341
- ): { variableName: string; sourceFile: string } | null => {
362
+ // Helper to resolve schema identifier to its actual source file and detect vendor.
363
+ // Logs a fatal error and returns null if vendor cannot be determined.
364
+ const resolveSchemaRef = (
365
+ identifier: ts.Identifier,
366
+ context: string
367
+ ): SchemaRef | null => {
342
368
  const symbol = checker.getSymbolAtLocation(identifier)
343
369
  if (!symbol) return null
344
370
 
345
371
  const decl = symbol.valueDeclaration || symbol.declarations?.[0]
346
372
  if (!decl) return null
347
373
 
374
+ let sourceFile: string
375
+
348
376
  // If it's an import specifier, resolve the aliased symbol to get the actual source
349
377
  if (ts.isImportSpecifier(decl)) {
350
378
  const aliasedSymbol = checker.getAliasedSymbol(symbol)
@@ -352,18 +380,30 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
352
380
  const aliasedDecl =
353
381
  aliasedSymbol.valueDeclaration || aliasedSymbol.declarations?.[0]
354
382
  if (aliasedDecl) {
355
- return {
356
- variableName: identifier.text,
357
- sourceFile: aliasedDecl.getSourceFile().fileName,
358
- }
383
+ sourceFile = aliasedDecl.getSourceFile().fileName
384
+ } else {
385
+ return null
359
386
  }
387
+ } else {
388
+ return null
360
389
  }
390
+ } else {
391
+ sourceFile = decl.getSourceFile().fileName
361
392
  }
362
393
 
363
- // Not an import - use the current source file
394
+ const vendor = detectSchemaVendorOrError(
395
+ identifier,
396
+ checker,
397
+ logger,
398
+ context,
399
+ sourceFile
400
+ )
401
+ if (!vendor) return null
402
+
364
403
  return {
365
404
  variableName: identifier.text,
366
- sourceFile: decl.getSourceFile().fileName,
405
+ sourceFile,
406
+ vendor,
367
407
  }
368
408
  }
369
409
 
@@ -371,35 +411,94 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
371
411
  if (ts.isObjectLiteralExpression(firstArg)) {
372
412
  objectNode = firstArg
373
413
  const metadata = getCommonWireMetaData(firstArg, 'Function', name, logger)
414
+ if (metadata.disabled) return
374
415
  title = metadata.title
375
416
  tags = metadata.tags
376
417
  summary = metadata.summary
377
418
  description = metadata.description
378
419
  errors = metadata.errors
379
420
  expose = getPropertyValue(firstArg, 'expose') as boolean | undefined
380
- internal = getPropertyValue(firstArg, 'internal') as boolean | undefined
421
+ remote = getPropertyValue(firstArg, 'remote') as boolean | undefined
422
+ mcp = getPropertyValue(firstArg, 'mcp') as boolean | undefined
423
+ requiresApproval = getPropertyValue(firstArg, 'requiresApproval') as
424
+ | boolean
425
+ | undefined
426
+
427
+ const versionRaw = getPropertyValue(firstArg, 'version')
428
+ if (versionRaw !== null && versionRaw !== undefined) {
429
+ const parsed = Number(versionRaw)
430
+ if (Number.isInteger(parsed) && parsed >= 1) {
431
+ version = parsed
432
+ }
433
+ }
434
+
435
+ // Extract node config from nested object
436
+ for (const prop of firstArg.properties) {
437
+ if (
438
+ ts.isPropertyAssignment(prop) &&
439
+ ts.isIdentifier(prop.name) &&
440
+ prop.name.text === 'node' &&
441
+ ts.isObjectLiteralExpression(prop.initializer)
442
+ ) {
443
+ const nodeObj = prop.initializer
444
+ nodeDisplayName = getPropertyValue(nodeObj, 'displayName') as
445
+ | string
446
+ | null
447
+ nodeCategory = getPropertyValue(nodeObj, 'category') as string | null
448
+ nodeType = getPropertyValue(nodeObj, 'type') as NodeType | null
449
+ nodeErrorOutput = getPropertyValue(nodeObj, 'errorOutput') as
450
+ | boolean
451
+ | null
452
+
453
+ if (!nodeDisplayName) {
454
+ logger.critical(
455
+ ErrorCode.MISSING_NAME,
456
+ `Function '${name}' node config is missing the required 'displayName' property.`
457
+ )
458
+ }
459
+ if (!nodeCategory) {
460
+ logger.critical(
461
+ ErrorCode.MISSING_NAME,
462
+ `Function '${name}' node config is missing the required 'category' property.`
463
+ )
464
+ }
465
+ if (!nodeType) {
466
+ logger.critical(
467
+ ErrorCode.MISSING_NAME,
468
+ `Function '${name}' node config is missing the required 'type' property.`
469
+ )
470
+ } else if (!['trigger', 'action', 'end'].includes(nodeType)) {
471
+ logger.critical(
472
+ ErrorCode.INVALID_VALUE,
473
+ `Function '${name}' node config has invalid type '${nodeType}'. Must be 'trigger', 'action', or 'end'.`
474
+ )
475
+ }
476
+ break
477
+ }
478
+ }
381
479
 
382
- // Extract zod schema variable names from input/output properties
480
+ // Extract schema variable names from input/output properties
383
481
  for (const prop of firstArg.properties) {
384
482
  if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
385
483
  const propName = prop.name.text
386
484
  if (propName === 'input' || propName === 'output') {
387
485
  if (ts.isIdentifier(prop.initializer)) {
388
- // Good - it's a variable reference, resolve its actual source file
389
- const ref = resolveSchemaSourceFile(prop.initializer)
486
+ // Good - it's a variable reference, resolve its actual source file and vendor
487
+ const context = `Function '${name}' ${propName}`
488
+ const ref = resolveSchemaRef(prop.initializer, context)
390
489
  if (ref) {
391
490
  if (propName === 'input') {
392
- inputZodSchemaRef = ref
491
+ inputSchemaRef = ref
393
492
  } else {
394
- outputZodSchemaRef = ref
493
+ outputSchemaRef = ref
395
494
  }
396
495
  }
397
496
  } else if (ts.isCallExpression(prop.initializer)) {
398
497
  // Bad - it's an inline expression
399
- const schemaName = `${name.charAt(0).toUpperCase() + name.slice(1)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`
498
+ const schemaName = `${funcIdToTypeName(name)}${propName.charAt(0).toUpperCase() + propName.slice(1)}`
400
499
  logger.critical(
401
- ErrorCode.INLINE_ZOD_SCHEMA,
402
- `Inline Zod schemas are not supported for '${propName}' in '${name}'.\n` +
500
+ ErrorCode.INLINE_SCHEMA,
501
+ `Inline schemas are not supported for '${propName}' in '${name}'.\n` +
403
502
  ` Extract to an exported variable:\n` +
404
503
  ` export const ${schemaName} = ${prop.initializer.getText()}\n` +
405
504
  ` Then use: ${propName}: ${schemaName}`
@@ -410,6 +509,14 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
410
509
  }
411
510
  }
412
511
 
512
+ if (version !== undefined) {
513
+ const baseName = explicitName || exportedName || pikkuFuncId
514
+ pikkuFuncId = formatVersionedId(baseName, version)
515
+ }
516
+
517
+ const isMCPToolFunc = expression.text === 'pikkuMCPToolFunc'
518
+ const mcpEnabled = mcp || isMCPToolFunc
519
+
413
520
  // Pick the handler: use resolvedFunc when it exists and is a function, otherwise fall back to handlerNode
414
521
  const handler =
415
522
  resolvedFunc &&
@@ -419,10 +526,11 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
419
526
 
420
527
  // Validate that we got a valid function
421
528
  if (!ts.isArrowFunction(handler) && !ts.isFunctionExpression(handler)) {
422
- logger.error(`• No valid 'func' property found for ${pikkuFuncName}.`)
529
+ logger.error(`• No valid 'func' property found for ${pikkuFuncId}.`)
423
530
  // Create stub metadata to prevent "function not found" errors in wirings
424
- state.functions.meta[pikkuFuncName] = {
425
- pikkuFuncName,
531
+ state.functions.meta[pikkuFuncId] = {
532
+ pikkuFuncId,
533
+ functionType: 'user',
426
534
  name,
427
535
  services: { optimized: false, services: [] },
428
536
  inputSchemaName: null,
@@ -453,43 +561,31 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
453
561
  services.services.push(original)
454
562
  }
455
563
  }
456
- } else if (ts.isIdentifier(firstParam.name)) {
564
+ } else if (
565
+ ts.isIdentifier(firstParam.name) &&
566
+ !firstParam.name.text.startsWith('_')
567
+ ) {
457
568
  services.optimized = false
458
569
  }
459
570
  }
460
571
 
461
- // --- Extract used wires from third parameter ---
462
- const usedWires: string[] = []
463
- const thirdParam = handler.parameters[2]
464
- if (thirdParam && ts.isObjectBindingPattern(thirdParam.name)) {
465
- for (const elem of thirdParam.name.elements) {
466
- const propertyName =
467
- elem.propertyName && ts.isIdentifier(elem.propertyName)
468
- ? elem.propertyName.text
469
- : ts.isIdentifier(elem.name)
470
- ? elem.name.text
471
- : undefined
472
- if (propertyName) {
473
- usedWires.push(propertyName)
474
- }
475
- }
476
- }
572
+ const wires = extractUsedWires(handler, 2)
477
573
 
478
574
  // --- Generics → ts.Type[], unwrapped from Promise ---
479
575
  const genericTypes: ts.Type[] = (typeArguments ?? [])
480
576
  .map((tn) => checker.getTypeFromTypeNode(tn))
481
577
  .map((t) => unwrapPromise(checker, t))
482
578
 
483
- const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1)
579
+ const capitalizedName = funcIdToTypeName(name)
484
580
 
485
581
  // --- Input Extraction ---
486
582
  let inputNames: string[] = []
487
583
  let inputTypes: ts.Type[] = []
488
584
 
489
- if (inputZodSchemaRef) {
585
+ if (inputSchemaRef) {
490
586
  const schemaName = `${capitalizedName}Input`
491
587
  inputNames = [schemaName]
492
- state.zodLookup.set(schemaName, inputZodSchemaRef)
588
+ state.schemaLookup.set(schemaName, inputSchemaRef)
493
589
  state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
494
590
  } else if (genericTypes.length >= 1 && genericTypes[0]) {
495
591
  // Fall back to extracting from generic type arguments
@@ -511,7 +607,7 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
511
607
  checker,
512
608
  state.functions.typesMap,
513
609
  'Input',
514
- pikkuFuncName,
610
+ pikkuFuncId,
515
611
  paramType
516
612
  )
517
613
  inputNames = result.names
@@ -522,10 +618,10 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
522
618
  // --- Output Extraction ---
523
619
  let outputNames: string[] = []
524
620
 
525
- if (outputZodSchemaRef) {
621
+ if (outputSchemaRef) {
526
622
  const schemaName = `${capitalizedName}Output`
527
623
  outputNames = [schemaName]
528
- state.zodLookup.set(schemaName, outputZodSchemaRef)
624
+ state.schemaLookup.set(schemaName, outputSchemaRef)
529
625
  state.functions.typesMap.addCustomType(schemaName, 'unknown', [])
530
626
  } else if (genericTypes.length >= 2) {
531
627
  outputNames = getNamesAndTypes(
@@ -544,12 +640,55 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
544
640
  checker,
545
641
  state.functions.typesMap,
546
642
  'Output',
547
- pikkuFuncName,
643
+ pikkuFuncId,
548
644
  unwrapped
549
645
  ).names
550
646
  }
551
647
  }
552
648
 
649
+ const mcpOutputTypes: Record<string, string> = {
650
+ pikkuMCPResourceFunc: 'MCPResourceResponse',
651
+ pikkuMCPToolFunc: 'MCPToolResponse',
652
+ pikkuMCPPromptFunc: 'MCPPromptResponse',
653
+ }
654
+ const mcpOutputType = mcpOutputTypes[expression.text]
655
+ if (mcpOutputType && outputNames[0] !== mcpOutputType) {
656
+ let resolved = false
657
+ const rawSymbol = checker.getSymbolAtLocation(expression)
658
+ const funcSymbol =
659
+ rawSymbol && rawSymbol.flags & ts.SymbolFlags.Alias
660
+ ? checker.getAliasedSymbol(rawSymbol)
661
+ : rawSymbol
662
+ const funcDecls = funcSymbol?.getDeclarations() || []
663
+ for (const funcDecl of funcDecls) {
664
+ if (resolved) break
665
+ const mcpTypeSymbol = checker.resolveName(
666
+ mcpOutputType,
667
+ funcDecl,
668
+ ts.SymbolFlags.Type,
669
+ false
670
+ )
671
+ if (mcpTypeSymbol) {
672
+ const aliased =
673
+ mcpTypeSymbol.flags & ts.SymbolFlags.Alias
674
+ ? checker.getAliasedSymbol(mcpTypeSymbol)
675
+ : mcpTypeSymbol
676
+ const typeDecl = aliased?.getDeclarations()?.[0]
677
+ if (typeDecl) {
678
+ const path = typeDecl.getSourceFile().fileName
679
+ if (!state.functions.typesMap.exists(mcpOutputType, path)) {
680
+ state.functions.typesMap.addType(mcpOutputType, path)
681
+ }
682
+ resolved = true
683
+ }
684
+ }
685
+ }
686
+ if (!resolved) {
687
+ state.functions.typesMap.addCustomType(mcpOutputType, mcpOutputType, [])
688
+ }
689
+ outputNames = [mcpOutputType]
690
+ }
691
+
553
692
  if (inputNames.length > 1) {
554
693
  logger.warn(
555
694
  'More than one input type detected, only the first one will be used as a schema.'
@@ -558,30 +697,68 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
558
697
 
559
698
  // Store the input type for later use
560
699
  if (inputTypes.length > 0) {
561
- state.typesLookup.set(pikkuFuncName, inputTypes)
700
+ state.typesLookup.set(pikkuFuncId, inputTypes)
562
701
  }
563
702
 
564
703
  // --- resolve middleware ---
565
- const middleware = objectNode
704
+ let middleware = objectNode
566
705
  ? resolveMiddleware(state, objectNode, tags, checker)
567
706
  : undefined
568
707
 
569
708
  // --- resolve permissions ---
570
- const permissions = objectNode
709
+ let permissions = objectNode
571
710
  ? resolvePermissions(state, objectNode, tags, checker)
572
711
  : undefined
573
712
 
574
- state.functions.meta[pikkuFuncName] = {
575
- pikkuFuncName,
713
+ if (options.tags?.length) {
714
+ tags = [...new Set([...(tags || []), ...options.tags])]
715
+ const tagEntries = options.tags.map((tag) => ({
716
+ type: 'tag' as const,
717
+ tag,
718
+ }))
719
+ const existingMiddlewareTags = new Set(
720
+ (middleware || [])
721
+ .filter((m) => m.type === 'tag')
722
+ .map((m) => (m as any).tag)
723
+ )
724
+ const newMiddleware = tagEntries.filter(
725
+ (e) => !existingMiddlewareTags.has(e.tag)
726
+ )
727
+ if (newMiddleware.length > 0) {
728
+ middleware = [...(middleware || []), ...newMiddleware]
729
+ }
730
+ const existingPermissionTags = new Set(
731
+ (permissions || [])
732
+ .filter((p) => p.type === 'tag')
733
+ .map((p) => (p as any).tag)
734
+ )
735
+ const newPermissions = tagEntries.filter(
736
+ (e) => !existingPermissionTags.has(e.tag)
737
+ )
738
+ if (newPermissions.length > 0) {
739
+ permissions = [...(permissions || []), ...newPermissions]
740
+ }
741
+ }
742
+
743
+ const sessionless = expression.text !== 'pikkuFunc'
744
+
745
+ state.functions.meta[pikkuFuncId] = {
746
+ pikkuFuncId,
747
+ functionType: 'user',
748
+ funcWrapper: expression.text,
749
+ sessionless,
576
750
  name,
577
751
  services,
578
- usedWires: usedWires.length > 0 ? usedWires : undefined,
752
+ wires: wires.wires.length > 0 || !wires.optimized ? wires : undefined,
579
753
  inputSchemaName: inputNames[0] ?? null,
580
754
  outputSchemaName: outputNames[0] ?? null,
581
755
  inputs: inputNames.filter((n) => n !== 'void') ?? null,
582
756
  outputs: outputNames.filter((n) => n !== 'void') ?? null,
583
757
  expose: expose || undefined,
584
- internal: internal || undefined,
758
+ remote: remote || undefined,
759
+ mcp: mcpEnabled || undefined,
760
+ requiresApproval: requiresApproval || undefined,
761
+ version,
585
762
  title,
586
763
  tags: tags || undefined,
587
764
  summary,
@@ -592,20 +769,78 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
592
769
  isDirectFunction,
593
770
  }
594
771
 
595
- // Store function file location for wiring generation
596
- if (exportedName) {
597
- state.functions.files.set(pikkuFuncName, {
598
- path: node.getSourceFile().fileName,
599
- exportedName,
600
- })
772
+ // Populate node metadata if node config is present
773
+ if (nodeDisplayName && nodeCategory && nodeType) {
774
+ state.nodes.files.add(node.getSourceFile().fileName)
775
+ state.nodes.meta[pikkuFuncId] = {
776
+ name: pikkuFuncId,
777
+ displayName: nodeDisplayName,
778
+ category: nodeCategory,
779
+ type: nodeType,
780
+ rpc: pikkuFuncId,
781
+ description,
782
+ errorOutput: nodeErrorOutput ?? false,
783
+ inputSchemaName: inputNames[0] ?? null,
784
+ outputSchemaName: outputNames[0] ?? null,
785
+ tags,
786
+ }
787
+ }
788
+
789
+ if (mcpEnabled) {
790
+ if (!description) {
791
+ logger.critical(
792
+ ErrorCode.MISSING_DESCRIPTION,
793
+ `MCP tool '${name}' is missing a description.`
794
+ )
795
+ return
796
+ }
797
+ state.mcpEndpoints.files.add(node.getSourceFile().fileName)
798
+ state.mcpEndpoints.toolsMeta[name] = {
799
+ pikkuFuncId,
800
+ name,
801
+ title: title || undefined,
802
+ description,
803
+ summary,
804
+ errors,
805
+ tags,
806
+ inputSchema: inputNames[0] ?? null,
807
+ outputSchema: outputNames[0] ?? null,
808
+ middleware,
809
+ permissions,
810
+ }
811
+ state.serviceAggregation.usedFunctions.add(pikkuFuncId)
812
+ extractWireNames(middleware).forEach((n) =>
813
+ state.serviceAggregation.usedMiddleware.add(n)
814
+ )
815
+ extractWireNames(permissions).forEach((n) =>
816
+ state.serviceAggregation.usedPermissions.add(n)
817
+ )
601
818
  }
602
819
 
603
820
  // Workflow functions don't get registered as RPC functions,
604
- // they are their own type handled by add-workdflow
821
+ // they are their own type handled by add-workflow
605
822
  if (expression.text.includes('Workflow')) {
606
823
  return
607
824
  }
608
825
 
826
+ // Trigger and channel connect/disconnect functions are not callable via RPC
827
+ const nonRPCPatterns = [
828
+ /Trigger/i,
829
+ /ChannelConnection/i,
830
+ /ChannelDisconnection/i,
831
+ ]
832
+ if (nonRPCPatterns.some((pattern) => pattern.test(expression.text))) {
833
+ return
834
+ }
835
+
836
+ // Store function file location for wiring generation
837
+ if (exportedName) {
838
+ state.functions.files.set(pikkuFuncId, {
839
+ path: node.getSourceFile().fileName,
840
+ exportedName,
841
+ })
842
+ }
843
+
609
844
  if (exportedName || explicitName) {
610
845
  if (!exportedName) {
611
846
  logger.error(
@@ -614,28 +849,37 @@ export const addFunctions: AddWiring = (logger, node, checker, state) => {
614
849
  return
615
850
  }
616
851
 
617
- // Mark internal functions as invoked to force bundling
618
- if (internal) {
619
- state.rpc.invokedFunctions.add(pikkuFuncName)
852
+ if (remote) {
853
+ state.rpc.invokedFunctions.add(pikkuFuncId)
620
854
  }
621
855
 
622
856
  if (expose) {
623
- state.rpc.exposedMeta[name] = pikkuFuncName
857
+ state.rpc.exposedMeta[name] = pikkuFuncId
624
858
  state.rpc.exposedFiles.set(name, {
625
859
  path: node.getSourceFile().fileName,
626
860
  exportedName,
627
861
  })
628
862
  // Track exposed RPC function for service aggregation
629
- state.serviceAggregation.usedFunctions.add(pikkuFuncName)
863
+ state.serviceAggregation.usedFunctions.add(pikkuFuncId)
630
864
  }
631
865
 
632
866
  // We add it to internal meta to allow autocomplete for everything
633
- state.rpc.internalMeta[name] = pikkuFuncName
867
+ state.rpc.internalMeta[name] = pikkuFuncId
868
+
869
+ if (version !== undefined) {
870
+ state.rpc.internalMeta[pikkuFuncId] = pikkuFuncId
871
+ state.rpc.invokedFunctions.add(pikkuFuncId)
872
+ }
634
873
 
635
874
  // But we only import the actual function if it's actually invoked to keep
636
875
  // bundle size down
637
- if (state.rpc.invokedFunctions.has(pikkuFuncName) || expose || internal) {
638
- state.rpc.internalFiles.set(pikkuFuncName, {
876
+ if (
877
+ state.rpc.invokedFunctions.has(pikkuFuncId) ||
878
+ expose ||
879
+ remote ||
880
+ mcpEnabled
881
+ ) {
882
+ state.rpc.internalFiles.set(pikkuFuncId, {
639
883
  path: node.getSourceFile().fileName,
640
884
  exportedName,
641
885
  })