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,234 @@
|
|
|
1
|
+
import type { JsonSchema, ValidationError, ValidationResult } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validate a value against a JSON Schema (draft-07 subset).
|
|
5
|
+
* Supports: type, properties, required, items, enum, minimum, maximum,
|
|
6
|
+
* minLength, maxLength, pattern, additionalProperties, oneOf, anyOf, allOf, nullable.
|
|
7
|
+
*/
|
|
8
|
+
export function validateSchema(
|
|
9
|
+
value: unknown,
|
|
10
|
+
schema: JsonSchema,
|
|
11
|
+
path = '$'
|
|
12
|
+
): ValidationResult {
|
|
13
|
+
const errors: ValidationError[] = [];
|
|
14
|
+
validate(value, schema, path, errors);
|
|
15
|
+
return { valid: errors.length === 0, errors };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate an HTTP response against expected status and optional schema.
|
|
20
|
+
*/
|
|
21
|
+
export function validateResponse(
|
|
22
|
+
response: { status: number; body: unknown },
|
|
23
|
+
expectedStatus: number,
|
|
24
|
+
expectedSchema?: JsonSchema
|
|
25
|
+
): ValidationResult {
|
|
26
|
+
const errors: ValidationError[] = [];
|
|
27
|
+
|
|
28
|
+
if (response.status !== expectedStatus) {
|
|
29
|
+
errors.push({
|
|
30
|
+
path: '$.status',
|
|
31
|
+
message: `Expected status ${expectedStatus}, got ${response.status}`,
|
|
32
|
+
expected: String(expectedStatus),
|
|
33
|
+
actual: String(response.status),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (expectedSchema) {
|
|
38
|
+
const bodyResult = validateSchema(response.body, expectedSchema, '$.body');
|
|
39
|
+
errors.push(...bodyResult.errors);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { valid: errors.length === 0, errors };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Internal Validation ─────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function validate(
|
|
48
|
+
value: unknown,
|
|
49
|
+
schema: JsonSchema,
|
|
50
|
+
path: string,
|
|
51
|
+
errors: ValidationError[]
|
|
52
|
+
): void {
|
|
53
|
+
// Handle nullable
|
|
54
|
+
if (schema.nullable && (value === null || value === undefined)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// allOf: all must match
|
|
59
|
+
if (schema.allOf) {
|
|
60
|
+
for (const sub of schema.allOf) {
|
|
61
|
+
validate(value, sub, path, errors);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// anyOf: at least one must match
|
|
67
|
+
if (schema.anyOf) {
|
|
68
|
+
const anyErrors: ValidationError[][] = [];
|
|
69
|
+
for (const sub of schema.anyOf) {
|
|
70
|
+
const subErrors: ValidationError[] = [];
|
|
71
|
+
validate(value, sub, path, subErrors);
|
|
72
|
+
if (subErrors.length === 0) return; // matched
|
|
73
|
+
anyErrors.push(subErrors);
|
|
74
|
+
}
|
|
75
|
+
errors.push({
|
|
76
|
+
path,
|
|
77
|
+
message: `Value does not match any of the ${schema.anyOf.length} schemas in anyOf`,
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// oneOf: exactly one must match
|
|
83
|
+
if (schema.oneOf) {
|
|
84
|
+
let matchCount = 0;
|
|
85
|
+
for (const sub of schema.oneOf) {
|
|
86
|
+
const subErrors: ValidationError[] = [];
|
|
87
|
+
validate(value, sub, path, subErrors);
|
|
88
|
+
if (subErrors.length === 0) matchCount++;
|
|
89
|
+
}
|
|
90
|
+
if (matchCount !== 1) {
|
|
91
|
+
errors.push({
|
|
92
|
+
path,
|
|
93
|
+
message: `Value must match exactly one schema in oneOf, but matched ${matchCount}`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// enum
|
|
100
|
+
if (schema.enum) {
|
|
101
|
+
if (!schema.enum.some((e) => JSON.stringify(e) === JSON.stringify(value))) {
|
|
102
|
+
errors.push({
|
|
103
|
+
path,
|
|
104
|
+
message: `Value must be one of: ${schema.enum.map((e) => JSON.stringify(e)).join(', ')}`,
|
|
105
|
+
actual: JSON.stringify(value),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// type checking
|
|
112
|
+
if (schema.type) {
|
|
113
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
114
|
+
const actualType = getJsonType(value);
|
|
115
|
+
|
|
116
|
+
if (!types.includes(actualType)) {
|
|
117
|
+
// Allow integer to match number
|
|
118
|
+
if (!(actualType === 'integer' && types.includes('number'))) {
|
|
119
|
+
// Allow null if nullable
|
|
120
|
+
if (!(actualType === 'null' && schema.nullable)) {
|
|
121
|
+
errors.push({
|
|
122
|
+
path,
|
|
123
|
+
message: `Expected type ${types.join('|')}, got ${actualType}`,
|
|
124
|
+
expected: types.join('|'),
|
|
125
|
+
actual: actualType,
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// string validations
|
|
134
|
+
if (typeof value === 'string') {
|
|
135
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
136
|
+
errors.push({
|
|
137
|
+
path,
|
|
138
|
+
message: `String length ${value.length} is less than minimum ${schema.minLength}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
142
|
+
errors.push({
|
|
143
|
+
path,
|
|
144
|
+
message: `String length ${value.length} exceeds maximum ${schema.maxLength}`,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
if (schema.pattern) {
|
|
148
|
+
try {
|
|
149
|
+
if (!new RegExp(schema.pattern).test(value)) {
|
|
150
|
+
errors.push({
|
|
151
|
+
path,
|
|
152
|
+
message: `String does not match pattern: ${schema.pattern}`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// Invalid regex — skip validation
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// number validations
|
|
162
|
+
if (typeof value === 'number') {
|
|
163
|
+
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
164
|
+
errors.push({
|
|
165
|
+
path,
|
|
166
|
+
message: `Value ${value} is less than minimum ${schema.minimum}`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
170
|
+
errors.push({
|
|
171
|
+
path,
|
|
172
|
+
message: `Value ${value} exceeds maximum ${schema.maximum}`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// object validations
|
|
178
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
179
|
+
const obj = value as Record<string, unknown>;
|
|
180
|
+
|
|
181
|
+
// required
|
|
182
|
+
if (schema.required) {
|
|
183
|
+
for (const req of schema.required) {
|
|
184
|
+
if (!(req in obj)) {
|
|
185
|
+
errors.push({
|
|
186
|
+
path: `${path}.${req}`,
|
|
187
|
+
message: `Missing required property: ${req}`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// properties
|
|
194
|
+
if (schema.properties) {
|
|
195
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
196
|
+
if (key in obj) {
|
|
197
|
+
validate(obj[key], propSchema, `${path}.${key}`, errors);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// additionalProperties
|
|
203
|
+
if (schema.additionalProperties === false && schema.properties) {
|
|
204
|
+
const allowed = new Set(Object.keys(schema.properties));
|
|
205
|
+
for (const key of Object.keys(obj)) {
|
|
206
|
+
if (!allowed.has(key)) {
|
|
207
|
+
errors.push({
|
|
208
|
+
path: `${path}.${key}`,
|
|
209
|
+
message: `Additional property not allowed: ${key}`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// array validations
|
|
217
|
+
if (Array.isArray(value)) {
|
|
218
|
+
if (schema.items) {
|
|
219
|
+
for (let i = 0; i < value.length; i++) {
|
|
220
|
+
validate(value[i], schema.items, `${path}[${i}]`, errors);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getJsonType(value: unknown): string {
|
|
227
|
+
if (value === null) return 'null';
|
|
228
|
+
if (value === undefined) return 'null';
|
|
229
|
+
if (Array.isArray(value)) return 'array';
|
|
230
|
+
if (typeof value === 'number') {
|
|
231
|
+
return Number.isInteger(value) ? 'integer' : 'number';
|
|
232
|
+
}
|
|
233
|
+
return typeof value; // 'string', 'boolean', 'object'
|
|
234
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { requirePro } from '../lib/license.js';
|
|
4
|
+
import { parseOpenApiSpec } from '../lib/openapi-parser.js';
|
|
5
|
+
import { compareSpecs } from '../lib/code-generator.js';
|
|
6
|
+
|
|
7
|
+
export function registerCompareSpecs(server: McpServer): void {
|
|
8
|
+
server.tool(
|
|
9
|
+
'compare_specs',
|
|
10
|
+
'[Pro] Diff two OpenAPI specs and find breaking changes, non-breaking changes, new endpoints, and removed endpoints. Essential for API versioning.',
|
|
11
|
+
{
|
|
12
|
+
old_spec: z.string().describe('The old/previous OpenAPI spec as JSON or YAML'),
|
|
13
|
+
new_spec: z.string().describe('The new/updated OpenAPI spec as JSON or YAML'),
|
|
14
|
+
},
|
|
15
|
+
async ({ old_spec, new_spec }) => {
|
|
16
|
+
try {
|
|
17
|
+
await requirePro();
|
|
18
|
+
|
|
19
|
+
const oldParsed = parseOpenApiSpec(old_spec);
|
|
20
|
+
const newParsed = parseOpenApiSpec(new_spec);
|
|
21
|
+
const comparison = compareSpecs(oldParsed, newParsed);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: 'text' as const,
|
|
27
|
+
text: JSON.stringify({
|
|
28
|
+
summary: {
|
|
29
|
+
breaking_changes: comparison.breaking_changes.length,
|
|
30
|
+
non_breaking_changes: comparison.non_breaking.length,
|
|
31
|
+
new_endpoints: comparison.new_endpoints.length,
|
|
32
|
+
removed_endpoints: comparison.removed_endpoints.length,
|
|
33
|
+
is_backwards_compatible: comparison.breaking_changes.length === 0,
|
|
34
|
+
},
|
|
35
|
+
breaking_changes: comparison.breaking_changes,
|
|
36
|
+
non_breaking_changes: comparison.non_breaking,
|
|
37
|
+
new_endpoints: comparison.new_endpoints,
|
|
38
|
+
removed_endpoints: comparison.removed_endpoints,
|
|
39
|
+
}, null, 2),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
} catch (err) {
|
|
44
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
45
|
+
const isLicense = message.includes('TRUSS Pro license');
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: 'text' as const,
|
|
51
|
+
text: JSON.stringify({
|
|
52
|
+
error: message,
|
|
53
|
+
...(isLicense
|
|
54
|
+
? {
|
|
55
|
+
upgrade_url: 'https://truss.dev/pricing',
|
|
56
|
+
price: '$25/mo',
|
|
57
|
+
}
|
|
58
|
+
: {}),
|
|
59
|
+
}, null, 2),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
isError: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { requirePro } from '../lib/license.js';
|
|
4
|
+
import { parseOpenApiSpec } from '../lib/openapi-parser.js';
|
|
5
|
+
import { generateClient } from '../lib/code-generator.js';
|
|
6
|
+
|
|
7
|
+
export function registerGenerateClient(server: McpServer): void {
|
|
8
|
+
server.tool(
|
|
9
|
+
'generate_client',
|
|
10
|
+
'[Pro] Generate a typed API client from an OpenAPI spec. Supports TypeScript (fetch/axios), Python (requests), and Go (net/http). Produces ready-to-use code with type definitions.',
|
|
11
|
+
{
|
|
12
|
+
spec: z.string().describe('OpenAPI/Swagger spec as JSON or YAML string'),
|
|
13
|
+
language: z
|
|
14
|
+
.enum(['typescript', 'python', 'go'])
|
|
15
|
+
.describe('Target language for the client'),
|
|
16
|
+
style: z
|
|
17
|
+
.enum(['fetch', 'axios'])
|
|
18
|
+
.optional()
|
|
19
|
+
.default('fetch')
|
|
20
|
+
.describe('HTTP library style (TypeScript only, default: fetch)'),
|
|
21
|
+
},
|
|
22
|
+
async ({ spec, language, style }) => {
|
|
23
|
+
try {
|
|
24
|
+
await requirePro();
|
|
25
|
+
|
|
26
|
+
const parsed = parseOpenApiSpec(spec);
|
|
27
|
+
const code = generateClient(parsed, language, style);
|
|
28
|
+
|
|
29
|
+
const extensions: Record<string, string> = {
|
|
30
|
+
typescript: '.ts',
|
|
31
|
+
python: '.py',
|
|
32
|
+
go: '.go',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const filename = `api_client${extensions[language]}`;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
content: [
|
|
39
|
+
{
|
|
40
|
+
type: 'text' as const,
|
|
41
|
+
text: JSON.stringify({
|
|
42
|
+
filename,
|
|
43
|
+
language,
|
|
44
|
+
style: language === 'typescript' ? style : undefined,
|
|
45
|
+
endpoint_count: parsed.endpoints.length,
|
|
46
|
+
code,
|
|
47
|
+
}, null, 2),
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
53
|
+
const isLicense = message.includes('TRUSS Pro license');
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
content: [
|
|
57
|
+
{
|
|
58
|
+
type: 'text' as const,
|
|
59
|
+
text: JSON.stringify({
|
|
60
|
+
error: message,
|
|
61
|
+
...(isLicense
|
|
62
|
+
? {
|
|
63
|
+
upgrade_url: 'https://truss.dev/pricing',
|
|
64
|
+
price: '$25/mo',
|
|
65
|
+
}
|
|
66
|
+
: {}),
|
|
67
|
+
}, null, 2),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { requirePro } from '../lib/license.js';
|
|
4
|
+
import { generateOpenApiFromExamples } from '../lib/openapi-parser.js';
|
|
5
|
+
|
|
6
|
+
export function registerGenerateOpenApi(server: McpServer): void {
|
|
7
|
+
server.tool(
|
|
8
|
+
'generate_openapi',
|
|
9
|
+
'[Pro] Auto-generate an OpenAPI 3.0 spec from example requests and responses. Infers schemas, path parameters, query parameters, and response types.',
|
|
10
|
+
{
|
|
11
|
+
examples: z.array(
|
|
12
|
+
z.object({
|
|
13
|
+
method: z.string().describe('HTTP method (GET, POST, etc.)'),
|
|
14
|
+
url: z.string().describe('Full URL of the request'),
|
|
15
|
+
request_body: z.unknown().optional().describe('Request body sent'),
|
|
16
|
+
response_body: z.unknown().describe('Response body received'),
|
|
17
|
+
status: z.number().describe('HTTP status code'),
|
|
18
|
+
headers: z.record(z.string(), z.string()).optional().describe('Request headers'),
|
|
19
|
+
})
|
|
20
|
+
).min(1).describe('Array of example request/response pairs'),
|
|
21
|
+
},
|
|
22
|
+
async ({ examples }) => {
|
|
23
|
+
try {
|
|
24
|
+
await requirePro();
|
|
25
|
+
|
|
26
|
+
const yamlSpec = generateOpenApiFromExamples(examples as import('../types.js').RequestExample[]);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
content: [
|
|
30
|
+
{
|
|
31
|
+
type: 'text' as const,
|
|
32
|
+
text: yamlSpec,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
} catch (err) {
|
|
37
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
+
const isLicense = message.includes('TRUSS Pro license');
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: 'text' as const,
|
|
44
|
+
text: JSON.stringify({
|
|
45
|
+
error: message,
|
|
46
|
+
...(isLicense
|
|
47
|
+
? {
|
|
48
|
+
upgrade_url: 'https://truss.dev/pricing',
|
|
49
|
+
price: '$25/mo',
|
|
50
|
+
features: [
|
|
51
|
+
'Generate OpenAPI specs from examples',
|
|
52
|
+
'Generate API test suites',
|
|
53
|
+
'Mock server generation',
|
|
54
|
+
'Spec comparison & breaking change detection',
|
|
55
|
+
'Multi-language client code generation',
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
: {}),
|
|
59
|
+
}, null, 2),
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
isError: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { requirePro } from '../lib/license.js';
|
|
4
|
+
import { parseOpenApiSpec } from '../lib/openapi-parser.js';
|
|
5
|
+
import { generateTests } from '../lib/code-generator.js';
|
|
6
|
+
|
|
7
|
+
export function registerGenerateTests(server: McpServer): void {
|
|
8
|
+
server.tool(
|
|
9
|
+
'generate_tests',
|
|
10
|
+
'[Pro] Generate a complete API test suite from an OpenAPI spec. Supports Jest, Vitest, and Pytest frameworks. Generates happy-path and error-case tests.',
|
|
11
|
+
{
|
|
12
|
+
spec: z.string().describe('OpenAPI/Swagger spec as JSON or YAML string'),
|
|
13
|
+
base_url: z.string().describe('Base URL of the API to test (e.g., http://localhost:3000)'),
|
|
14
|
+
framework: z
|
|
15
|
+
.enum(['jest', 'vitest', 'pytest'])
|
|
16
|
+
.optional()
|
|
17
|
+
.default('vitest')
|
|
18
|
+
.describe('Test framework to generate for (default: vitest)'),
|
|
19
|
+
},
|
|
20
|
+
async ({ spec, base_url, framework }) => {
|
|
21
|
+
try {
|
|
22
|
+
await requirePro();
|
|
23
|
+
|
|
24
|
+
const parsed = parseOpenApiSpec(spec);
|
|
25
|
+
const testCode = generateTests(parsed, base_url, framework);
|
|
26
|
+
|
|
27
|
+
const filename =
|
|
28
|
+
framework === 'pytest'
|
|
29
|
+
? 'test_api.py'
|
|
30
|
+
: `api.test.${framework === 'jest' ? 'js' : 'ts'}`;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: 'text' as const,
|
|
36
|
+
text: JSON.stringify({
|
|
37
|
+
filename,
|
|
38
|
+
framework,
|
|
39
|
+
endpoint_count: parsed.endpoints.length,
|
|
40
|
+
code: testCode,
|
|
41
|
+
}, null, 2),
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
} catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
47
|
+
const isLicense = message.includes('TRUSS Pro license');
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: 'text' as const,
|
|
53
|
+
text: JSON.stringify({
|
|
54
|
+
error: message,
|
|
55
|
+
...(isLicense
|
|
56
|
+
? {
|
|
57
|
+
upgrade_url: 'https://truss.dev/pricing',
|
|
58
|
+
price: '$25/mo',
|
|
59
|
+
}
|
|
60
|
+
: {}),
|
|
61
|
+
}, null, 2),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { requirePro } from '../lib/license.js';
|
|
4
|
+
import { parseOpenApiSpec } from '../lib/openapi-parser.js';
|
|
5
|
+
import { generateMockServer } from '../lib/code-generator.js';
|
|
6
|
+
|
|
7
|
+
export function registerMockServer(server: McpServer): void {
|
|
8
|
+
server.tool(
|
|
9
|
+
'mock_server',
|
|
10
|
+
'[Pro] Generate a mock server configuration from an OpenAPI spec. Produces routes with sample responses and a db.json file ready for json-server or similar.',
|
|
11
|
+
{
|
|
12
|
+
spec: z.string().describe('OpenAPI/Swagger spec as JSON or YAML string'),
|
|
13
|
+
},
|
|
14
|
+
async ({ spec }) => {
|
|
15
|
+
try {
|
|
16
|
+
await requirePro();
|
|
17
|
+
|
|
18
|
+
const parsed = parseOpenApiSpec(spec);
|
|
19
|
+
const mockConfig = generateMockServer(parsed);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: 'text' as const,
|
|
25
|
+
text: JSON.stringify({
|
|
26
|
+
route_count: mockConfig.routes.length,
|
|
27
|
+
routes: mockConfig.routes,
|
|
28
|
+
mock_data: mockConfig.mock_data,
|
|
29
|
+
usage_instructions: {
|
|
30
|
+
json_server: [
|
|
31
|
+
'1. Save mock_data to db.json',
|
|
32
|
+
'2. npx json-server db.json --port 3001',
|
|
33
|
+
'3. Access endpoints at http://localhost:3001',
|
|
34
|
+
],
|
|
35
|
+
custom: [
|
|
36
|
+
'Use the routes array to configure any mock server.',
|
|
37
|
+
'Each route has: method, path, status, response, headers.',
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
}, null, 2),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
} catch (err) {
|
|
45
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
46
|
+
const isLicense = message.includes('TRUSS Pro license');
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: 'text' as const,
|
|
52
|
+
text: JSON.stringify({
|
|
53
|
+
error: message,
|
|
54
|
+
...(isLicense
|
|
55
|
+
? {
|
|
56
|
+
upgrade_url: 'https://truss.dev/pricing',
|
|
57
|
+
price: '$25/mo',
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
60
|
+
}, null, 2),
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { parseOpenApiSpec } from '../lib/openapi-parser.js';
|
|
4
|
+
|
|
5
|
+
export function registerParseOpenApi(server: McpServer): void {
|
|
6
|
+
server.tool(
|
|
7
|
+
'parse_openapi',
|
|
8
|
+
'Parse an OpenAPI 3.x or Swagger 2.0 spec (JSON or YAML) and list all endpoints with their parameters, request bodies, and response schemas.',
|
|
9
|
+
{
|
|
10
|
+
spec: z.string().describe('OpenAPI/Swagger spec content as JSON or YAML string'),
|
|
11
|
+
},
|
|
12
|
+
async ({ spec }) => {
|
|
13
|
+
try {
|
|
14
|
+
const parsed = parseOpenApiSpec(spec);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
content: [
|
|
18
|
+
{
|
|
19
|
+
type: 'text' as const,
|
|
20
|
+
text: JSON.stringify({
|
|
21
|
+
info: parsed.info,
|
|
22
|
+
servers: parsed.servers,
|
|
23
|
+
endpoint_count: parsed.endpoints.length,
|
|
24
|
+
endpoints: parsed.endpoints.map((ep) => ({
|
|
25
|
+
method: ep.method,
|
|
26
|
+
path: ep.path,
|
|
27
|
+
summary: ep.summary,
|
|
28
|
+
operationId: ep.operationId,
|
|
29
|
+
tags: ep.tags,
|
|
30
|
+
parameters: ep.parameters,
|
|
31
|
+
request_body: ep.requestBody,
|
|
32
|
+
responses: ep.responses,
|
|
33
|
+
})),
|
|
34
|
+
}, null, 2),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: 'text' as const,
|
|
43
|
+
text: JSON.stringify({
|
|
44
|
+
error: err instanceof Error ? err.message : String(err),
|
|
45
|
+
hint: 'Provide a valid OpenAPI 3.x or Swagger 2.0 spec as JSON or YAML string.',
|
|
46
|
+
}, null, 2),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
isError: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
}
|