@stackkedjohn/mcp-factory-cli 0.1.1 → 0.2.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.
@@ -0,0 +1,354 @@
1
+ import drafterModule from 'drafter.js';
2
+ import { ParseError } from '../utils/errors.js';
3
+ // Type assertion for drafter.js API (type definitions are incomplete)
4
+ const drafter = drafterModule;
5
+ // Helper to check if element has a class
6
+ function hasClass(element, className) {
7
+ if (!element?.meta?.classes)
8
+ return false;
9
+ if (element.meta.classes.element === 'array') {
10
+ return element.meta.classes.content.some((c) => c.content === className);
11
+ }
12
+ return false;
13
+ }
14
+ export async function parseAPIBlueprint(content) {
15
+ try {
16
+ // Parse the API Blueprint document using drafter.js (synchronous)
17
+ const result = drafter.parseSync(content, {});
18
+ // Check for parsing errors
19
+ if (result.error) {
20
+ throw new ParseError(`API Blueprint parsing failed: ${result.error.message}`);
21
+ }
22
+ const ast = result;
23
+ // Extract API metadata
24
+ const apiMetadata = ast.content[0];
25
+ const name = apiMetadata.meta?.title?.content || 'Untitled API';
26
+ const baseUrl = extractBaseUrl(ast);
27
+ // Extract all endpoints
28
+ const endpoints = [];
29
+ for (const resourceGroup of ast.content) {
30
+ if (resourceGroup.element === 'category' && hasClass(resourceGroup, 'resourceGroup')) {
31
+ for (const resource of resourceGroup.content) {
32
+ if (resource.element === 'resource') {
33
+ const resourcePath = resource.attributes?.href?.content || '';
34
+ for (const transition of resource.content) {
35
+ if (transition.element === 'transition') {
36
+ const transaction = transition.content.find((c) => c.element === 'httpTransaction');
37
+ if (transaction) {
38
+ const endpoint = parseAction(transaction, resourcePath, transition);
39
+ endpoints.push(endpoint);
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ // Also check for resources at the top level (like in api category)
47
+ if (resourceGroup.element === 'category' && hasClass(resourceGroup, 'api')) {
48
+ for (const item of resourceGroup.content) {
49
+ if (item.element === 'resource') {
50
+ const resourcePath = item.attributes?.href?.content || '';
51
+ for (const transition of item.content) {
52
+ if (transition.element === 'transition') {
53
+ const transaction = transition.content.find((c) => c.element === 'httpTransaction');
54
+ if (transaction) {
55
+ const endpoint = parseAction(transaction, resourcePath, transition);
56
+ endpoints.push(endpoint);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ }
62
+ }
63
+ // Also check for resources at the very top level
64
+ if (resourceGroup.element === 'resource') {
65
+ const resourcePath = resourceGroup.attributes?.href?.content || '';
66
+ for (const transition of resourceGroup.content) {
67
+ if (transition.element === 'transition') {
68
+ const transaction = transition.content.find((c) => c.element === 'httpTransaction');
69
+ if (transaction) {
70
+ const endpoint = parseAction(transaction, resourcePath, transition);
71
+ endpoints.push(endpoint);
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ // Detect authentication
78
+ const auth = detectAuth(ast);
79
+ return {
80
+ name,
81
+ baseUrl,
82
+ auth,
83
+ endpoints,
84
+ };
85
+ }
86
+ catch (error) {
87
+ if (error instanceof ParseError) {
88
+ throw error;
89
+ }
90
+ throw new ParseError(`Failed to parse API Blueprint: ${error.message}`);
91
+ }
92
+ }
93
+ function extractBaseUrl(ast) {
94
+ // Look for HOST in metadata within category
95
+ for (const element of ast.content) {
96
+ if (element.element === 'category' && hasClass(element, 'api')) {
97
+ // Check attributes.metadata array
98
+ if (element.attributes?.metadata?.element === 'array') {
99
+ for (const metaItem of element.attributes.metadata.content) {
100
+ if (metaItem.element === 'member') {
101
+ const key = metaItem.content?.key?.content;
102
+ if (key === 'HOST') {
103
+ return metaItem.content.value.content;
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ return 'https://api.example.com';
111
+ }
112
+ function parseAction(transaction, resourcePath, transition) {
113
+ const request = transaction.content.find((c) => c.element === 'httpRequest');
114
+ const response = transaction.content.find((c) => c.element === 'httpResponse');
115
+ const method = (request?.attributes?.method?.content || 'GET').toUpperCase();
116
+ const path = request?.attributes?.href?.content || resourcePath;
117
+ // Extract description - check transition's copy element first, then meta.title
118
+ let description = '';
119
+ const copyElement = transition.content.find((c) => c.element === 'copy');
120
+ if (copyElement?.content) {
121
+ description = copyElement.content;
122
+ }
123
+ else if (transition.meta?.title?.content) {
124
+ description = transition.meta.title.content;
125
+ }
126
+ else if (transaction.meta?.title?.content) {
127
+ description = transaction.meta.title.content;
128
+ }
129
+ else if (request?.meta?.title?.content) {
130
+ description = request.meta.title.content;
131
+ }
132
+ // Parse parameters
133
+ const parameters = parseParameters(request);
134
+ // Parse request body
135
+ const requestBody = parseRequestBody(request);
136
+ // Parse response
137
+ const responseSchema = parseResponse(response);
138
+ // Parse errors (look for non-2xx responses)
139
+ const errors = parseErrors(transaction);
140
+ const id = `${method.toLowerCase()}-${path.replace(/\//g, '-').replace(/[{}]/g, '')}`;
141
+ return {
142
+ id,
143
+ method,
144
+ path,
145
+ description,
146
+ parameters,
147
+ requestBody,
148
+ response: responseSchema,
149
+ errors,
150
+ };
151
+ }
152
+ function parseParameters(request) {
153
+ const parameters = [];
154
+ if (!request?.attributes?.hrefVariables) {
155
+ return parameters;
156
+ }
157
+ const hrefVariables = request.attributes.hrefVariables.content;
158
+ for (const variable of hrefVariables) {
159
+ if (variable.element === 'member') {
160
+ const name = variable.content.key.content;
161
+ const description = variable.meta?.description?.content || '';
162
+ const required = variable.attributes?.typeAttributes?.includes('required') || false;
163
+ // Determine parameter location
164
+ let location = 'path';
165
+ if (variable.attributes?.typeAttributes?.includes('query')) {
166
+ location = 'query';
167
+ }
168
+ else if (variable.attributes?.typeAttributes?.includes('header')) {
169
+ location = 'header';
170
+ }
171
+ const schema = mapTypeToSchemaType(variable.content.value.element);
172
+ parameters.push({
173
+ name,
174
+ in: location,
175
+ description,
176
+ required,
177
+ schema,
178
+ });
179
+ }
180
+ }
181
+ return parameters;
182
+ }
183
+ function parseRequestBody(request) {
184
+ if (!request?.content) {
185
+ return undefined;
186
+ }
187
+ const asset = request.content.find((c) => c.element === 'asset');
188
+ if (!asset) {
189
+ return undefined;
190
+ }
191
+ const contentType = asset.attributes?.contentType?.content || 'application/json';
192
+ const bodyContent = asset.content;
193
+ // Try to parse body as JSON to infer schema
194
+ let schema = { type: 'object' };
195
+ if (bodyContent) {
196
+ try {
197
+ const parsed = JSON.parse(bodyContent);
198
+ schema = inferSchemaFromValue(parsed);
199
+ }
200
+ catch {
201
+ // If not JSON, keep as generic object
202
+ schema = { type: 'object' };
203
+ }
204
+ }
205
+ return {
206
+ required: true,
207
+ contentType,
208
+ schema,
209
+ };
210
+ }
211
+ function parseResponse(response) {
212
+ const statusCode = parseInt(response?.attributes?.statusCode?.content || '200');
213
+ const description = response?.meta?.title?.content || '';
214
+ let contentType = 'application/json';
215
+ let schema = { type: 'object' };
216
+ if (response?.content) {
217
+ const asset = response.content.find((c) => c.element === 'asset');
218
+ if (asset) {
219
+ contentType = asset.attributes?.contentType?.content || 'application/json';
220
+ const bodyContent = asset.content;
221
+ if (bodyContent) {
222
+ try {
223
+ const parsed = JSON.parse(bodyContent);
224
+ schema = inferSchemaFromValue(parsed);
225
+ }
226
+ catch {
227
+ schema = { type: 'object' };
228
+ }
229
+ }
230
+ }
231
+ }
232
+ return {
233
+ statusCode,
234
+ description,
235
+ contentType,
236
+ schema,
237
+ };
238
+ }
239
+ function parseErrors(transaction) {
240
+ const errors = [];
241
+ if (!transaction?.content) {
242
+ return errors;
243
+ }
244
+ for (const item of transaction.content) {
245
+ if (item.element === 'httpResponse') {
246
+ const statusCode = parseInt(item.attributes?.statusCode?.content || '200');
247
+ // Only include error responses (4xx and 5xx)
248
+ if (statusCode >= 400) {
249
+ const description = item.meta?.title?.content || `Error ${statusCode}`;
250
+ let schema;
251
+ const asset = item.content?.find((c) => c.element === 'asset');
252
+ if (asset?.content) {
253
+ try {
254
+ const parsed = JSON.parse(asset.content);
255
+ schema = inferSchemaFromValue(parsed);
256
+ }
257
+ catch {
258
+ schema = { type: 'object' };
259
+ }
260
+ }
261
+ errors.push({
262
+ statusCode,
263
+ description,
264
+ schema,
265
+ });
266
+ }
267
+ }
268
+ }
269
+ return errors;
270
+ }
271
+ function detectAuth(ast) {
272
+ // Convert AST to string for pattern matching
273
+ const astString = JSON.stringify(ast);
274
+ // Look for common authentication patterns
275
+ if (astString.includes('Authorization') || astString.includes('bearer')) {
276
+ return {
277
+ type: 'bearer',
278
+ location: 'header',
279
+ name: 'Authorization',
280
+ description: 'Bearer token authentication',
281
+ };
282
+ }
283
+ if (astString.includes('api-key') || astString.includes('apiKey') || astString.includes('X-API-Key')) {
284
+ return {
285
+ type: 'api-key',
286
+ location: 'header',
287
+ name: 'X-API-Key',
288
+ description: 'API key authentication',
289
+ };
290
+ }
291
+ if (astString.includes('oauth') || astString.includes('OAuth')) {
292
+ return {
293
+ type: 'oauth',
294
+ description: 'OAuth 2.0 authentication',
295
+ };
296
+ }
297
+ if (astString.includes('Basic')) {
298
+ return {
299
+ type: 'basic',
300
+ description: 'Basic authentication',
301
+ };
302
+ }
303
+ return {
304
+ type: 'none',
305
+ };
306
+ }
307
+ function mapTypeToSchemaType(element) {
308
+ switch (element) {
309
+ case 'string':
310
+ return { type: 'string' };
311
+ case 'number':
312
+ return { type: 'number' };
313
+ case 'boolean':
314
+ return { type: 'boolean' };
315
+ case 'array':
316
+ return { type: 'array', items: { type: 'object' } };
317
+ case 'object':
318
+ return { type: 'object' };
319
+ default:
320
+ return { type: 'string' };
321
+ }
322
+ }
323
+ function inferSchemaFromValue(value) {
324
+ if (Array.isArray(value)) {
325
+ const itemSchema = value.length > 0 ? inferSchemaFromValue(value[0]) : { type: 'object' };
326
+ return {
327
+ type: 'array',
328
+ items: itemSchema,
329
+ };
330
+ }
331
+ if (typeof value === 'object' && value !== null) {
332
+ const properties = {};
333
+ const required = [];
334
+ for (const [key, val] of Object.entries(value)) {
335
+ properties[key] = inferSchemaFromValue(val);
336
+ required.push(key);
337
+ }
338
+ return {
339
+ type: 'object',
340
+ properties,
341
+ required,
342
+ };
343
+ }
344
+ if (typeof value === 'string') {
345
+ return { type: 'string' };
346
+ }
347
+ if (typeof value === 'number') {
348
+ return { type: 'number' };
349
+ }
350
+ if (typeof value === 'boolean') {
351
+ return { type: 'boolean' };
352
+ }
353
+ return { type: 'object' };
354
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,59 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { parseAPIBlueprint } from './apib-parser.js';
4
+ describe('API Blueprint Parser', () => {
5
+ it('should parse a basic API Blueprint document', async () => {
6
+ const apibContent = `
7
+ FORMAT: 1A
8
+ HOST: https://api.example.com
9
+
10
+ # My API
11
+ This is my API description.
12
+
13
+ ## GET /users
14
+ Get a list of users
15
+
16
+ + Response 200 (application/json)
17
+ + Body
18
+
19
+ [
20
+ {
21
+ "id": 1,
22
+ "name": "John Doe"
23
+ }
24
+ ]
25
+
26
+ ## POST /users
27
+ Create a new user
28
+
29
+ + Request (application/json)
30
+ + Body
31
+
32
+ {
33
+ "name": "John Doe"
34
+ }
35
+
36
+ + Response 201 (application/json)
37
+ + Body
38
+
39
+ {
40
+ "id": 1,
41
+ "name": "John Doe"
42
+ }
43
+ `;
44
+ const schema = await parseAPIBlueprint(apibContent);
45
+ assert.strictEqual(schema.name, 'My API');
46
+ assert.strictEqual(schema.baseUrl, 'https://api.example.com');
47
+ assert.strictEqual(schema.endpoints.length, 2);
48
+ // Check GET endpoint
49
+ const getEndpoint = schema.endpoints.find((e) => e.method === 'GET');
50
+ assert.ok(getEndpoint);
51
+ assert.strictEqual(getEndpoint.path, '/users');
52
+ assert.strictEqual(getEndpoint.description, 'Get a list of users');
53
+ // Check POST endpoint
54
+ const postEndpoint = schema.endpoints.find((e) => e.method === 'POST');
55
+ assert.ok(postEndpoint);
56
+ assert.strictEqual(postEndpoint.path, '/users');
57
+ assert.strictEqual(postEndpoint.description, 'Create a new user');
58
+ });
59
+ });
@@ -1,4 +1,4 @@
1
- export type InputFormat = 'openapi' | 'swagger' | 'postman' | 'unknown';
1
+ export type InputFormat = 'openapi' | 'swagger' | 'postman' | 'apib' | 'unknown';
2
2
  export interface DetectionResult {
3
3
  format: InputFormat;
4
4
  content: any;
@@ -10,6 +10,14 @@ export async function detectFormat(input) {
10
10
  catch {
11
11
  throw new ParseError(`Could not read file: ${input}`);
12
12
  }
13
+ // Check file extension for API Blueprint
14
+ if (input.endsWith('.apib') || input.endsWith('.apiblueprint')) {
15
+ return { format: 'apib', content };
16
+ }
17
+ // Alternatively, check content for API Blueprint markers
18
+ if (content.trim().startsWith('FORMAT: 1A')) {
19
+ return { format: 'apib', content };
20
+ }
13
21
  // Try parsing as JSON
14
22
  let parsed;
15
23
  try {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { detectFormat } from './detector.js';
4
+ import * as fs from 'fs/promises';
5
+ import * as path from 'path';
6
+ describe('Format Detector', () => {
7
+ it('should detect API Blueprint format from .apib file', async () => {
8
+ // Create temp .apib file
9
+ const tempFile = path.join(process.cwd(), 'test.apib');
10
+ await fs.writeFile(tempFile, 'FORMAT: 1A\n# My API\n## GET /users', 'utf-8');
11
+ try {
12
+ const result = await detectFormat(tempFile);
13
+ assert.strictEqual(result.format, 'apib');
14
+ assert.ok(result.content.includes('FORMAT: 1A'));
15
+ }
16
+ finally {
17
+ await fs.unlink(tempFile);
18
+ }
19
+ });
20
+ });