@stackkedjohn/mcp-factory-cli 0.1.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/README.md +100 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +33 -0
- package/dist/commands/create.d.ts +4 -0
- package/dist/commands/create.js +56 -0
- package/dist/commands/install.d.ts +1 -0
- package/dist/commands/install.js +79 -0
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +24 -0
- package/dist/commands/validate.d.ts +1 -0
- package/dist/commands/validate.js +27 -0
- package/dist/generator/analyzer.d.ts +2 -0
- package/dist/generator/analyzer.js +14 -0
- package/dist/generator/engine.d.ts +10 -0
- package/dist/generator/engine.js +46 -0
- package/dist/parsers/ai-parser.d.ts +2 -0
- package/dist/parsers/ai-parser.js +7 -0
- package/dist/parsers/detector.d.ts +6 -0
- package/dist/parsers/detector.js +38 -0
- package/dist/parsers/openapi.d.ts +5 -0
- package/dist/parsers/openapi.js +205 -0
- package/dist/parsers/postman.d.ts +2 -0
- package/dist/parsers/postman.js +4 -0
- package/dist/registry/manager.d.ts +13 -0
- package/dist/registry/manager.js +43 -0
- package/dist/schema/api-schema.d.ts +77 -0
- package/dist/schema/api-schema.js +1 -0
- package/dist/utils/errors.d.ts +13 -0
- package/dist/utils/errors.js +26 -0
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.js +19 -0
- package/docs/plans/2026-02-02-mcp-factory-design.md +306 -0
- package/docs/plans/2026-02-02-mcp-factory-implementation.md +1866 -0
- package/package.json +48 -0
- package/src/cli.ts +41 -0
- package/src/commands/create.ts +65 -0
- package/src/commands/install.ts +92 -0
- package/src/commands/list.ts +28 -0
- package/src/commands/validate.ts +29 -0
- package/src/generator/analyzer.ts +20 -0
- package/src/generator/engine.ts +73 -0
- package/src/parsers/ai-parser.ts +10 -0
- package/src/parsers/detector.ts +49 -0
- package/src/parsers/openapi.ts +238 -0
- package/src/parsers/postman.ts +6 -0
- package/src/registry/manager.ts +62 -0
- package/src/schema/api-schema.ts +87 -0
- package/src/utils/errors.ts +27 -0
- package/src/utils/logger.ts +23 -0
- package/templates/README.md.hbs +40 -0
- package/templates/client.ts.hbs +45 -0
- package/templates/index.ts.hbs +36 -0
- package/templates/package.json.hbs +20 -0
- package/templates/test.ts.hbs +1 -0
- package/templates/tools.ts.hbs +38 -0
- package/templates/tsconfig.json.hbs +13 -0
- package/templates/types.ts.hbs +1 -0
- package/templates/validation.ts.hbs +1 -0
- package/test-fixtures/weather-api.json +49 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse OpenAPI 3.x or Swagger 2.0 specification into unified APISchema
|
|
3
|
+
*/
|
|
4
|
+
export function parseOpenAPI(spec) {
|
|
5
|
+
const isSwagger2 = spec.swagger === '2.0';
|
|
6
|
+
const isOpenAPI3 = spec.openapi?.startsWith('3.');
|
|
7
|
+
if (!isSwagger2 && !isOpenAPI3) {
|
|
8
|
+
throw new Error('Unsupported spec version. Only OpenAPI 3.x and Swagger 2.0 are supported.');
|
|
9
|
+
}
|
|
10
|
+
return {
|
|
11
|
+
name: spec.info.title || 'API',
|
|
12
|
+
baseUrl: getBaseUrl(spec),
|
|
13
|
+
auth: detectAuth(spec),
|
|
14
|
+
endpoints: parseEndpoints(spec),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Extract base URL from spec
|
|
19
|
+
*/
|
|
20
|
+
function getBaseUrl(spec) {
|
|
21
|
+
// OpenAPI 3.x
|
|
22
|
+
if (spec.servers && spec.servers.length > 0) {
|
|
23
|
+
return spec.servers[0].url;
|
|
24
|
+
}
|
|
25
|
+
// Swagger 2.0
|
|
26
|
+
if (spec.host) {
|
|
27
|
+
const scheme = spec.schemes?.[0] || 'https';
|
|
28
|
+
const basePath = spec.basePath || '';
|
|
29
|
+
return `${scheme}://${spec.host}${basePath}`;
|
|
30
|
+
}
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Detect authentication configuration
|
|
35
|
+
*/
|
|
36
|
+
function detectAuth(spec) {
|
|
37
|
+
const securitySchemes = spec.components?.securitySchemes || spec.securityDefinitions || {};
|
|
38
|
+
const firstScheme = Object.values(securitySchemes)[0];
|
|
39
|
+
if (!firstScheme) {
|
|
40
|
+
return { type: 'none' };
|
|
41
|
+
}
|
|
42
|
+
switch (firstScheme.type) {
|
|
43
|
+
case 'apiKey':
|
|
44
|
+
return {
|
|
45
|
+
type: 'api-key',
|
|
46
|
+
location: firstScheme.in === 'header' ? 'header' : 'query',
|
|
47
|
+
name: firstScheme.name,
|
|
48
|
+
description: firstScheme.description,
|
|
49
|
+
};
|
|
50
|
+
case 'http':
|
|
51
|
+
if (firstScheme.scheme === 'bearer') {
|
|
52
|
+
return { type: 'bearer', description: firstScheme.description };
|
|
53
|
+
}
|
|
54
|
+
if (firstScheme.scheme === 'basic') {
|
|
55
|
+
return { type: 'basic', description: firstScheme.description };
|
|
56
|
+
}
|
|
57
|
+
return { type: 'none' };
|
|
58
|
+
case 'oauth2':
|
|
59
|
+
return { type: 'oauth', description: firstScheme.description };
|
|
60
|
+
default:
|
|
61
|
+
return { type: 'none' };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Parse all endpoints from spec
|
|
66
|
+
*/
|
|
67
|
+
function parseEndpoints(spec) {
|
|
68
|
+
const endpoints = [];
|
|
69
|
+
const paths = spec.paths || {};
|
|
70
|
+
for (const [path, pathItem] of Object.entries(paths)) {
|
|
71
|
+
const methods = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head'];
|
|
72
|
+
for (const method of methods) {
|
|
73
|
+
const operation = pathItem[method];
|
|
74
|
+
if (operation) {
|
|
75
|
+
endpoints.push(parseOperation(path, method, operation, pathItem));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return endpoints;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse a single operation into an Endpoint
|
|
83
|
+
*/
|
|
84
|
+
function parseOperation(path, method, operation, pathItem) {
|
|
85
|
+
const parameters = [];
|
|
86
|
+
// Parse path-level parameters
|
|
87
|
+
if (pathItem.parameters) {
|
|
88
|
+
parameters.push(...parseParameters(pathItem.parameters));
|
|
89
|
+
}
|
|
90
|
+
// Parse operation-level parameters
|
|
91
|
+
if (operation.parameters) {
|
|
92
|
+
parameters.push(...parseParameters(operation.parameters));
|
|
93
|
+
}
|
|
94
|
+
// Parse request body (OpenAPI 3.x)
|
|
95
|
+
let requestBody;
|
|
96
|
+
if (operation.requestBody) {
|
|
97
|
+
const content = operation.requestBody.content || {};
|
|
98
|
+
const jsonContent = content['application/json'];
|
|
99
|
+
if (jsonContent?.schema) {
|
|
100
|
+
requestBody = {
|
|
101
|
+
description: operation.requestBody.description || '',
|
|
102
|
+
required: operation.requestBody.required || false,
|
|
103
|
+
contentType: 'application/json',
|
|
104
|
+
schema: parseSchema(jsonContent.schema),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Parse response (use first successful response)
|
|
109
|
+
const responses = operation.responses || {};
|
|
110
|
+
const successCode = Object.keys(responses).find(code => code.startsWith('2')) || '200';
|
|
111
|
+
const successResponse = responses[successCode] || {};
|
|
112
|
+
const responseContent = successResponse.content?.['application/json'];
|
|
113
|
+
const response = {
|
|
114
|
+
statusCode: parseInt(successCode),
|
|
115
|
+
description: successResponse.description || '',
|
|
116
|
+
contentType: 'application/json',
|
|
117
|
+
schema: responseContent?.schema ? parseSchema(responseContent.schema) : { type: 'object' },
|
|
118
|
+
};
|
|
119
|
+
// Parse error responses
|
|
120
|
+
const errors = [];
|
|
121
|
+
for (const [code, resp] of Object.entries(responses)) {
|
|
122
|
+
if (!code.startsWith('2') && code !== 'default') {
|
|
123
|
+
const errorResp = resp;
|
|
124
|
+
const errorContent = errorResp.content?.['application/json'];
|
|
125
|
+
errors.push({
|
|
126
|
+
statusCode: parseInt(code),
|
|
127
|
+
description: errorResp.description || `Error ${code}`,
|
|
128
|
+
schema: errorContent?.schema ? parseSchema(errorContent.schema) : undefined,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
id: operation.operationId || `${method}_${path.replace(/\//g, '_')}`,
|
|
134
|
+
method: method.toUpperCase(),
|
|
135
|
+
path,
|
|
136
|
+
description: operation.description || operation.summary || '',
|
|
137
|
+
parameters,
|
|
138
|
+
requestBody,
|
|
139
|
+
response,
|
|
140
|
+
errors,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Parse parameters array
|
|
145
|
+
*/
|
|
146
|
+
function parseParameters(params) {
|
|
147
|
+
return params.map((param) => {
|
|
148
|
+
// Handle $ref
|
|
149
|
+
if (param.$ref) {
|
|
150
|
+
// For simplicity, we'll skip resolving refs
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
// Only parse path, query, and header parameters (not body)
|
|
154
|
+
if (!['path', 'query', 'header'].includes(param.in)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
const schema = param.schema || { type: param.type };
|
|
158
|
+
return {
|
|
159
|
+
name: param.name,
|
|
160
|
+
in: param.in,
|
|
161
|
+
required: param.required || false,
|
|
162
|
+
description: param.description || '',
|
|
163
|
+
schema: parseSchema(schema),
|
|
164
|
+
};
|
|
165
|
+
}).filter(Boolean);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Parse JSON Schema into SchemaType
|
|
169
|
+
*/
|
|
170
|
+
function parseSchema(schema) {
|
|
171
|
+
if (!schema) {
|
|
172
|
+
return { type: 'string' };
|
|
173
|
+
}
|
|
174
|
+
// Handle basic types
|
|
175
|
+
if (schema.type === 'object' || schema.properties) {
|
|
176
|
+
const properties = {};
|
|
177
|
+
const required = schema.required || [];
|
|
178
|
+
for (const [key, value] of Object.entries(schema.properties || {})) {
|
|
179
|
+
properties[key] = parseSchema(value);
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties,
|
|
184
|
+
required,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
if (schema.type === 'array') {
|
|
188
|
+
return {
|
|
189
|
+
type: 'array',
|
|
190
|
+
items: parseSchema(schema.items),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
// Enum
|
|
194
|
+
if (schema.enum) {
|
|
195
|
+
return {
|
|
196
|
+
type: schema.type || 'string',
|
|
197
|
+
enum: schema.enum,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
// Basic types with format
|
|
201
|
+
return {
|
|
202
|
+
type: schema.type || 'string',
|
|
203
|
+
format: schema.format,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface RegistryEntry {
|
|
2
|
+
name: string;
|
|
3
|
+
path: string;
|
|
4
|
+
createdAt: string;
|
|
5
|
+
}
|
|
6
|
+
export interface Registry {
|
|
7
|
+
servers: RegistryEntry[];
|
|
8
|
+
}
|
|
9
|
+
export declare function loadRegistry(): Promise<Registry>;
|
|
10
|
+
export declare function saveRegistry(registry: Registry): Promise<void>;
|
|
11
|
+
export declare function addServer(name: string, serverPath: string): Promise<void>;
|
|
12
|
+
export declare function getServer(name: string): Promise<RegistryEntry | undefined>;
|
|
13
|
+
export declare function listServers(): Promise<RegistryEntry[]>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
const REGISTRY_DIR = path.join(os.homedir(), '.mcp-factory');
|
|
5
|
+
const REGISTRY_FILE = path.join(REGISTRY_DIR, 'registry.json');
|
|
6
|
+
async function ensureRegistry() {
|
|
7
|
+
try {
|
|
8
|
+
await fs.access(REGISTRY_FILE);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
await fs.mkdir(REGISTRY_DIR, { recursive: true });
|
|
12
|
+
await fs.writeFile(REGISTRY_FILE, JSON.stringify({ servers: [] }, null, 2));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function loadRegistry() {
|
|
16
|
+
await ensureRegistry();
|
|
17
|
+
const content = await fs.readFile(REGISTRY_FILE, 'utf-8');
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
}
|
|
20
|
+
export async function saveRegistry(registry) {
|
|
21
|
+
await ensureRegistry();
|
|
22
|
+
await fs.writeFile(REGISTRY_FILE, JSON.stringify(registry, null, 2));
|
|
23
|
+
}
|
|
24
|
+
export async function addServer(name, serverPath) {
|
|
25
|
+
const registry = await loadRegistry();
|
|
26
|
+
// Remove existing entry if present
|
|
27
|
+
registry.servers = registry.servers.filter(s => s.name !== name);
|
|
28
|
+
// Add new entry
|
|
29
|
+
registry.servers.push({
|
|
30
|
+
name,
|
|
31
|
+
path: path.resolve(serverPath),
|
|
32
|
+
createdAt: new Date().toISOString(),
|
|
33
|
+
});
|
|
34
|
+
await saveRegistry(registry);
|
|
35
|
+
}
|
|
36
|
+
export async function getServer(name) {
|
|
37
|
+
const registry = await loadRegistry();
|
|
38
|
+
return registry.servers.find(s => s.name === name);
|
|
39
|
+
}
|
|
40
|
+
export async function listServers() {
|
|
41
|
+
const registry = await loadRegistry();
|
|
42
|
+
return registry.servers;
|
|
43
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export interface APISchema {
|
|
2
|
+
name: string;
|
|
3
|
+
baseUrl: string;
|
|
4
|
+
auth: AuthConfig;
|
|
5
|
+
endpoints: Endpoint[];
|
|
6
|
+
commonHeaders?: Record<string, string>;
|
|
7
|
+
rateLimit?: RateLimitConfig;
|
|
8
|
+
pagination?: PaginationConfig;
|
|
9
|
+
}
|
|
10
|
+
export interface AuthConfig {
|
|
11
|
+
type: 'api-key' | 'bearer' | 'oauth' | 'basic' | 'none';
|
|
12
|
+
location?: 'header' | 'query';
|
|
13
|
+
name?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface Endpoint {
|
|
17
|
+
id: string;
|
|
18
|
+
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
19
|
+
path: string;
|
|
20
|
+
description: string;
|
|
21
|
+
parameters: Parameter[];
|
|
22
|
+
requestBody?: RequestBody;
|
|
23
|
+
response: ResponseSchema;
|
|
24
|
+
errors: ErrorSchema[];
|
|
25
|
+
}
|
|
26
|
+
export interface Parameter {
|
|
27
|
+
name: string;
|
|
28
|
+
in: 'path' | 'query' | 'header';
|
|
29
|
+
description?: string;
|
|
30
|
+
required: boolean;
|
|
31
|
+
schema: SchemaType;
|
|
32
|
+
}
|
|
33
|
+
export interface RequestBody {
|
|
34
|
+
description?: string;
|
|
35
|
+
required: boolean;
|
|
36
|
+
contentType: string;
|
|
37
|
+
schema: SchemaType;
|
|
38
|
+
}
|
|
39
|
+
export interface ResponseSchema {
|
|
40
|
+
statusCode: number;
|
|
41
|
+
description?: string;
|
|
42
|
+
contentType: string;
|
|
43
|
+
schema: SchemaType;
|
|
44
|
+
}
|
|
45
|
+
export interface ErrorSchema {
|
|
46
|
+
statusCode: number;
|
|
47
|
+
description: string;
|
|
48
|
+
schema?: SchemaType;
|
|
49
|
+
}
|
|
50
|
+
export interface SchemaType {
|
|
51
|
+
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
|
|
52
|
+
properties?: Record<string, SchemaType>;
|
|
53
|
+
items?: SchemaType;
|
|
54
|
+
required?: string[];
|
|
55
|
+
enum?: string[];
|
|
56
|
+
format?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface RateLimitConfig {
|
|
59
|
+
strategy: 'header-based' | 'retry-after' | 'none';
|
|
60
|
+
headerName?: string;
|
|
61
|
+
requestsPerWindow?: number;
|
|
62
|
+
windowSeconds?: number;
|
|
63
|
+
}
|
|
64
|
+
export interface PaginationConfig {
|
|
65
|
+
style: 'cursor' | 'offset' | 'page' | 'link-header';
|
|
66
|
+
cursorParam?: string;
|
|
67
|
+
limitParam?: string;
|
|
68
|
+
offsetParam?: string;
|
|
69
|
+
pageParam?: string;
|
|
70
|
+
}
|
|
71
|
+
export interface DetectedPatterns {
|
|
72
|
+
authPattern: 'api-key' | 'bearer' | 'oauth' | 'basic' | 'none';
|
|
73
|
+
paginationStyle?: 'cursor' | 'offset' | 'page' | 'link-header';
|
|
74
|
+
rateLimitStrategy?: 'header-based' | 'retry-after' | 'none';
|
|
75
|
+
errorFormat: 'standard' | 'custom';
|
|
76
|
+
hasWebhooks: boolean;
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class MCPFactoryError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
constructor(message: string, code: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class ParseError extends MCPFactoryError {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
export declare class ValidationError extends MCPFactoryError {
|
|
9
|
+
constructor(message: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class GenerationError extends MCPFactoryError {
|
|
12
|
+
constructor(message: string);
|
|
13
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class MCPFactoryError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = 'MCPFactoryError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class ParseError extends MCPFactoryError {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message, 'PARSE_ERROR');
|
|
12
|
+
this.name = 'ParseError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class ValidationError extends MCPFactoryError {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message, 'VALIDATION_ERROR');
|
|
18
|
+
this.name = 'ValidationError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class GenerationError extends MCPFactoryError {
|
|
22
|
+
constructor(message) {
|
|
23
|
+
super(message, 'GENERATION_ERROR');
|
|
24
|
+
this.name = 'GenerationError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const logger = {
|
|
2
|
+
info: (message) => {
|
|
3
|
+
console.log(`ℹ ${message}`);
|
|
4
|
+
},
|
|
5
|
+
success: (message) => {
|
|
6
|
+
console.log(`✓ ${message}`);
|
|
7
|
+
},
|
|
8
|
+
error: (message) => {
|
|
9
|
+
console.error(`✗ ${message}`);
|
|
10
|
+
},
|
|
11
|
+
warn: (message) => {
|
|
12
|
+
console.warn(`⚠ ${message}`);
|
|
13
|
+
},
|
|
14
|
+
debug: (message) => {
|
|
15
|
+
if (process.env.DEBUG) {
|
|
16
|
+
console.log(`[DEBUG] ${message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|