mcphosting-cli 0.1.0 → 0.1.2

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.
@@ -1,186 +0,0 @@
1
- import { Command } from 'commander';
2
- import { createServer } from 'http';
3
- import open from 'open';
4
- import { Config } from '../lib/config.js';
5
- import { MCPHostingAPI } from '../lib/api.js';
6
- import { Logger } from '../lib/logger.js';
7
-
8
- export function createAuthCommands(): Command {
9
- const auth = new Command('auth');
10
-
11
- auth
12
- .command('login')
13
- .description('Authenticate with MCPHosting')
14
- .option('--token <token>', 'Provide API token directly')
15
- .action(async (options) => {
16
- const config = new Config();
17
-
18
- if (options.token) {
19
- // Manual token auth
20
- config.token = options.token;
21
-
22
- const api = new MCPHostingAPI(options.token);
23
- const user = await api.whoami();
24
-
25
- if (user) {
26
- config.user = user;
27
- Logger.success(`Logged in as ${user.email}${user.org ? ` (${user.org})` : ''}`);
28
- } else {
29
- Logger.warning('Token saved, but could not verify user info');
30
- }
31
- return;
32
- }
33
-
34
- // Browser OAuth flow
35
- const spinner = Logger.spinner('Starting login flow...');
36
-
37
- let server: any;
38
- let resolved = false;
39
-
40
- try {
41
- const authPromise = new Promise<string>((resolve, reject) => {
42
- server = createServer((req, res) => {
43
- if (req.url?.startsWith('/callback')) {
44
- const url = new URL(req.url, 'http://localhost');
45
- const token = url.searchParams.get('token');
46
-
47
- if (token) {
48
- res.writeHead(200, { 'Content-Type': 'text/html' });
49
- res.end(`
50
- <html>
51
- <body style="font-family: system-ui; text-align: center; padding: 50px;">
52
- <h2>✅ Login Successful!</h2>
53
- <p>You can now close this window and return to your terminal.</p>
54
- </body>
55
- </html>
56
- `);
57
- resolve(token);
58
- } else {
59
- res.writeHead(400, { 'Content-Type': 'text/html' });
60
- res.end(`
61
- <html>
62
- <body style="font-family: system-ui; text-align: center; padding: 50px;">
63
- <h2>❌ Login Failed</h2>
64
- <p>No token received. Please try again.</p>
65
- </body>
66
- </html>
67
- `);
68
- reject(new Error('No token received'));
69
- }
70
- } else {
71
- res.writeHead(404);
72
- res.end('Not found');
73
- }
74
- });
75
-
76
- server.listen(0, () => {
77
- const port = (server.address() as any).port;
78
- const callbackUrl = `http://localhost:${port}/callback`;
79
- const authUrl = `https://mcphosting.com/cli/auth?callback=${encodeURIComponent(callbackUrl)}`;
80
-
81
- setTimeout(() => {
82
- if (!resolved) {
83
- reject(new Error('Login timeout - please try again'));
84
- }
85
- }, 120000); // 2 minute timeout
86
-
87
- open(authUrl).catch(() => {
88
- Logger.warning(`Could not open browser automatically. Please visit: ${authUrl}`);
89
- });
90
- });
91
- });
92
-
93
- const token = await authPromise;
94
- resolved = true;
95
-
96
- spinner.succeed('Login successful!');
97
-
98
- config.token = token;
99
-
100
- const api = new MCPHostingAPI(token);
101
- const user = await api.whoami();
102
-
103
- if (user) {
104
- config.user = user;
105
- Logger.success(`Logged in as ${user.email}${user.org ? ` (${user.org})` : ''}`);
106
- }
107
-
108
- } catch (error) {
109
- spinner.fail('Login failed');
110
- Logger.error(`Login error: ${error}`);
111
- process.exit(1);
112
- } finally {
113
- if (server) {
114
- server.close();
115
- }
116
- }
117
- });
118
-
119
- auth
120
- .command('logout')
121
- .description('Remove stored authentication')
122
- .action(() => {
123
- const config = new Config();
124
- config.token = undefined;
125
- config.user = undefined;
126
- Logger.success('Logged out successfully');
127
- });
128
-
129
- auth
130
- .command('whoami')
131
- .description('Show current user information')
132
- .action(async () => {
133
- const config = new Config();
134
- const token = config.token;
135
-
136
- if (!token) {
137
- Logger.warning('Not logged in. Use `mcphost auth login` to authenticate.');
138
- return;
139
- }
140
-
141
- const user = config.user;
142
- if (user) {
143
- Logger.info(`Logged in as: ${user.email}${user.org ? ` (${user.org})` : ''}`);
144
- } else {
145
- Logger.warning('User info not available. Try logging in again.');
146
- }
147
- });
148
-
149
- return auth;
150
- }
151
-
152
- // Legacy commands (backwards compatibility)
153
- export function createLegacyAuthCommands(): Command[] {
154
- const config = new Config();
155
-
156
- const login = new Command('login')
157
- .description('Authenticate with MCPHosting')
158
- .option('--token <token>', 'Provide API token directly')
159
- .action(async (options) => {
160
- const authCmd = createAuthCommands();
161
- const loginCmd = authCmd.commands.find(cmd => cmd.name() === 'login');
162
- if (loginCmd) {
163
- await loginCmd.action(options);
164
- }
165
- });
166
-
167
- const logout = new Command('logout')
168
- .description('Remove stored authentication')
169
- .action(() => {
170
- config.token = undefined;
171
- config.user = undefined;
172
- Logger.success('Logged out successfully');
173
- });
174
-
175
- const whoami = new Command('whoami')
176
- .description('Show current user information')
177
- .action(async () => {
178
- const authCmd = createAuthCommands();
179
- const whoamiCmd = authCmd.commands.find(cmd => cmd.name() === 'whoami');
180
- if (whoamiCmd) {
181
- await whoamiCmd.action();
182
- }
183
- });
184
-
185
- return [login, logout, whoami];
186
- }
@@ -1,172 +0,0 @@
1
- import { Command } from 'commander';
2
- import { Config } from '../lib/config.js';
3
- import { ClientManager } from '../lib/clients.js';
4
- import { Logger } from '../lib/logger.js';
5
- import { SupportedClient } from '../types.js';
6
- import chalk from 'chalk';
7
-
8
- export function createConnectCommands(): Command {
9
- const connect = new Command('connect')
10
- .description('Connect an MCP server to AI clients')
11
- .argument('<url-or-slug>', 'MCP server URL or slug')
12
- .option('--client <client>', 'Target specific client (claude, cursor, vscode, openclaw, chatgpt)')
13
- .option('--name <name>', 'Custom name for the connection')
14
- .action(async (urlOrSlug: string, options) => {
15
- const config = new Config();
16
- const spinner = Logger.spinner('Setting up MCP connection...');
17
-
18
- try {
19
- const url = ClientManager.resolveUrl(urlOrSlug);
20
- const slug = ClientManager.extractSlug(url);
21
- const name = options.name || slug;
22
-
23
- // Check if already connected
24
- const existing = config.findConnection(slug);
25
- if (existing) {
26
- spinner.warn(`Already connected to ${slug}`);
27
- Logger.info(`Use \`mcphost disconnect ${slug}\` to remove first`);
28
- return;
29
- }
30
-
31
- const clients: string[] = [];
32
-
33
- if (options.client) {
34
- // Connect to specific client
35
- const clientName = options.client as SupportedClient;
36
- const success = await ClientManager.addToClient(clientName, slug, url);
37
-
38
- if (success) {
39
- clients.push(clientName);
40
- spinner.succeed(`Connected to ${clientName}`);
41
- } else {
42
- spinner.fail(`Failed to connect to ${clientName}`);
43
- process.exit(1);
44
- }
45
- } else {
46
- // Auto-detect and connect to all available clients
47
- const detectedClients = await ClientManager.detectInstalledClients();
48
- const availableClients = detectedClients.filter(c => c.exists);
49
-
50
- if (availableClients.length === 0) {
51
- spinner.warn('No supported clients found');
52
- Logger.info('Supported clients: Claude Desktop, Cursor, VS Code, OpenClaw');
53
- Logger.info('Install a client and try again, or use --client chatgpt for web setup');
54
- return;
55
- }
56
-
57
- spinner.text = `Configuring ${availableClients.length} client${availableClients.length > 1 ? 's' : ''}...`;
58
-
59
- for (const client of availableClients) {
60
- try {
61
- const success = await ClientManager.addToClient(
62
- client.name as SupportedClient,
63
- slug,
64
- url
65
- );
66
-
67
- if (success) {
68
- clients.push(client.name);
69
- }
70
- } catch (error) {
71
- Logger.warning(`Failed to configure ${client.name}: ${error}`);
72
- }
73
- }
74
-
75
- spinner.succeed(`Connected to ${clients.length} client${clients.length > 1 ? 's' : ''}`);
76
- }
77
-
78
- // Save connection to config
79
- config.addConnection({
80
- slug,
81
- url,
82
- clients
83
- });
84
-
85
- Logger.success(`🔗 ${name} connected successfully!`);
86
- Logger.dim(` URL: ${url}`);
87
- Logger.dim(` Clients: ${clients.join(', ')}`);
88
-
89
- // Growth hacking: show sharing message after successful connection
90
- console.log('\n' + chalk.green('🎉 Connected! Share with your team:'));
91
- console.log(chalk.cyan(`npx @mcphosting/cli connect ${urlOrSlug}`));
92
- console.log('\n' + chalk.yellow('⭐ Star us: ') + chalk.blue('https://github.com/gorlomi-enzo/mcphosting-cli'));
93
- console.log('');
94
-
95
- } catch (error) {
96
- spinner.fail('Connection failed');
97
- Logger.error(`Error: ${error}`);
98
- process.exit(1);
99
- }
100
- });
101
-
102
- return connect;
103
- }
104
-
105
- export function createDisconnectCommand(): Command {
106
- return new Command('disconnect')
107
- .description('Disconnect an MCP server')
108
- .argument('<slug-or-id>', 'MCP server slug or connection ID')
109
- .action(async (slugOrId: string) => {
110
- const config = new Config();
111
- const connection = config.findConnection(slugOrId);
112
-
113
- if (!connection) {
114
- Logger.error(`Connection not found: ${slugOrId}`);
115
- Logger.info('Use `mcphost list` to see active connections');
116
- return;
117
- }
118
-
119
- const spinner = Logger.spinner(`Disconnecting ${connection.slug}...`);
120
-
121
- try {
122
- const removedFrom = await ClientManager.removeFromAllClients(connection.slug);
123
- config.removeConnection(connection.id);
124
-
125
- spinner.succeed(`Disconnected ${connection.slug}`);
126
-
127
- if (removedFrom.length > 0) {
128
- Logger.info(`Removed from: ${removedFrom.join(', ')}`);
129
- }
130
-
131
- } catch (error) {
132
- spinner.fail('Disconnect failed');
133
- Logger.error(`Error: ${error}`);
134
- }
135
- });
136
- }
137
-
138
- export function createListCommand(): Command {
139
- return new Command('list')
140
- .description('List connected MCP servers')
141
- .option('--json', 'Output as JSON')
142
- .action(async (options) => {
143
- const config = new Config();
144
- const connections = config.connections;
145
-
146
- if (connections.length === 0) {
147
- Logger.info('No MCP connections found');
148
- Logger.dim('Use `mcphost connect <url>` to add one');
149
- return;
150
- }
151
-
152
- if (options.json) {
153
- Logger.json(connections);
154
- return;
155
- }
156
-
157
- Logger.bold(`📋 Connected MCP Servers (${connections.length})`);
158
- console.log('');
159
-
160
- const tableData = connections.map(conn => ({
161
- Slug: conn.slug,
162
- URL: conn.url.length > 50 ? conn.url.slice(0, 47) + '...' : conn.url,
163
- Clients: conn.clients.join(', '),
164
- Added: new Date(conn.addedAt).toLocaleDateString()
165
- }));
166
-
167
- Logger.table(tableData);
168
-
169
- console.log('');
170
- Logger.dim('Use `mcphost disconnect <slug>` to remove a connection');
171
- });
172
- }
@@ -1,213 +0,0 @@
1
- import { Command } from 'commander';
2
- import { readFile, access } from 'fs/promises';
3
- import { join } from 'path';
4
- import { homedir } from 'os';
5
- import { Config } from '../lib/config.js';
6
- import { ClientManager } from '../lib/clients.js';
7
- import { Logger } from '../lib/logger.js';
8
- import chalk from 'chalk';
9
-
10
- interface SmitheryConfig {
11
- servers?: Record<string, {
12
- url?: string;
13
- command?: string;
14
- args?: string[];
15
- env?: Record<string, string>;
16
- }>;
17
- mcpServers?: Record<string, {
18
- url?: string;
19
- command?: string;
20
- args?: string[];
21
- env?: Record<string, string>;
22
- }>;
23
- }
24
-
25
- export function createImportCommand(): Command {
26
- return new Command('import')
27
- .description('Import MCP connections from other tools')
28
- .option('--from <tool>', 'Import from: smithery', 'smithery')
29
- .option('--dry-run', 'Show what would be imported without actually importing')
30
- .option('--config-path <path>', 'Custom config file path')
31
- .action(async (options) => {
32
- if (options.from !== 'smithery') {
33
- Logger.error('Only Smithery import is currently supported');
34
- Logger.info('Usage: mcphost import --from smithery');
35
- return;
36
- }
37
-
38
- await importFromSmitery(options);
39
- });
40
- }
41
-
42
- async function importFromSmitery(options: { dryRun?: boolean; configPath?: string }) {
43
- const config = new Config();
44
- const spinner = Logger.spinner('Looking for Smithery config...');
45
-
46
- try {
47
- // Try to find Smithery config
48
- const smitheryPaths = [
49
- options.configPath,
50
- join(homedir(), '.smithery', 'config.json'),
51
- join(homedir(), '.config', 'smithery', 'config.json'),
52
- join(homedir(), 'Library', 'Application Support', 'smithery', 'config.json'),
53
- join(process.cwd(), 'smithery.json'),
54
- join(process.cwd(), '.smithery.json')
55
- ].filter(Boolean) as string[];
56
-
57
- let smitheryConfigPath: string | null = null;
58
- for (const path of smitheryPaths) {
59
- try {
60
- await access(path);
61
- smitheryConfigPath = path;
62
- break;
63
- } catch {
64
- // Continue to next path
65
- }
66
- }
67
-
68
- if (!smitheryConfigPath) {
69
- spinner.fail('Smithery config not found');
70
- Logger.warning('Searched locations:');
71
- smitheryPaths.forEach(path => Logger.dim(` ${path}`));
72
- Logger.info('\nIf Smithery is installed, you can specify the config path:');
73
- Logger.info(' mcphost import --from smithery --config-path /path/to/smithery/config.json');
74
- return;
75
- }
76
-
77
- spinner.succeed(`Found Smithery config: ${smitheryConfigPath}`);
78
-
79
- // Read and parse Smithery config
80
- const loadSpinner = Logger.spinner('Reading Smithery config...');
81
- const configContent = await readFile(smitheryConfigPath, 'utf-8');
82
- const smitheryConfig: SmitheryConfig = JSON.parse(configContent);
83
-
84
- // Extract MCP servers from both possible locations
85
- const servers = {
86
- ...smitheryConfig.servers,
87
- ...smitheryConfig.mcpServers
88
- };
89
-
90
- if (!servers || Object.keys(servers).length === 0) {
91
- loadSpinner.warn('No MCP servers found in Smithery config');
92
- return;
93
- }
94
-
95
- const serverEntries = Object.entries(servers);
96
- loadSpinner.succeed(`Found ${serverEntries.length} MCP server(s) in Smithery config`);
97
-
98
- if (options.dryRun) {
99
- console.log('\n' + chalk.bold('🔍 Preview: Would import the following MCP servers:'));
100
- console.log('');
101
-
102
- serverEntries.forEach(([slug, serverConfig]) => {
103
- const url = serverConfig.url || `https://${slug}.mcphost.dev`;
104
- console.log(`• ${chalk.cyan(slug)}`);
105
- console.log(` URL: ${chalk.dim(url)}`);
106
-
107
- if (serverConfig.command) {
108
- console.log(` Command: ${chalk.dim(serverConfig.command + ' ' + (serverConfig.args?.join(' ') || ''))}`);
109
- }
110
-
111
- console.log('');
112
- });
113
-
114
- Logger.info('Run without --dry-run to perform the import');
115
- return;
116
- }
117
-
118
- // Actually import the servers
119
- const importSpinner = Logger.spinner('Importing MCP servers...');
120
- let imported = 0;
121
- let skipped = 0;
122
-
123
- for (const [slug, serverConfig] of serverEntries) {
124
- try {
125
- // Check if already exists
126
- const existing = config.findConnection(slug);
127
- if (existing) {
128
- Logger.warning(`Skipping ${slug} - already connected`);
129
- skipped++;
130
- continue;
131
- }
132
-
133
- // Determine URL
134
- let url = serverConfig.url;
135
- if (!url) {
136
- // If no URL but has command, try to extract from args
137
- if (serverConfig.command === 'npx' && serverConfig.args?.[0] === '@mcphosting/cli') {
138
- url = serverConfig.args[2]; // Should be the URL after 'proxy'
139
- } else {
140
- // Default to mcphost.dev format
141
- url = `https://${slug}.mcphost.dev`;
142
- }
143
- }
144
-
145
- if (!url) {
146
- Logger.warning(`Skipping ${slug} - no URL found`);
147
- skipped++;
148
- continue;
149
- }
150
-
151
- // Connect to available clients
152
- const detectedClients = await ClientManager.detectInstalledClients();
153
- const availableClients = detectedClients.filter(c => c.exists);
154
- const connectedClients: string[] = [];
155
-
156
- for (const client of availableClients) {
157
- try {
158
- const success = await ClientManager.addToClient(
159
- client.name as any,
160
- slug,
161
- url
162
- );
163
-
164
- if (success) {
165
- connectedClients.push(client.name);
166
- }
167
- } catch (error) {
168
- // Continue with other clients
169
- }
170
- }
171
-
172
- // Save connection
173
- config.addConnection({
174
- slug,
175
- url,
176
- clients: connectedClients
177
- });
178
-
179
- imported++;
180
-
181
- } catch (error) {
182
- Logger.warning(`Failed to import ${slug}: ${error}`);
183
- skipped++;
184
- }
185
- }
186
-
187
- importSpinner.succeed('Import completed');
188
-
189
- Logger.success(`✅ Imported ${imported} MCP server(s)`);
190
- if (skipped > 0) {
191
- Logger.warning(`⚠️ Skipped ${skipped} server(s)`);
192
- }
193
-
194
- console.log('');
195
- Logger.info('Use `mcphost list` to see your imported connections');
196
-
197
- // Growth hacking message
198
- console.log('\n' + chalk.green('🎉 Welcome to MCPHosting! Share with your team:'));
199
- console.log(chalk.cyan('npx @mcphosting/cli import --from smithery'));
200
- console.log('\n' + chalk.yellow('⭐ Star us: ') + chalk.blue('https://github.com/gorlomi-enzo/mcphosting-cli'));
201
- console.log('');
202
-
203
- } catch (error) {
204
- spinner.fail('Import failed');
205
- Logger.error(`Error: ${error}`);
206
-
207
- if (error instanceof SyntaxError) {
208
- Logger.warning('Invalid JSON in Smithery config file');
209
- }
210
-
211
- process.exit(1);
212
- }
213
- }