termux-dev 1.6.0 → 1.8.1
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/README.md +214 -166
- package/assets/banner.svg +23 -28
- package/assets/preview.png +0 -0
- package/assets/preview.svg +101 -10
- package/dist/cli/banner.js +3 -4
- package/dist/cli/clipboard-write.js +55 -0
- package/dist/cli/clipboard.js +182 -106
- package/dist/cli/colors.js +67 -0
- package/dist/cli/confirm.js +50 -0
- package/dist/cli/doctor.js +4 -4
- package/dist/cli/effort.js +80 -0
- package/dist/cli/export.js +3 -3
- package/dist/cli/files.js +19 -5
- package/dist/cli/finder.js +241 -0
- package/dist/cli/headless.js +23 -11
- package/dist/cli/index.js +1557 -744
- package/dist/cli/keys.js +69 -0
- package/dist/cli/liveblock.js +45 -0
- package/dist/cli/log.js +19 -0
- package/dist/cli/logo.js +30 -0
- package/dist/cli/markdown.js +174 -37
- package/dist/cli/mcp-panel.js +432 -0
- package/dist/cli/permission.js +84 -0
- package/dist/cli/picker.js +143 -0
- package/dist/cli/prompt.js +256 -56
- package/dist/cli/resume.js +15 -0
- package/dist/cli/rewind.js +51 -0
- package/dist/cli/runtime_errors.js +6 -0
- package/dist/cli/server.js +85 -24
- package/dist/cli/settings.js +307 -0
- package/dist/cli/statusline.js +1 -1
- package/dist/cli/theme.js +126 -108
- package/dist/cli/transcript.js +134 -0
- package/dist/cli/typeahead.js +371 -0
- package/dist/cli/ui.js +1022 -0
- package/dist/cli/updater.js +23 -21
- package/dist/cli/usage-panel.js +242 -0
- package/dist/cli/welcome.js +56 -0
- package/dist/cli/width.js +178 -0
- package/dist/core/capabilities.js +433 -0
- package/dist/core/catalog-util.js +27 -0
- package/dist/core/checkpoint.js +90 -0
- package/dist/core/context-stats.js +29 -0
- package/dist/core/cost.js +191 -0
- package/dist/core/formats.js +199 -0
- package/dist/core/history.js +51 -1
- package/dist/core/hooks.js +79 -0
- package/dist/core/i18n.js +42 -0
- package/dist/core/local-config.js +69 -0
- package/dist/core/locales/de.js +265 -0
- package/dist/core/locales/en.js +276 -0
- package/dist/core/locales/es.js +265 -0
- package/dist/core/locales/fr.js +265 -0
- package/dist/core/locales/ja.js +265 -0
- package/dist/core/locales/pt.js +265 -0
- package/dist/core/locales/ru.js +265 -0
- package/dist/core/locales/uk.js +265 -0
- package/dist/core/locales/zh.js +265 -0
- package/dist/core/loop.js +125 -49
- package/dist/core/memory.js +39 -12
- package/dist/core/models.js +33 -20
- package/dist/core/network.js +12 -11
- package/dist/core/notify.js +54 -0
- package/dist/core/planfile.js +43 -0
- package/dist/core/price-catalog.json +1 -0
- package/dist/core/prices.js +249 -0
- package/dist/core/reasoning-catalog.json +1 -0
- package/dist/core/reasoning.js +393 -0
- package/dist/core/redact.js +41 -0
- package/dist/core/secure-file.js +22 -0
- package/dist/core/session.js +16 -5
- package/dist/core/snapshot.js +14 -2
- package/dist/core/title.js +61 -0
- package/dist/core/types.js +1 -1
- package/dist/core/usage.js +43 -7
- package/dist/core/vision-catalog.json +1 -0
- package/dist/core/vision.js +123 -0
- package/dist/mcp/client.js +36 -5
- package/dist/mcp/http-client.js +174 -0
- package/dist/mcp/manager.js +330 -93
- package/dist/permissions/guard.js +385 -117
- package/dist/prompts/builder.js +46 -38
- package/dist/providers/anthropic.js +517 -0
- package/dist/providers/index.js +18 -1
- package/dist/providers/openai.js +138 -125
- package/dist/providers/shared.js +116 -0
- package/dist/providers/toolguard.js +101 -0
- package/dist/tools/bash.js +141 -51
- package/dist/tools/diagnostics.js +76 -48
- package/dist/tools/fs.js +77 -5
- package/dist/tools/index.js +2 -1
- package/dist/tools/mcp.js +72 -0
- package/dist/tools/packages.js +16 -1
- package/dist/tools/plan.js +8 -8
- package/dist/tools/questions.js +50 -54
- package/dist/tools/server.js +6 -0
- package/dist/tools/todo.js +20 -18
- package/dist/tools/web.js +143 -8
- package/package.json +1 -1
package/dist/mcp/manager.js
CHANGED
|
@@ -1,19 +1,224 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
1
|
import fsSync from 'fs';
|
|
3
2
|
import path from 'path';
|
|
4
3
|
import os from 'os';
|
|
4
|
+
import { createHash } from 'crypto';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import { MCPClient } from './client.js';
|
|
7
|
+
import { HttpMCPClient } from './http-client.js';
|
|
7
8
|
import { getCurrentTheme } from '../cli/theme.js';
|
|
8
9
|
function stripAnsi(str) {
|
|
9
10
|
return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
10
11
|
}
|
|
12
|
+
// Config files that live inside the project. MCP servers are arbitrary commands, so
|
|
13
|
+
// whatever a cloned repository declares here is NOT started until the user approved it.
|
|
14
|
+
const PROJECT_CONFIG_FILES = [
|
|
15
|
+
path.join('.devx', 'mcp.json'),
|
|
16
|
+
path.join('.claude', 'mcp.json'),
|
|
17
|
+
'.devx.json',
|
|
18
|
+
'.mcp.json'
|
|
19
|
+
];
|
|
20
|
+
/** In the home folder the "project" files are the user's own files: they are not a project's, and need no approval. */
|
|
21
|
+
function isUserFile(file) {
|
|
22
|
+
const abs = path.resolve(file);
|
|
23
|
+
return USER_FILES.some((f) => path.resolve(os.homedir(), f.rel) === abs);
|
|
24
|
+
}
|
|
25
|
+
function readMcpServers(file) {
|
|
26
|
+
const result = {};
|
|
27
|
+
try {
|
|
28
|
+
if (!fsSync.existsSync(file))
|
|
29
|
+
return result;
|
|
30
|
+
const parsed = JSON.parse(fsSync.readFileSync(file, 'utf8'));
|
|
31
|
+
if (parsed && typeof parsed.mcpServers === 'object' && parsed.mcpServers) {
|
|
32
|
+
for (const [name, cfg] of Object.entries(parsed.mcpServers)) {
|
|
33
|
+
if (cfg && typeof cfg === 'object' && (cfg.command || cfg.url)) {
|
|
34
|
+
result[name] = cfg;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
/** MCP servers declared by the current project (not started, only listed). */
|
|
43
|
+
export function discoverProjectMcpServers(cwd = process.cwd()) {
|
|
44
|
+
const found = [];
|
|
45
|
+
for (const rel of PROJECT_CONFIG_FILES) {
|
|
46
|
+
const file = path.join(cwd, rel);
|
|
47
|
+
if (isUserFile(file))
|
|
48
|
+
continue;
|
|
49
|
+
for (const [name, config] of Object.entries(readMcpServers(file))) {
|
|
50
|
+
found.push({ file: rel, name, config });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
/** Stable fingerprint of the commands, arguments and env of the project's servers. */
|
|
56
|
+
export function hashProjectMcpServers(servers) {
|
|
57
|
+
const normalized = servers
|
|
58
|
+
.map(s => ({
|
|
59
|
+
file: s.file,
|
|
60
|
+
name: s.name,
|
|
61
|
+
command: s.config.command ?? '',
|
|
62
|
+
url: s.config.url ?? '',
|
|
63
|
+
headers: Object.fromEntries(Object.entries(s.config.headers || {}).sort(([a], [b]) => a.localeCompare(b))),
|
|
64
|
+
args: s.config.args || [],
|
|
65
|
+
env: Object.fromEntries(Object.entries(s.config.env || {}).sort(([a], [b]) => a.localeCompare(b))),
|
|
66
|
+
disabled: !!s.config.disabled
|
|
67
|
+
}))
|
|
68
|
+
.sort((a, b) => (a.file + a.name).localeCompare(b.file + b.name));
|
|
69
|
+
return createHash('sha256').update(JSON.stringify(normalized)).digest('hex');
|
|
70
|
+
}
|
|
71
|
+
const USER_FILES = [
|
|
72
|
+
{ rel: '.devxrc.json', label: '~/.devxrc.json' },
|
|
73
|
+
{ rel: path.join('.devx', 'mcp.json'), label: '~/.devx/mcp.json' },
|
|
74
|
+
];
|
|
75
|
+
/** What the user switched on or off in the /mcp panel. Kept apart from the config files, which are never edited. */
|
|
76
|
+
/** Provider APIs only accept [A-Za-z0-9_-]{1,64} as a tool name; servers may use dots, spaces or other scripts. */
|
|
77
|
+
export function mcpToolName(server, tool) {
|
|
78
|
+
const clean = (v) => v.replace(/[^A-Za-z0-9_-]+/g, '_');
|
|
79
|
+
const full = `mcp__${clean(server)}__${clean(tool)}`;
|
|
80
|
+
if (full.length <= 64)
|
|
81
|
+
return full;
|
|
82
|
+
let h = 0;
|
|
83
|
+
for (const ch of full)
|
|
84
|
+
h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
|
85
|
+
return `${full.slice(0, 55)}_${h.toString(36).padStart(8, '0').slice(-8)}`;
|
|
86
|
+
}
|
|
87
|
+
function uniqueName(name, existing) {
|
|
88
|
+
let out = name;
|
|
89
|
+
for (let i = 2; existing.some((t) => t.name === out); i++)
|
|
90
|
+
out = `${name.slice(0, 60)}_${i}`;
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
const stateFile = () => path.join(os.homedir(), '.devx', 'mcp-state.json');
|
|
94
|
+
function readOverrides() {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(fsSync.readFileSync(stateFile(), 'utf8'));
|
|
97
|
+
return parsed && typeof parsed.disabled === 'object' && parsed.disabled ? parsed.disabled : {};
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function writeOverrides(disabled) {
|
|
104
|
+
try {
|
|
105
|
+
const file = stateFile();
|
|
106
|
+
fsSync.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
107
|
+
fsSync.writeFileSync(file, JSON.stringify({ disabled }, null, 2), { mode: 0o600 });
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
}
|
|
111
|
+
const stateKey = (scope, name, cwd = process.cwd()) => (scope === 'project' ? `${cwd}::${name}` : `user::${name}`);
|
|
112
|
+
function toolInfo(def) {
|
|
113
|
+
const props = def.inputSchema?.properties && typeof def.inputSchema.properties === 'object' ? def.inputSchema.properties : {};
|
|
114
|
+
const required = Array.isArray(def.inputSchema?.required) ? def.inputSchema.required : [];
|
|
115
|
+
const a = def.annotations || {};
|
|
116
|
+
return {
|
|
117
|
+
name: def.name,
|
|
118
|
+
description: String(def.description || ''),
|
|
119
|
+
readOnly: a.readOnlyHint === true,
|
|
120
|
+
// MCP assumes a tool may be destructive unless it says otherwise; only say so when it is stated.
|
|
121
|
+
destructive: a.destructiveHint === true && a.readOnlyHint !== true,
|
|
122
|
+
openWorld: a.openWorldHint === true,
|
|
123
|
+
params: Object.entries(props).map(([name, spec]) => ({
|
|
124
|
+
name,
|
|
125
|
+
type: Array.isArray(spec?.type) ? spec.type.join('|') : String(spec?.type ?? 'any'),
|
|
126
|
+
required: required.includes(name),
|
|
127
|
+
description: String(spec?.description || ''),
|
|
128
|
+
})),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
const userMcpFile = () => path.join(os.homedir(), '.devx', 'mcp.json');
|
|
132
|
+
/**
|
|
133
|
+
* The servers of Claude Code (`~/.claude.json`: the user's own and the ones of this folder and its parents).
|
|
134
|
+
* A local server is taken as it is; a remote one (an address) is reached through the
|
|
135
|
+
* `mcp-remote` bridge, which devx can start like any local command.
|
|
136
|
+
*/
|
|
137
|
+
export function readClaudeServers(cwd = process.cwd()) {
|
|
138
|
+
const out = {};
|
|
139
|
+
let data;
|
|
140
|
+
try {
|
|
141
|
+
data = JSON.parse(fsSync.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
// Claude Code keeps some servers per folder: the ones of this folder and of the folders above it count.
|
|
147
|
+
const ancestors = [];
|
|
148
|
+
for (let dir = cwd;; dir = path.dirname(dir)) {
|
|
149
|
+
ancestors.unshift(dir);
|
|
150
|
+
if (path.dirname(dir) === dir)
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
const groups = [data?.mcpServers, ...ancestors.map((dir) => data?.projects?.[dir]?.mcpServers)];
|
|
154
|
+
for (const group of groups) {
|
|
155
|
+
if (!group || typeof group !== 'object')
|
|
156
|
+
continue;
|
|
157
|
+
for (const [name, raw] of Object.entries(group)) {
|
|
158
|
+
if (!raw || typeof raw !== 'object')
|
|
159
|
+
continue;
|
|
160
|
+
if (typeof raw.command === 'string' && raw.command) {
|
|
161
|
+
out[name] = { command: raw.command, ...(Array.isArray(raw.args) ? { args: raw.args.map(String) } : {}), ...(raw.env && typeof raw.env === 'object' ? { env: raw.env } : {}) };
|
|
162
|
+
}
|
|
163
|
+
else if (typeof raw.url === 'string' && raw.url) {
|
|
164
|
+
out[name] = { command: 'npx', args: ['-y', 'mcp-remote', raw.url] };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
/** Writes servers into the user's own file `~/.devx/mcp.json`. A name that is already there is left alone unless `overwrite`. Returns the names that were written. */
|
|
171
|
+
export function saveUserServers(servers, overwrite = false) {
|
|
172
|
+
const file = userMcpFile();
|
|
173
|
+
let current = {};
|
|
174
|
+
try {
|
|
175
|
+
current = JSON.parse(fsSync.readFileSync(file, 'utf8'));
|
|
176
|
+
}
|
|
177
|
+
catch { }
|
|
178
|
+
const existing = current && typeof current.mcpServers === 'object' && current.mcpServers ? current.mcpServers : {};
|
|
179
|
+
const written = [];
|
|
180
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
181
|
+
if (name in existing && !overwrite)
|
|
182
|
+
continue;
|
|
183
|
+
existing[name] = cfg;
|
|
184
|
+
written.push(name);
|
|
185
|
+
}
|
|
186
|
+
if (written.length > 0) {
|
|
187
|
+
fsSync.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
188
|
+
// The file may hold tokens in `env`: readable by the owner only.
|
|
189
|
+
fsSync.writeFileSync(file, JSON.stringify({ ...current, mcpServers: existing }, null, 2), { mode: 0o600 });
|
|
190
|
+
}
|
|
191
|
+
return written;
|
|
192
|
+
}
|
|
193
|
+
/** Removes a server from the user's own file `~/.devx/mcp.json`. Returns false when it is not there (a server of another file has to be removed by hand). */
|
|
194
|
+
export function removeUserServer(name) {
|
|
195
|
+
const file = userMcpFile();
|
|
196
|
+
try {
|
|
197
|
+
const current = JSON.parse(fsSync.readFileSync(file, 'utf8'));
|
|
198
|
+
if (!current?.mcpServers || !(name in current.mcpServers))
|
|
199
|
+
return false;
|
|
200
|
+
delete current.mcpServers[name];
|
|
201
|
+
fsSync.writeFileSync(file, JSON.stringify(current, null, 2), { mode: 0o600 });
|
|
202
|
+
// What the panel remembered about it (switched off, ...) goes too.
|
|
203
|
+
const overrides = readOverrides();
|
|
204
|
+
if (overrides[stateKey('user', name)] !== undefined) {
|
|
205
|
+
delete overrides[stateKey('user', name)];
|
|
206
|
+
writeOverrides(overrides);
|
|
207
|
+
}
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
11
214
|
export class MCPManager {
|
|
12
215
|
static instance;
|
|
13
216
|
clients = new Map();
|
|
217
|
+
entries = new Map();
|
|
14
218
|
tools = [];
|
|
15
219
|
serverStatuses = new Map();
|
|
16
220
|
isInitialized = false;
|
|
221
|
+
trustProject = false;
|
|
17
222
|
constructor() { }
|
|
18
223
|
static getInstance() {
|
|
19
224
|
if (!MCPManager.instance) {
|
|
@@ -21,81 +226,98 @@ export class MCPManager {
|
|
|
21
226
|
}
|
|
22
227
|
return MCPManager.instance;
|
|
23
228
|
}
|
|
24
|
-
|
|
229
|
+
/**
|
|
230
|
+
* @param explicitConfigs use exactly these servers instead of reading config files
|
|
231
|
+
* @param trustProject also start the servers declared inside the current project
|
|
232
|
+
* (only pass true after the user approved them)
|
|
233
|
+
*/
|
|
234
|
+
async init(explicitConfigs, trustProject = false) {
|
|
25
235
|
if (this.isInitialized) {
|
|
26
236
|
return this.tools;
|
|
27
237
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (
|
|
238
|
+
this.trustProject = trustProject;
|
|
239
|
+
this.entries.clear();
|
|
240
|
+
if (explicitConfigs) {
|
|
241
|
+
for (const [name, config] of Object.entries(explicitConfigs))
|
|
242
|
+
this.entries.set(name, { config, source: 'explicit', scope: 'user' });
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
for (const [name, entry] of await this.loadEntries(trustProject))
|
|
246
|
+
this.entries.set(name, entry);
|
|
247
|
+
}
|
|
248
|
+
if (this.entries.size === 0) {
|
|
31
249
|
this.isInitialized = true;
|
|
32
250
|
return [];
|
|
33
251
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
252
|
+
await Promise.all(Array.from(this.entries.keys()).map((name) => this.startServer(name)));
|
|
253
|
+
this.isInitialized = true;
|
|
254
|
+
return this.tools;
|
|
255
|
+
}
|
|
256
|
+
isDisabled(name) {
|
|
257
|
+
const entry = this.entries.get(name);
|
|
258
|
+
const override = readOverrides()[stateKey(entry.scope, name)];
|
|
259
|
+
return override !== undefined ? override : !!entry.config.disabled;
|
|
260
|
+
}
|
|
261
|
+
/** Starts one server and registers its tools. Never throws: a failure becomes the server's status. */
|
|
262
|
+
async startServer(name) {
|
|
263
|
+
const entry = this.entries.get(name);
|
|
264
|
+
if (!entry)
|
|
265
|
+
return;
|
|
266
|
+
const { config: cfg, source, scope } = entry;
|
|
267
|
+
const command = cfg.url ? cfg.url : `${cfg.command ?? ''} ${(cfg.args || []).join(' ')}`.trim();
|
|
268
|
+
const base = { name, command, source, scope, toolsCount: 0, tools: [] };
|
|
269
|
+
if (this.isDisabled(name)) {
|
|
270
|
+
this.serverStatuses.set(name, { ...base, status: 'disabled' });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
this.serverStatuses.set(name, { ...base, status: 'connecting' });
|
|
274
|
+
const client = cfg.url ? new HttpMCPClient(name, { ...cfg, disabled: false }) : new MCPClient(name, { ...cfg, disabled: false });
|
|
275
|
+
this.clients.set(name, client);
|
|
276
|
+
try {
|
|
277
|
+
const mcpTools = await client.start();
|
|
45
278
|
this.serverStatuses.set(name, {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
279
|
+
...base,
|
|
280
|
+
status: 'connected',
|
|
281
|
+
toolsCount: mcpTools.length,
|
|
282
|
+
tools: mcpTools.map((t) => t.name),
|
|
283
|
+
capabilities: client.getCapabilities(),
|
|
284
|
+
toolInfo: mcpTools.map(toolInfo),
|
|
51
285
|
});
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
command: `${cfg.command} ${(cfg.args || []).join(' ')}`.trim(),
|
|
59
|
-
status: 'connected',
|
|
60
|
-
toolsCount: mcpTools.length,
|
|
61
|
-
tools: mcpTools.map(t => t.name)
|
|
62
|
-
});
|
|
63
|
-
// Convert MCP tools into devx Tool format
|
|
64
|
-
for (const mt of mcpTools) {
|
|
65
|
-
const namespacedName = `mcp__${name}__${mt.name}`;
|
|
66
|
-
const devxTool = {
|
|
286
|
+
// Convert MCP tools into devx Tool format
|
|
287
|
+
for (const mt of mcpTools) {
|
|
288
|
+
const namespacedName = uniqueName(mcpToolName(name, mt.name), this.tools);
|
|
289
|
+
const devxTool = {
|
|
290
|
+
name: namespacedName,
|
|
291
|
+
definition: {
|
|
67
292
|
name: namespacedName,
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
properties: mt.inputSchema?.properties || {},
|
|
74
|
-
required: mt.inputSchema?.required
|
|
75
|
-
}
|
|
293
|
+
description: `[MCP: ${name}] ${mt.description || mt.name}`,
|
|
294
|
+
parameters: {
|
|
295
|
+
type: 'object',
|
|
296
|
+
properties: mt.inputSchema?.properties || {},
|
|
297
|
+
required: mt.inputSchema?.required,
|
|
76
298
|
},
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
299
|
+
},
|
|
300
|
+
validateArgs(_args) { },
|
|
301
|
+
async execute(args) {
|
|
302
|
+
return await client.callTool(mt.name, args);
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
this.tools.push(devxTool);
|
|
84
306
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
this.
|
|
98
|
-
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
this.clients.delete(name);
|
|
310
|
+
this.serverStatuses.set(name, { ...base, status: 'failed', error: err.message });
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** Stops a server and takes its tools away from the agent. */
|
|
314
|
+
stopServer(name) {
|
|
315
|
+
try {
|
|
316
|
+
this.clients.get(name)?.close();
|
|
317
|
+
}
|
|
318
|
+
catch { }
|
|
319
|
+
this.clients.delete(name);
|
|
320
|
+
this.tools = this.tools.filter((t) => !t.name.startsWith(mcpToolName(name, '')));
|
|
99
321
|
}
|
|
100
322
|
getTools() {
|
|
101
323
|
return this.tools;
|
|
@@ -103,13 +325,36 @@ export class MCPManager {
|
|
|
103
325
|
getStatuses() {
|
|
104
326
|
return Array.from(this.serverStatuses.values());
|
|
105
327
|
}
|
|
328
|
+
getStatus(name) {
|
|
329
|
+
return this.serverStatuses.get(name);
|
|
330
|
+
}
|
|
331
|
+
/** Starts a server again (also after it failed). The tools of the agent follow at once. */
|
|
332
|
+
async reconnect(name) {
|
|
333
|
+
if (!this.entries.has(name))
|
|
334
|
+
return undefined;
|
|
335
|
+
this.stopServer(name);
|
|
336
|
+
await this.startServer(name);
|
|
337
|
+
return this.serverStatuses.get(name);
|
|
338
|
+
}
|
|
339
|
+
/** Switches a server off: it is stopped and stays off in the next sessions too. */
|
|
340
|
+
async setEnabled(name, enabled) {
|
|
341
|
+
const entry = this.entries.get(name);
|
|
342
|
+
if (!entry)
|
|
343
|
+
return undefined;
|
|
344
|
+
const overrides = readOverrides();
|
|
345
|
+
overrides[stateKey(entry.scope, name)] = !enabled;
|
|
346
|
+
writeOverrides(overrides);
|
|
347
|
+
this.stopServer(name);
|
|
348
|
+
await this.startServer(name);
|
|
349
|
+
return this.serverStatuses.get(name);
|
|
350
|
+
}
|
|
106
351
|
async reload() {
|
|
107
352
|
this.stopAll();
|
|
108
353
|
this.isInitialized = false;
|
|
109
354
|
this.clients.clear();
|
|
110
355
|
this.tools = [];
|
|
111
356
|
this.serverStatuses.clear();
|
|
112
|
-
return await this.init();
|
|
357
|
+
return await this.init(undefined, this.trustProject);
|
|
113
358
|
}
|
|
114
359
|
stopAll() {
|
|
115
360
|
for (const client of this.clients.values()) {
|
|
@@ -131,7 +376,7 @@ export class MCPManager {
|
|
|
131
376
|
const spaces = Math.max(1, innerWidth - plainLen);
|
|
132
377
|
return `│ ${left}${' '.repeat(spaces)}${right} │`;
|
|
133
378
|
};
|
|
134
|
-
const header = theme.colorFn('┌─ ') + pc.bold('
|
|
379
|
+
const header = theme.colorFn('┌─ ') + pc.bold('Model Context Protocol (MCP) Servers') + ' ' + theme.colorFn('─'.repeat(Math.max(2, boxWidth - 43)) + '┐');
|
|
135
380
|
const divider = theme.colorFn('├' + '─'.repeat(boxWidth - 2) + '┤');
|
|
136
381
|
const footer = theme.colorFn('└' + '─'.repeat(boxWidth - 2) + '┘');
|
|
137
382
|
const lines = [header];
|
|
@@ -145,13 +390,13 @@ export class MCPManager {
|
|
|
145
390
|
const s = statuses[i];
|
|
146
391
|
if (i > 0)
|
|
147
392
|
lines.push(divider);
|
|
148
|
-
let statusBadge = pc.green('
|
|
393
|
+
let statusBadge = pc.green('● Connected');
|
|
149
394
|
if (s.status === 'connecting')
|
|
150
|
-
statusBadge = pc.yellow('
|
|
395
|
+
statusBadge = pc.yellow('● Connecting');
|
|
151
396
|
if (s.status === 'failed')
|
|
152
|
-
statusBadge = pc.red('
|
|
397
|
+
statusBadge = pc.red('● Failed');
|
|
153
398
|
if (s.status === 'disabled')
|
|
154
|
-
statusBadge = pc.dim('
|
|
399
|
+
statusBadge = pc.dim('○ Disabled');
|
|
155
400
|
lines.push(padRow(pc.bold(pc.white(`Server: ${s.name}`)), statusBadge));
|
|
156
401
|
lines.push(padRow(pc.dim(` Command: ${s.command.slice(0, 36)}${s.command.length > 36 ? '...' : ''}`), pc.cyan(`${s.toolsCount} tools`)));
|
|
157
402
|
if (s.tools.length > 0) {
|
|
@@ -166,30 +411,22 @@ export class MCPManager {
|
|
|
166
411
|
lines.push(footer);
|
|
167
412
|
return lines.join('\n') + '\n';
|
|
168
413
|
}
|
|
169
|
-
async
|
|
170
|
-
const result =
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
path.join(
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (parsed && typeof parsed.mcpServers === 'object') {
|
|
184
|
-
for (const [name, cfg] of Object.entries(parsed.mcpServers)) {
|
|
185
|
-
if (cfg && typeof cfg === 'object' && cfg.command) {
|
|
186
|
-
result[name] = cfg;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
414
|
+
async loadEntries(includeProject) {
|
|
415
|
+
const result = new Map();
|
|
416
|
+
// User-level configuration is always trusted: the user wrote it. A later file wins over an earlier one.
|
|
417
|
+
for (const f of USER_FILES) {
|
|
418
|
+
for (const [name, config] of Object.entries(readMcpServers(path.join(os.homedir(), f.rel)))) {
|
|
419
|
+
result.set(name, { config, source: f.label, scope: 'user' });
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (includeProject) {
|
|
423
|
+
for (const rel of PROJECT_CONFIG_FILES) {
|
|
424
|
+
if (isUserFile(path.join(process.cwd(), rel)))
|
|
425
|
+
continue;
|
|
426
|
+
for (const [name, config] of Object.entries(readMcpServers(path.join(process.cwd(), rel)))) {
|
|
427
|
+
result.set(name, { config, source: rel, scope: 'project' });
|
|
190
428
|
}
|
|
191
429
|
}
|
|
192
|
-
catch { }
|
|
193
430
|
}
|
|
194
431
|
return result;
|
|
195
432
|
}
|