truss-api-mcp 1.0.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +89 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/lib/code-generator.d.ts +6 -0
  8. package/dist/lib/code-generator.d.ts.map +1 -0
  9. package/dist/lib/code-generator.js +890 -0
  10. package/dist/lib/code-generator.js.map +1 -0
  11. package/dist/lib/http-client.d.ts +6 -0
  12. package/dist/lib/http-client.d.ts.map +1 -0
  13. package/dist/lib/http-client.js +76 -0
  14. package/dist/lib/http-client.js.map +1 -0
  15. package/dist/lib/license.d.ts +4 -0
  16. package/dist/lib/license.d.ts.map +1 -0
  17. package/dist/lib/license.js +97 -0
  18. package/dist/lib/license.js.map +1 -0
  19. package/dist/lib/openapi-parser.d.ts +11 -0
  20. package/dist/lib/openapi-parser.d.ts.map +1 -0
  21. package/dist/lib/openapi-parser.js +390 -0
  22. package/dist/lib/openapi-parser.js.map +1 -0
  23. package/dist/lib/schema-validator.d.ts +15 -0
  24. package/dist/lib/schema-validator.d.ts.map +1 -0
  25. package/dist/lib/schema-validator.js +206 -0
  26. package/dist/lib/schema-validator.js.map +1 -0
  27. package/dist/tools/compare-specs.d.ts +3 -0
  28. package/dist/tools/compare-specs.d.ts.map +1 -0
  29. package/dist/tools/compare-specs.js +59 -0
  30. package/dist/tools/compare-specs.js.map +1 -0
  31. package/dist/tools/generate-client.d.ts +3 -0
  32. package/dist/tools/generate-client.d.ts.map +1 -0
  33. package/dist/tools/generate-client.js +65 -0
  34. package/dist/tools/generate-client.js.map +1 -0
  35. package/dist/tools/generate-openapi.d.ts +3 -0
  36. package/dist/tools/generate-openapi.d.ts.map +1 -0
  37. package/dist/tools/generate-openapi.js +57 -0
  38. package/dist/tools/generate-openapi.js.map +1 -0
  39. package/dist/tools/generate-tests.d.ts +3 -0
  40. package/dist/tools/generate-tests.d.ts.map +1 -0
  41. package/dist/tools/generate-tests.js +59 -0
  42. package/dist/tools/generate-tests.js.map +1 -0
  43. package/dist/tools/mock-server.d.ts +3 -0
  44. package/dist/tools/mock-server.d.ts.map +1 -0
  45. package/dist/tools/mock-server.js +60 -0
  46. package/dist/tools/mock-server.js.map +1 -0
  47. package/dist/tools/parse-openapi.d.ts +3 -0
  48. package/dist/tools/parse-openapi.d.ts.map +1 -0
  49. package/dist/tools/parse-openapi.js +48 -0
  50. package/dist/tools/parse-openapi.js.map +1 -0
  51. package/dist/tools/test-endpoint.d.ts +3 -0
  52. package/dist/tools/test-endpoint.d.ts.map +1 -0
  53. package/dist/tools/test-endpoint.js +66 -0
  54. package/dist/tools/test-endpoint.js.map +1 -0
  55. package/dist/tools/validate-response.d.ts +3 -0
  56. package/dist/tools/validate-response.d.ts.map +1 -0
  57. package/dist/tools/validate-response.js +44 -0
  58. package/dist/tools/validate-response.js.map +1 -0
  59. package/dist/types.d.ts +121 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +3 -0
  62. package/dist/types.js.map +1 -0
  63. package/evals/eval-http.ts +163 -0
  64. package/evals/eval-openapi.ts +506 -0
  65. package/evals/run-evals.ts +29 -0
  66. package/glama.json +4 -0
  67. package/package.json +37 -0
  68. package/smithery.yaml +9 -0
  69. package/src/index.ts +110 -0
  70. package/src/lib/code-generator.ts +1045 -0
  71. package/src/lib/http-client.ts +87 -0
  72. package/src/lib/license.ts +121 -0
  73. package/src/lib/openapi-parser.ts +456 -0
  74. package/src/lib/schema-validator.ts +234 -0
  75. package/src/tools/compare-specs.ts +67 -0
  76. package/src/tools/generate-client.ts +75 -0
  77. package/src/tools/generate-openapi.ts +67 -0
  78. package/src/tools/generate-tests.ts +69 -0
  79. package/src/tools/mock-server.ts +68 -0
  80. package/src/tools/parse-openapi.ts +54 -0
  81. package/src/tools/test-endpoint.ts +71 -0
  82. package/src/tools/validate-response.ts +54 -0
  83. package/src/types.ts +156 -0
  84. package/tsconfig.json +19 -0
