#!/usr/bin/env node
import prompts from 'prompts';
import { writeFile } from 'fs/promises';
import { join, resolve, dirname, basename } from 'path';
import { existsSync, readdirSync, statSync } from 'fs';
import { homedir } from 'os';
interface SetupConfig {
clientId: string;
clientSecret: string;
apiBaseUrl: string;
authPath: string;
openApiSpecPath: string;
toolPrefix: string;
serverName: string;
includeOnly?: string;
exclude?: string;
}
async function detectClaudeConfigPath(): Promise<string> {
const possiblePaths = [
join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
join(homedir(), '.config', 'claude', 'claude_desktop_config.json'),
join(homedir(), 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json')
];
for (const path of possiblePaths) {
if (existsSync(path)) {
return path;
}
}
return possiblePaths[0]; // Default to macOS path
}
function expandTilde(filepath: string): string {
if (filepath.startsWith('~/')) {
return join(homedir(), filepath.slice(2));
}
return filepath;
}
function resolveFilePath(filepath: string): string {
const expanded = expandTilde(filepath);
// If it's already absolute, return as-is
if (expanded.startsWith('/')) {
return expanded;
}
// Otherwise resolve relative to current directory
return resolve(expanded);
}
async function collectConfiguration(): Promise<SetupConfig | null> {
console.log('š MCP OpenAPI Connector Setup\n');
console.log('This wizard will help you configure the MCP OpenAPI Connector for your API.\n');
const questions: prompts.PromptObject[] = [
{
type: 'text',
name: 'serverName',
message: 'š Server name for Claude Desktop (e.g., "my-api")',
initial: 'openapi-connector',
validate: (value: string) => value.length > 0 || 'Server name is required'
},
{
type: 'text',
name: 'apiBaseUrl',
message: 'š API Base URL (e.g., https://api.example.com)',
validate: (value: string) => {
if (!value) return 'API Base URL is required';
try {
new URL(value);
return true;
} catch {
return 'Please enter a valid URL';
}
}
},
{
type: 'text',
name: 'authPath',
message: 'š Authentication path',
initial: '/oauth/token'
},
{
type: 'text',
name: 'clientId',
message: 'š¤ Client ID',
validate: (value: string) => value.length > 0 || 'Client ID is required'
},
{
type: 'password',
name: 'clientSecret',
message: 'š Client Secret',
validate: (value: string) => value.length > 0 || 'Client Secret is required'
},
{
type: 'text',
name: 'openApiSpecPath',
message: 'š OpenAPI spec file path\n Examples: ./openapi.json, ~/Downloads/api-spec.json, /absolute/path/to/spec.json',
validate: (value: string) => {
if (!value) return 'OpenAPI spec path is required';
const expanded = expandTilde(value);
if (!existsSync(expanded)) {
const suggestions = [
'./config/openapi.json',
'./openapi.json',
'~/Downloads/openapi.json',
'~/Desktop/openapi.json'
];
return `File not found: ${value}\n Try: ${suggestions.join(', ')}`;
}
if (!expanded.endsWith('.json')) {
return 'OpenAPI spec file must be a .json file';
}
return true;
}
},
{
type: 'text',
name: 'toolPrefix',
message: 'š·ļø Tool name prefix (optional, e.g., "api_")',
initial: ''
},
{
type: 'text',
name: 'includeOnly',
message: 'ā
Include only specific operations (comma-separated, optional)',
initial: ''
},
{
type: 'text',
name: 'exclude',
message: 'ā Exclude specific operations (comma-separated, optional)',
initial: ''
}
];
const response = await prompts(questions, {
onCancel: () => {
console.log('\nā ļø Setup cancelled by user');
return false;
}
});
// Check if user cancelled
if (!response.serverName) {
return null;
}
return {
...response,
openApiSpecPath: resolveFilePath(response.openApiSpecPath),
includeOnly: response.includeOnly || undefined,
exclude: response.exclude || undefined
};
}
async function generateEnvFile(config: SetupConfig): Promise<void> {
const envContent = `# MCP OpenAPI Connector Configuration
# Generated by setup wizard on ${new Date().toISOString()}
# OAuth2 Authentication
CLIENT_ID=${config.clientId}
CLIENT_SECRET=${config.clientSecret}
# API Configuration
API_BASE_URL=${config.apiBaseUrl}
AUTH_PATH=${config.authPath}
# OpenAPI Configuration
OPENAPI_SPEC_PATH=${config.openApiSpecPath}
${config.toolPrefix ? `OPENAPI_TOOL_PREFIX=${config.toolPrefix}` : '# OPENAPI_TOOL_PREFIX='}
${config.includeOnly ? `OPENAPI_INCLUDE_ONLY=${config.includeOnly}` : '# OPENAPI_INCLUDE_ONLY='}
${config.exclude ? `OPENAPI_EXCLUDE=${config.exclude}` : '# OPENAPI_EXCLUDE='}
# Environment
NODE_ENV=production
`;
// Create generated directory if it doesn't exist
const generatedDir = 'generated';
if (!existsSync(generatedDir)) {
const { mkdir } = await import('fs/promises');
await mkdir(generatedDir);
}
await writeFile(join(generatedDir, '.env'), envContent);
console.log('ā
Generated generated/.env file');
}
async function generateClaudeConfig(config: SetupConfig): Promise<void> {
const projectPath = resolve('.');
const claudeConfig = {
mcpServers: {
[config.serverName]: {
command: "npx",
args: ["tsx", `${projectPath}/src/mcp-openapi-connector.ts`],
cwd: projectPath,
env: {
CLIENT_ID: config.clientId,
CLIENT_SECRET: config.clientSecret,
API_BASE_URL: config.apiBaseUrl,
AUTH_PATH: config.authPath,
OPENAPI_SPEC_PATH: resolve(config.openApiSpecPath),
...(config.toolPrefix && { OPENAPI_TOOL_PREFIX: config.toolPrefix }),
...(config.includeOnly && { OPENAPI_INCLUDE_ONLY: config.includeOnly }),
...(config.exclude && { OPENAPI_EXCLUDE: config.exclude })
}
}
}
};
// Create generated directory if it doesn't exist
const generatedDir = 'generated';
if (!existsSync(generatedDir)) {
const { mkdir } = await import('fs/promises');
await mkdir(generatedDir);
}
await writeFile(join(generatedDir, 'claude-desktop-config.json'), JSON.stringify(claudeConfig, null, 2));
console.log('ā
Generated generated/claude-desktop-config.json');
}
async function showInstructions(config: SetupConfig): Promise<void> {
const claudeConfigPath = await detectClaudeConfigPath();
console.log('\nš Setup Complete!\n');
console.log('š Generated files in ./generated/:');
console.log(' ⢠generated/.env - Environment configuration');
console.log(' ⢠generated/claude-desktop-config.json - Claude Desktop MCP server configuration\n');
console.log('š§ Next steps:\n');
console.log('1. Add the server configuration to your Claude Desktop config:');
console.log(` File location: ${claudeConfigPath}\n`);
console.log('2. Merge the generated configuration:');
console.log(' ⢠Open the Claude Desktop config file in a text editor');
console.log(' ⢠Copy the server configuration from generated/claude-desktop-config.json');
console.log(' ⢠Add it to the "mcpServers" section of your Claude config\n');
console.log(' Example merge:');
console.log(' ```json');
console.log(' {');
console.log(' "mcpServers": {');
console.log(' // ... your existing servers ...');
console.log(` "${config.serverName}": {`);
console.log(' // ... configuration from generated/claude-desktop-config.json ...');
console.log(' }');
console.log(' }');
console.log(' }');
console.log(' ```\n');
console.log('3. Restart Claude Desktop to load the new server\n');
console.log('4. Test the connection:');
console.log(' ⢠Start a new conversation in Claude Desktop');
console.log(' ⢠Ask: "What tools are available?"');
console.log(` ⢠You should see tools prefixed with "${config.toolPrefix || 'your-prefix'}"\n`);
console.log('š Alternative setup (production):');
console.log(' For production use, you can also use the compiled version:');
console.log(' ⢠Run: npm run build');
console.log(' ⢠Change "command" to "node"');
console.log(` ⢠Change args to ["${resolve('.')}/dist/mcp-openapi-connector.js"]\n`);
console.log('š For more information, see the README.md file.');
}
async function main(): Promise<void> {
try {
const config = await collectConfiguration();
if (!config) {
process.exit(0);
}
console.log('\nš¦ Generating configuration files...\n');
await generateEnvFile(config);
await generateClaudeConfig(config);
await showInstructions(config);
} catch (error) {
console.error('\nā Setup failed:', error instanceof Error ? error.message : String(error));
process.exit(1);
}
}
// Handle Ctrl+C gracefully
process.on('SIGINT', () => {
console.log('\n\nā ļø Setup cancelled by user');
process.exit(0);
});
main().catch((error) => {
console.error('Unexpected error:', error);
process.exit(1);
});