@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.
Files changed (60) hide show
  1. package/README.md +100 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +33 -0
  4. package/dist/commands/create.d.ts +4 -0
  5. package/dist/commands/create.js +56 -0
  6. package/dist/commands/install.d.ts +1 -0
  7. package/dist/commands/install.js +79 -0
  8. package/dist/commands/list.d.ts +1 -0
  9. package/dist/commands/list.js +24 -0
  10. package/dist/commands/validate.d.ts +1 -0
  11. package/dist/commands/validate.js +27 -0
  12. package/dist/generator/analyzer.d.ts +2 -0
  13. package/dist/generator/analyzer.js +14 -0
  14. package/dist/generator/engine.d.ts +10 -0
  15. package/dist/generator/engine.js +46 -0
  16. package/dist/parsers/ai-parser.d.ts +2 -0
  17. package/dist/parsers/ai-parser.js +7 -0
  18. package/dist/parsers/detector.d.ts +6 -0
  19. package/dist/parsers/detector.js +38 -0
  20. package/dist/parsers/openapi.d.ts +5 -0
  21. package/dist/parsers/openapi.js +205 -0
  22. package/dist/parsers/postman.d.ts +2 -0
  23. package/dist/parsers/postman.js +4 -0
  24. package/dist/registry/manager.d.ts +13 -0
  25. package/dist/registry/manager.js +43 -0
  26. package/dist/schema/api-schema.d.ts +77 -0
  27. package/dist/schema/api-schema.js +1 -0
  28. package/dist/utils/errors.d.ts +13 -0
  29. package/dist/utils/errors.js +26 -0
  30. package/dist/utils/logger.d.ts +7 -0
  31. package/dist/utils/logger.js +19 -0
  32. package/docs/plans/2026-02-02-mcp-factory-design.md +306 -0
  33. package/docs/plans/2026-02-02-mcp-factory-implementation.md +1866 -0
  34. package/package.json +48 -0
  35. package/src/cli.ts +41 -0
  36. package/src/commands/create.ts +65 -0
  37. package/src/commands/install.ts +92 -0
  38. package/src/commands/list.ts +28 -0
  39. package/src/commands/validate.ts +29 -0
  40. package/src/generator/analyzer.ts +20 -0
  41. package/src/generator/engine.ts +73 -0
  42. package/src/parsers/ai-parser.ts +10 -0
  43. package/src/parsers/detector.ts +49 -0
  44. package/src/parsers/openapi.ts +238 -0
  45. package/src/parsers/postman.ts +6 -0
  46. package/src/registry/manager.ts +62 -0
  47. package/src/schema/api-schema.ts +87 -0
  48. package/src/utils/errors.ts +27 -0
  49. package/src/utils/logger.ts +23 -0
  50. package/templates/README.md.hbs +40 -0
  51. package/templates/client.ts.hbs +45 -0
  52. package/templates/index.ts.hbs +36 -0
  53. package/templates/package.json.hbs +20 -0
  54. package/templates/test.ts.hbs +1 -0
  55. package/templates/tools.ts.hbs +38 -0
  56. package/templates/tsconfig.json.hbs +13 -0
  57. package/templates/types.ts.hbs +1 -0
  58. package/templates/validation.ts.hbs +1 -0
  59. package/test-fixtures/weather-api.json +49 -0
  60. package/tsconfig.json +17 -0
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@stackkedjohn/mcp-factory-cli",
3
+ "version": "0.1.0",
4
+ "description": "Generate production-ready MCP servers from API documentation",
5
+ "type": "module",
6
+ "main": "dist/cli.js",
7
+ "bin": {
8
+ "mcp-factory": "dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "test": "node --test dist/**/*.test.js",
14
+ "prepare": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "mcp",
18
+ "api",
19
+ "codegen",
20
+ "cli",
21
+ "model-context-protocol",
22
+ "openapi",
23
+ "swagger",
24
+ "code-generation"
25
+ ],
26
+ "author": "John Lohr",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/StackkedJohn/mcp-factory.git"
31
+ },
32
+ "homepage": "https://github.com/StackkedJohn/mcp-factory#readme",
33
+ "bugs": {
34
+ "url": "https://github.com/StackkedJohn/mcp-factory/issues"
35
+ },
36
+ "dependencies": {
37
+ "@anthropic-ai/sdk": "^0.72.1",
38
+ "commander": "^14.0.3",
39
+ "handlebars": "^4.7.8",
40
+ "yaml": "^2.8.2",
41
+ "zod": "^4.3.6"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^25.2.0",
45
+ "tsx": "^4.21.0",
46
+ "typescript": "^5.9.3"
47
+ }
48
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { createCommand } from './commands/create.js';
5
+ import { validateCommand } from './commands/validate.js';
6
+ import { listCommand } from './commands/list.js';
7
+ import { installCommand } from './commands/install.js';
8
+
9
+ const program = new Command();
10
+
11
+ program
12
+ .name('mcp-factory')
13
+ .description('Generate production-ready MCP servers from API documentation')
14
+ .version('0.1.0');
15
+
16
+ program
17
+ .command('create')
18
+ .description('Generate MCP server from API documentation')
19
+ .argument('<input>', 'Path to API spec file or URL')
20
+ .option('--ai-parse', 'Use AI to parse unstructured documentation')
21
+ .option('-o, --output <dir>', 'Output directory for generated server')
22
+ .action(createCommand);
23
+
24
+ program
25
+ .command('validate')
26
+ .description('Validate API specification without generating code')
27
+ .argument('<input>', 'Path to API spec file')
28
+ .action(validateCommand);
29
+
30
+ program
31
+ .command('list')
32
+ .description('List all generated MCP servers')
33
+ .action(listCommand);
34
+
35
+ program
36
+ .command('install')
37
+ .description('Install MCP server to Claude Desktop/Code configuration')
38
+ .argument('<server-name>', 'Name of the server to install')
39
+ .action(installCommand);
40
+
41
+ program.parse();
@@ -0,0 +1,65 @@
1
+ import * as path from 'path';
2
+ import { detectFormat } from '../parsers/detector.js';
3
+ import { parseOpenAPI } from '../parsers/openapi.js';
4
+ import { parsePostman } from '../parsers/postman.js';
5
+ import { parseWithAI } from '../parsers/ai-parser.js';
6
+ import { analyzePatterns } from '../generator/analyzer.js';
7
+ import { generateServer } from '../generator/engine.js';
8
+ import { addServer } from '../registry/manager.js';
9
+ import { logger } from '../utils/logger.js';
10
+ import { ParseError } from '../utils/errors.js';
11
+
12
+ export async function createCommand(
13
+ input: string,
14
+ options: { aiParse?: boolean; output?: string }
15
+ ): Promise<void> {
16
+ try {
17
+ logger.info(`Detecting format for: ${input}`);
18
+
19
+ // Detect format
20
+ const detection = await detectFormat(input);
21
+ logger.info(`Detected format: ${detection.format}`);
22
+
23
+ // Parse to APISchema
24
+ let schema;
25
+ if (detection.format === 'openapi' || detection.format === 'swagger') {
26
+ schema = parseOpenAPI(detection.content);
27
+ } else if (detection.format === 'postman') {
28
+ schema = parsePostman(detection.content);
29
+ } else if (options.aiParse) {
30
+ logger.info('Using AI parser for unstructured docs...');
31
+ schema = await parseWithAI(JSON.stringify(detection.content));
32
+ } else {
33
+ throw new ParseError('Could not detect format. Use --ai-parse for unstructured docs.');
34
+ }
35
+
36
+ logger.success(`Parsed API: ${schema.name}`);
37
+
38
+ // Analyze patterns
39
+ const patterns = analyzePatterns(schema);
40
+ logger.info(`Detected patterns: auth=${patterns.authPattern}, pagination=${patterns.paginationStyle || 'none'}`);
41
+
42
+ // Generate server
43
+ const outputDir = options.output || path.join(process.cwd(), `${schema.name}-mcp`);
44
+ logger.info(`Generating server in: ${outputDir}`);
45
+
46
+ await generateServer(schema, patterns, outputDir);
47
+
48
+ // Add to registry
49
+ await addServer(schema.name, outputDir);
50
+
51
+ logger.success(`Generated MCP server: ${schema.name}`);
52
+ logger.info(`Next steps:`);
53
+ logger.info(` cd ${outputDir}`);
54
+ logger.info(` npm install`);
55
+ logger.info(` npm run build`);
56
+ logger.info(` npm test`);
57
+
58
+ } catch (error) {
59
+ if (error instanceof Error) {
60
+ logger.error(error.message);
61
+ process.exit(1);
62
+ }
63
+ throw error;
64
+ }
65
+ }
@@ -0,0 +1,92 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { getServer } from '../registry/manager.js';
5
+ import { logger } from '../utils/logger.js';
6
+
7
+ export async function installCommand(serverName: string): Promise<void> {
8
+ try {
9
+ // Get server from registry
10
+ const server = await getServer(serverName);
11
+ if (!server) {
12
+ logger.error(`Server not found: ${serverName}`);
13
+ logger.info('Run "mcp-factory list" to see available servers');
14
+ process.exit(1);
15
+ }
16
+
17
+ // Check if server build exists
18
+ const buildPath = path.join(server.path, 'build', 'index.js');
19
+ try {
20
+ await fs.access(buildPath);
21
+ } catch {
22
+ logger.error(`Server not built yet. Run:`);
23
+ logger.info(` cd ${server.path}`);
24
+ logger.info(` npm install && npm run build`);
25
+ process.exit(1);
26
+ }
27
+
28
+ // Determine Claude config paths
29
+ const configPaths = [
30
+ path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
31
+ path.join(os.homedir(), '.claude', 'config.json'),
32
+ ];
33
+
34
+ let installed = false;
35
+
36
+ for (const configPath of configPaths) {
37
+ try {
38
+ await fs.access(configPath);
39
+ await installToConfig(configPath, serverName, buildPath);
40
+ installed = true;
41
+
42
+ const configName = configPath.includes('claude_desktop_config.json')
43
+ ? 'Claude Desktop'
44
+ : 'Claude Code';
45
+ logger.success(`Installed ${serverName} to ${configName}`);
46
+
47
+ } catch {
48
+ // Config file doesn't exist, skip
49
+ }
50
+ }
51
+
52
+ if (!installed) {
53
+ logger.warn('No Claude configuration files found');
54
+ logger.info('Expected locations:');
55
+ configPaths.forEach(p => logger.info(` ${p}`));
56
+ } else {
57
+ logger.info('\nNext steps:');
58
+ logger.info(' 1. Edit the config file and add your API credentials');
59
+ logger.info(' 2. Restart Claude Desktop/Code to load the server');
60
+ }
61
+
62
+ } catch (error) {
63
+ if (error instanceof Error) {
64
+ logger.error(error.message);
65
+ process.exit(1);
66
+ }
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ async function installToConfig(
72
+ configPath: string,
73
+ serverName: string,
74
+ buildPath: string
75
+ ): Promise<void> {
76
+ const content = await fs.readFile(configPath, 'utf-8');
77
+ const config = JSON.parse(content);
78
+
79
+ if (!config.mcpServers) {
80
+ config.mcpServers = {};
81
+ }
82
+
83
+ config.mcpServers[serverName] = {
84
+ command: 'node',
85
+ args: [buildPath],
86
+ env: {
87
+ API_KEY: 'YOUR_API_KEY_HERE',
88
+ },
89
+ };
90
+
91
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2));
92
+ }
@@ -0,0 +1,28 @@
1
+ import { listServers } from '../registry/manager.js';
2
+ import { logger } from '../utils/logger.js';
3
+
4
+ export async function listCommand(): Promise<void> {
5
+ try {
6
+ const servers = await listServers();
7
+
8
+ if (servers.length === 0) {
9
+ logger.info('No MCP servers generated yet');
10
+ return;
11
+ }
12
+
13
+ logger.info(`Generated MCP servers (${servers.length}):\n`);
14
+
15
+ for (const server of servers) {
16
+ console.log(` ${server.name}`);
17
+ console.log(` Path: ${server.path}`);
18
+ console.log(` Created: ${new Date(server.createdAt).toLocaleString()}\n`);
19
+ }
20
+
21
+ } catch (error) {
22
+ if (error instanceof Error) {
23
+ logger.error(error.message);
24
+ process.exit(1);
25
+ }
26
+ throw error;
27
+ }
28
+ }
@@ -0,0 +1,29 @@
1
+ import { detectFormat } from '../parsers/detector.js';
2
+ import { parseOpenAPI } from '../parsers/openapi.js';
3
+ import { logger } from '../utils/logger.js';
4
+
5
+ export async function validateCommand(input: string): Promise<void> {
6
+ try {
7
+ logger.info(`Validating: ${input}`);
8
+
9
+ const detection = await detectFormat(input);
10
+ logger.success(`Format detected: ${detection.format}`);
11
+
12
+ if (detection.format === 'openapi' || detection.format === 'swagger') {
13
+ const schema = parseOpenAPI(detection.content);
14
+ logger.success(`Valid API specification: ${schema.name}`);
15
+ logger.info(`Base URL: ${schema.baseUrl}`);
16
+ logger.info(`Endpoints: ${schema.endpoints.length}`);
17
+ logger.info(`Auth type: ${schema.auth.type}`);
18
+ } else {
19
+ logger.warn('Format detected but parsing not implemented yet');
20
+ }
21
+
22
+ } catch (error) {
23
+ if (error instanceof Error) {
24
+ logger.error(`Validation failed: ${error.message}`);
25
+ process.exit(1);
26
+ }
27
+ throw error;
28
+ }
29
+ }
@@ -0,0 +1,20 @@
1
+ import { APISchema, DetectedPatterns } from '../schema/api-schema.js';
2
+
3
+ export function analyzePatterns(schema: APISchema): DetectedPatterns {
4
+ return {
5
+ authPattern: schema.auth.type,
6
+ paginationStyle: schema.pagination?.style,
7
+ rateLimitStrategy: schema.rateLimit?.strategy || 'none',
8
+ errorFormat: detectErrorFormat(schema),
9
+ hasWebhooks: false,
10
+ };
11
+ }
12
+
13
+ function detectErrorFormat(schema: APISchema): 'standard' | 'custom' {
14
+ // Check if any endpoint has custom error schemas
15
+ const hasCustomErrors = schema.endpoints.some(
16
+ endpoint => endpoint.errors.length > 0 && endpoint.errors.some(e => e.schema)
17
+ );
18
+
19
+ return hasCustomErrors ? 'custom' : 'standard';
20
+ }
@@ -0,0 +1,73 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+ import Handlebars from 'handlebars';
4
+ import { APISchema, DetectedPatterns } from '../schema/api-schema.js';
5
+ import { GenerationError } from '../utils/errors.js';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ // Register Handlebars helper for equality check
12
+ Handlebars.registerHelper('eq', (a, b) => a === b);
13
+
14
+ export interface GenerationContext {
15
+ name: string;
16
+ baseUrl: string;
17
+ auth: any;
18
+ endpoints: any[];
19
+ patterns: DetectedPatterns;
20
+ absolutePath?: string;
21
+ }
22
+
23
+ export async function generateServer(
24
+ schema: APISchema,
25
+ patterns: DetectedPatterns,
26
+ outputDir: string
27
+ ): Promise<void> {
28
+ const context: GenerationContext = {
29
+ name: schema.name,
30
+ baseUrl: schema.baseUrl,
31
+ auth: schema.auth,
32
+ endpoints: schema.endpoints,
33
+ patterns,
34
+ absolutePath: path.resolve(outputDir),
35
+ };
36
+
37
+ // Create output directory structure
38
+ await fs.mkdir(path.join(outputDir, 'src'), { recursive: true });
39
+
40
+ // Get template directory
41
+ const templateDir = path.join(__dirname, '..', '..', 'templates');
42
+
43
+ // Generate files from templates
44
+ await generateFile(templateDir, outputDir, 'package.json.hbs', 'package.json', context);
45
+ await generateFile(templateDir, outputDir, 'tsconfig.json.hbs', 'tsconfig.json', context);
46
+ await generateFile(templateDir, outputDir, 'README.md.hbs', 'README.md', context);
47
+ await generateFile(templateDir, outputDir, 'index.ts.hbs', 'src/index.ts', context);
48
+ await generateFile(templateDir, outputDir, 'client.ts.hbs', 'src/client.ts', context);
49
+ await generateFile(templateDir, outputDir, 'tools.ts.hbs', 'src/tools.ts', context);
50
+ await generateFile(templateDir, outputDir, 'types.ts.hbs', 'src/types.ts', context);
51
+ await generateFile(templateDir, outputDir, 'validation.ts.hbs', 'src/validation.ts', context);
52
+ await generateFile(templateDir, outputDir, 'test.ts.hbs', 'test.ts', context);
53
+ }
54
+
55
+ async function generateFile(
56
+ templateDir: string,
57
+ outputDir: string,
58
+ templateFile: string,
59
+ outputFile: string,
60
+ context: GenerationContext
61
+ ): Promise<void> {
62
+ try {
63
+ const templatePath = path.join(templateDir, templateFile);
64
+ const templateContent = await fs.readFile(templatePath, 'utf-8');
65
+ const template = Handlebars.compile(templateContent);
66
+ const output = template(context);
67
+
68
+ const outputPath = path.join(outputDir, outputFile);
69
+ await fs.writeFile(outputPath, output, 'utf-8');
70
+ } catch (error) {
71
+ throw new GenerationError(`Failed to generate ${outputFile}: ${error}`);
72
+ }
73
+ }
@@ -0,0 +1,10 @@
1
+ import { APISchema } from '../schema/api-schema.js';
2
+ import { ParseError } from '../utils/errors.js';
3
+
4
+ export async function parseWithAI(content: string): Promise<APISchema> {
5
+ if (!process.env.ANTHROPIC_API_KEY) {
6
+ throw new ParseError('ANTHROPIC_API_KEY environment variable required for AI parsing');
7
+ }
8
+
9
+ throw new ParseError('AI-powered parsing not yet implemented');
10
+ }
@@ -0,0 +1,49 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as yaml from 'yaml';
3
+ import { ParseError } from '../utils/errors.js';
4
+
5
+ export type InputFormat = 'openapi' | 'swagger' | 'postman' | 'unknown';
6
+
7
+ export interface DetectionResult {
8
+ format: InputFormat;
9
+ content: any;
10
+ }
11
+
12
+ export async function detectFormat(input: string): Promise<DetectionResult> {
13
+ let content: string;
14
+
15
+ // Check if input is a file path
16
+ try {
17
+ content = await fs.readFile(input, 'utf-8');
18
+ } catch {
19
+ throw new ParseError(`Could not read file: ${input}`);
20
+ }
21
+
22
+ // Try parsing as JSON
23
+ let parsed: any;
24
+ try {
25
+ parsed = JSON.parse(content);
26
+ } catch {
27
+ // Try parsing as YAML
28
+ try {
29
+ parsed = yaml.parse(content);
30
+ } catch {
31
+ throw new ParseError('Could not parse input as JSON or YAML');
32
+ }
33
+ }
34
+
35
+ // Detect format from parsed content
36
+ if (parsed.openapi) {
37
+ return { format: 'openapi', content: parsed };
38
+ }
39
+
40
+ if (parsed.swagger) {
41
+ return { format: 'swagger', content: parsed };
42
+ }
43
+
44
+ if (parsed.info?.schema?.includes('postman')) {
45
+ return { format: 'postman', content: parsed };
46
+ }
47
+
48
+ return { format: 'unknown', content: parsed };
49
+ }