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.
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +89 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/code-generator.d.ts +6 -0
- package/dist/lib/code-generator.d.ts.map +1 -0
- package/dist/lib/code-generator.js +890 -0
- package/dist/lib/code-generator.js.map +1 -0
- package/dist/lib/http-client.d.ts +6 -0
- package/dist/lib/http-client.d.ts.map +1 -0
- package/dist/lib/http-client.js +76 -0
- package/dist/lib/http-client.js.map +1 -0
- package/dist/lib/license.d.ts +4 -0
- package/dist/lib/license.d.ts.map +1 -0
- package/dist/lib/license.js +97 -0
- package/dist/lib/license.js.map +1 -0
- package/dist/lib/openapi-parser.d.ts +11 -0
- package/dist/lib/openapi-parser.d.ts.map +1 -0
- package/dist/lib/openapi-parser.js +390 -0
- package/dist/lib/openapi-parser.js.map +1 -0
- package/dist/lib/schema-validator.d.ts +15 -0
- package/dist/lib/schema-validator.d.ts.map +1 -0
- package/dist/lib/schema-validator.js +206 -0
- package/dist/lib/schema-validator.js.map +1 -0
- package/dist/tools/compare-specs.d.ts +3 -0
- package/dist/tools/compare-specs.d.ts.map +1 -0
- package/dist/tools/compare-specs.js +59 -0
- package/dist/tools/compare-specs.js.map +1 -0
- package/dist/tools/generate-client.d.ts +3 -0
- package/dist/tools/generate-client.d.ts.map +1 -0
- package/dist/tools/generate-client.js +65 -0
- package/dist/tools/generate-client.js.map +1 -0
- package/dist/tools/generate-openapi.d.ts +3 -0
- package/dist/tools/generate-openapi.d.ts.map +1 -0
- package/dist/tools/generate-openapi.js +57 -0
- package/dist/tools/generate-openapi.js.map +1 -0
- package/dist/tools/generate-tests.d.ts +3 -0
- package/dist/tools/generate-tests.d.ts.map +1 -0
- package/dist/tools/generate-tests.js +59 -0
- package/dist/tools/generate-tests.js.map +1 -0
- package/dist/tools/mock-server.d.ts +3 -0
- package/dist/tools/mock-server.d.ts.map +1 -0
- package/dist/tools/mock-server.js +60 -0
- package/dist/tools/mock-server.js.map +1 -0
- package/dist/tools/parse-openapi.d.ts +3 -0
- package/dist/tools/parse-openapi.d.ts.map +1 -0
- package/dist/tools/parse-openapi.js +48 -0
- package/dist/tools/parse-openapi.js.map +1 -0
- package/dist/tools/test-endpoint.d.ts +3 -0
- package/dist/tools/test-endpoint.d.ts.map +1 -0
- package/dist/tools/test-endpoint.js +66 -0
- package/dist/tools/test-endpoint.js.map +1 -0
- package/dist/tools/validate-response.d.ts +3 -0
- package/dist/tools/validate-response.d.ts.map +1 -0
- package/dist/tools/validate-response.js +44 -0
- package/dist/tools/validate-response.js.map +1 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/evals/eval-http.ts +163 -0
- package/evals/eval-openapi.ts +506 -0
- package/evals/run-evals.ts +29 -0
- package/glama.json +4 -0
- package/package.json +37 -0
- package/smithery.yaml +9 -0
- package/src/index.ts +110 -0
- package/src/lib/code-generator.ts +1045 -0
- package/src/lib/http-client.ts +87 -0
- package/src/lib/license.ts +121 -0
- package/src/lib/openapi-parser.ts +456 -0
- package/src/lib/schema-validator.ts +234 -0
- package/src/tools/compare-specs.ts +67 -0
- package/src/tools/generate-client.ts +75 -0
- package/src/tools/generate-openapi.ts +67 -0
- package/src/tools/generate-tests.ts +69 -0
- package/src/tools/mock-server.ts +68 -0
- package/src/tools/parse-openapi.ts +54 -0
- package/src/tools/test-endpoint.ts +71 -0
- package/src/tools/validate-response.ts +54 -0
- package/src/types.ts +156 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evaluation: OpenAPI parsing, spec generation, code generation, comparison
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { parseOpenApiSpec, generateOpenApiFromExamples } from '../src/lib/openapi-parser.js';
|
|
6
|
+
import { generateClient, generateTests, generateMockServer, compareSpecs } from '../src/lib/code-generator.js';
|
|
7
|
+
import type { ParsedOpenApiSpec } from '../src/types.js';
|
|
8
|
+
|
|
9
|
+
interface EvalResult {
|
|
10
|
+
name: string;
|
|
11
|
+
passed: boolean;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const results: EvalResult[] = [];
|
|
16
|
+
|
|
17
|
+
function run(name: string, fn: () => void): void {
|
|
18
|
+
try {
|
|
19
|
+
fn();
|
|
20
|
+
results.push({ name, passed: true });
|
|
21
|
+
} catch (err) {
|
|
22
|
+
results.push({
|
|
23
|
+
name,
|
|
24
|
+
passed: false,
|
|
25
|
+
error: err instanceof Error ? err.message : String(err),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function assert(condition: boolean, message: string): void {
|
|
31
|
+
if (!condition) throw new Error(message);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Sample Specs ────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
const PETSTORE_SPEC = JSON.stringify({
|
|
37
|
+
openapi: '3.0.3',
|
|
38
|
+
info: { title: 'Petstore', version: '1.0.0', description: 'A sample pet store API' },
|
|
39
|
+
servers: [{ url: 'https://petstore.example.com/v1' }],
|
|
40
|
+
paths: {
|
|
41
|
+
'/pets': {
|
|
42
|
+
get: {
|
|
43
|
+
summary: 'List all pets',
|
|
44
|
+
operationId: 'listPets',
|
|
45
|
+
tags: ['pets'],
|
|
46
|
+
parameters: [
|
|
47
|
+
{ name: 'limit', in: 'query', required: false, schema: { type: 'integer', maximum: 100 } },
|
|
48
|
+
],
|
|
49
|
+
responses: {
|
|
50
|
+
'200': {
|
|
51
|
+
description: 'A list of pets',
|
|
52
|
+
content: {
|
|
53
|
+
'application/json': {
|
|
54
|
+
schema: {
|
|
55
|
+
type: 'array',
|
|
56
|
+
items: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
required: ['id', 'name'],
|
|
59
|
+
properties: {
|
|
60
|
+
id: { type: 'integer' },
|
|
61
|
+
name: { type: 'string' },
|
|
62
|
+
tag: { type: 'string' },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
post: {
|
|
72
|
+
summary: 'Create a pet',
|
|
73
|
+
operationId: 'createPet',
|
|
74
|
+
tags: ['pets'],
|
|
75
|
+
requestBody: {
|
|
76
|
+
required: true,
|
|
77
|
+
content: {
|
|
78
|
+
'application/json': {
|
|
79
|
+
schema: {
|
|
80
|
+
type: 'object',
|
|
81
|
+
required: ['name'],
|
|
82
|
+
properties: {
|
|
83
|
+
name: { type: 'string' },
|
|
84
|
+
tag: { type: 'string' },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
responses: {
|
|
91
|
+
'201': { description: 'Pet created' },
|
|
92
|
+
'400': { description: 'Bad request' },
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
'/pets/{petId}': {
|
|
97
|
+
get: {
|
|
98
|
+
summary: 'Get a pet by ID',
|
|
99
|
+
operationId: 'getPet',
|
|
100
|
+
tags: ['pets'],
|
|
101
|
+
parameters: [
|
|
102
|
+
{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } },
|
|
103
|
+
],
|
|
104
|
+
responses: {
|
|
105
|
+
'200': {
|
|
106
|
+
description: 'A pet',
|
|
107
|
+
content: {
|
|
108
|
+
'application/json': {
|
|
109
|
+
schema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
required: ['id', 'name'],
|
|
112
|
+
properties: {
|
|
113
|
+
id: { type: 'integer' },
|
|
114
|
+
name: { type: 'string' },
|
|
115
|
+
tag: { type: 'string' },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
'404': { description: 'Pet not found' },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
delete: {
|
|
125
|
+
summary: 'Delete a pet',
|
|
126
|
+
operationId: 'deletePet',
|
|
127
|
+
tags: ['pets'],
|
|
128
|
+
parameters: [
|
|
129
|
+
{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } },
|
|
130
|
+
],
|
|
131
|
+
responses: {
|
|
132
|
+
'204': { description: 'Pet deleted' },
|
|
133
|
+
'404': { description: 'Pet not found' },
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const SWAGGER2_SPEC = JSON.stringify({
|
|
141
|
+
swagger: '2.0',
|
|
142
|
+
info: { title: 'Legacy API', version: '1.0.0' },
|
|
143
|
+
host: 'api.example.com',
|
|
144
|
+
basePath: '/v1',
|
|
145
|
+
schemes: ['https'],
|
|
146
|
+
paths: {
|
|
147
|
+
'/users': {
|
|
148
|
+
get: {
|
|
149
|
+
summary: 'List users',
|
|
150
|
+
parameters: [
|
|
151
|
+
{ name: 'page', in: 'query', type: 'integer', required: false },
|
|
152
|
+
],
|
|
153
|
+
responses: {
|
|
154
|
+
'200': {
|
|
155
|
+
description: 'A list of users',
|
|
156
|
+
schema: {
|
|
157
|
+
type: 'array',
|
|
158
|
+
items: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
id: { type: 'integer' },
|
|
162
|
+
email: { type: 'string' },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
post: {
|
|
170
|
+
summary: 'Create user',
|
|
171
|
+
parameters: [
|
|
172
|
+
{
|
|
173
|
+
name: 'body',
|
|
174
|
+
in: 'body',
|
|
175
|
+
required: true,
|
|
176
|
+
schema: {
|
|
177
|
+
type: 'object',
|
|
178
|
+
required: ['email'],
|
|
179
|
+
properties: {
|
|
180
|
+
email: { type: 'string' },
|
|
181
|
+
name: { type: 'string' },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
responses: {
|
|
187
|
+
'201': { description: 'User created' },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// ── OpenAPI Parsing Tests ───────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
run('Parse OpenAPI 3.0 JSON spec', () => {
|
|
197
|
+
const result = parseOpenApiSpec(PETSTORE_SPEC);
|
|
198
|
+
|
|
199
|
+
assert(result.info.title === 'Petstore', `Title should be Petstore, got ${result.info.title}`);
|
|
200
|
+
assert(result.info.version === '1.0.0', 'Version should be 1.0.0');
|
|
201
|
+
assert(result.endpoints.length === 4, `Expected 4 endpoints, got ${result.endpoints.length}`);
|
|
202
|
+
assert(result.servers?.length === 1, 'Should have 1 server');
|
|
203
|
+
assert(result.servers![0].url === 'https://petstore.example.com/v1', 'Server URL mismatch');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
run('Parse Swagger 2.0 spec', () => {
|
|
207
|
+
const result = parseOpenApiSpec(SWAGGER2_SPEC);
|
|
208
|
+
|
|
209
|
+
assert(result.info.title === 'Legacy API', 'Title should be Legacy API');
|
|
210
|
+
assert(result.endpoints.length === 2, `Expected 2 endpoints, got ${result.endpoints.length}`);
|
|
211
|
+
assert(result.servers?.[0]?.url === 'https://api.example.com/v1', 'Server URL mismatch');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
run('Parse YAML spec', () => {
|
|
215
|
+
const yamlSpec = `
|
|
216
|
+
openapi: "3.0.0"
|
|
217
|
+
info:
|
|
218
|
+
title: YAML API
|
|
219
|
+
version: "2.0.0"
|
|
220
|
+
paths:
|
|
221
|
+
/health:
|
|
222
|
+
get:
|
|
223
|
+
summary: Health check
|
|
224
|
+
responses:
|
|
225
|
+
"200":
|
|
226
|
+
description: OK
|
|
227
|
+
`;
|
|
228
|
+
const result = parseOpenApiSpec(yamlSpec);
|
|
229
|
+
|
|
230
|
+
assert(result.info.title === 'YAML API', 'Title should be YAML API');
|
|
231
|
+
assert(result.endpoints.length === 1, 'Should have 1 endpoint');
|
|
232
|
+
assert(result.endpoints[0].method === 'GET', 'Method should be GET');
|
|
233
|
+
assert(result.endpoints[0].path === '/health', 'Path should be /health');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
run('Parse spec with $ref resolution', () => {
|
|
237
|
+
const specWithRefs = JSON.stringify({
|
|
238
|
+
openapi: '3.0.0',
|
|
239
|
+
info: { title: 'Ref Test', version: '1.0.0' },
|
|
240
|
+
paths: {
|
|
241
|
+
'/items': {
|
|
242
|
+
get: {
|
|
243
|
+
responses: {
|
|
244
|
+
'200': {
|
|
245
|
+
description: 'OK',
|
|
246
|
+
content: {
|
|
247
|
+
'application/json': {
|
|
248
|
+
schema: { $ref: '#/components/schemas/Item' },
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
components: {
|
|
257
|
+
schemas: {
|
|
258
|
+
Item: {
|
|
259
|
+
type: 'object',
|
|
260
|
+
properties: {
|
|
261
|
+
id: { type: 'integer' },
|
|
262
|
+
name: { type: 'string' },
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const result = parseOpenApiSpec(specWithRefs);
|
|
270
|
+
const endpoint = result.endpoints[0];
|
|
271
|
+
const responseSchema = endpoint.responses[0].schema;
|
|
272
|
+
|
|
273
|
+
assert(responseSchema?.properties?.id !== undefined, 'Should resolve $ref to Item with id');
|
|
274
|
+
assert(responseSchema?.properties?.name !== undefined, 'Should resolve $ref to Item with name');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
run('Reject invalid spec', () => {
|
|
278
|
+
let threw = false;
|
|
279
|
+
try {
|
|
280
|
+
parseOpenApiSpec('not a valid spec');
|
|
281
|
+
} catch {
|
|
282
|
+
threw = true;
|
|
283
|
+
}
|
|
284
|
+
assert(threw, 'Should throw on invalid spec');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
run('Extract path parameters', () => {
|
|
288
|
+
const result = parseOpenApiSpec(PETSTORE_SPEC);
|
|
289
|
+
const getPet = result.endpoints.find((e) => e.operationId === 'getPet');
|
|
290
|
+
|
|
291
|
+
assert(getPet !== undefined, 'Should find getPet endpoint');
|
|
292
|
+
assert(getPet!.parameters.length === 1, 'Should have 1 parameter');
|
|
293
|
+
assert(getPet!.parameters[0].name === 'petId', 'Parameter should be petId');
|
|
294
|
+
assert(getPet!.parameters[0].in === 'path', 'Parameter should be in path');
|
|
295
|
+
assert(getPet!.parameters[0].required === true, 'Path parameter should be required');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
run('Extract request body', () => {
|
|
299
|
+
const result = parseOpenApiSpec(PETSTORE_SPEC);
|
|
300
|
+
const createPet = result.endpoints.find((e) => e.operationId === 'createPet');
|
|
301
|
+
|
|
302
|
+
assert(createPet !== undefined, 'Should find createPet endpoint');
|
|
303
|
+
assert(createPet!.requestBody !== undefined, 'Should have request body');
|
|
304
|
+
assert(createPet!.requestBody!.properties?.name !== undefined, 'Body should have name property');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ── Generate OpenAPI from Examples ──────────────────────────────────
|
|
308
|
+
|
|
309
|
+
run('Generate OpenAPI from examples', () => {
|
|
310
|
+
const yamlOutput = generateOpenApiFromExamples([
|
|
311
|
+
{
|
|
312
|
+
method: 'GET',
|
|
313
|
+
url: 'https://api.example.com/users',
|
|
314
|
+
response_body: [{ id: 1, name: 'Alice', email: 'alice@example.com' }],
|
|
315
|
+
status: 200,
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
method: 'POST',
|
|
319
|
+
url: 'https://api.example.com/users',
|
|
320
|
+
request_body: { name: 'Bob', email: 'bob@example.com' },
|
|
321
|
+
response_body: { id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
322
|
+
status: 201,
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
method: 'GET',
|
|
326
|
+
url: 'https://api.example.com/users/1',
|
|
327
|
+
response_body: { id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
328
|
+
status: 200,
|
|
329
|
+
},
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
assert(yamlOutput.includes('openapi:'), 'Should contain openapi version');
|
|
333
|
+
assert(yamlOutput.includes('/users'), 'Should contain /users path');
|
|
334
|
+
assert(yamlOutput.includes('Auto-Generated API'), 'Should contain title');
|
|
335
|
+
|
|
336
|
+
// Verify it parses back
|
|
337
|
+
const parsed = parseOpenApiSpec(yamlOutput);
|
|
338
|
+
assert(parsed.endpoints.length >= 2, `Expected >= 2 endpoints, got ${parsed.endpoints.length}`);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ── Client Code Generation ──────────────────────────────────────────
|
|
342
|
+
|
|
343
|
+
let parsedPetstore: ParsedOpenApiSpec;
|
|
344
|
+
|
|
345
|
+
run('Setup: Parse petstore for code gen', () => {
|
|
346
|
+
parsedPetstore = parseOpenApiSpec(PETSTORE_SPEC);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
run('Generate TypeScript client (fetch)', () => {
|
|
350
|
+
const code = generateClient(parsedPetstore, 'typescript', 'fetch');
|
|
351
|
+
|
|
352
|
+
assert(code.includes('class ApiClient'), 'Should contain ApiClient class');
|
|
353
|
+
assert(code.includes('async listPets'), 'Should contain listPets method');
|
|
354
|
+
assert(code.includes('async createPet'), 'Should contain createPet method');
|
|
355
|
+
assert(code.includes('async getPet'), 'Should contain getPet method');
|
|
356
|
+
assert(code.includes('fetch('), 'Should use fetch()');
|
|
357
|
+
assert(code.includes("method: 'GET'"), 'Should have GET method');
|
|
358
|
+
assert(code.includes("method: 'POST'"), 'Should have POST method');
|
|
359
|
+
assert(code.includes('petstore.example.com'), 'Should contain base URL');
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
run('Generate TypeScript client (axios)', () => {
|
|
363
|
+
const code = generateClient(parsedPetstore, 'typescript', 'axios');
|
|
364
|
+
|
|
365
|
+
assert(code.includes('import axios'), 'Should import axios');
|
|
366
|
+
assert(code.includes('this.client'), 'Should use axios client instance');
|
|
367
|
+
assert(code.includes('class ApiClient'), 'Should contain ApiClient class');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
run('Generate Python client', () => {
|
|
371
|
+
const code = generateClient(parsedPetstore, 'python');
|
|
372
|
+
|
|
373
|
+
assert(code.includes('import requests'), 'Should import requests');
|
|
374
|
+
assert(code.includes('class ApiClient:'), 'Should contain ApiClient class');
|
|
375
|
+
assert(code.includes('def list_pets'), 'Should contain list_pets method');
|
|
376
|
+
assert(code.includes('def create_pet'), 'Should contain create_pet method');
|
|
377
|
+
assert(code.includes('self.session'), 'Should use session');
|
|
378
|
+
assert(code.includes('raise_for_status'), 'Should check status');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
run('Generate Go client', () => {
|
|
382
|
+
const code = generateClient(parsedPetstore, 'go');
|
|
383
|
+
|
|
384
|
+
assert(code.includes('package apiclient'), 'Should have package declaration');
|
|
385
|
+
assert(code.includes('type Client struct'), 'Should contain Client struct');
|
|
386
|
+
assert(code.includes('func NewClient'), 'Should have constructor');
|
|
387
|
+
assert(code.includes('func (c *Client)'), 'Should have methods on Client');
|
|
388
|
+
assert(code.includes('net/http'), 'Should import net/http');
|
|
389
|
+
assert(code.includes('json.Marshal'), 'Should use JSON marshaling');
|
|
390
|
+
assert(code.includes('json:"'), 'Should have JSON struct tags');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// ── Test Generation ─────────────────────────────────────────────────
|
|
394
|
+
|
|
395
|
+
run('Generate Vitest tests', () => {
|
|
396
|
+
const code = generateTests(parsedPetstore, 'http://localhost:3000', 'vitest');
|
|
397
|
+
|
|
398
|
+
assert(code.includes("import { describe, it, expect } from 'vitest'"), 'Should import vitest');
|
|
399
|
+
assert(code.includes("describe('"), 'Should have describe blocks');
|
|
400
|
+
assert(code.includes('it('), 'Should have test cases');
|
|
401
|
+
assert(code.includes("expect(response.status)"), 'Should assert status');
|
|
402
|
+
assert(code.includes('BASE_URL'), 'Should use BASE_URL constant');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
run('Generate Jest tests', () => {
|
|
406
|
+
const code = generateTests(parsedPetstore, 'http://localhost:3000', 'jest');
|
|
407
|
+
|
|
408
|
+
assert(code.includes("describe('"), 'Should have describe blocks');
|
|
409
|
+
assert(code.includes('.toBe('), 'Should use Jest matchers');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
run('Generate Pytest tests', () => {
|
|
413
|
+
const code = generateTests(parsedPetstore, 'http://localhost:3000', 'pytest');
|
|
414
|
+
|
|
415
|
+
assert(code.includes('import pytest'), 'Should import pytest');
|
|
416
|
+
assert(code.includes('import requests'), 'Should import requests');
|
|
417
|
+
assert(code.includes('def test_'), 'Should have test functions');
|
|
418
|
+
assert(code.includes('assert response.status_code'), 'Should assert status code');
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ── Mock Server Generation ──────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
run('Generate mock server config', () => {
|
|
424
|
+
const config = generateMockServer(parsedPetstore);
|
|
425
|
+
|
|
426
|
+
assert(config.routes.length === 4, `Expected 4 routes, got ${config.routes.length}`);
|
|
427
|
+
assert(config.routes.some((r) => r.method === 'GET' && r.path === '/pets'), 'Should have GET /pets');
|
|
428
|
+
assert(config.routes.some((r) => r.method === 'POST' && r.path === '/pets'), 'Should have POST /pets');
|
|
429
|
+
assert(config.routes.some((r) => r.path === '/pets/:petId'), 'Should convert path params to :param style');
|
|
430
|
+
|
|
431
|
+
// Mock data should exist
|
|
432
|
+
assert(typeof config.mock_data === 'object', 'Should have mock_data object');
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// ── Spec Comparison ─────────────────────────────────────────────────
|
|
436
|
+
|
|
437
|
+
run('Compare specs — detect breaking changes', () => {
|
|
438
|
+
const oldParsed = parseOpenApiSpec(PETSTORE_SPEC);
|
|
439
|
+
|
|
440
|
+
// Create a modified spec with breaking changes
|
|
441
|
+
const modified = JSON.parse(PETSTORE_SPEC);
|
|
442
|
+
// Remove an endpoint (breaking)
|
|
443
|
+
delete modified.paths['/pets/{petId}'].delete;
|
|
444
|
+
// Add a new required parameter (breaking)
|
|
445
|
+
modified.paths['/pets'].get.parameters.push({
|
|
446
|
+
name: 'api_key',
|
|
447
|
+
in: 'header',
|
|
448
|
+
required: true,
|
|
449
|
+
schema: { type: 'string' },
|
|
450
|
+
});
|
|
451
|
+
// Add a new endpoint (non-breaking)
|
|
452
|
+
modified.paths['/pets/{petId}/photos'] = {
|
|
453
|
+
get: {
|
|
454
|
+
summary: 'Get pet photos',
|
|
455
|
+
parameters: [{ name: 'petId', in: 'path', required: true, schema: { type: 'integer' } }],
|
|
456
|
+
responses: { '200': { description: 'Photos' } },
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const newParsed = parseOpenApiSpec(JSON.stringify(modified));
|
|
461
|
+
const comparison = compareSpecs(oldParsed, newParsed);
|
|
462
|
+
|
|
463
|
+
assert(comparison.breaking_changes.length > 0, 'Should detect breaking changes');
|
|
464
|
+
assert(
|
|
465
|
+
comparison.breaking_changes.some((c) => c.description.includes('removed')),
|
|
466
|
+
'Should detect removed endpoint'
|
|
467
|
+
);
|
|
468
|
+
assert(
|
|
469
|
+
comparison.breaking_changes.some((c) => c.description.includes('required parameter')),
|
|
470
|
+
'Should detect new required parameter'
|
|
471
|
+
);
|
|
472
|
+
assert(comparison.new_endpoints.length > 0, 'Should detect new endpoints');
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
run('Compare specs — backwards compatible', () => {
|
|
476
|
+
const oldParsed = parseOpenApiSpec(PETSTORE_SPEC);
|
|
477
|
+
|
|
478
|
+
// Add only non-breaking changes
|
|
479
|
+
const modified = JSON.parse(PETSTORE_SPEC);
|
|
480
|
+
modified.paths['/pets'].get.parameters.push({
|
|
481
|
+
name: 'sort',
|
|
482
|
+
in: 'query',
|
|
483
|
+
required: false,
|
|
484
|
+
schema: { type: 'string' },
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const newParsed = parseOpenApiSpec(JSON.stringify(modified));
|
|
488
|
+
const comparison = compareSpecs(oldParsed, newParsed);
|
|
489
|
+
|
|
490
|
+
assert(comparison.breaking_changes.length === 0, 'Should have no breaking changes');
|
|
491
|
+
assert(comparison.non_breaking.length > 0, 'Should have non-breaking changes');
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// ── Report ──────────────────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
export { results };
|
|
497
|
+
|
|
498
|
+
const passed = results.filter((r) => r.passed).length;
|
|
499
|
+
const failed = results.filter((r) => !r.passed).length;
|
|
500
|
+
|
|
501
|
+
console.log(`\nOpenAPI & Code Gen Evals: ${passed} passed, ${failed} failed\n`);
|
|
502
|
+
for (const r of results) {
|
|
503
|
+
const icon = r.passed ? 'PASS' : 'FAIL';
|
|
504
|
+
console.log(` [${icon}] ${r.name}`);
|
|
505
|
+
if (r.error) console.log(` ${r.error}`);
|
|
506
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run all evaluations for the API Testing MCP server.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
console.log('=== TRUSS API Testing MCP — Evaluations ===\n');
|
|
6
|
+
|
|
7
|
+
// Import eval modules (they self-execute and print results)
|
|
8
|
+
const httpModule = await import('./eval-http.js');
|
|
9
|
+
const openapiModule = await import('./eval-openapi.js');
|
|
10
|
+
|
|
11
|
+
// Aggregate results
|
|
12
|
+
const allResults = [...httpModule.results, ...openapiModule.results];
|
|
13
|
+
const passed = allResults.filter((r) => r.passed).length;
|
|
14
|
+
const failed = allResults.filter((r) => !r.passed).length;
|
|
15
|
+
const total = allResults.length;
|
|
16
|
+
|
|
17
|
+
console.log('\n' + '='.repeat(50));
|
|
18
|
+
console.log(`TOTAL: ${passed}/${total} passed, ${failed} failed`);
|
|
19
|
+
console.log('='.repeat(50));
|
|
20
|
+
|
|
21
|
+
if (failed > 0) {
|
|
22
|
+
console.log('\nFailing tests:');
|
|
23
|
+
for (const r of allResults.filter((r) => !r.passed)) {
|
|
24
|
+
console.log(` - ${r.name}: ${r.error}`);
|
|
25
|
+
}
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
process.exit(0);
|
package/glama.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "truss-api-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "API testing and documentation tools for Claude Code — test endpoints, parse OpenAPI specs, generate clients and tests",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"truss-api-mcp": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/index.js",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"eval": "tsx evals/run-evals.ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"api",
|
|
18
|
+
"testing",
|
|
19
|
+
"openapi",
|
|
20
|
+
"swagger",
|
|
21
|
+
"documentation",
|
|
22
|
+
"claude",
|
|
23
|
+
"claude-code"
|
|
24
|
+
],
|
|
25
|
+
"author": "TRUSS",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
29
|
+
"js-yaml": "^4.1.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"typescript": "^5.5.0",
|
|
33
|
+
"tsx": "^4.19.0",
|
|
34
|
+
"@types/js-yaml": "^4.0.9",
|
|
35
|
+
"@types/node": "^22.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|