@indiccoder/mentis-cli 1.1.4 → 1.1.5
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/.claude/settings.local.json +8 -0
- package/.mentis/session.json +15 -0
- package/.mentis/sessions/1769189035730.json +23 -0
- package/.mentis/sessions/1769189569160.json +23 -0
- package/.mentis/sessions/1769767538672.json +23 -0
- package/.mentis/sessions/1769767785155.json +23 -0
- package/.mentis/sessions/1769768745802.json +23 -0
- package/.mentis/sessions/1769769600884.json +31 -0
- package/.mentis/sessions/1769770030160.json +31 -0
- package/.mentis/sessions/1769770606004.json +78 -0
- package/.mentis/sessions/1769771084515.json +141 -0
- package/.mentis/sessions/1769881926630.json +57 -0
- package/README.md +17 -0
- package/dist/checkpoint/CheckpointManager.js +92 -0
- package/dist/debug_google.js +61 -0
- package/dist/debug_lite.js +49 -0
- package/dist/debug_lite_headers.js +57 -0
- package/dist/debug_search.js +16 -0
- package/dist/index.js +10 -0
- package/dist/mcp/JsonRpcClient.js +16 -0
- package/dist/mcp/McpConfig.js +132 -0
- package/dist/mcp/McpManager.js +189 -0
- package/dist/repl/PersistentShell.js +20 -1
- package/dist/repl/ReplManager.js +410 -138
- package/dist/tools/AskQuestionTool.js +172 -0
- package/dist/tools/EditFileTool.js +141 -0
- package/dist/tools/FileTools.js +7 -1
- package/dist/tools/PlanModeTool.js +53 -0
- package/dist/tools/WebSearchTool.js +190 -27
- package/dist/ui/DiffViewer.js +110 -0
- package/dist/ui/InputBox.js +16 -2
- package/dist/ui/MultiFileSelector.js +123 -0
- package/dist/ui/PlanModeUI.js +105 -0
- package/dist/ui/ToolExecutor.js +154 -0
- package/dist/ui/UIManager.js +12 -2
- package/docs/MCP_INTEGRATION.md +290 -0
- package/google_dump.html +18 -0
- package/lite_dump.html +176 -0
- package/lite_headers_dump.html +176 -0
- package/package.json +16 -5
- package/scripts/test_exa_mcp.ts +90 -0
- package/src/checkpoint/CheckpointManager.ts +102 -0
- package/src/debug_google.ts +30 -0
- package/src/debug_lite.ts +18 -0
- package/src/debug_lite_headers.ts +25 -0
- package/src/debug_search.ts +18 -0
- package/src/index.ts +12 -0
- package/src/mcp/JsonRpcClient.ts +19 -0
- package/src/mcp/McpConfig.ts +153 -0
- package/src/mcp/McpManager.ts +224 -0
- package/src/repl/PersistentShell.ts +24 -1
- package/src/repl/ReplManager.ts +1521 -1204
- package/src/tools/AskQuestionTool.ts +197 -0
- package/src/tools/EditFileTool.ts +172 -0
- package/src/tools/FileTools.ts +3 -0
- package/src/tools/PlanModeTool.ts +50 -0
- package/src/tools/WebSearchTool.ts +235 -63
- package/src/ui/DiffViewer.ts +117 -0
- package/src/ui/InputBox.ts +17 -2
- package/src/ui/MultiFileSelector.ts +135 -0
- package/src/ui/PlanModeUI.ts +121 -0
- package/src/ui/ToolExecutor.ts +182 -0
- package/src/ui/UIManager.ts +15 -2
- package/console.log(tick) +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
const query = "expo go latest sdk";
|
|
6
|
+
const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
|
|
7
|
+
console.log(`Fetching Lite DDG: ${url}`);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const cmd = `curl -s -L -A "Mozilla/5.0" "${url}"`;
|
|
11
|
+
const html = execSync(cmd, { encoding: 'utf-8' });
|
|
12
|
+
|
|
13
|
+
fs.writeFileSync('lite_dump.html', html);
|
|
14
|
+
console.log('Dumped HTML to lite_dump.html');
|
|
15
|
+
|
|
16
|
+
} catch (e: any) {
|
|
17
|
+
console.error('Error:', e.message);
|
|
18
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
const query = "expo go latest sdk";
|
|
6
|
+
const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
|
|
7
|
+
console.log(`Testing Lite Headers: ${url}`);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const cmd = `curl -s -L -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" -H "Referer: https://duckduckgo.com/" -H "Accept-Language: en-US,en;q=0.9" "${url}"`;
|
|
11
|
+
const html = execSync(cmd, { encoding: 'utf-8' });
|
|
12
|
+
|
|
13
|
+
fs.writeFileSync('lite_headers_dump.html', html);
|
|
14
|
+
|
|
15
|
+
if (html.includes('anomaly-modal')) {
|
|
16
|
+
console.log('STILL BLOCKED by Captcha');
|
|
17
|
+
} else if (html.includes('result-link')) {
|
|
18
|
+
console.log('SUCCESS! Found result links.');
|
|
19
|
+
} else {
|
|
20
|
+
console.log('Unknown response. Check dump.');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
} catch (e: any) {
|
|
24
|
+
console.error('Error:', e.message);
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import { execSync } from 'child_process';
|
|
3
|
+
|
|
4
|
+
const query = "test";
|
|
5
|
+
const url = `https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`;
|
|
6
|
+
console.log(`Testing Lite DDG: ${url}`);
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const cmd = `curl -s -L -A "Mozilla/5.0" "${url}"`;
|
|
10
|
+
const html = execSync(cmd, { encoding: 'utf-8' });
|
|
11
|
+
|
|
12
|
+
console.log('HTML Length:', html.length);
|
|
13
|
+
console.log('Snippet (first 2000 chars):');
|
|
14
|
+
console.log(html.substring(0, 2000));
|
|
15
|
+
|
|
16
|
+
} catch (e: any) {
|
|
17
|
+
console.error('Error:', e.message);
|
|
18
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -116,6 +116,18 @@ async function main(): Promise<void> {
|
|
|
116
116
|
await repl.start();
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
// Global error handlers to prevent silent crashes
|
|
120
|
+
process.on('uncaughtException', (error) => {
|
|
121
|
+
console.error('Uncaught Exception:', error.message);
|
|
122
|
+
console.error(error.stack);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
127
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
130
|
+
|
|
119
131
|
// Start the application
|
|
120
132
|
main().catch((error) => {
|
|
121
133
|
console.error('Fatal error:', error);
|
package/src/mcp/JsonRpcClient.ts
CHANGED
|
@@ -23,6 +23,7 @@ export class JsonRpcClient {
|
|
|
23
23
|
private process: ChildProcess;
|
|
24
24
|
private sequence = 0;
|
|
25
25
|
private pendingRequests = new Map<number | string, { resolve: Function; reject: Function }>();
|
|
26
|
+
private readonly REQUEST_TIMEOUT = 30000; // 30 seconds timeout
|
|
26
27
|
|
|
27
28
|
constructor(command: string, args: string[]) {
|
|
28
29
|
this.process = spawn(command, args, {
|
|
@@ -50,6 +51,10 @@ export class JsonRpcClient {
|
|
|
50
51
|
const handler = this.pendingRequests.get(message.id);
|
|
51
52
|
if (handler) {
|
|
52
53
|
this.pendingRequests.delete(message.id);
|
|
54
|
+
// Clear timeout if present
|
|
55
|
+
if ((handler as any).timeout) {
|
|
56
|
+
clearTimeout((handler as any).timeout);
|
|
57
|
+
}
|
|
53
58
|
if (message.error) {
|
|
54
59
|
handler.reject(new Error(message.error.message));
|
|
55
60
|
} else {
|
|
@@ -76,12 +81,26 @@ export class JsonRpcClient {
|
|
|
76
81
|
|
|
77
82
|
return new Promise((resolve, reject) => {
|
|
78
83
|
this.pendingRequests.set(id, { resolve, reject });
|
|
84
|
+
|
|
85
|
+
// Set timeout to prevent indefinite hangs
|
|
86
|
+
const timeout = setTimeout(() => {
|
|
87
|
+
this.pendingRequests.delete(id);
|
|
88
|
+
reject(new Error(`MCP request timeout after ${this.REQUEST_TIMEOUT}ms`));
|
|
89
|
+
}, this.REQUEST_TIMEOUT);
|
|
90
|
+
|
|
91
|
+
// Store timeout with the request for cleanup
|
|
92
|
+
const handler = this.pendingRequests.get(id);
|
|
93
|
+
if (handler) {
|
|
94
|
+
(handler as any).timeout = timeout;
|
|
95
|
+
}
|
|
96
|
+
|
|
79
97
|
try {
|
|
80
98
|
if (!this.process.stdin) throw new Error('Stdin not available');
|
|
81
99
|
const data = JSON.stringify(request) + '\n';
|
|
82
100
|
this.process.stdin.write(data);
|
|
83
101
|
} catch (e) {
|
|
84
102
|
this.pendingRequests.delete(id);
|
|
103
|
+
clearTimeout(timeout);
|
|
85
104
|
reject(e);
|
|
86
105
|
}
|
|
87
106
|
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
2
|
+
import { homedir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
export interface McpServerConfig {
|
|
6
|
+
name: string;
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
description?: string;
|
|
10
|
+
autoConnect?: boolean;
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface McpConfig {
|
|
15
|
+
servers: McpServerConfig[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class McpConfigManager {
|
|
19
|
+
private configPath: string;
|
|
20
|
+
private config: McpConfig;
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
// Config path: ~/.mentis/mcp.json
|
|
24
|
+
this.configPath = join(homedir(), '.mentis', 'mcp.json');
|
|
25
|
+
this.config = this.loadConfig();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private loadConfig(): McpConfig {
|
|
29
|
+
if (existsSync(this.configPath)) {
|
|
30
|
+
try {
|
|
31
|
+
const content = readFileSync(this.configPath, 'utf-8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.warn('Failed to load MCP config, using defaults:', error);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Default configuration with popular MCP servers
|
|
39
|
+
return this.getDefaultConfig();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private getDefaultConfig(): McpConfig {
|
|
43
|
+
return {
|
|
44
|
+
servers: [
|
|
45
|
+
{
|
|
46
|
+
name: 'Exa Search',
|
|
47
|
+
command: 'npx',
|
|
48
|
+
args: ['-y', '@exa-labs/mcp-server-exa'],
|
|
49
|
+
description: 'Web search via Exa API (requires EXA_API_KEY)',
|
|
50
|
+
autoConnect: false,
|
|
51
|
+
env: {
|
|
52
|
+
EXA_API_KEY: process.env.EXA_API_KEY || ''
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'Memory',
|
|
57
|
+
command: 'npx',
|
|
58
|
+
args: ['-y', '@modelcontextprotocol/server-memory'],
|
|
59
|
+
description: 'Persistent memory storage for conversations',
|
|
60
|
+
autoConnect: false
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'Filesystem',
|
|
64
|
+
command: 'npx',
|
|
65
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', process.cwd()],
|
|
66
|
+
description: 'Enhanced filesystem operations',
|
|
67
|
+
autoConnect: false
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'GitHub',
|
|
71
|
+
command: 'npx',
|
|
72
|
+
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
73
|
+
description: 'GitHub repository management and operations',
|
|
74
|
+
autoConnect: false,
|
|
75
|
+
env: {
|
|
76
|
+
GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_PERSONAL_ACCESS_TOKEN || ''
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: 'Puppeteer',
|
|
81
|
+
command: 'npx',
|
|
82
|
+
args: ['-y', '@modelcontextprotocol/server-puppeteer'],
|
|
83
|
+
description: 'Web browser automation and scraping',
|
|
84
|
+
autoConnect: false
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: 'Brave Search',
|
|
88
|
+
command: 'npx',
|
|
89
|
+
args: ['-y', '@modelcontextprotocol/server-brave-search'],
|
|
90
|
+
description: 'Web search via Brave Search API',
|
|
91
|
+
autoConnect: false,
|
|
92
|
+
env: {
|
|
93
|
+
BRAVE_API_KEY: process.env.BRAVE_API_KEY || ''
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'Slack',
|
|
98
|
+
command: 'npx',
|
|
99
|
+
args: ['-y', '@modelcontextprotocol/server-slack'],
|
|
100
|
+
description: 'Slack workspace integration',
|
|
101
|
+
autoConnect: false,
|
|
102
|
+
env: {
|
|
103
|
+
SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN || ''
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public getConfig(): McpConfig {
|
|
111
|
+
return this.config;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
public saveConfig(): void {
|
|
115
|
+
try {
|
|
116
|
+
const dir = join(homedir(), '.mentis');
|
|
117
|
+
if (!existsSync(dir)) {
|
|
118
|
+
require('fs').mkdirSync(dir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error('Failed to save MCP config:', error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public addServer(server: McpServerConfig): void {
|
|
127
|
+
// Remove existing server with same name
|
|
128
|
+
this.config.servers = this.config.servers.filter(s => s.name !== server.name);
|
|
129
|
+
this.config.servers.push(server);
|
|
130
|
+
this.saveConfig();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public removeServer(name: string): void {
|
|
134
|
+
this.config.servers = this.config.servers.filter(s => s.name !== name);
|
|
135
|
+
this.saveConfig();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public getServer(name: string): McpServerConfig | undefined {
|
|
139
|
+
return this.config.servers.find(s => s.name === name);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public getAutoConnectServers(): McpServerConfig[] {
|
|
143
|
+
return this.config.servers.filter(s => s.autoConnect === true);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public updateServer(name: string, updates: Partial<McpServerConfig>): void {
|
|
147
|
+
const server = this.config.servers.find(s => s.name === name);
|
|
148
|
+
if (server) {
|
|
149
|
+
Object.assign(server, updates);
|
|
150
|
+
this.saveConfig();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { McpClient } from './McpClient';
|
|
2
|
+
import { McpConfigManager, McpServerConfig } from './McpConfig';
|
|
3
|
+
import { Tool } from '../tools/Tool';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
export interface McpConnection {
|
|
8
|
+
client: McpClient;
|
|
9
|
+
config: McpServerConfig;
|
|
10
|
+
tools: Tool[];
|
|
11
|
+
connectedAt: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class McpManager {
|
|
15
|
+
private configManager: McpConfigManager;
|
|
16
|
+
private connections: Map<string, McpConnection> = new Map();
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this.configManager = new McpConfigManager();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public getConfig(): McpConfigManager {
|
|
23
|
+
return this.configManager;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async connectToServer(serverName: string): Promise<McpConnection | null> {
|
|
27
|
+
const config = this.configManager.getServer(serverName);
|
|
28
|
+
if (!config) {
|
|
29
|
+
console.error(chalk.red(`MCP server "${serverName}" not found in configuration`));
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Check if already connected
|
|
34
|
+
if (this.connections.has(serverName)) {
|
|
35
|
+
console.log(chalk.yellow(`Already connected to ${serverName}`));
|
|
36
|
+
return this.connections.get(serverName)!;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const spinner = ora(`Connecting to MCP server: ${serverName}...`).start();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
// Set environment variables if specified
|
|
43
|
+
if (config.env) {
|
|
44
|
+
for (const [key, value] of Object.entries(config.env)) {
|
|
45
|
+
if (value) {
|
|
46
|
+
process.env[key] = value;
|
|
47
|
+
} else if (!process.env[key]) {
|
|
48
|
+
spinner.warn(chalk.yellow(`Environment variable ${key} is required for ${serverName}`));
|
|
49
|
+
console.log(chalk.dim(`Set it with: export ${key}=your_key`));
|
|
50
|
+
console.log(chalk.dim(`Or add it to your MCP configuration`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const client = new McpClient(config.command, config.args);
|
|
56
|
+
await client.initialize();
|
|
57
|
+
const tools = await client.listTools();
|
|
58
|
+
|
|
59
|
+
const connection: McpConnection = {
|
|
60
|
+
client,
|
|
61
|
+
config,
|
|
62
|
+
tools,
|
|
63
|
+
connectedAt: new Date()
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
this.connections.set(serverName, connection);
|
|
67
|
+
|
|
68
|
+
spinner.succeed(chalk.green(`Connected to ${client.serverName}!`));
|
|
69
|
+
|
|
70
|
+
if (tools.length > 0) {
|
|
71
|
+
console.log(chalk.green(`Added ${tools.length} tools:`));
|
|
72
|
+
tools.forEach(t => {
|
|
73
|
+
const description = t.description.length > 60
|
|
74
|
+
? t.description.substring(0, 60) + '...'
|
|
75
|
+
: t.description;
|
|
76
|
+
console.log(chalk.dim(` - ${chalk.cyan(t.name)}: ${description}`));
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
console.log(chalk.yellow('No tools available from this server'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return connection;
|
|
83
|
+
|
|
84
|
+
} catch (error: any) {
|
|
85
|
+
spinner.fail(chalk.red(`Failed to connect to ${serverName}: ${error.message}`));
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
public async disconnectFromServer(serverName: string): Promise<boolean> {
|
|
91
|
+
const connection = this.connections.get(serverName);
|
|
92
|
+
if (!connection) {
|
|
93
|
+
console.log(chalk.yellow(`Not connected to ${serverName}`));
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
connection.client.disconnect();
|
|
99
|
+
this.connections.delete(serverName);
|
|
100
|
+
console.log(chalk.green(`Disconnected from ${serverName}`));
|
|
101
|
+
return true;
|
|
102
|
+
} catch (error: any) {
|
|
103
|
+
console.error(chalk.red(`Error disconnecting from ${serverName}: ${error.message}`));
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public disconnectAll(): void {
|
|
109
|
+
const serverNames = Array.from(this.connections.keys());
|
|
110
|
+
for (const name of serverNames) {
|
|
111
|
+
this.disconnectFromServer(name);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public getConnections(): McpConnection[] {
|
|
116
|
+
return Array.from(this.connections.values());
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public getConnection(name: string): McpConnection | undefined {
|
|
120
|
+
return this.connections.get(name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public getAllTools(): Tool[] {
|
|
124
|
+
const allTools: Tool[] = [];
|
|
125
|
+
for (const connection of this.connections.values()) {
|
|
126
|
+
allTools.push(...connection.tools);
|
|
127
|
+
}
|
|
128
|
+
return allTools;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
public getServerNames(): string[] {
|
|
132
|
+
return Array.from(this.connections.keys());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public getAvailableServers(): McpServerConfig[] {
|
|
136
|
+
return this.configManager.getConfig().servers;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public async autoConnect(): Promise<void> {
|
|
140
|
+
const autoConnectServers = this.configManager.getAutoConnectServers();
|
|
141
|
+
|
|
142
|
+
if (autoConnectServers.length === 0) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log(chalk.blue(`\nAuto-connecting to ${autoConnectServers.length} MCP servers...`));
|
|
147
|
+
|
|
148
|
+
for (const config of autoConnectServers) {
|
|
149
|
+
await this.connectToServer(config.name);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public async listServers(): Promise<void> {
|
|
154
|
+
const availableServers = this.configManager.getConfig().servers;
|
|
155
|
+
const connectedServers = this.getServerNames();
|
|
156
|
+
|
|
157
|
+
if (availableServers.length === 0) {
|
|
158
|
+
console.log(chalk.yellow('No MCP servers configured.'));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(chalk.cyan('\nMCP Servers:\n'));
|
|
163
|
+
|
|
164
|
+
for (const server of availableServers) {
|
|
165
|
+
const isConnected = connectedServers.includes(server.name);
|
|
166
|
+
const status = isConnected ? chalk.green('● Connected') : chalk.gray('○ Disconnected');
|
|
167
|
+
const auto = server.autoConnect ? chalk.dim('[auto]') : '';
|
|
168
|
+
|
|
169
|
+
console.log(`${status} ${chalk.bold(server.name)} ${auto}`);
|
|
170
|
+
if (server.description) {
|
|
171
|
+
console.log(chalk.dim(` ${server.description}`));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (isConnected) {
|
|
175
|
+
const connection = this.getConnection(server.name);
|
|
176
|
+
if (connection && connection.tools.length > 0) {
|
|
177
|
+
console.log(chalk.dim(` Tools: ${connection.tools.map(t => t.name).join(', ')}`));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log('');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
public async addServer(name: string, command: string, args: string[], description?: string): Promise<void> {
|
|
185
|
+
const serverConfig: McpServerConfig = {
|
|
186
|
+
name,
|
|
187
|
+
command,
|
|
188
|
+
args,
|
|
189
|
+
description,
|
|
190
|
+
autoConnect: false
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
this.configManager.addServer(serverConfig);
|
|
194
|
+
console.log(chalk.green(`Added MCP server "${name}" to configuration`));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public async removeServer(name: string): Promise<void> {
|
|
198
|
+
// Disconnect if connected
|
|
199
|
+
if (this.connections.has(name)) {
|
|
200
|
+
await this.disconnectFromServer(name);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
this.configManager.removeServer(name);
|
|
204
|
+
console.log(chalk.green(`Removed MCP server "${name}" from configuration`));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public async testConnection(serverName: string): Promise<boolean> {
|
|
208
|
+
const connection = this.getConnection(serverName);
|
|
209
|
+
if (!connection) {
|
|
210
|
+
console.log(chalk.red(`Not connected to ${serverName}`));
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// Try to list tools as a test
|
|
216
|
+
await connection.client.listTools();
|
|
217
|
+
console.log(chalk.green(`${serverName} connection is healthy`));
|
|
218
|
+
return true;
|
|
219
|
+
} catch (error: any) {
|
|
220
|
+
console.log(chalk.red(`${serverName} connection test failed: ${error.message}`));
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -7,6 +7,7 @@ export class PersistentShell {
|
|
|
7
7
|
private delimiter: string = 'MENTIS_SHELL_DELIMITER';
|
|
8
8
|
private resolveCallback: ((output: string) => void) | null = null;
|
|
9
9
|
private rejectCallback: ((error: Error) => void) | null = null;
|
|
10
|
+
private readonly COMMAND_TIMEOUT = 120000; // 2 minutes timeout for shell commands
|
|
10
11
|
|
|
11
12
|
constructor() {
|
|
12
13
|
// Lazy init: Do not spawn here.
|
|
@@ -68,7 +69,29 @@ export class PersistentShell {
|
|
|
68
69
|
this.rejectCallback = reject;
|
|
69
70
|
this.buffer = '';
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
// Set timeout to prevent indefinite hangs
|
|
73
|
+
const timeout = setTimeout(() => {
|
|
74
|
+
this.resolveCallback = null;
|
|
75
|
+
this.rejectCallback = null;
|
|
76
|
+
reject(new Error(`Shell command timeout after ${this.COMMAND_TIMEOUT}ms`));
|
|
77
|
+
}, this.COMMAND_TIMEOUT);
|
|
78
|
+
|
|
79
|
+
// Store timeout for cleanup
|
|
80
|
+
const originalResolve = resolve;
|
|
81
|
+
const wrappedResolve = (output: string) => {
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
originalResolve(output);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
this.resolveCallback = wrappedResolve;
|
|
87
|
+
|
|
88
|
+
let cleanCommand = command.replace(/\n/g, '; ');
|
|
89
|
+
|
|
90
|
+
// Fix: PowerShell alias 'curl' -> 'Invoke-WebRequest' causes issues for linux-style curl commands.
|
|
91
|
+
// Force usage of 'curl.exe' if on Windows.
|
|
92
|
+
if (os.platform() === 'win32') {
|
|
93
|
+
cleanCommand = cleanCommand.replace(/(^|[\s|;&])curl(\s|$)/g, '$1curl.exe$2');
|
|
94
|
+
}
|
|
72
95
|
|
|
73
96
|
const fullCommand = `${cleanCommand}; echo "${this.delimiter}"\n`;
|
|
74
97
|
|