@@ -0,0 +1,890 @@
1
+ // ── Client Code Generation ──────────────────────────────────────────
2
+ export function generateClient(spec, language, style = 'fetch') {
3
+ switch (language) {
4
+ case 'typescript':
5
+ return generateTypeScriptClient(spec, style);
6
+ case 'python':
7
+ return generatePythonClient(spec);
8
+ case 'go':
9
+ return generateGoClient(spec);
10
+ }
11
+ }
12
+ // ── TypeScript Client ───────────────────────────────────────────────
13
+ function generateTypeScriptClient(spec, style) {
14
+ const baseUrl = spec.servers?.[0]?.url ?? 'http://localhost:3000';
15
+ const lines = [];
16
+ lines.push(`// Auto-generated API client for ${spec.info.title} v${spec.info.version}`);
17
+ lines.push(`// Generated by TRUSS API MCP`);
18
+ lines.push('');
19
+ if (style === 'axios') {
20
+ lines.push(`import axios, { type AxiosInstance, type AxiosResponse } from 'axios';`);
21
+ lines.push('');
22
+ }
23
+ // Generate interfaces from endpoints
24
+ const interfaces = generateTypeScriptInterfaces(spec);
25
+ if (interfaces) {
26
+ lines.push(interfaces);
27
+ lines.push('');
28
+ }
29
+ // Client class
30
+ lines.push(`export class ApiClient {`);
31
+ lines.push(` private baseUrl: string;`);
32
+ if (style === 'axios') {
33
+ lines.push(` private client: AxiosInstance;`);
34
+ }
35
+ else {
36
+ lines.push(` private headers: Record<string, string>;`);
37
+ }
38
+ lines.push('');
39
+ lines.push(` constructor(baseUrl = '${baseUrl}', headers: Record<string, string> = {}) {`);
40
+ lines.push(` this.baseUrl = baseUrl.replace(/\\/$/, '');`);
41
+ if (style === 'axios') {
42
+ lines.push(` this.client = axios.create({`);
43
+ lines.push(` baseURL: this.baseUrl,`);
44
+ lines.push(` headers: { 'Content-Type': 'application/json', ...headers },`);
45
+ lines.push(` });`);
46
+ }
47
+ else {
48
+ lines.push(` this.headers = { 'Content-Type': 'application/json', ...headers };`);
49
+ }
50
+ lines.push(` }`);
51
+ lines.push('');
52
+ // Generate methods
53
+ for (const endpoint of spec.endpoints) {
54
+ const methodName = endpointToMethodName(endpoint);
55
+ const hasBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
56
+ const pathParams = endpoint.parameters.filter((p) => p.in === 'path');
57
+ const queryParams = endpoint.parameters.filter((p) => p.in === 'query');
58
+ // Build parameter list
59
+ const params = [];
60
+ for (const pp of pathParams) {
61
+ params.push(`${pp.name}: ${tsType(pp.schema)}`);
62
+ }
63
+ if (hasBody) {
64
+ params.push(`body: ${tsBodyType(endpoint.requestBody)}`);
65
+ }
66
+ if (queryParams.length > 0) {
67
+ const queryType = queryParams
68
+ .map((q) => `${q.name}${q.required ? '' : '?'}: ${tsType(q.schema)}`)
69
+ .join('; ');
70
+ params.push(`query?: { ${queryType} }`);
71
+ }
72
+ // JSDoc
73
+ if (endpoint.summary) {
74
+ lines.push(` /** ${endpoint.summary} */`);
75
+ }
76
+ const returnType = tsResponseType(endpoint);
77
+ if (style === 'axios') {
78
+ lines.push(` async ${methodName}(${params.join(', ')}): Promise<AxiosResponse<${returnType}>> {`);
79
+ const urlExpr = buildUrlExpr(endpoint, 'this.baseUrl');
80
+ const queryExpr = queryParams.length > 0 ? ', params: query' : '';
81
+ if (hasBody) {
82
+ lines.push(` return this.client.${endpoint.method.toLowerCase()}(${urlExpr}, body${queryExpr ? `, { ${queryExpr.slice(2)} }` : ''});`);
83
+ }
84
+ else {
85
+ lines.push(` return this.client.${endpoint.method.toLowerCase()}(${urlExpr}${queryExpr ? `, { ${queryExpr.slice(2)} }` : ''});`);
86
+ }
87
+ }
88
+ else {
89
+ lines.push(` async ${methodName}(${params.join(', ')}): Promise<${returnType}> {`);
90
+ const urlExpr = buildUrlExpr(endpoint, 'this.baseUrl');
91
+ if (queryParams.length > 0) {
92
+ lines.push(` const url = new URL(${urlExpr});`);
93
+ lines.push(` if (query) {`);
94
+ lines.push(` for (const [k, v] of Object.entries(query)) {`);
95
+ lines.push(` if (v !== undefined) url.searchParams.set(k, String(v));`);
96
+ lines.push(` }`);
97
+ lines.push(` }`);
98
+ lines.push(` const response = await fetch(url.toString(), {`);
99
+ }
100
+ else {
101
+ lines.push(` const response = await fetch(${urlExpr}, {`);
102
+ }
103
+ lines.push(` method: '${endpoint.method}',`);
104
+ lines.push(` headers: this.headers,`);
105
+ if (hasBody) {
106
+ lines.push(` body: JSON.stringify(body),`);
107
+ }
108
+ lines.push(` });`);
109
+ lines.push(` if (!response.ok) {`);
110
+ lines.push(` throw new Error(\`HTTP \${response.status}: \${response.statusText}\`);`);
111
+ lines.push(` }`);
112
+ lines.push(` return response.json() as Promise<${returnType}>;`);
113
+ }
114
+ lines.push(` }`);
115
+ lines.push('');
116
+ }
117
+ lines.push(`}`);
118
+ return lines.join('\n');
119
+ }
120
+ function generateTypeScriptInterfaces(spec) {
121
+ const lines = [];
122
+ const seen = new Set();
123
+ for (const endpoint of spec.endpoints) {
124
+ // Generate request body interface
125
+ if (endpoint.requestBody?.properties) {
126
+ const name = pascalCase(endpointToMethodName(endpoint)) + 'Request';
127
+ if (!seen.has(name)) {
128
+ seen.add(name);
129
+ lines.push(schemaToTsInterface(name, endpoint.requestBody));
130
+ lines.push('');
131
+ }
132
+ }
133
+ // Generate response interface
134
+ const okResponse = endpoint.responses.find((r) => r.status === '200' || r.status === '201');
135
+ if (okResponse?.schema?.properties) {
136
+ const name = pascalCase(endpointToMethodName(endpoint)) + 'Response';
137
+ if (!seen.has(name)) {
138
+ seen.add(name);
139
+ lines.push(schemaToTsInterface(name, okResponse.schema));
140
+ lines.push('');
141
+ }
142
+ }
143
+ }
144
+ return lines.join('\n');
145
+ }
146
+ function schemaToTsInterface(name, schema) {
147
+ const lines = [`export interface ${name} {`];
148
+ if (schema.properties) {
149
+ const required = new Set(schema.required ?? []);
150
+ for (const [key, prop] of Object.entries(schema.properties)) {
151
+ const optional = required.has(key) ? '' : '?';
152
+ lines.push(` ${key}${optional}: ${tsType(prop)};`);
153
+ }
154
+ }
155
+ lines.push('}');
156
+ return lines.join('\n');
157
+ }
158
+ function tsType(schema) {
159
+ if (!schema)
160
+ return 'unknown';
161
+ if (schema.enum) {
162
+ return schema.enum.map((v) => JSON.stringify(v)).join(' | ');
163
+ }
164
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
165
+ switch (type) {
166
+ case 'string':
167
+ return 'string';
168
+ case 'integer':
169
+ case 'number':
170
+ return 'number';
171
+ case 'boolean':
172
+ return 'boolean';
173
+ case 'array':
174
+ return `${tsType(schema.items)}[]`;
175
+ case 'object':
176
+ if (schema.properties) {
177
+ const props = Object.entries(schema.properties)
178
+ .map(([k, v]) => `${k}: ${tsType(v)}`)
179
+ .join('; ');
180
+ return `{ ${props} }`;
181
+ }
182
+ return 'Record<string, unknown>';
183
+ default:
184
+ return 'unknown';
185
+ }
186
+ }
187
+ function tsBodyType(schema) {
188
+ if (!schema)
189
+ return 'Record<string, unknown>';
190
+ return tsType(schema);
191
+ }
192
+ function tsResponseType(endpoint) {
193
+ const okResp = endpoint.responses.find((r) => r.status === '200' || r.status === '201');
194
+ if (okResp?.schema)
195
+ return tsType(okResp.schema);
196
+ return 'unknown';
197
+ }
198
+ // ── Python Client ───────────────────────────────────────────────────
199
+ function generatePythonClient(spec) {
200
+ const baseUrl = spec.servers?.[0]?.url ?? 'http://localhost:3000';
201
+ const lines = [];
202
+ lines.push(`"""Auto-generated API client for ${spec.info.title} v${spec.info.version}"""`);
203
+ lines.push(`# Generated by TRUSS API MCP`);
204
+ lines.push('');
205
+ lines.push('from __future__ import annotations');
206
+ lines.push('');
207
+ lines.push('from dataclasses import dataclass');
208
+ lines.push('from typing import Any, Optional');
209
+ lines.push('');
210
+ lines.push('import requests');
211
+ lines.push('');
212
+ lines.push('');
213
+ // Dataclasses for request/response types
214
+ for (const endpoint of spec.endpoints) {
215
+ if (endpoint.requestBody?.properties) {
216
+ const className = snakeToPascal(endpointToMethodName(endpoint)) + 'Request';
217
+ lines.push(`@dataclass`);
218
+ lines.push(`class ${className}:`);
219
+ for (const [key, prop] of Object.entries(endpoint.requestBody.properties)) {
220
+ lines.push(` ${key}: ${pyType(prop)}`);
221
+ }
222
+ lines.push('');
223
+ lines.push('');
224
+ }
225
+ }
226
+ lines.push(`class ApiClient:`);
227
+ lines.push(` """API client for ${spec.info.title}"""`);
228
+ lines.push('');
229
+ lines.push(` def __init__(self, base_url: str = "${baseUrl}", headers: Optional[dict[str, str]] = None):`);
230
+ lines.push(` self.base_url = base_url.rstrip("/")`);
231
+ lines.push(` self.session = requests.Session()`);
232
+ lines.push(` self.session.headers.update({"Content-Type": "application/json"})`);
233
+ lines.push(` if headers:`);
234
+ lines.push(` self.session.headers.update(headers)`);
235
+ lines.push('');
236
+ for (const endpoint of spec.endpoints) {
237
+ const methodName = camelToSnake(endpointToMethodName(endpoint));
238
+ const hasBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
239
+ const pathParams = endpoint.parameters.filter((p) => p.in === 'path');
240
+ const queryParams = endpoint.parameters.filter((p) => p.in === 'query');
241
+ const params = ['self'];
242
+ for (const pp of pathParams) {
243
+ params.push(`${pp.name}: ${pyType(pp.schema)}`);
244
+ }
245
+ if (hasBody) {
246
+ params.push(`body: dict[str, Any]`);
247
+ }
248
+ if (queryParams.length > 0) {
249
+ params.push(`params: Optional[dict[str, Any]] = None`);
250
+ }
251
+ const docstring = endpoint.summary ? ` """${endpoint.summary}"""` : '';
252
+ lines.push(` def ${methodName}(${params.join(', ')}) -> requests.Response:`);
253
+ if (docstring)
254
+ lines.push(docstring);
255
+ // Build URL
256
+ let urlPath = endpoint.path;
257
+ for (const pp of pathParams) {
258
+ urlPath = urlPath.replace(`{${pp.name}}`, `{${pp.name}}`);
259
+ }
260
+ lines.push(` url = f"{self.base_url}${urlPath}"`);
261
+ const kwargs = [];
262
+ if (hasBody)
263
+ kwargs.push('json=body');
264
+ if (queryParams.length > 0)
265
+ kwargs.push('params=params');
266
+ lines.push(` response = self.session.${endpoint.method.toLowerCase()}(url${kwargs.length ? ', ' + kwargs.join(', ') : ''})`);
267
+ lines.push(` response.raise_for_status()`);
268
+ lines.push(` return response`);
269
+ lines.push('');
270
+ }
271
+ return lines.join('\n');
272
+ }
273
+ function pyType(schema) {
274
+ if (!schema)
275
+ return 'Any';
276
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
277
+ switch (type) {
278
+ case 'string':
279
+ return 'str';
280
+ case 'integer':
281
+ return 'int';
282
+ case 'number':
283
+ return 'float';
284
+ case 'boolean':
285
+ return 'bool';
286
+ case 'array':
287
+ return `list[${pyType(schema.items)}]`;
288
+ case 'object':
289
+ return 'dict[str, Any]';
290
+ default:
291
+ return 'Any';
292
+ }
293
+ }
294
+ // ── Go Client ───────────────────────────────────────────────────────
295
+ function generateGoClient(spec) {
296
+ const baseUrl = spec.servers?.[0]?.url ?? 'http://localhost:3000';
297
+ const lines = [];
298
+ lines.push(`// Auto-generated API client for ${spec.info.title} v${spec.info.version}`);
299
+ lines.push(`// Generated by TRUSS API MCP`);
300
+ lines.push('');
301
+ lines.push('package apiclient');
302
+ lines.push('');
303
+ lines.push('import (');
304
+ lines.push('\t"bytes"');
305
+ lines.push('\t"encoding/json"');
306
+ lines.push('\t"fmt"');
307
+ lines.push('\t"io"');
308
+ lines.push('\t"net/http"');
309
+ lines.push('\t"net/url"');
310
+ lines.push(')');
311
+ lines.push('');
312
+ // Generate structs for request/response types
313
+ for (const endpoint of spec.endpoints) {
314
+ if (endpoint.requestBody?.properties) {
315
+ const structName = pascalCase(endpointToMethodName(endpoint)) + 'Request';
316
+ lines.push(schemaToGoStruct(structName, endpoint.requestBody));
317
+ lines.push('');
318
+ }
319
+ const okResp = endpoint.responses.find((r) => r.status === '200' || r.status === '201');
320
+ if (okResp?.schema?.properties) {
321
+ const structName = pascalCase(endpointToMethodName(endpoint)) + 'Response';
322
+ lines.push(schemaToGoStruct(structName, okResp.schema));
323
+ lines.push('');
324
+ }
325
+ }
326
+ // Client struct
327
+ lines.push('// Client is the API client.');
328
+ lines.push('type Client struct {');
329
+ lines.push('\tBaseURL string');
330
+ lines.push('\tHTTPClient *http.Client');
331
+ lines.push('\tHeaders map[string]string');
332
+ lines.push('}');
333
+ lines.push('');
334
+ // Constructor
335
+ lines.push('// NewClient creates a new API client.');
336
+ lines.push(`func NewClient(baseURL string) *Client {`);
337
+ lines.push('\tif baseURL == "" {');
338
+ lines.push(`\t\tbaseURL = "${baseUrl}"`);
339
+ lines.push('\t}');
340
+ lines.push('\treturn &Client{');
341
+ lines.push('\t\tBaseURL: baseURL,');
342
+ lines.push('\t\tHTTPClient: &http.Client{},');
343
+ lines.push('\t\tHeaders: map[string]string{');
344
+ lines.push('\t\t\t"Content-Type": "application/json",');
345
+ lines.push('\t\t},');
346
+ lines.push('\t}');
347
+ lines.push('}');
348
+ lines.push('');
349
+ // Helper method
350
+ lines.push('func (c *Client) do(method, path string, body interface{}, result interface{}) error {');
351
+ lines.push('\tvar bodyReader io.Reader');
352
+ lines.push('\tif body != nil {');
353
+ lines.push('\t\tdata, err := json.Marshal(body)');
354
+ lines.push('\t\tif err != nil {');
355
+ lines.push('\t\t\treturn fmt.Errorf("marshal request: %w", err)');
356
+ lines.push('\t\t}');
357
+ lines.push('\t\tbodyReader = bytes.NewReader(data)');
358
+ lines.push('\t}');
359
+ lines.push('');
360
+ lines.push('\treq, err := http.NewRequest(method, c.BaseURL+path, bodyReader)');
361
+ lines.push('\tif err != nil {');
362
+ lines.push('\t\treturn fmt.Errorf("create request: %w", err)');
363
+ lines.push('\t}');
364
+ lines.push('\tfor k, v := range c.Headers {');
365
+ lines.push('\t\treq.Header.Set(k, v)');
366
+ lines.push('\t}');
367
+ lines.push('');
368
+ lines.push('\tresp, err := c.HTTPClient.Do(req)');
369
+ lines.push('\tif err != nil {');
370
+ lines.push('\t\treturn fmt.Errorf("execute request: %w", err)');
371
+ lines.push('\t}');
372
+ lines.push('\tdefer resp.Body.Close()');
373
+ lines.push('');
374
+ lines.push('\tif resp.StatusCode >= 400 {');
375
+ lines.push('\t\treturn fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status)');
376
+ lines.push('\t}');
377
+ lines.push('');
378
+ lines.push('\tif result != nil {');
379
+ lines.push('\t\tif err := json.NewDecoder(resp.Body).Decode(result); err != nil {');
380
+ lines.push('\t\t\treturn fmt.Errorf("decode response: %w", err)');
381
+ lines.push('\t\t}');
382
+ lines.push('\t}');
383
+ lines.push('\treturn nil');
384
+ lines.push('}');
385
+ lines.push('');
386
+ // Generate methods
387
+ for (const endpoint of spec.endpoints) {
388
+ const methodName = pascalCase(endpointToMethodName(endpoint));
389
+ const hasBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
390
+ const pathParams = endpoint.parameters.filter((p) => p.in === 'path');
391
+ const queryParams = endpoint.parameters.filter((p) => p.in === 'query');
392
+ // Build parameters
393
+ const goParams = [];
394
+ for (const pp of pathParams) {
395
+ goParams.push(`${pp.name} ${goType(pp.schema)}`);
396
+ }
397
+ if (hasBody) {
398
+ const bodyStruct = pascalCase(endpointToMethodName(endpoint)) + 'Request';
399
+ const hasStruct = endpoint.requestBody?.properties;
400
+ goParams.push(`body ${hasStruct ? '*' + bodyStruct : 'interface{}'}`);
401
+ }
402
+ if (queryParams.length > 0) {
403
+ goParams.push(`params map[string]string`);
404
+ }
405
+ const okResp = endpoint.responses.find((r) => r.status === '200' || r.status === '201');
406
+ const hasResponseStruct = okResp?.schema?.properties;
407
+ const respType = hasResponseStruct
408
+ ? '*' + pascalCase(endpointToMethodName(endpoint)) + 'Response'
409
+ : 'error';
410
+ const returnType = hasResponseStruct ? `(${respType}, error)` : 'error';
411
+ if (endpoint.summary) {
412
+ lines.push(`// ${methodName} ${endpoint.summary}`);
413
+ }
414
+ lines.push(`func (c *Client) ${methodName}(${goParams.join(', ')}) ${returnType} {`);
415
+ // Build URL path with substitutions
416
+ let urlPath = endpoint.path;
417
+ for (const pp of pathParams) {
418
+ urlPath = urlPath.replace(`{${pp.name}}`, `" + fmt.Sprintf("%v", ${pp.name}) + "`);
419
+ }
420
+ // Clean up empty concatenations
421
+ urlPath = urlPath.replace(/\+ ""$/, '').replace(/"" \+ /, '');
422
+ if (queryParams.length > 0) {
423
+ lines.push(`\tpath := "${urlPath}"`);
424
+ lines.push(`\tif len(params) > 0 {`);
425
+ lines.push(`\t\tv := url.Values{}`);
426
+ lines.push(`\t\tfor k, val := range params {`);
427
+ lines.push(`\t\t\tv.Set(k, val)`);
428
+ lines.push(`\t\t}`);
429
+ lines.push(`\t\tpath += "?" + v.Encode()`);
430
+ lines.push(`\t}`);
431
+ if (hasResponseStruct) {
432
+ lines.push(`\tvar result ${pascalCase(endpointToMethodName(endpoint))}Response`);
433
+ lines.push(`\terr := c.do("${endpoint.method}", path, ${hasBody ? 'body' : 'nil'}, &result)`);
434
+ lines.push(`\treturn &result, err`);
435
+ }
436
+ else {
437
+ lines.push(`\treturn c.do("${endpoint.method}", path, ${hasBody ? 'body' : 'nil'}, nil)`);
438
+ }
439
+ }
440
+ else {
441
+ if (hasResponseStruct) {
442
+ lines.push(`\tvar result ${pascalCase(endpointToMethodName(endpoint))}Response`);
443
+ lines.push(`\terr := c.do("${endpoint.method}", "${urlPath}", ${hasBody ? 'body' : 'nil'}, &result)`);
444
+ lines.push(`\treturn &result, err`);
445
+ }
446
+ else {
447
+ lines.push(`\treturn c.do("${endpoint.method}", "${urlPath}", ${hasBody ? 'body' : 'nil'}, nil)`);
448
+ }
449
+ }
450
+ lines.push('}');
451
+ lines.push('');
452
+ }
453
+ return lines.join('\n');
454
+ }
455
+ function schemaToGoStruct(name, schema) {
456
+ const lines = [`type ${name} struct {`];
457
+ if (schema.properties) {
458
+ for (const [key, prop] of Object.entries(schema.properties)) {
459
+ const fieldName = pascalCase(key);
460
+ const tag = `\`json:"${key}"\``;
461
+ lines.push(`\t${fieldName} ${goType(prop)} ${tag}`);
462
+ }
463
+ }
464
+ lines.push('}');
465
+ return lines.join('\n');
466
+ }
467
+ function goType(schema) {
468
+ if (!schema)
469
+ return 'interface{}';
470
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
471
+ switch (type) {
472
+ case 'string':
473
+ return 'string';
474
+ case 'integer':
475
+ return 'int64';
476
+ case 'number':
477
+ return 'float64';
478
+ case 'boolean':
479
+ return 'bool';
480
+ case 'array':
481
+ return `[]${goType(schema.items)}`;
482
+ case 'object':
483
+ return 'map[string]interface{}';
484
+ default:
485
+ return 'interface{}';
486
+ }
487
+ }
488
+ // ── Test Generation ─────────────────────────────────────────────────
489
+ export function generateTests(spec, baseUrl, framework = 'vitest') {
490
+ switch (framework) {
491
+ case 'jest':
492
+ case 'vitest':
493
+ return generateJsTests(spec, baseUrl, framework);
494
+ case 'pytest':
495
+ return generatePytestTests(spec, baseUrl);
496
+ }
497
+ }
498
+ function generateJsTests(spec, baseUrl, framework) {
499
+ const lines = [];
500
+ lines.push(`// Auto-generated API tests for ${spec.info.title}`);
501
+ lines.push(`// Generated by TRUSS API MCP`);
502
+ lines.push('');
503
+ if (framework === 'vitest') {
504
+ lines.push(`import { describe, it, expect } from 'vitest';`);
505
+ }
506
+ lines.push('');
507
+ lines.push(`const BASE_URL = '${baseUrl}';`);
508
+ lines.push('');
509
+ // Group by tags or paths
510
+ const groups = groupEndpoints(spec.endpoints);
511
+ for (const [groupName, endpoints] of groups) {
512
+ lines.push(`describe('${groupName}', () => {`);
513
+ for (const endpoint of endpoints) {
514
+ const testName = `${endpoint.method} ${endpoint.path}`;
515
+ const hasBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
516
+ // Happy path test
517
+ lines.push(` it('${testName} — returns success', async () => {`);
518
+ // Build URL with sample path params
519
+ let testUrl = endpoint.path;
520
+ for (const param of endpoint.parameters.filter((p) => p.in === 'path')) {
521
+ testUrl = testUrl.replace(`{${param.name}}`, sampleValue(param.schema, param.name));
522
+ }
523
+ // Add query params
524
+ const queryParams = endpoint.parameters.filter((p) => p.in === 'query' && p.required);
525
+ if (queryParams.length > 0) {
526
+ const qs = queryParams
527
+ .map((q) => `${q.name}=${encodeURIComponent(sampleValue(q.schema, q.name))}`)
528
+ .join('&');
529
+ testUrl += `?${qs}`;
530
+ }
531
+ lines.push(` const response = await fetch(\`\${BASE_URL}${testUrl}\`, {`);
532
+ lines.push(` method: '${endpoint.method}',`);
533
+ lines.push(` headers: { 'Content-Type': 'application/json' },`);
534
+ if (hasBody && endpoint.requestBody) {
535
+ const sampleBody = generateSampleBody(endpoint.requestBody);
536
+ lines.push(` body: JSON.stringify(${JSON.stringify(sampleBody, null, 6).replace(/\n/g, '\n ')}),`);
537
+ }
538
+ lines.push(` });`);
539
+ lines.push('');
540
+ // Assertions
541
+ const okStatus = endpoint.responses.find((r) => r.status === '200' || r.status === '201' || r.status === '204');
542
+ const expectedStatus = okStatus ? parseInt(okStatus.status, 10) : 200;
543
+ lines.push(` expect(response.status).toBe(${expectedStatus});`);
544
+ if (expectedStatus !== 204) {
545
+ lines.push(` const data = await response.json();`);
546
+ lines.push(` expect(data).toBeDefined();`);
547
+ // Add property checks if schema is known
548
+ if (okStatus?.schema?.properties) {
549
+ for (const key of Object.keys(okStatus.schema.properties)) {
550
+ lines.push(` expect(data).toHaveProperty('${key}');`);
551
+ }
552
+ }
553
+ }
554
+ lines.push(` });`);
555
+ lines.push('');
556
+ // 404 test for endpoints with path params
557
+ if (endpoint.parameters.some((p) => p.in === 'path')) {
558
+ lines.push(` it('${testName} — returns 404 for invalid ID', async () => {`);
559
+ let badUrl = endpoint.path;
560
+ for (const param of endpoint.parameters.filter((p) => p.in === 'path')) {
561
+ badUrl = badUrl.replace(`{${param.name}}`, '99999999');
562
+ }
563
+ lines.push(` const response = await fetch(\`\${BASE_URL}${badUrl}\`, {`);
564
+ lines.push(` method: '${endpoint.method}',`);
565
+ lines.push(` headers: { 'Content-Type': 'application/json' },`);
566
+ lines.push(` });`);
567
+ lines.push(` expect(response.status).toBe(404);`);
568
+ lines.push(` });`);
569
+ lines.push('');
570
+ }
571
+ }
572
+ lines.push('});');
573
+ lines.push('');
574
+ }
575
+ return lines.join('\n');
576
+ }
577
+ function generatePytestTests(spec, baseUrl) {
578
+ const lines = [];
579
+ lines.push(`"""Auto-generated API tests for ${spec.info.title}"""`);
580
+ lines.push(`# Generated by TRUSS API MCP`);
581
+ lines.push('');
582
+ lines.push('import pytest');
583
+ lines.push('import requests');
584
+ lines.push('');
585
+ lines.push(`BASE_URL = "${baseUrl}"`);
586
+ lines.push('');
587
+ lines.push('');
588
+ for (const endpoint of spec.endpoints) {
589
+ const funcName = `test_${endpoint.method.toLowerCase()}_${endpoint.path.replace(/[/{}\-]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')}`;
590
+ const hasBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
591
+ let testUrl = endpoint.path;
592
+ for (const param of endpoint.parameters.filter((p) => p.in === 'path')) {
593
+ testUrl = testUrl.replace(`{${param.name}}`, sampleValue(param.schema, param.name));
594
+ }
595
+ lines.push(`def ${funcName}():`);
596
+ if (endpoint.summary) {
597
+ lines.push(` """${endpoint.summary}"""`);
598
+ }
599
+ lines.push(` url = f"{BASE_URL}${testUrl}"`);
600
+ if (hasBody && endpoint.requestBody) {
601
+ const sampleBody = generateSampleBody(endpoint.requestBody);
602
+ lines.push(` payload = ${JSON.stringify(sampleBody)}`);
603
+ lines.push(` response = requests.${endpoint.method.toLowerCase()}(url, json=payload, headers={"Content-Type": "application/json"})`);
604
+ }
605
+ else {
606
+ lines.push(` response = requests.${endpoint.method.toLowerCase()}(url, headers={"Content-Type": "application/json"})`);
607
+ }
608
+ const okStatus = endpoint.responses.find((r) => r.status === '200' || r.status === '201' || r.status === '204');
609
+ const expectedStatus = okStatus ? parseInt(okStatus.status, 10) : 200;
610
+ lines.push(` assert response.status_code == ${expectedStatus}`);
611
+ if (expectedStatus !== 204) {
612
+ lines.push(` data = response.json()`);
613
+ lines.push(` assert data is not None`);
614
+ }
615
+ lines.push('');
616
+ lines.push('');
617
+ }
618
+ return lines.join('\n');
619
+ }
620
+ // ── Mock Server Generation ──────────────────────────────────────────
621
+ export function generateMockServer(spec) {
622
+ const routes = [];
623
+ const mockData = {};
624
+ for (const endpoint of spec.endpoints) {
625
+ const okResp = endpoint.responses.find((r) => r.status === '200' || r.status === '201');
626
+ const status = okResp ? parseInt(okResp.status, 10) : 200;
627
+ let mockResponse;
628
+ if (okResp?.schema) {
629
+ mockResponse = generateMockData(okResp.schema, endpoint.path);
630
+ }
631
+ else {
632
+ mockResponse = { message: 'OK' };
633
+ }
634
+ // Convert OpenAPI path params to Express-style for json-server
635
+ const routePath = endpoint.path.replace(/\{(\w+)\}/g, ':$1');
636
+ routes.push({
637
+ method: endpoint.method,
638
+ path: routePath,
639
+ status,
640
+ response: mockResponse,
641
+ headers: { 'Content-Type': 'application/json' },
642
+ });
643
+ // Store in mock_data for json-server db.json format
644
+ const resourceName = endpoint.path
645
+ .split('/')[1]
646
+ ?.replace(/[^a-zA-Z0-9]/g, '') ?? 'data';
647
+ if (!mockData[resourceName] && Array.isArray(mockResponse)) {
648
+ mockData[resourceName] = mockResponse;
649
+ }
650
+ else if (!mockData[resourceName] && typeof mockResponse === 'object') {
651
+ // For list endpoints, wrap in array
652
+ if (endpoint.method === 'GET' && !endpoint.parameters.some((p) => p.in === 'path')) {
653
+ mockData[resourceName] = [mockResponse];
654
+ }
655
+ }
656
+ }
657
+ return { routes, mock_data: mockData };
658
+ }
659
+ function generateMockData(schema, context) {
660
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
661
+ switch (type) {
662
+ case 'string': {
663
+ if (schema.enum)
664
+ return schema.enum[0];
665
+ if (schema.format === 'date-time')
666
+ return '2026-01-15T10:00:00Z';
667
+ if (schema.format === 'email')
668
+ return 'user@example.com';
669
+ if (schema.format === 'uri')
670
+ return 'https://example.com';
671
+ return `sample_${context.replace(/[^a-z]/gi, '_')}`;
672
+ }
673
+ case 'integer':
674
+ return 1;
675
+ case 'number':
676
+ return 1.0;
677
+ case 'boolean':
678
+ return true;
679
+ case 'array':
680
+ return schema.items ? [generateMockData(schema.items, context)] : [];
681
+ case 'object': {
682
+ if (!schema.properties)
683
+ return {};
684
+ const obj = {};
685
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
686
+ obj[key] = generateMockData(propSchema, key);
687
+ }
688
+ return obj;
689
+ }
690
+ default:
691
+ return null;
692
+ }
693
+ }
694
+ // ── Spec Comparison ─────────────────────────────────────────────────
695
+ export function compareSpecs(oldSpec, newSpec) {
696
+ const breaking = [];
697
+ const nonBreaking = [];
698
+ const newEndpoints = [];
699
+ const removedEndpoints = [];
700
+ // Build maps
701
+ const oldMap = new Map();
702
+ const newMap = new Map();
703
+ for (const ep of oldSpec.endpoints) {
704
+ oldMap.set(`${ep.method} ${ep.path}`, ep);
705
+ }
706
+ for (const ep of newSpec.endpoints) {
707
+ newMap.set(`${ep.method} ${ep.path}`, ep);
708
+ }
709
+ // Find removed endpoints (breaking)
710
+ for (const [key, ep] of oldMap) {
711
+ if (!newMap.has(key)) {
712
+ const change = {
713
+ path: ep.path,
714
+ method: ep.method,
715
+ description: `Endpoint removed: ${ep.method} ${ep.path}`,
716
+ };
717
+ removedEndpoints.push(change);
718
+ breaking.push(change);
719
+ }
720
+ }
721
+ // Find new endpoints
722
+ for (const [key, ep] of newMap) {
723
+ if (!oldMap.has(key)) {
724
+ newEndpoints.push({
725
+ path: ep.path,
726
+ method: ep.method,
727
+ description: `New endpoint: ${ep.method} ${ep.path}`,
728
+ });
729
+ }
730
+ }
731
+ // Compare matching endpoints
732
+ for (const [key, oldEp] of oldMap) {
733
+ const newEp = newMap.get(key);
734
+ if (!newEp)
735
+ continue;
736
+ // Check required parameters
737
+ const oldRequired = new Set(oldEp.parameters.filter((p) => p.required).map((p) => p.name));
738
+ const newRequired = new Set(newEp.parameters.filter((p) => p.required).map((p) => p.name));
739
+ // New required parameters = breaking
740
+ for (const name of newRequired) {
741
+ if (!oldRequired.has(name)) {
742
+ breaking.push({
743
+ path: oldEp.path,
744
+ method: oldEp.method,
745
+ description: `New required parameter: ${name}`,
746
+ });
747
+ }
748
+ }
749
+ // Removed required parameters = non-breaking (parameter relaxed)
750
+ for (const name of oldRequired) {
751
+ if (!newRequired.has(name)) {
752
+ nonBreaking.push({
753
+ path: oldEp.path,
754
+ method: oldEp.method,
755
+ description: `Required parameter removed (relaxed): ${name}`,
756
+ });
757
+ }
758
+ }
759
+ // New optional parameters = non-breaking
760
+ const oldParamNames = new Set(oldEp.parameters.map((p) => p.name));
761
+ for (const p of newEp.parameters) {
762
+ if (!oldParamNames.has(p.name) && !p.required) {
763
+ nonBreaking.push({
764
+ path: oldEp.path,
765
+ method: oldEp.method,
766
+ description: `New optional parameter: ${p.name}`,
767
+ });
768
+ }
769
+ }
770
+ // Check response schema changes
771
+ for (const oldResp of oldEp.responses) {
772
+ const newResp = newEp.responses.find((r) => r.status === oldResp.status);
773
+ if (!newResp) {
774
+ breaking.push({
775
+ path: oldEp.path,
776
+ method: oldEp.method,
777
+ description: `Response ${oldResp.status} removed`,
778
+ });
779
+ continue;
780
+ }
781
+ // Check if response properties were removed (breaking)
782
+ if (oldResp.schema?.properties && newResp.schema?.properties) {
783
+ for (const key of Object.keys(oldResp.schema.properties)) {
784
+ if (!(key in newResp.schema.properties)) {
785
+ breaking.push({
786
+ path: oldEp.path,
787
+ method: oldEp.method,
788
+ description: `Response property '${key}' removed from ${oldResp.status}`,
789
+ });
790
+ }
791
+ }
792
+ // New response properties = non-breaking
793
+ for (const key of Object.keys(newResp.schema.properties)) {
794
+ if (!(key in oldResp.schema.properties)) {
795
+ nonBreaking.push({
796
+ path: oldEp.path,
797
+ method: oldEp.method,
798
+ description: `New response property '${key}' added to ${newResp.status}`,
799
+ });
800
+ }
801
+ }
802
+ }
803
+ }
804
+ // Summary change = non-breaking
805
+ if (oldEp.summary !== newEp.summary) {
806
+ nonBreaking.push({
807
+ path: oldEp.path,
808
+ method: oldEp.method,
809
+ description: `Summary changed: "${oldEp.summary}" -> "${newEp.summary}"`,
810
+ });
811
+ }
812
+ }
813
+ return {
814
+ breaking_changes: breaking,
815
+ non_breaking: nonBreaking,
816
+ new_endpoints: newEndpoints,
817
+ removed_endpoints: removedEndpoints,
818
+ };
819
+ }
820
+ // ── Helpers ─────────────────────────────────────────────────────────
821
+ function endpointToMethodName(endpoint) {
822
+ if (endpoint.operationId) {
823
+ return camelCase(endpoint.operationId);
824
+ }
825
+ const parts = endpoint.path
826
+ .split('/')
827
+ .filter((p) => p && !p.startsWith('{'))
828
+ .map((p) => p.replace(/[^a-zA-Z0-9]/g, ''));
829
+ const verb = endpoint.method.toLowerCase();
830
+ return camelCase(`${verb}_${parts.join('_')}`);
831
+ }
832
+ function camelCase(str) {
833
+ return str
834
+ .replace(/[-_]+(.)/g, (_, c) => c.toUpperCase())
835
+ .replace(/^(.)/, (_, c) => c.toLowerCase());
836
+ }
837
+ function pascalCase(str) {
838
+ const cc = camelCase(str);
839
+ return cc.charAt(0).toUpperCase() + cc.slice(1);
840
+ }
841
+ function snakeToPascal(str) {
842
+ return str
843
+ .split(/[-_]/)
844
+ .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
845
+ .join('');
846
+ }
847
+ function camelToSnake(str) {
848
+ return str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
849
+ }
850
+ function buildUrlExpr(endpoint, baseVarName) {
851
+ const pathParams = endpoint.parameters.filter((p) => p.in === 'path');
852
+ if (pathParams.length === 0) {
853
+ return `\`\${${baseVarName}}${endpoint.path}\``;
854
+ }
855
+ let path = endpoint.path;
856
+ for (const pp of pathParams) {
857
+ path = path.replace(`{${pp.name}}`, `\${${pp.name}}`);
858
+ }
859
+ return `\`\${${baseVarName}}${path}\``;
860
+ }
861
+ function sampleValue(schema, name) {
862
+ if (!schema)
863
+ return '1';
864
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
865
+ if (type === 'integer' || type === 'number')
866
+ return '1';
867
+ if (name?.toLowerCase().includes('id'))
868
+ return '1';
869
+ return 'test';
870
+ }
871
+ function generateSampleBody(schema) {
872
+ if (!schema.properties)
873
+ return {};
874
+ const body = {};
875
+ for (const [key, prop] of Object.entries(schema.properties)) {
876
+ body[key] = generateMockData(prop, key);
877
+ }
878
+ return body;
879
+ }
880
+ function groupEndpoints(endpoints) {
881
+ const groups = new Map();
882
+ for (const ep of endpoints) {
883
+ const group = ep.tags?.[0] ?? ep.path.split('/')[1] ?? 'API';
884
+ if (!groups.has(group))
885
+ groups.set(group, []);
886
+ groups.get(group).push(ep);
887
+ }
888
+ return groups;
889
+ }
890
+ //# sourceMappingURL=code-generator.js.map