@pikku/openapi-parser 0.12.11 → 0.12.12

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @pikku/openapi-parser
2
2
 
3
+ ## 0.12.12
4
+
5
+ ### Patch Changes
6
+
7
+ - d97f2a1: Always emit a description for generated addon functions (and their MCP tools). When an OpenAPI operation omits both `description` and `summary` (common), the generator now synthesizes one — a humanized `operationId` (with the `Controller` segment stripped), else `METHOD /path` — instead of emitting none. This removes the "MCP tool is missing a description" warnings and makes `--mcp`-exposed tools usable.
8
+
3
9
  ## 0.12.11
4
10
 
5
11
  ### Patch Changes
package/dist/codegen.js CHANGED
@@ -27,12 +27,41 @@ const STATUS_TO_ERROR = {
27
27
  function capitalize(str) {
28
28
  return str.charAt(0).toUpperCase() + str.slice(1);
29
29
  }
30
+ /**
31
+ * Turn an operationId like `contactsControllerGetContacts` (or snake/kebab
32
+ * variants) into a readable phrase — split camel/snake/kebab into words, drop
33
+ * noise segments ("controller"), and capitalize. Returns undefined if nothing
34
+ * usable remains.
35
+ */
36
+ function humanizeOperationId(operationId) {
37
+ if (!operationId)
38
+ return undefined;
39
+ const words = operationId
40
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
41
+ .replace(/[_-]+/g, ' ')
42
+ .split(/\s+/)
43
+ .map((w) => w.trim())
44
+ .filter((w) => w && w.toLowerCase() !== 'controller');
45
+ if (words.length === 0)
46
+ return undefined;
47
+ return capitalize(words.join(' ').toLowerCase());
48
+ }
49
+ /**
50
+ * A description for the generated function (and its MCP tool). Prefers the
51
+ * spec's `description`, then `summary`; when a spec omits both (common), a
52
+ * synthesized description is still emitted so no function — and no MCP tool —
53
+ * ships description-less: a humanized operationId, else "METHOD /path".
54
+ */
30
55
  function humanDescription(parsed) {
31
56
  const description = parsed.description?.trim();
32
57
  if (description && !GENERIC_SUMMARIES.has(description.toLowerCase())) {
33
58
  return capitalize(description);
34
59
  }
35
- return undefined;
60
+ const summary = parsed.summary?.trim();
61
+ if (summary && !GENERIC_SUMMARIES.has(summary.toLowerCase())) {
62
+ return capitalize(summary);
63
+ }
64
+ return humanizeOperationId(parsed.operationId) ?? `${parsed.method.toUpperCase()} ${parsed.path}`;
36
65
  }
37
66
  function getErrorClassesForResponses(errorResponses) {
38
67
  const classes = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/openapi-parser",
3
- "version": "0.12.11",
3
+ "version": "0.12.12",
4
4
  "description": "OpenAPI spec parser and code generator for Pikku",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
@@ -780,3 +780,68 @@ describe('camelCase flag', () => {
780
780
  )
781
781
  })
782
782
  })
