@abacus-ai/cli 1.106.25007 → 2.0.0-canary.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/.oxlintrc.json +8 -0
- package/dist/index.mjs +12603 -0
- package/package.json +7 -39
- package/resources/abacus.ico +0 -0
- package/resources/entitlements.plist +9 -0
- package/src/__e2e__/README.md +196 -0
- package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
- package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
- package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
- package/src/__e2e__/conversation.e2e.test.tsx +56 -0
- package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
- package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
- package/src/__e2e__/helpers/test-helpers.ts +450 -0
- package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
- package/src/__e2e__/llm-models.e2e.test.ts +402 -0
- package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
- package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
- package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
- package/src/__e2e__/repl.e2e.test.tsx +78 -0
- package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
- package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
- package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
- package/src/args.ts +22 -0
- package/src/components/__tests__/react-compiler.test.tsx +78 -0
- package/src/components/__tests__/status-indicator.test.tsx +403 -0
- package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
- package/src/components/composer/agent-mode-indicator.tsx +63 -0
- package/src/components/composer/bash-runner.tsx +54 -0
- package/src/components/composer/commands/default-commands.tsx +615 -0
- package/src/components/composer/commands/handler.tsx +59 -0
- package/src/components/composer/commands/picker.tsx +273 -0
- package/src/components/composer/commands/registry.ts +233 -0
- package/src/components/composer/commands/types.ts +33 -0
- package/src/components/composer/context.tsx +88 -0
- package/src/components/composer/file-mention-picker.tsx +83 -0
- package/src/components/composer/help.tsx +44 -0
- package/src/components/composer/index.tsx +1006 -0
- package/src/components/composer/mentions.ts +57 -0
- package/src/components/composer/message-queue.tsx +70 -0
- package/src/components/composer/mode-panel.tsx +35 -0
- package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
- package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
- package/src/components/composer/modes/bash-handler.tsx +132 -0
- package/src/components/composer/modes/bash-renderer.tsx +175 -0
- package/src/components/composer/modes/default-handlers.tsx +33 -0
- package/src/components/composer/modes/index.ts +41 -0
- package/src/components/composer/modes/types.ts +21 -0
- package/src/components/composer/persistent-shell.ts +283 -0
- package/src/components/composer/process.ts +65 -0
- package/src/components/composer/types.ts +9 -0
- package/src/components/composer/use-mention-search.ts +68 -0
- package/src/components/error-boundry.tsx +60 -0
- package/src/components/exit-message.tsx +29 -0
- package/src/components/expanded-view.tsx +74 -0
- package/src/components/file-completion.tsx +127 -0
- package/src/components/header.tsx +47 -0
- package/src/components/logo.tsx +37 -0
- package/src/components/segments.tsx +356 -0
- package/src/components/status-indicator.tsx +306 -0
- package/src/components/tool-group-summary.tsx +263 -0
- package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +312 -0
- package/src/components/tool-permissions/diff-preview.tsx +355 -0
- package/src/components/tool-permissions/index.ts +5 -0
- package/src/components/tool-permissions/permission-options.tsx +375 -0
- package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
- package/src/components/tool-permissions/tool-permission-ui.tsx +398 -0
- package/src/components/tools/agent/ask-user-question.tsx +101 -0
- package/src/components/tools/agent/enter-plan-mode.tsx +49 -0
- package/src/components/tools/agent/exit-plan-mode.tsx +75 -0
- package/src/components/tools/agent/handoff-to-main.tsx +27 -0
- package/src/components/tools/agent/subagent.tsx +37 -0
- package/src/components/tools/agent/todo-write.tsx +104 -0
- package/src/components/tools/browser/close-tab.tsx +58 -0
- package/src/components/tools/browser/computer.tsx +70 -0
- package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
- package/src/components/tools/browser/get-tab-content.tsx +51 -0
- package/src/components/tools/browser/navigate-to.tsx +59 -0
- package/src/components/tools/browser/new-tab.tsx +60 -0
- package/src/components/tools/browser/perform-action.tsx +63 -0
- package/src/components/tools/browser/refresh-tab.tsx +43 -0
- package/src/components/tools/browser/switch-tab.tsx +58 -0
- package/src/components/tools/filesystem/delete-file.tsx +104 -0
- package/src/components/tools/filesystem/edit.tsx +220 -0
- package/src/components/tools/filesystem/list-dir.tsx +78 -0
- package/src/components/tools/filesystem/read-file.tsx +180 -0
- package/src/components/tools/filesystem/upload-image.tsx +76 -0
- package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
- package/src/components/tools/index.ts +91 -0
- package/src/components/tools/mcp/mcp-tool.tsx +158 -0
- package/src/components/tools/search/fetch-url.tsx +73 -0
- package/src/components/tools/search/file-search.tsx +78 -0
- package/src/components/tools/search/grep.tsx +90 -0
- package/src/components/tools/search/semantic-search.tsx +66 -0
- package/src/components/tools/search/web-search.tsx +71 -0
- package/src/components/tools/shared/index.tsx +48 -0
- package/src/components/tools/shared/zod-coercion.ts +35 -0
- package/src/components/tools/terminal/bash-tool-output.tsx +174 -0
- package/src/components/tools/terminal/get-terminal-output.tsx +85 -0
- package/src/components/tools/terminal/run-in-terminal.tsx +106 -0
- package/src/components/tools/types.ts +16 -0
- package/src/components/tools.tsx +66 -0
- package/src/components/ui/__tests__/divider.test.tsx +61 -0
- package/src/components/ui/__tests__/gradient.test.tsx +125 -0
- package/src/components/ui/__tests__/input.test.tsx +166 -0
- package/src/components/ui/__tests__/select.test.tsx +273 -0
- package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
- package/src/components/ui/blinking-indicator.tsx +25 -0
- package/src/components/ui/divider.tsx +162 -0
- package/src/components/ui/gradient.tsx +56 -0
- package/src/components/ui/input.tsx +228 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/shimmer.tsx +84 -0
- package/src/context/agent-mode.tsx +95 -0
- package/src/context/extension-file.tsx +136 -0
- package/src/context/network-activity.tsx +45 -0
- package/src/context/notification.tsx +62 -0
- package/src/context/shell-size.tsx +49 -0
- package/src/context/shell-title.tsx +38 -0
- package/src/entrypoints/print-mode.ts +312 -0
- package/src/entrypoints/repl.tsx +401 -0
- package/src/hooks/use-agent.ts +15 -0
- package/src/hooks/use-api-client.ts +1 -0
- package/src/hooks/use-available-height.ts +8 -0
- package/src/hooks/use-cleanup.ts +29 -0
- package/src/hooks/use-interrupt-manager.ts +242 -0
- package/src/hooks/use-models.ts +22 -0
- package/src/index.ts +217 -0
- package/src/lib/__tests__/ansi.test.ts +255 -0
- package/src/lib/__tests__/cli.test.ts +122 -0
- package/src/lib/__tests__/commands.test.ts +325 -0
- package/src/lib/__tests__/constants.test.ts +15 -0
- package/src/lib/__tests__/focusables.test.ts +25 -0
- package/src/lib/__tests__/fs.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.tsx +348 -0
- package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
- package/src/lib/__tests__/mcpManagement.test.ts +38 -0
- package/src/lib/__tests__/path-paste.test.ts +144 -0
- package/src/lib/__tests__/path.test.ts +300 -0
- package/src/lib/__tests__/queries.test.ts +39 -0
- package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
- package/src/lib/__tests__/text-buffer.test.ts +328 -0
- package/src/lib/__tests__/text-utils.test.ts +32 -0
- package/src/lib/__tests__/timing.test.ts +78 -0
- package/src/lib/__tests__/utils.test.ts +238 -0
- package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
- package/src/lib/ansi.ts +150 -0
- package/src/lib/cli-push-server.ts +112 -0
- package/src/lib/cli.ts +44 -0
- package/src/lib/clipboard.ts +226 -0
- package/src/lib/command-utils.ts +93 -0
- package/src/lib/commands.ts +270 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/extension-connection.ts +181 -0
- package/src/lib/focusables.ts +7 -0
- package/src/lib/fs.ts +533 -0
- package/src/lib/markdown/code-block.tsx +63 -0
- package/src/lib/markdown/index.ts +4 -0
- package/src/lib/markdown/link.tsx +19 -0
- package/src/lib/markdown/markdown.tsx +372 -0
- package/src/lib/markdown/types.ts +15 -0
- package/src/lib/mcpCommandHandler.ts +121 -0
- package/src/lib/mcpManagement.ts +44 -0
- package/src/lib/path-paste.ts +185 -0
- package/src/lib/path.ts +179 -0
- package/src/lib/queries.ts +15 -0
- package/src/lib/standaloneMcpService.ts +688 -0
- package/src/lib/status-utils.ts +237 -0
- package/src/lib/test-utils.tsx +72 -0
- package/src/lib/text-buffer.ts +2415 -0
- package/src/lib/text-utils.ts +272 -0
- package/src/lib/timing.ts +63 -0
- package/src/lib/types.ts +295 -0
- package/src/lib/utils.ts +182 -0
- package/src/lib/vim-buffer-actions.ts +732 -0
- package/src/providers/agent.tsx +1075 -0
- package/src/providers/api-client.tsx +43 -0
- package/src/services/logger.ts +85 -0
- package/src/terminal/detection.ts +187 -0
- package/src/terminal/exit.ts +279 -0
- package/src/terminal/notification.ts +83 -0
- package/src/terminal/progress.ts +201 -0
- package/src/terminal/setup.ts +797 -0
- package/src/terminal/suspend.ts +58 -0
- package/src/terminal/types.ts +51 -0
- package/src/theme/context.tsx +57 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/themed.tsx +35 -0
- package/src/theme/themes.json +546 -0
- package/src/theme/types.ts +110 -0
- package/src/tools/types.ts +59 -0
- package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
- package/src/tools/utils/tool-ui-components.tsx +631 -0
- package/src/tools/utils/zod-coercion.ts +35 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +29 -0
- package/tsconfig.test.json +27 -0
- package/tsdown.config.ts +17 -0
- package/vitest.config.ts +76 -0
- package/README.md +0 -28
- package/dist/index.js +0 -26
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import { product } from "@codellm/product";
|
|
2
|
+
import fs, { promises as fsPromises } from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
import type { IMcpServerInfo, IMcpManagementService, IMcpServerConfig } from "./types.js";
|
|
7
|
+
|
|
8
|
+
import { helpText } from "./mcpCommandHandler.js";
|
|
9
|
+
|
|
10
|
+
interface McpConfigFile {
|
|
11
|
+
inputs?: unknown[];
|
|
12
|
+
servers?: Record<string, IMcpServerConfig>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CursorGlobalConfig {
|
|
16
|
+
mcpServers?: Record<
|
|
17
|
+
string,
|
|
18
|
+
{
|
|
19
|
+
command?: string;
|
|
20
|
+
args?: string[];
|
|
21
|
+
env?: Record<string, string>;
|
|
22
|
+
url?: string;
|
|
23
|
+
}
|
|
24
|
+
>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface VSCodeSettings {
|
|
28
|
+
mcp?: McpConfigFile;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ConfigSource {
|
|
33
|
+
path: string;
|
|
34
|
+
type:
|
|
35
|
+
| "cursor-global"
|
|
36
|
+
| "cursor-workspace"
|
|
37
|
+
| "vscode-user"
|
|
38
|
+
| "vscode-workspace"
|
|
39
|
+
| "claude-desktop"
|
|
40
|
+
| "windsurf"
|
|
41
|
+
| "abacusai-user";
|
|
42
|
+
label: string;
|
|
43
|
+
exists: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Standalone MCP service that reads configurations directly from files
|
|
48
|
+
* without requiring any workbench dependencies.
|
|
49
|
+
*/
|
|
50
|
+
export class StandaloneMcpService implements IMcpManagementService {
|
|
51
|
+
constructor() {}
|
|
52
|
+
|
|
53
|
+
async listServers(): Promise<IMcpServerInfo[]> {
|
|
54
|
+
const configSources = await this.findConfigSources();
|
|
55
|
+
const servers: IMcpServerInfo[] = [];
|
|
56
|
+
|
|
57
|
+
for (const source of configSources) {
|
|
58
|
+
if (!source.exists) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const sourceServers = await this.loadServersFromSource(source);
|
|
64
|
+
servers.push(...sourceServers);
|
|
65
|
+
} catch {
|
|
66
|
+
// Failed to load from this source, skip
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return servers;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async addServer(name: string, config: IMcpServerConfig): Promise<void> {
|
|
74
|
+
// Validate the server configuration before saving
|
|
75
|
+
this.validateServerConfig(config);
|
|
76
|
+
// Prefer product-specific User settings over other formats
|
|
77
|
+
const configSources = await this.findConfigSources();
|
|
78
|
+
|
|
79
|
+
// Priority order: Product User > VSCode User > existing files > Cursor global
|
|
80
|
+
const abacusaiUser = configSources.find((s) => s.type === "abacusai-user" && s.exists);
|
|
81
|
+
const vscodeUser = configSources.find((s) => s.type === "vscode-user" && s.exists);
|
|
82
|
+
const existingFiles = configSources.filter((s) => s.exists);
|
|
83
|
+
const cursorGlobal = configSources.find((s) => s.type === "cursor-global");
|
|
84
|
+
const vscodeUserNonExisting = configSources.find((s) => s.type === "vscode-user");
|
|
85
|
+
|
|
86
|
+
// Try existing files first, then fall back to creating new ones
|
|
87
|
+
const targetSource =
|
|
88
|
+
abacusaiUser || vscodeUser || existingFiles[0] || vscodeUserNonExisting || cursorGlobal;
|
|
89
|
+
if (!targetSource) {
|
|
90
|
+
throw new Error("No suitable configuration location found.");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await this.addServerToSource(targetSource, name, config);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async removeServer(name: string): Promise<void> {
|
|
97
|
+
const configSources = await this.findConfigSources();
|
|
98
|
+
let removed = false;
|
|
99
|
+
|
|
100
|
+
for (const source of configSources) {
|
|
101
|
+
if (!source.exists) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const wasRemoved = await this.removeServerFromSource(source.path, name);
|
|
107
|
+
if (wasRemoved) {
|
|
108
|
+
removed = true;
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Failed to remove from this source, continue
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!removed) {
|
|
116
|
+
throw new Error(`Server '${name}' not found in any configuration file`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async getServer(name: string): Promise<IMcpServerInfo | undefined> {
|
|
121
|
+
const servers = await this.listServers();
|
|
122
|
+
return servers.find((server) => server.name === name || server.id === name);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async addServerFromJson(name: string, jsonConfig: string): Promise<void> {
|
|
126
|
+
let config: IMcpServerConfig;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
config = this.parseJsonWithFallback(jsonConfig) as IMcpServerConfig;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Invalid JSON syntax: ${error instanceof Error ? error.message : String(error)}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
this.validateServerConfig(config);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Invalid server configuration: ${error instanceof Error ? error.message : String(error)}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await this.addServer(name, config);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async addServerFromUrl(name: string, url: string, args?: string[]): Promise<void> {
|
|
148
|
+
let config: IMcpServerConfig;
|
|
149
|
+
|
|
150
|
+
// Check if it's an HTTP URL
|
|
151
|
+
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
152
|
+
config = {
|
|
153
|
+
url,
|
|
154
|
+
};
|
|
155
|
+
} else {
|
|
156
|
+
// Treat as command
|
|
157
|
+
config = {
|
|
158
|
+
command: url,
|
|
159
|
+
args: args || [],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
await this.addServer(name, config);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
formatServerInfo(server: IMcpServerInfo): string {
|
|
167
|
+
const lines = [
|
|
168
|
+
`Name: ${server.name}`,
|
|
169
|
+
`ID: ${server.id}`,
|
|
170
|
+
`Source: ${server.description || "Unknown"}`,
|
|
171
|
+
`Status: ${server.status || "unknown"}`,
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
if ("url" in server.config) {
|
|
175
|
+
lines.push(`Type: HTTP`);
|
|
176
|
+
lines.push(`URL: ${server.config.url}`);
|
|
177
|
+
if (server.config.headers && Object.keys(server.config.headers).length > 0) {
|
|
178
|
+
lines.push(`Headers: ${JSON.stringify(server.config.headers, null, 2)}`);
|
|
179
|
+
}
|
|
180
|
+
} else if ("command" in server.config) {
|
|
181
|
+
lines.push(`Type: Command`);
|
|
182
|
+
lines.push(`Command: ${server.config.command}`);
|
|
183
|
+
if (server.config.args && server.config.args.length > 0) {
|
|
184
|
+
lines.push(`Arguments: ${server.config.args.join(" ")}`);
|
|
185
|
+
}
|
|
186
|
+
if (server.config.env && Object.keys(server.config.env).length > 0) {
|
|
187
|
+
lines.push(`Environment: ${JSON.stringify(server.config.env, null, 2)}`);
|
|
188
|
+
}
|
|
189
|
+
if (server.config.cwd) {
|
|
190
|
+
lines.push(`Working Directory: ${server.config.cwd}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return lines.join("\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
formatServerList(servers: IMcpServerInfo[]): string {
|
|
198
|
+
const lines: string[] = [];
|
|
199
|
+
|
|
200
|
+
if (servers.length === 0) {
|
|
201
|
+
lines.push("No MCP servers found.");
|
|
202
|
+
lines.push("");
|
|
203
|
+
lines.push("Use the following commands to manage MCP servers:");
|
|
204
|
+
lines.push("");
|
|
205
|
+
// Add the MCP help content
|
|
206
|
+
lines.push(helpText);
|
|
207
|
+
} else {
|
|
208
|
+
lines.push("📡 MCP Servers:");
|
|
209
|
+
lines.push("");
|
|
210
|
+
|
|
211
|
+
// Group servers by source
|
|
212
|
+
const serversBySource: Record<string, IMcpServerInfo[]> = {};
|
|
213
|
+
for (const server of servers) {
|
|
214
|
+
const source = this.getServerSourceLabel(server);
|
|
215
|
+
if (!serversBySource[source]) {
|
|
216
|
+
serversBySource[source] = [];
|
|
217
|
+
}
|
|
218
|
+
serversBySource[source].push(server);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const [source, sourceServers] of Object.entries(serversBySource)) {
|
|
222
|
+
lines.push(` ${source}:`);
|
|
223
|
+
for (const server of sourceServers) {
|
|
224
|
+
const typeLabel = "url" in server.config ? "🌐" : "💻";
|
|
225
|
+
lines.push(` ${typeLabel} ${server.name}`);
|
|
226
|
+
}
|
|
227
|
+
lines.push("");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
lines.push("📝 Example - Add GitHub MCP Server:");
|
|
231
|
+
lines.push(
|
|
232
|
+
' /mcp add-json github \'{"command":"npx","args":["-y","@modelcontextprotocol/server-github"],"env":{"GITHUB_PERSONAL_ACCESS_TOKEN":"your_token"}}\'',
|
|
233
|
+
);
|
|
234
|
+
lines.push("");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async findConfigSources(): Promise<ConfigSource[]> {
|
|
241
|
+
const sources: ConfigSource[] = [];
|
|
242
|
+
|
|
243
|
+
// Claude Desktop
|
|
244
|
+
const claudePath =
|
|
245
|
+
process.platform === "win32"
|
|
246
|
+
? path.join(os.homedir(), "AppData", "Roaming", "Claude", "claude_desktop_config.json")
|
|
247
|
+
: process.platform === "darwin"
|
|
248
|
+
? path.join(
|
|
249
|
+
os.homedir(),
|
|
250
|
+
"Library",
|
|
251
|
+
"Application Support",
|
|
252
|
+
"Claude",
|
|
253
|
+
"claude_desktop_config.json",
|
|
254
|
+
)
|
|
255
|
+
: path.join(os.homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
256
|
+
sources.push({
|
|
257
|
+
path: claudePath,
|
|
258
|
+
type: "claude-desktop",
|
|
259
|
+
label: "Claude Desktop",
|
|
260
|
+
exists: fs.existsSync(claudePath),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Windsurf
|
|
264
|
+
const windsurfPath = path.join(os.homedir(), ".codeium", "windsurf", "mcp_config.json");
|
|
265
|
+
sources.push({
|
|
266
|
+
path: windsurfPath,
|
|
267
|
+
type: "windsurf",
|
|
268
|
+
label: "Windsurf",
|
|
269
|
+
exists: fs.existsSync(windsurfPath),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Cursor Global: ~/.cursor/mcp.json
|
|
273
|
+
const cursorGlobal = path.join(os.homedir(), ".cursor", "mcp.json");
|
|
274
|
+
sources.push({
|
|
275
|
+
path: cursorGlobal,
|
|
276
|
+
type: "cursor-global",
|
|
277
|
+
label: "Cursor (Global)",
|
|
278
|
+
exists: fs.existsSync(cursorGlobal),
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Cursor Workspace: .cursor/mcp.json
|
|
282
|
+
const cursorWorkspace = path.join(process.cwd(), ".cursor", "mcp.json");
|
|
283
|
+
sources.push({
|
|
284
|
+
path: cursorWorkspace,
|
|
285
|
+
type: "cursor-workspace",
|
|
286
|
+
label: "Cursor (Workspace)",
|
|
287
|
+
exists: fs.existsSync(cursorWorkspace),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const possibleProductNames = [product.fullName, "AbacusAI", "CodeLLM"];
|
|
291
|
+
for (const productName of possibleProductNames) {
|
|
292
|
+
const userDataDir = this.getUserDataDir(productName);
|
|
293
|
+
if (userDataDir) {
|
|
294
|
+
const mcpJsonPath = path.join(userDataDir, "User", "mcp.json");
|
|
295
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
296
|
+
sources.push({
|
|
297
|
+
path: mcpJsonPath,
|
|
298
|
+
type: "abacusai-user",
|
|
299
|
+
label: `${productName} (User)`,
|
|
300
|
+
exists: true,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Standard VSCode User Settings (fallback)
|
|
307
|
+
const vscodeUserDir =
|
|
308
|
+
process.platform === "win32"
|
|
309
|
+
? path.join(os.homedir(), "AppData", "Roaming", "Code", "User")
|
|
310
|
+
: process.platform === "darwin"
|
|
311
|
+
? path.join(os.homedir(), "Library", "Application Support", "Code", "User")
|
|
312
|
+
: path.join(os.homedir(), ".config", "Code", "User");
|
|
313
|
+
const vscodeMcpJson = path.join(vscodeUserDir, "mcp.json");
|
|
314
|
+
sources.push({
|
|
315
|
+
path: vscodeMcpJson,
|
|
316
|
+
type: "vscode-user",
|
|
317
|
+
label: "VSCode (User)",
|
|
318
|
+
exists: fs.existsSync(vscodeMcpJson),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
return sources;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private getUserDataDir(productName: string): string | null {
|
|
325
|
+
try {
|
|
326
|
+
switch (process.platform) {
|
|
327
|
+
case "win32": {
|
|
328
|
+
const appData = process.env["APPDATA"];
|
|
329
|
+
if (!appData) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
return path.join(appData, productName);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
case "darwin": {
|
|
336
|
+
return path.join(os.homedir(), "Library", "Application Support", productName);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
case "linux": {
|
|
340
|
+
const configHome = process.env["XDG_CONFIG_HOME"] || path.join(os.homedir(), ".config");
|
|
341
|
+
return path.join(configHome, productName);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
default:
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private async loadServersFromSource(source: ConfigSource): Promise<IMcpServerInfo[]> {
|
|
353
|
+
const content = await fsPromises.readFile(source.path, "utf8");
|
|
354
|
+
const servers: IMcpServerInfo[] = [];
|
|
355
|
+
|
|
356
|
+
// Parse JSON with better error handling and trailing comma support
|
|
357
|
+
let parsedContent: any;
|
|
358
|
+
try {
|
|
359
|
+
parsedContent = this.parseJsonWithFallback(content);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`JSON parsing failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (source.type.startsWith("vscode") || source.type === "abacusai-user") {
|
|
367
|
+
let serversConfig: Record<string, IMcpServerConfig> | undefined;
|
|
368
|
+
|
|
369
|
+
if (parsedContent.servers && !parsedContent.mcp) {
|
|
370
|
+
serversConfig = parsedContent.servers;
|
|
371
|
+
} else if (parsedContent.mcp?.servers) {
|
|
372
|
+
serversConfig = parsedContent.mcp.servers;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (serversConfig) {
|
|
376
|
+
for (const [name, serverConfig] of Object.entries(serversConfig)) {
|
|
377
|
+
servers.push({
|
|
378
|
+
id: `${source.type}.${name}`,
|
|
379
|
+
name,
|
|
380
|
+
config: serverConfig,
|
|
381
|
+
status: "stopped",
|
|
382
|
+
description: `${source.label} - ${this.getServerTypeDescription(serverConfig)}`,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} else if (
|
|
387
|
+
["cursor-global", "cursor-workspace", "claude-desktop", "windsurf"].includes(source.type)
|
|
388
|
+
) {
|
|
389
|
+
const cursorConfig: CursorGlobalConfig = parsedContent;
|
|
390
|
+
|
|
391
|
+
if (cursorConfig.mcpServers) {
|
|
392
|
+
for (const [name, serverConfig] of Object.entries(cursorConfig.mcpServers)) {
|
|
393
|
+
// Convert format to our internal format
|
|
394
|
+
const config: IMcpServerConfig = serverConfig.url
|
|
395
|
+
? {
|
|
396
|
+
type: "http",
|
|
397
|
+
url: serverConfig.url,
|
|
398
|
+
headers: {},
|
|
399
|
+
}
|
|
400
|
+
: {
|
|
401
|
+
type: "stdio",
|
|
402
|
+
command: serverConfig.command || "",
|
|
403
|
+
args: serverConfig.args || [],
|
|
404
|
+
env: serverConfig.env || {},
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
servers.push({
|
|
408
|
+
id: `${source.type}.${name}`,
|
|
409
|
+
name,
|
|
410
|
+
config,
|
|
411
|
+
status: "stopped",
|
|
412
|
+
description: `${source.label} - ${this.getServerTypeDescription(config)}`,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return servers;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private parseJsonWithFallback(content: string): any {
|
|
422
|
+
// First try standard JSON parsing
|
|
423
|
+
try {
|
|
424
|
+
return JSON.parse(content);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
// If that fails, try to fix common JSON issues like trailing commas
|
|
427
|
+
try {
|
|
428
|
+
// Remove trailing commas before closing braces/brackets
|
|
429
|
+
const fixedContent = content
|
|
430
|
+
.replace(/,(\s*[}\]])/g, "$1") // Remove trailing commas
|
|
431
|
+
.replace(/([}\]]),(\s*[}\]])/g, "$1$2"); // Remove commas before closing braces
|
|
432
|
+
|
|
433
|
+
return JSON.parse(fixedContent);
|
|
434
|
+
} catch (fallbackError) {
|
|
435
|
+
// If still failing, provide more detailed error info
|
|
436
|
+
const originalError = error instanceof Error ? error.message : String(error);
|
|
437
|
+
const fallbackErrorMsg =
|
|
438
|
+
fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
439
|
+
throw new Error(
|
|
440
|
+
`JSON parsing failed. Original error: ${originalError}. After fixing trailing commas: ${fallbackErrorMsg}`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private async addServerToSource(
|
|
447
|
+
source: ConfigSource,
|
|
448
|
+
name: string,
|
|
449
|
+
config: IMcpServerConfig,
|
|
450
|
+
): Promise<void> {
|
|
451
|
+
const configPath = source.path;
|
|
452
|
+
// Ensure directory exists
|
|
453
|
+
const dir = path.dirname(configPath);
|
|
454
|
+
await fsPromises.mkdir(dir, { recursive: true });
|
|
455
|
+
|
|
456
|
+
// Determine config type based on source type
|
|
457
|
+
const isVSCodeLikeSettings = ["vscode-user", "vscode-workspace", "abacusai-user"].includes(
|
|
458
|
+
source.type,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
if (isVSCodeLikeSettings) {
|
|
462
|
+
if (configPath.endsWith("mcp.json")) {
|
|
463
|
+
let mcpConfig: McpConfigFile = { servers: {} };
|
|
464
|
+
if (fs.existsSync(configPath)) {
|
|
465
|
+
try {
|
|
466
|
+
const content = await fsPromises.readFile(configPath, "utf8");
|
|
467
|
+
mcpConfig = this.parseJsonWithFallback(content);
|
|
468
|
+
} catch {}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
mcpConfig.servers = mcpConfig.servers || {};
|
|
472
|
+
mcpConfig.servers[name] = config;
|
|
473
|
+
|
|
474
|
+
const newContent = JSON.stringify(mcpConfig, null, 2);
|
|
475
|
+
await fsPromises.writeFile(configPath, newContent, "utf8");
|
|
476
|
+
} else {
|
|
477
|
+
let vscodeSettings: VSCodeSettings = {};
|
|
478
|
+
if (fs.existsSync(configPath)) {
|
|
479
|
+
try {
|
|
480
|
+
const content = await fsPromises.readFile(configPath, "utf8");
|
|
481
|
+
vscodeSettings = this.parseJsonWithFallback(content);
|
|
482
|
+
} catch {}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
vscodeSettings.mcp = vscodeSettings.mcp || { servers: {} };
|
|
486
|
+
vscodeSettings.mcp.servers = vscodeSettings.mcp.servers || {};
|
|
487
|
+
vscodeSettings.mcp.servers[name] = config;
|
|
488
|
+
|
|
489
|
+
const newContent = JSON.stringify(vscodeSettings, null, 2);
|
|
490
|
+
await fsPromises.writeFile(configPath, newContent, "utf8");
|
|
491
|
+
}
|
|
492
|
+
} else if (source.type === "cursor-global") {
|
|
493
|
+
// Cursor Global format: { "mcpServers": { ... } }
|
|
494
|
+
let cursorConfig: CursorGlobalConfig = { mcpServers: {} };
|
|
495
|
+
if (fs.existsSync(configPath)) {
|
|
496
|
+
try {
|
|
497
|
+
const content = await fsPromises.readFile(configPath, "utf8");
|
|
498
|
+
cursorConfig = this.parseJsonWithFallback(content);
|
|
499
|
+
} catch {
|
|
500
|
+
// Invalid JSON, start fresh
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
cursorConfig.mcpServers = cursorConfig.mcpServers || {};
|
|
505
|
+
|
|
506
|
+
// Convert our internal format to Cursor format
|
|
507
|
+
const cursorServerConfig =
|
|
508
|
+
"url" in config
|
|
509
|
+
? {
|
|
510
|
+
url: config.url,
|
|
511
|
+
}
|
|
512
|
+
: {
|
|
513
|
+
command: config.command,
|
|
514
|
+
args: config.args ? [...config.args] : undefined,
|
|
515
|
+
env: config.env
|
|
516
|
+
? Object.fromEntries(Object.entries(config.env).map(([k, v]) => [k, String(v)]))
|
|
517
|
+
: undefined,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
cursorConfig.mcpServers[name] = cursorServerConfig;
|
|
521
|
+
|
|
522
|
+
const newContent = JSON.stringify(cursorConfig, null, 2);
|
|
523
|
+
await fsPromises.writeFile(configPath, newContent, "utf8");
|
|
524
|
+
} else if (source.type === "cursor-workspace") {
|
|
525
|
+
let cursorWorkspaceConfig: CursorGlobalConfig = { mcpServers: {} };
|
|
526
|
+
if (fs.existsSync(configPath)) {
|
|
527
|
+
try {
|
|
528
|
+
const content = await fsPromises.readFile(configPath, "utf8");
|
|
529
|
+
cursorWorkspaceConfig = this.parseJsonWithFallback(content);
|
|
530
|
+
} catch {
|
|
531
|
+
// Invalid JSON, start fresh
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
cursorWorkspaceConfig.mcpServers = cursorWorkspaceConfig.mcpServers || {};
|
|
536
|
+
|
|
537
|
+
const cursorServerConfig =
|
|
538
|
+
"url" in config
|
|
539
|
+
? {
|
|
540
|
+
url: config.url,
|
|
541
|
+
}
|
|
542
|
+
: {
|
|
543
|
+
command: config.command,
|
|
544
|
+
args: config.args ? [...config.args] : undefined,
|
|
545
|
+
env: config.env
|
|
546
|
+
? Object.fromEntries(Object.entries(config.env).map(([k, v]) => [k, String(v)]))
|
|
547
|
+
: undefined,
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
cursorWorkspaceConfig.mcpServers[name] = cursorServerConfig;
|
|
551
|
+
const newContent = JSON.stringify(cursorWorkspaceConfig, null, 2);
|
|
552
|
+
await fsPromises.writeFile(configPath, newContent, "utf8");
|
|
553
|
+
} else {
|
|
554
|
+
throw new Error(`Unsupported config source type: ${source.type}`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
private async removeServerFromSource(configPath: string, name: string): Promise<boolean> {
|
|
559
|
+
if (!fs.existsSync(configPath)) {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const content = await fsPromises.readFile(configPath, "utf8");
|
|
564
|
+
const isCursorOrClaudeFormat =
|
|
565
|
+
configPath.includes(".cursor") ||
|
|
566
|
+
configPath.includes("claude_desktop_config.json") ||
|
|
567
|
+
configPath.includes("mcp_config.json"); // Windsurf
|
|
568
|
+
|
|
569
|
+
if (configPath.endsWith("mcp.json")) {
|
|
570
|
+
const mcpConfig: McpConfigFile = this.parseJsonWithFallback(content);
|
|
571
|
+
|
|
572
|
+
if (!mcpConfig.servers || !mcpConfig.servers[name]) {
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
delete mcpConfig.servers[name];
|
|
577
|
+
|
|
578
|
+
const newContent = JSON.stringify(mcpConfig, null, 2);
|
|
579
|
+
await fsPromises.writeFile(configPath, newContent, "utf8");
|
|
580
|
+
return true;
|
|
581
|
+
} else if (configPath.endsWith("settings.json")) {
|
|
582
|
+
const vscodeSettings: VSCodeSettings = this.parseJsonWithFallback(content);
|
|
583
|
+
const config = vscodeSettings?.mcp || { servers: {} };
|
|
584
|
+
|
|
585
|
+
if (!config.servers || !config.servers[name]) {
|
|
586
|
+
return false;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
delete config.servers[name];
|
|
590
|
+
vscodeSettings.mcp = config;
|
|
591
|
+
|
|
592
|
+
const newContent = JSON.stringify(vscodeSettings, null, 2);
|
|
593
|
+
await fsPromises.writeFile(configPath, newContent, "utf8");
|
|
594
|
+
return true;
|
|
595
|
+
} else if (isCursorOrClaudeFormat) {
|
|
596
|
+
// Claude/Cursor/Windsurf format - matches GUI's claudeConfigToServerDefinition
|
|
597
|
+
const cursorConfig: CursorGlobalConfig = this.parseJsonWithFallback(content);
|
|
598
|
+
|
|
599
|
+
if (!cursorConfig.mcpServers || !cursorConfig.mcpServers[name]) {
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
delete cursorConfig.mcpServers[name];
|
|
604
|
+
|
|
605
|
+
const newContent = JSON.stringify(cursorConfig, null, 2);
|
|
606
|
+
await fsPromises.writeFile(configPath, newContent, "utf8");
|
|
607
|
+
return true;
|
|
608
|
+
} else {
|
|
609
|
+
throw new Error(`Unsupported config file format: ${configPath}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
private validateServerConfig(config: IMcpServerConfig): void {
|
|
614
|
+
// Check if config is null, undefined, or not an object
|
|
615
|
+
if (!config || typeof config !== "object") {
|
|
616
|
+
throw new Error("Server configuration must be a valid object");
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Check for invalid top-level properties that suggest wrong format
|
|
620
|
+
if ("mcpServers" in config || "servers" in config) {
|
|
621
|
+
throw new Error(
|
|
622
|
+
'Invalid configuration format. Server configuration should not contain "mcpServers" or "servers" properties. Each server should be configured individually.',
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if ("url" in config) {
|
|
627
|
+
// HTTP server validation
|
|
628
|
+
if (!config.url) {
|
|
629
|
+
throw new Error("URL is required for HTTP servers");
|
|
630
|
+
}
|
|
631
|
+
if (typeof config.url !== "string") {
|
|
632
|
+
throw new Error("URL must be a string");
|
|
633
|
+
}
|
|
634
|
+
if (!config.url.startsWith("http://") && !config.url.startsWith("https://")) {
|
|
635
|
+
throw new Error("URL must start with http:// or https://");
|
|
636
|
+
}
|
|
637
|
+
} else if ("command" in config) {
|
|
638
|
+
// Stdio server validation
|
|
639
|
+
if (!config.command) {
|
|
640
|
+
throw new Error("Command is required for stdio servers");
|
|
641
|
+
}
|
|
642
|
+
if (typeof config.command !== "string") {
|
|
643
|
+
throw new Error("Command must be a string");
|
|
644
|
+
}
|
|
645
|
+
// Validate args if present
|
|
646
|
+
if (config.args && !Array.isArray(config.args)) {
|
|
647
|
+
throw new Error("Arguments must be an array");
|
|
648
|
+
}
|
|
649
|
+
if (config.args && config.args.some((arg) => typeof arg !== "string")) {
|
|
650
|
+
throw new Error("All arguments must be strings");
|
|
651
|
+
}
|
|
652
|
+
} else {
|
|
653
|
+
throw new Error('Server configuration must have either "url" or "command" property');
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private getServerTypeDescription(config: IMcpServerConfig): string {
|
|
658
|
+
if ("url" in config) {
|
|
659
|
+
return `HTTP server at ${config.url}`;
|
|
660
|
+
} else if ("command" in config) {
|
|
661
|
+
const argsStr = config.args && config.args.length > 0 ? ` ${config.args.join(" ")}` : "";
|
|
662
|
+
return `Command: ${config.command}${argsStr}`;
|
|
663
|
+
}
|
|
664
|
+
return "Unknown server type";
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
private getServerSourceLabel(server: IMcpServerInfo): string {
|
|
668
|
+
if (server.id.startsWith("claude-desktop")) {
|
|
669
|
+
return "Claude Desktop";
|
|
670
|
+
} else if (server.id.startsWith("windsurf")) {
|
|
671
|
+
return "Windsurf";
|
|
672
|
+
} else if (server.id.startsWith("cursor-global")) {
|
|
673
|
+
return "Cursor (Global)";
|
|
674
|
+
} else if (server.id.startsWith("cursor-workspace")) {
|
|
675
|
+
return "Cursor (Workspace)";
|
|
676
|
+
} else if (server.id.startsWith("abacusai-user")) {
|
|
677
|
+
const isDev =
|
|
678
|
+
server.description?.includes(`${product.fullName}-dev`) ||
|
|
679
|
+
server.description?.includes("CodeLLM-dev");
|
|
680
|
+
return isDev ? `${product.fullName}-dev (User)` : `${product.fullName} (User)`;
|
|
681
|
+
} else if (server.id.startsWith("vscode-user")) {
|
|
682
|
+
return "VSCode (User)";
|
|
683
|
+
} else if (server.id.startsWith("vscode-workspace")) {
|
|
684
|
+
return "VSCode (Workspace)";
|
|
685
|
+
}
|
|
686
|
+
return "Other";
|
|
687
|
+
}
|
|
688
|
+
}
|