@nils.del/supergrok 1.0.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/.env +45 -0
- package/.grok/settings.json +13 -0
- package/dist/agent/index.d.ts +35 -0
- package/dist/agent/index.d.ts.map +1 -0
- package/dist/agent/index.js +244 -0
- package/dist/agent/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +242 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/client.d.ts +33 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/client.js +134 -0
- package/dist/mcp/client.js.map +1 -0
- package/dist/mcp/config.d.ts +15 -0
- package/dist/mcp/config.d.ts.map +1 -0
- package/dist/mcp/config.js +123 -0
- package/dist/mcp/config.js.map +1 -0
- package/dist/mcp/transports.d.ts +24 -0
- package/dist/mcp/transports.d.ts.map +1 -0
- package/dist/mcp/transports.js +91 -0
- package/dist/mcp/transports.js.map +1 -0
- package/dist/tools/bash.d.ts +28 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/bash.js +70 -0
- package/dist/tools/bash.js.map +1 -0
- package/dist/tools/index.d.ts +77 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +80 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/text-editor.d.ts +69 -0
- package/dist/tools/text-editor.d.ts.map +1 -0
- package/dist/tools/text-editor.js +178 -0
- package/dist/tools/text-editor.js.map +1 -0
- package/dist/tools/types.d.ts +14 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/ui/readline-ui.d.ts +32 -0
- package/dist/ui/readline-ui.d.ts.map +1 -0
- package/dist/ui/readline-ui.js +256 -0
- package/dist/ui/readline-ui.js.map +1 -0
- package/dist/utils/settings.d.ts +20 -0
- package/dist/utils/settings.d.ts.map +1 -0
- package/dist/utils/settings.js +101 -0
- package/dist/utils/settings.js.map +1 -0
- package/package.json +51 -0
- package/src/agent/index.ts +302 -0
- package/src/index.ts +275 -0
- package/src/mcp/client.ts +178 -0
- package/src/mcp/config.ts +149 -0
- package/src/mcp/transports.ts +142 -0
- package/src/tools/bash.ts +84 -0
- package/src/tools/index.ts +91 -0
- package/src/tools/text-editor.ts +222 -0
- package/src/tools/types.ts +14 -0
- package/src/ui/readline-ui.ts +294 -0
- package/src/utils/settings.ts +129 -0
- package/tsconfig.json +20 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from 'commander';
|
|
3
|
+
import * as dotenv from 'dotenv';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { GrokAgent } from './agent/index.js';
|
|
6
|
+
import { runReadlineUI } from './ui/readline-ui.js';
|
|
7
|
+
import { getApiKey, getBaseURL, getCurrentModel, saveUserSettings, loadUserSettings } from './utils/settings.js';
|
|
8
|
+
|
|
9
|
+
// Load environment variables
|
|
10
|
+
dotenv.config();
|
|
11
|
+
|
|
12
|
+
// Handle uncaught exceptions
|
|
13
|
+
process.on('uncaughtException', (error) => {
|
|
14
|
+
console.error(chalk.red('Uncaught exception:'), error.message);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
process.on('unhandledRejection', (reason: any) => {
|
|
19
|
+
console.error(chalk.red('Unhandled rejection:'), reason?.message || reason);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Headless mode processing
|
|
24
|
+
async function processPromptHeadless(
|
|
25
|
+
prompt: string,
|
|
26
|
+
apiKey: string,
|
|
27
|
+
baseURL?: string,
|
|
28
|
+
model?: string,
|
|
29
|
+
maxToolRounds?: number
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
const agent = new GrokAgent(apiKey, baseURL, model, maxToolRounds);
|
|
33
|
+
|
|
34
|
+
console.log(chalk.gray('Processing...'));
|
|
35
|
+
console.log();
|
|
36
|
+
|
|
37
|
+
for await (const chunk of agent.processUserMessageStream(prompt)) {
|
|
38
|
+
switch (chunk.type) {
|
|
39
|
+
case 'content':
|
|
40
|
+
if (chunk.content) {
|
|
41
|
+
process.stdout.write(chunk.content);
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
|
|
45
|
+
case 'tool_calls':
|
|
46
|
+
if (chunk.toolCalls) {
|
|
47
|
+
console.log();
|
|
48
|
+
for (const tc of chunk.toolCalls) {
|
|
49
|
+
console.log(chalk.blue(`⚡ ${tc.function.name}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
case 'tool_result':
|
|
55
|
+
if (chunk.toolResult) {
|
|
56
|
+
if (chunk.toolResult.success) {
|
|
57
|
+
console.log(chalk.green('✓'), chalk.gray(chunk.toolResult.output?.substring(0, 200) || 'Success'));
|
|
58
|
+
} else {
|
|
59
|
+
console.log(chalk.red('✗'), chunk.toolResult.error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
case 'done':
|
|
65
|
+
console.log();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
await agent.shutdown();
|
|
71
|
+
} catch (error: any) {
|
|
72
|
+
console.error(chalk.red('Error:'), error.message);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
program
|
|
78
|
+
.name('grok-fixed')
|
|
79
|
+
.description('Grok CLI (Fixed Edition) - Works properly with MCP on Linux. No Ink flickering issues.')
|
|
80
|
+
.version('1.0.0')
|
|
81
|
+
.argument('[message...]', 'Initial message to send to Grok')
|
|
82
|
+
.option('-d, --directory <dir>', 'Set working directory', process.cwd())
|
|
83
|
+
.option('-k, --api-key <key>', 'Grok API key (or set GROK_API_KEY env var)')
|
|
84
|
+
.option('-u, --base-url <url>', 'Grok API base URL (or set GROK_BASE_URL env var)')
|
|
85
|
+
.option('-m, --model <model>', 'AI model to use (or set GROK_MODEL env var)')
|
|
86
|
+
.option('-p, --prompt <prompt>', 'Process a single prompt and exit (headless mode)')
|
|
87
|
+
.option('--max-tool-rounds <rounds>', 'Maximum tool execution rounds (default: 400)', '400')
|
|
88
|
+
.action(async (message, options) => {
|
|
89
|
+
// Change working directory if specified
|
|
90
|
+
if (options.directory) {
|
|
91
|
+
try {
|
|
92
|
+
process.chdir(options.directory);
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
console.error(chalk.red(`Error changing directory: ${error.message}`));
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Get API key
|
|
101
|
+
const apiKey = options.apiKey || getApiKey();
|
|
102
|
+
const baseURL = options.baseUrl || getBaseURL();
|
|
103
|
+
const model = options.model || getCurrentModel();
|
|
104
|
+
const maxToolRounds = parseInt(options.maxToolRounds) || 400;
|
|
105
|
+
|
|
106
|
+
if (!apiKey) {
|
|
107
|
+
console.error(chalk.red('Error: API key required.'));
|
|
108
|
+
console.error(chalk.gray('Set GROK_API_KEY environment variable, use --api-key flag,'));
|
|
109
|
+
console.error(chalk.gray('or add "apiKey" to ~/.grok/user-settings.json'));
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Save API key if provided via command line
|
|
114
|
+
if (options.apiKey) {
|
|
115
|
+
const settings = loadUserSettings();
|
|
116
|
+
settings.apiKey = options.apiKey;
|
|
117
|
+
if (options.baseUrl) {
|
|
118
|
+
settings.baseURL = options.baseUrl;
|
|
119
|
+
}
|
|
120
|
+
saveUserSettings(settings);
|
|
121
|
+
console.log(chalk.green('✓ API key saved to ~/.grok/user-settings.json'));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Headless mode
|
|
125
|
+
if (options.prompt) {
|
|
126
|
+
await processPromptHeadless(options.prompt, apiKey, baseURL, model, maxToolRounds);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Interactive mode
|
|
131
|
+
const agent = new GrokAgent(apiKey, baseURL, model, maxToolRounds);
|
|
132
|
+
const initialMessage = Array.isArray(message) ? message.join(' ') : message;
|
|
133
|
+
|
|
134
|
+
await runReadlineUI(agent, initialMessage || undefined);
|
|
135
|
+
} catch (error: any) {
|
|
136
|
+
console.error(chalk.red('Error:'), error.message);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// MCP subcommand
|
|
142
|
+
const mcpCommand = program
|
|
143
|
+
.command('mcp')
|
|
144
|
+
.description('Manage MCP (Model Context Protocol) servers');
|
|
145
|
+
|
|
146
|
+
mcpCommand
|
|
147
|
+
.command('list')
|
|
148
|
+
.description('List configured MCP servers')
|
|
149
|
+
.action(async () => {
|
|
150
|
+
const { loadMCPConfig } = await import('./mcp/config.js');
|
|
151
|
+
const config = loadMCPConfig();
|
|
152
|
+
|
|
153
|
+
if (config.servers.length === 0) {
|
|
154
|
+
console.log(chalk.gray('No MCP servers configured.'));
|
|
155
|
+
console.log(chalk.gray('Use "grok-fixed mcp add" to add a server.'));
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(chalk.cyan('Configured MCP Servers:'));
|
|
160
|
+
for (const server of config.servers) {
|
|
161
|
+
console.log(chalk.white(` • ${server.name}`));
|
|
162
|
+
console.log(chalk.gray(` Transport: ${server.transport.type}`));
|
|
163
|
+
if (server.transport.type === 'stdio') {
|
|
164
|
+
console.log(chalk.gray(` Command: ${server.transport.command} ${(server.transport.args || []).join(' ')}`));
|
|
165
|
+
} else if ('url' in server.transport) {
|
|
166
|
+
console.log(chalk.gray(` URL: ${server.transport.url}`));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
mcpCommand
|
|
172
|
+
.command('add <name>')
|
|
173
|
+
.description('Add a new MCP server')
|
|
174
|
+
.option('-t, --transport <type>', 'Transport type (stdio, http, sse)', 'stdio')
|
|
175
|
+
.option('-c, --command <command>', 'Command to run (for stdio transport)')
|
|
176
|
+
.option('-a, --args <args...>', 'Arguments for the command')
|
|
177
|
+
.option('-u, --url <url>', 'URL (for http/sse transport)')
|
|
178
|
+
.option('-e, --env <env...>', 'Environment variables (KEY=value format)')
|
|
179
|
+
.action(async (name, options) => {
|
|
180
|
+
const { saveMCPServerConfig } = await import('./mcp/config.js');
|
|
181
|
+
|
|
182
|
+
let transport: any;
|
|
183
|
+
|
|
184
|
+
if (options.transport === 'stdio') {
|
|
185
|
+
if (!options.command) {
|
|
186
|
+
console.error(chalk.red('Error: --command is required for stdio transport'));
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const env: Record<string, string> = {};
|
|
191
|
+
if (options.env) {
|
|
192
|
+
for (const e of options.env) {
|
|
193
|
+
const [key, ...valueParts] = e.split('=');
|
|
194
|
+
env[key] = valueParts.join('=');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
transport = {
|
|
199
|
+
type: 'stdio',
|
|
200
|
+
command: options.command,
|
|
201
|
+
args: options.args || [],
|
|
202
|
+
env
|
|
203
|
+
};
|
|
204
|
+
} else if (options.transport === 'http' || options.transport === 'sse') {
|
|
205
|
+
if (!options.url) {
|
|
206
|
+
console.error(chalk.red(`Error: --url is required for ${options.transport} transport`));
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
transport = {
|
|
211
|
+
type: options.transport,
|
|
212
|
+
url: options.url
|
|
213
|
+
};
|
|
214
|
+
} else {
|
|
215
|
+
console.error(chalk.red(`Error: Unknown transport type: ${options.transport}`));
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
saveMCPServerConfig({ name, transport });
|
|
221
|
+
console.log(chalk.green(`✓ MCP server "${name}" added successfully`));
|
|
222
|
+
} catch (error: any) {
|
|
223
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
mcpCommand
|
|
229
|
+
.command('remove <name>')
|
|
230
|
+
.description('Remove an MCP server')
|
|
231
|
+
.action(async (name) => {
|
|
232
|
+
const { removeMCPServerConfig } = await import('./mcp/config.js');
|
|
233
|
+
|
|
234
|
+
if (removeMCPServerConfig(name)) {
|
|
235
|
+
console.log(chalk.green(`✓ MCP server "${name}" removed`));
|
|
236
|
+
} else {
|
|
237
|
+
console.log(chalk.yellow(`Server "${name}" not found`));
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
mcpCommand
|
|
242
|
+
.command('test [name]')
|
|
243
|
+
.description('Test connection to MCP server(s)')
|
|
244
|
+
.action(async (name) => {
|
|
245
|
+
const { loadMCPConfig } = await import('./mcp/config.js');
|
|
246
|
+
const { getMCPManager } = await import('./mcp/client.js');
|
|
247
|
+
|
|
248
|
+
const config = loadMCPConfig();
|
|
249
|
+
const serversToTest = name
|
|
250
|
+
? config.servers.filter(s => s.name === name)
|
|
251
|
+
: config.servers;
|
|
252
|
+
|
|
253
|
+
if (serversToTest.length === 0) {
|
|
254
|
+
console.log(chalk.yellow('No servers to test.'));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const mcpManager = getMCPManager();
|
|
259
|
+
|
|
260
|
+
for (const server of serversToTest) {
|
|
261
|
+
process.stdout.write(chalk.gray(`Testing ${server.name}... `));
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await mcpManager.addServer(server);
|
|
265
|
+
const tools = mcpManager.getTools().filter(t => t.serverName === server.name);
|
|
266
|
+
console.log(chalk.green(`✓ Connected (${tools.length} tools)`));
|
|
267
|
+
} catch (error: any) {
|
|
268
|
+
console.log(chalk.red(`✗ Failed: ${error.message}`));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await mcpManager.shutdown();
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
program.parse();
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { EventEmitter } from "events";
|
|
4
|
+
import { createTransport, MCPTransport, TransportType, TransportConfig } from "./transports.js";
|
|
5
|
+
|
|
6
|
+
export interface MCPServerConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
transport: TransportConfig;
|
|
9
|
+
command?: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MCPTool {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
inputSchema: any;
|
|
18
|
+
serverName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class MCPManager extends EventEmitter {
|
|
22
|
+
private clients: Map<string, Client> = new Map();
|
|
23
|
+
private transports: Map<string, MCPTransport> = new Map();
|
|
24
|
+
private tools: Map<string, MCPTool> = new Map();
|
|
25
|
+
private initPromise: Promise<void> | null = null;
|
|
26
|
+
|
|
27
|
+
async addServer(config: MCPServerConfig): Promise<void> {
|
|
28
|
+
try {
|
|
29
|
+
let transportConfig = config.transport;
|
|
30
|
+
if (!transportConfig && config.command) {
|
|
31
|
+
transportConfig = {
|
|
32
|
+
type: 'stdio',
|
|
33
|
+
command: config.command,
|
|
34
|
+
args: config.args,
|
|
35
|
+
env: config.env
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!transportConfig) {
|
|
40
|
+
throw new Error('Transport configuration is required');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const transport = createTransport(transportConfig);
|
|
44
|
+
this.transports.set(config.name, transport);
|
|
45
|
+
|
|
46
|
+
const client = new Client({
|
|
47
|
+
name: "grok-cli-fixed",
|
|
48
|
+
version: "1.0.0"
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this.clients.set(config.name, client);
|
|
52
|
+
|
|
53
|
+
const sdkTransport = await transport.connect();
|
|
54
|
+
await client.connect(sdkTransport);
|
|
55
|
+
|
|
56
|
+
const toolsResult = await client.listTools();
|
|
57
|
+
|
|
58
|
+
for (const tool of toolsResult.tools) {
|
|
59
|
+
const mcpTool: MCPTool = {
|
|
60
|
+
name: `mcp__${config.name}__${tool.name}`,
|
|
61
|
+
description: tool.description || `Tool from ${config.name} server`,
|
|
62
|
+
inputSchema: tool.inputSchema,
|
|
63
|
+
serverName: config.name
|
|
64
|
+
};
|
|
65
|
+
this.tools.set(mcpTool.name, mcpTool);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.emit('serverAdded', config.name, toolsResult.tools.length);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.emit('serverError', config.name, error);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async removeServer(serverName: string): Promise<void> {
|
|
76
|
+
for (const [toolName, tool] of this.tools.entries()) {
|
|
77
|
+
if (tool.serverName === serverName) {
|
|
78
|
+
this.tools.delete(toolName);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const client = this.clients.get(serverName);
|
|
83
|
+
if (client) {
|
|
84
|
+
await client.close();
|
|
85
|
+
this.clients.delete(serverName);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const transport = this.transports.get(serverName);
|
|
89
|
+
if (transport) {
|
|
90
|
+
await transport.disconnect();
|
|
91
|
+
this.transports.delete(serverName);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.emit('serverRemoved', serverName);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async callTool(toolName: string, arguments_: any): Promise<CallToolResult> {
|
|
98
|
+
const tool = this.tools.get(toolName);
|
|
99
|
+
if (!tool) {
|
|
100
|
+
throw new Error(`Tool ${toolName} not found`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const client = this.clients.get(tool.serverName);
|
|
104
|
+
if (!client) {
|
|
105
|
+
throw new Error(`Server ${tool.serverName} not connected`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const originalToolName = toolName.replace(`mcp__${tool.serverName}__`, '');
|
|
109
|
+
|
|
110
|
+
const result = await client.callTool({
|
|
111
|
+
name: originalToolName,
|
|
112
|
+
arguments: arguments_
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Ensure result has proper content structure
|
|
116
|
+
return {
|
|
117
|
+
content: result.content || [],
|
|
118
|
+
isError: result.isError,
|
|
119
|
+
_meta: result._meta
|
|
120
|
+
} as CallToolResult;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getTools(): MCPTool[] {
|
|
124
|
+
return Array.from(this.tools.values());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
getServers(): string[] {
|
|
128
|
+
return Array.from(this.clients.keys());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async shutdown(): Promise<void> {
|
|
132
|
+
const serverNames = Array.from(this.clients.keys());
|
|
133
|
+
await Promise.all(serverNames.map(name => this.removeServer(name)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
getTransportType(serverName: string): TransportType | undefined {
|
|
137
|
+
const transport = this.transports.get(serverName);
|
|
138
|
+
return transport?.getType();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async ensureServersInitialized(): Promise<void> {
|
|
142
|
+
if (this.initPromise) {
|
|
143
|
+
return this.initPromise;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (this.clients.size > 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
this.initPromise = this.initializeServers();
|
|
151
|
+
return this.initPromise;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async initializeServers(): Promise<void> {
|
|
155
|
+
const { loadMCPConfig } = await import('./config.js');
|
|
156
|
+
const config = loadMCPConfig();
|
|
157
|
+
|
|
158
|
+
const initPromises = config.servers.map(async (serverConfig) => {
|
|
159
|
+
try {
|
|
160
|
+
await this.addServer(serverConfig);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// Silently fail for individual servers
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await Promise.all(initPromises);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Singleton instance
|
|
171
|
+
let mcpManager: MCPManager | null = null;
|
|
172
|
+
|
|
173
|
+
export function getMCPManager(): MCPManager {
|
|
174
|
+
if (!mcpManager) {
|
|
175
|
+
mcpManager = new MCPManager();
|
|
176
|
+
}
|
|
177
|
+
return mcpManager;
|
|
178
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import type { TransportConfig } from './transports.js';
|
|
5
|
+
|
|
6
|
+
export interface MCPServerConfig {
|
|
7
|
+
name: string;
|
|
8
|
+
transport: TransportConfig;
|
|
9
|
+
command?: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface MCPConfig {
|
|
15
|
+
servers: MCPServerConfig[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getProjectSettingsPath(): string {
|
|
19
|
+
return path.join(process.cwd(), '.grok', 'settings.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getUserSettingsPath(): string {
|
|
23
|
+
return path.join(os.homedir(), '.grok', 'user-settings.json');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function loadMCPConfig(): MCPConfig {
|
|
27
|
+
const config: MCPConfig = { servers: [] };
|
|
28
|
+
|
|
29
|
+
// Try loading project-level settings first
|
|
30
|
+
const projectSettingsPath = getProjectSettingsPath();
|
|
31
|
+
if (fs.existsSync(projectSettingsPath)) {
|
|
32
|
+
try {
|
|
33
|
+
const projectSettings = JSON.parse(fs.readFileSync(projectSettingsPath, 'utf-8'));
|
|
34
|
+
if (projectSettings.mcpServers) {
|
|
35
|
+
for (const [name, serverConfig] of Object.entries(projectSettings.mcpServers)) {
|
|
36
|
+
const server = serverConfig as any;
|
|
37
|
+
config.servers.push({
|
|
38
|
+
name,
|
|
39
|
+
transport: server.transport || {
|
|
40
|
+
type: 'stdio',
|
|
41
|
+
command: server.command,
|
|
42
|
+
args: server.args,
|
|
43
|
+
env: server.env
|
|
44
|
+
},
|
|
45
|
+
command: server.command,
|
|
46
|
+
args: server.args,
|
|
47
|
+
env: server.env
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn('Failed to parse project MCP settings:', error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Also check user-level settings for global MCP servers
|
|
57
|
+
const userSettingsPath = getUserSettingsPath();
|
|
58
|
+
if (fs.existsSync(userSettingsPath)) {
|
|
59
|
+
try {
|
|
60
|
+
const userSettings = JSON.parse(fs.readFileSync(userSettingsPath, 'utf-8'));
|
|
61
|
+
if (userSettings.mcpServers) {
|
|
62
|
+
for (const [name, serverConfig] of Object.entries(userSettings.mcpServers)) {
|
|
63
|
+
// Don't duplicate if already defined in project settings
|
|
64
|
+
if (!config.servers.find(s => s.name === name)) {
|
|
65
|
+
const server = serverConfig as any;
|
|
66
|
+
config.servers.push({
|
|
67
|
+
name,
|
|
68
|
+
transport: server.transport || {
|
|
69
|
+
type: 'stdio',
|
|
70
|
+
command: server.command,
|
|
71
|
+
args: server.args,
|
|
72
|
+
env: server.env
|
|
73
|
+
},
|
|
74
|
+
command: server.command,
|
|
75
|
+
args: server.args,
|
|
76
|
+
env: server.env
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.warn('Failed to parse user MCP settings:', error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return config;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function saveMCPServerConfig(serverConfig: MCPServerConfig): void {
|
|
90
|
+
const settingsPath = getProjectSettingsPath();
|
|
91
|
+
const settingsDir = path.dirname(settingsPath);
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(settingsDir)) {
|
|
94
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let settings: any = {};
|
|
98
|
+
if (fs.existsSync(settingsPath)) {
|
|
99
|
+
try {
|
|
100
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Start fresh if parsing fails
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!settings.mcpServers) {
|
|
107
|
+
settings.mcpServers = {};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Save the server configuration
|
|
111
|
+
const serverData: any = {
|
|
112
|
+
name: serverConfig.name,
|
|
113
|
+
transport: serverConfig.transport.type
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (serverConfig.transport.type === 'stdio') {
|
|
117
|
+
serverData.command = serverConfig.transport.command;
|
|
118
|
+
serverData.args = serverConfig.transport.args;
|
|
119
|
+
serverData.env = serverConfig.transport.env;
|
|
120
|
+
} else if (serverConfig.transport.type === 'http' || serverConfig.transport.type === 'sse') {
|
|
121
|
+
serverData.url = (serverConfig.transport as any).url;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
settings.mcpServers[serverConfig.name] = serverData;
|
|
125
|
+
|
|
126
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function removeMCPServerConfig(serverName: string): boolean {
|
|
130
|
+
const settingsPath = getProjectSettingsPath();
|
|
131
|
+
|
|
132
|
+
if (!fs.existsSync(settingsPath)) {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
138
|
+
|
|
139
|
+
if (!settings.mcpServers || !settings.mcpServers[serverName]) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
delete settings.mcpServers[serverName];
|
|
144
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
145
|
+
return true;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|