783
+
784
+ // ---------------------------------------------------------------------------
785
+ // Function/MCP-tool description synthesis (specs that omit descriptions)
786
+ // ---------------------------------------------------------------------------
787
+ describe('function description synthesis', () => {
788
+ test('synthesizes a description from operationId when the spec omits one', () => {
789
+ const spec = makeSpec({
790
+ operations: [
791
+ makeOp({
792
+ operationId: 'contactsControllerGetContacts',
793
+ method: 'get',
794
+ path: '/contacts',
795
+ }),
796
+ ],
797
+ })
798
+
799
+ const files = generateAddonFromOpenAPI(spec, makeVars(), {
800
+ oauth: false,
801
+ secret: false,
802
+ mcp: true,
803
+ })
804
+ const funcFile = files['src/functions/contactsControllerGetContacts.function.ts']
805
+ assert.ok(funcFile, 'function file should exist')
806
+ assert.ok(
807
+ funcFile.includes('description: "Contacts get contacts"'),
808
+ 'humanized operationId (Controller stripped) should be emitted as the description'
809
+ )
810
+ assert.ok(funcFile.includes('mcp: true'), 'mcp tool flag should be present')
811
+ })
812
+
813
+ test('falls back to "METHOD /path" when operationId is absent', () => {
814
+ const spec = makeSpec({
815
+ operations: [makeOp({ operationId: undefined, method: 'get', path: '/health' })],
816
+ })
817
+
818
+ const files = generateAddonFromOpenAPI(spec, makeVars(), {
819
+ oauth: false,
820
+ secret: false,
821
+ })
822
+ const key = Object.keys(files).find((k) => k.startsWith('src/functions/'))
823
+ assert.ok(key, 'a function file should be generated')
824
+ assert.ok(
825
+ files[key].includes('description: "GET /health"'),
826
+ 'should fall back to METHOD path when nothing else is available'
827
+ )
828
+ })
829
+
830
+ test('prefers the spec description over synthesis', () => {
831
+ const spec = makeSpec({
832
+ operations: [
833
+ makeOp({ operationId: 'getPet', description: 'Fetch a single pet by id' }),
834
+ ],
835
+ })
836
+
837
+ const files = generateAddonFromOpenAPI(spec, makeVars(), {
838
+ oauth: false,
839
+ secret: false,
840
+ })
841
+ const funcFile = files['src/functions/getPet.function.ts']
842
+ assert.ok(
843
+ funcFile.includes('description: "Fetch a single pet by id"'),
844
+ 'the real spec description should win over the synthesized one'
845
+ )
846
+ })
847
+ })
package/src/codegen.ts CHANGED
@@ -69,12 +69,40 @@ function capitalize(str: string): string {
69
69
  return str.charAt(0).toUpperCase() + str.slice(1)
70
70
  }
71
71
 
72
- function humanDescription(parsed: ParsedOperation): string | undefined {
72
+ /**
73
+ * Turn an operationId like `contactsControllerGetContacts` (or snake/kebab
74
+ * variants) into a readable phrase — split camel/snake/kebab into words, drop
75
+ * noise segments ("controller"), and capitalize. Returns undefined if nothing
76
+ * usable remains.
77
+ */
78
+ function humanizeOperationId(operationId?: string): string | undefined {
79
+ if (!operationId) return undefined
80
+ const words = operationId
81
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
82
+ .replace(/[_-]+/g, ' ')
83
+ .split(/\s+/)
84
+ .map((w) => w.trim())
85
+ .filter((w) => w && w.toLowerCase() !== 'controller')
86
+ if (words.length === 0) return undefined
87
+ return capitalize(words.join(' ').toLowerCase())
88
+ }
89
+
90
+ /**
91
+ * A description for the generated function (and its MCP tool). Prefers the
92
+ * spec's `description`, then `summary`; when a spec omits both (common), a
93
+ * synthesized description is still emitted so no function — and no MCP tool —
94
+ * ships description-less: a humanized operationId, else "METHOD /path".
95
+ */
96
+ function humanDescription(parsed: ParsedOperation): string {
73
97
  const description = parsed.description?.trim()
74
98
  if (description && !GENERIC_SUMMARIES.has(description.toLowerCase())) {
75
99
  return capitalize(description)
76
100
  }
77
- return undefined
101
+ const summary = parsed.summary?.trim()
102
+ if (summary && !GENERIC_SUMMARIES.has(summary.toLowerCase())) {
103
+ return capitalize(summary)
104
+ }
105
+ return humanizeOperationId(parsed.operationId) ?? `${parsed.method.toUpperCase()} ${parsed.path}`
78
106
  }
79
107
 
80
108
  function getErrorClassesForResponses(