@kernel.chat/kbot 2.23.2 → 2.24.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.
@@ -0,0 +1,759 @@
1
+ // K:BOT MCP Marketplace — Discover, install, and manage MCP servers
2
+ //
3
+ // Lets users browse an MCP server registry, install servers (npm or git),
4
+ // and manage their ~/.kbot/mcp-config.json configuration.
5
+ //
6
+ // Flow:
7
+ // 1. mcp_search — Search the MCP registry for servers by keyword
8
+ // 2. mcp_install — Install an MCP server (npm package or git repo)
9
+ // 3. mcp_uninstall — Remove an installed MCP server
10
+ // 4. mcp_list — List installed MCP servers with status
11
+ // 5. mcp_update — Update an installed MCP server
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { homedir } from 'node:os';
15
+ import { execFile } from 'node:child_process';
16
+ import { registerTool } from './index.js';
17
+ // ── Paths ────────────────────────────────────────────────────────────────────
18
+ const KBOT_DIR = join(homedir(), '.kbot');
19
+ const MCP_CONFIG_PATH = join(KBOT_DIR, 'mcp-config.json');
20
+ const MCP_REGISTRY_CACHE_PATH = join(KBOT_DIR, 'mcp-registry.json');
21
+ const MCP_SERVERS_DIR = join(KBOT_DIR, 'mcp-servers');
22
+ const REGISTRY_SOURCE_URL = 'https://raw.githubusercontent.com/modelcontextprotocol/servers/main/README.md';
23
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
24
+ // ── Bundled servers (fallback when registry unavailable) ─────────────────────
25
+ const BUNDLED_SERVERS = [
26
+ // Official servers
27
+ {
28
+ name: 'filesystem',
29
+ package: '@modelcontextprotocol/server-filesystem',
30
+ description: 'Secure file system access with configurable allowed directories. Read, write, search, and manage files and directories.',
31
+ source: 'official',
32
+ install: '@modelcontextprotocol/server-filesystem',
33
+ transport: 'stdio',
34
+ },
35
+ {
36
+ name: 'github',
37
+ package: '@modelcontextprotocol/server-github',
38
+ description: 'GitHub API integration for repository management, file operations, issues, pull requests, branches, and search.',
39
+ source: 'official',
40
+ install: '@modelcontextprotocol/server-github',
41
+ transport: 'stdio',
42
+ },
43
+ {
44
+ name: 'postgres',
45
+ package: '@modelcontextprotocol/server-postgres',
46
+ description: 'PostgreSQL database integration with read-only query access and schema inspection.',
47
+ source: 'official',
48
+ install: '@modelcontextprotocol/server-postgres',
49
+ transport: 'stdio',
50
+ },
51
+ {
52
+ name: 'sqlite',
53
+ package: '@modelcontextprotocol/server-sqlite',
54
+ description: 'SQLite database operations including querying, analysis, schema inspection, and business intelligence.',
55
+ source: 'official',
56
+ install: '@modelcontextprotocol/server-sqlite',
57
+ transport: 'stdio',
58
+ },
59
+ {
60
+ name: 'brave-search',
61
+ package: '@modelcontextprotocol/server-brave-search',
62
+ description: 'Web and local search using the Brave Search API. Supports both web and local business searches.',
63
+ source: 'official',
64
+ install: '@modelcontextprotocol/server-brave-search',
65
+ transport: 'stdio',
66
+ },
67
+ {
68
+ name: 'puppeteer',
69
+ package: '@modelcontextprotocol/server-puppeteer',
70
+ description: 'Browser automation via Puppeteer. Navigate pages, take screenshots, click elements, fill forms, and execute JavaScript.',
71
+ source: 'official',
72
+ install: '@modelcontextprotocol/server-puppeteer',
73
+ transport: 'stdio',
74
+ },
75
+ {
76
+ name: 'slack',
77
+ package: '@modelcontextprotocol/server-slack',
78
+ description: 'Slack workspace integration for channel management, messaging, user lookup, and thread replies.',
79
+ source: 'official',
80
+ install: '@modelcontextprotocol/server-slack',
81
+ transport: 'stdio',
82
+ },
83
+ {
84
+ name: 'google-maps',
85
+ package: '@modelcontextprotocol/server-google-maps',
86
+ description: 'Google Maps Platform integration for geocoding, directions, elevation, and place search.',
87
+ source: 'official',
88
+ install: '@modelcontextprotocol/server-google-maps',
89
+ transport: 'stdio',
90
+ },
91
+ {
92
+ name: 'memory',
93
+ package: '@modelcontextprotocol/server-memory',
94
+ description: 'Knowledge graph-based persistent memory system. Store and retrieve entities, relations, and observations.',
95
+ source: 'official',
96
+ install: '@modelcontextprotocol/server-memory',
97
+ transport: 'stdio',
98
+ },
99
+ {
100
+ name: 'sequential-thinking',
101
+ package: '@modelcontextprotocol/server-sequential-thinking',
102
+ description: 'Dynamic chain-of-thought reasoning. Break down complex problems with branching, revision, and hypothesis tracking.',
103
+ source: 'official',
104
+ install: '@modelcontextprotocol/server-sequential-thinking',
105
+ transport: 'stdio',
106
+ },
107
+ // Community servers
108
+ {
109
+ name: 'everything',
110
+ package: '@modelcontextprotocol/server-everything',
111
+ description: 'Reference/test MCP server that exercises all MCP features: tools, resources, prompts, sampling, and more.',
112
+ source: 'community',
113
+ install: '@modelcontextprotocol/server-everything',
114
+ transport: 'stdio',
115
+ },
116
+ {
117
+ name: 'fetch',
118
+ package: '@modelcontextprotocol/server-fetch',
119
+ description: 'Fetch web content and convert HTML to markdown for LM-friendly consumption. Supports robots.txt.',
120
+ source: 'official',
121
+ install: '@modelcontextprotocol/server-fetch',
122
+ transport: 'stdio',
123
+ },
124
+ {
125
+ name: 'git',
126
+ package: '@modelcontextprotocol/server-git',
127
+ description: 'Git repository operations including reading, searching, and analyzing local Git repositories.',
128
+ source: 'official',
129
+ install: '@modelcontextprotocol/server-git',
130
+ transport: 'stdio',
131
+ },
132
+ {
133
+ name: 'playwright',
134
+ package: '@playwright/mcp',
135
+ description: 'Browser automation with Playwright. Snapshot-based interactions, navigation, screenshots, and form filling.',
136
+ source: 'community',
137
+ install: '@playwright/mcp',
138
+ transport: 'stdio',
139
+ },
140
+ {
141
+ name: 'redis',
142
+ package: '@modelcontextprotocol/server-redis',
143
+ description: 'Redis database integration with key-value operations, pub/sub, and data structure manipulation.',
144
+ source: 'community',
145
+ install: '@modelcontextprotocol/server-redis',
146
+ transport: 'stdio',
147
+ },
148
+ {
149
+ name: 'linear',
150
+ package: '@jlowin/linear-mcp',
151
+ description: 'Linear project management integration. Manage issues, projects, teams, and comments.',
152
+ source: 'community',
153
+ install: '@jlowin/linear-mcp',
154
+ transport: 'stdio',
155
+ },
156
+ {
157
+ name: 'notion',
158
+ package: '@notionhq/notion-mcp-server',
159
+ description: 'Notion workspace integration. Search, read, and create pages, databases, and comments.',
160
+ source: 'community',
161
+ install: '@notionhq/notion-mcp-server',
162
+ transport: 'stdio',
163
+ },
164
+ {
165
+ name: 'supabase',
166
+ package: '@supabase/mcp-server',
167
+ description: 'Supabase platform management. Database queries, storage, auth, edge functions, and project configuration.',
168
+ source: 'community',
169
+ install: '@supabase/mcp-server',
170
+ transport: 'stdio',
171
+ },
172
+ {
173
+ name: 'sentry',
174
+ package: '@sentry/mcp-server',
175
+ description: 'Sentry error tracking integration. Search, view, and resolve issues and events across projects.',
176
+ source: 'community',
177
+ install: '@sentry/mcp-server',
178
+ transport: 'stdio',
179
+ },
180
+ {
181
+ name: 'cloudflare',
182
+ package: '@cloudflare/mcp-server-cloudflare',
183
+ description: 'Cloudflare platform management. Workers, KV, R2, D1, and zone configuration.',
184
+ source: 'community',
185
+ install: '@cloudflare/mcp-server-cloudflare',
186
+ transport: 'stdio',
187
+ },
188
+ ];
189
+ // ── Shell helper ─────────────────────────────────────────────────────────────
190
+ function shell(cmd, args, timeout = 60_000) {
191
+ return new Promise((resolve, reject) => {
192
+ execFile(cmd, args, { timeout, maxBuffer: 2 * 1024 * 1024 }, (err, stdout, stderr) => {
193
+ if (err)
194
+ reject(new Error(stderr || err.message));
195
+ else
196
+ resolve((stdout || stderr).trim());
197
+ });
198
+ });
199
+ }
200
+ // ── Ensure directories ───────────────────────────────────────────────────────
201
+ function ensureDir(dir) {
202
+ if (!existsSync(dir)) {
203
+ mkdirSync(dir, { recursive: true });
204
+ }
205
+ }
206
+ // ── MCP Config (mcp-config.json) ─────────────────────────────────────────────
207
+ function readMcpConfig() {
208
+ if (!existsSync(MCP_CONFIG_PATH)) {
209
+ return { servers: {} };
210
+ }
211
+ try {
212
+ const raw = readFileSync(MCP_CONFIG_PATH, 'utf-8');
213
+ const parsed = JSON.parse(raw);
214
+ if (!parsed.servers || typeof parsed.servers !== 'object') {
215
+ return { servers: {} };
216
+ }
217
+ return parsed;
218
+ }
219
+ catch {
220
+ return { servers: {} };
221
+ }
222
+ }
223
+ function writeMcpConfig(config) {
224
+ ensureDir(KBOT_DIR);
225
+ writeFileSync(MCP_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
226
+ }
227
+ function addServerToConfig(name, entry) {
228
+ const config = readMcpConfig();
229
+ config.servers[name] = entry;
230
+ writeMcpConfig(config);
231
+ }
232
+ function removeServerFromConfig(name) {
233
+ const config = readMcpConfig();
234
+ if (!(name in config.servers))
235
+ return false;
236
+ delete config.servers[name];
237
+ writeMcpConfig(config);
238
+ return true;
239
+ }
240
+ // ── Registry cache ───────────────────────────────────────────────────────────
241
+ function readRegistryCache() {
242
+ if (!existsSync(MCP_REGISTRY_CACHE_PATH))
243
+ return null;
244
+ try {
245
+ const raw = readFileSync(MCP_REGISTRY_CACHE_PATH, 'utf-8');
246
+ return JSON.parse(raw);
247
+ }
248
+ catch {
249
+ return null;
250
+ }
251
+ }
252
+ function writeRegistryCache(servers) {
253
+ ensureDir(KBOT_DIR);
254
+ const cache = { servers, fetchedAt: Date.now() };
255
+ writeFileSync(MCP_REGISTRY_CACHE_PATH, JSON.stringify(cache, null, 2), 'utf-8');
256
+ }
257
+ function isCacheValid(cache) {
258
+ return Date.now() - cache.fetchedAt < CACHE_TTL_MS;
259
+ }
260
+ // ── Parse servers from MCP README ────────────────────────────────────────────
261
+ function parseServersFromReadme(markdown) {
262
+ const servers = [];
263
+ const lines = markdown.split('\n');
264
+ // The README has markdown tables or bullet lists with server names and links.
265
+ // We look for lines like:
266
+ // - [@modelcontextprotocol/server-xxx](link) - Description
267
+ // | [name](link) | description |
268
+ // Or markdown list entries referencing npm packages / GitHub repos.
269
+ let currentSource = 'official';
270
+ for (const line of lines) {
271
+ const trimmed = line.trim();
272
+ // Detect section headers to classify official vs community
273
+ if (/^#{1,3}\s/.test(trimmed)) {
274
+ const headerLower = trimmed.toLowerCase();
275
+ if (headerLower.includes('community') || headerLower.includes('third-party') || headerLower.includes('partner')) {
276
+ currentSource = 'community';
277
+ }
278
+ else if (headerLower.includes('reference') || headerLower.includes('official')) {
279
+ currentSource = 'official';
280
+ }
281
+ }
282
+ // Match bullet list items with npm package patterns:
283
+ // - [Package Name](url) - Description text
284
+ // - **Package Name** - Description text
285
+ // Look for npm-style package names: @scope/name or simple-name
286
+ const bulletMatch = trimmed.match(/^[-*]\s+\[([^\]]+)\]\(([^)]+)\)\s*[-–—:]\s*(.+)$/);
287
+ if (bulletMatch) {
288
+ const linkText = bulletMatch[1];
289
+ const url = bulletMatch[2];
290
+ const desc = bulletMatch[3].trim();
291
+ // Try to extract npm package name from link text or URL
292
+ let pkg = '';
293
+ let name = '';
294
+ // Check if link text looks like an npm package
295
+ const npmMatch = linkText.match(/^(@[a-z0-9-]+\/[a-z0-9._-]+|[a-z0-9._-]+)$/i);
296
+ if (npmMatch) {
297
+ pkg = npmMatch[1];
298
+ // Derive a short name from the package
299
+ name = pkg.replace(/^@[^/]+\//, '').replace(/^server-/, '').replace(/^mcp-server-?/, '').replace(/^mcp-/, '');
300
+ }
301
+ else {
302
+ // Use link text as name, try to extract package from URL
303
+ name = linkText.toLowerCase().replace(/\s+/g, '-');
304
+ const npmUrlMatch = url.match(/npmjs\.com\/package\/([^/\s]+(?:\/[^/\s]+)?)/);
305
+ if (npmUrlMatch) {
306
+ pkg = npmUrlMatch[1];
307
+ }
308
+ else {
309
+ // GitHub URL — use as the install source
310
+ const ghMatch = url.match(/github\.com\/([^/]+\/[^/\s#]+)/);
311
+ if (ghMatch) {
312
+ pkg = ghMatch[1].replace(/\.git$/, '');
313
+ }
314
+ }
315
+ }
316
+ if (pkg && name && desc) {
317
+ // Skip if we already have this server from bundled list
318
+ const alreadyKnown = servers.some(s => s.package === pkg || s.name === name);
319
+ if (!alreadyKnown) {
320
+ servers.push({
321
+ name,
322
+ package: pkg,
323
+ description: desc.slice(0, 200),
324
+ source: currentSource,
325
+ install: pkg,
326
+ transport: 'stdio',
327
+ });
328
+ }
329
+ }
330
+ }
331
+ // Also match markdown table rows: | [name](url) | desc |
332
+ const tableMatch = trimmed.match(/^\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(.+?)\s*\|?$/);
333
+ if (tableMatch) {
334
+ const linkText = tableMatch[1];
335
+ const url = tableMatch[2];
336
+ const desc = tableMatch[3].replace(/\s*\|$/, '').trim();
337
+ let pkg = '';
338
+ let name = '';
339
+ const npmMatch2 = linkText.match(/^(@[a-z0-9-]+\/[a-z0-9._-]+|[a-z0-9._-]+)$/i);
340
+ if (npmMatch2) {
341
+ pkg = npmMatch2[1];
342
+ name = pkg.replace(/^@[^/]+\//, '').replace(/^server-/, '').replace(/^mcp-server-?/, '').replace(/^mcp-/, '');
343
+ }
344
+ else {
345
+ name = linkText.toLowerCase().replace(/\s+/g, '-');
346
+ const ghMatch = url.match(/github\.com\/([^/]+\/[^/\s#]+)/);
347
+ if (ghMatch) {
348
+ pkg = ghMatch[1].replace(/\.git$/, '');
349
+ }
350
+ }
351
+ if (pkg && name && desc && !desc.startsWith('---')) {
352
+ const alreadyKnown = servers.some(s => s.package === pkg || s.name === name);
353
+ if (!alreadyKnown) {
354
+ servers.push({
355
+ name,
356
+ package: pkg,
357
+ description: desc.slice(0, 200),
358
+ source: currentSource,
359
+ install: pkg,
360
+ transport: 'stdio',
361
+ });
362
+ }
363
+ }
364
+ }
365
+ }
366
+ return servers;
367
+ }
368
+ // ── Fetch registry ───────────────────────────────────────────────────────────
369
+ async function fetchRegistry() {
370
+ // Check cache first
371
+ const cached = readRegistryCache();
372
+ if (cached && isCacheValid(cached)) {
373
+ return cached.servers;
374
+ }
375
+ // Fetch from GitHub
376
+ try {
377
+ const res = await fetch(REGISTRY_SOURCE_URL, {
378
+ headers: { 'User-Agent': 'KBot/2.14' },
379
+ signal: AbortSignal.timeout(15_000),
380
+ });
381
+ if (!res.ok) {
382
+ throw new Error(`Registry returned ${res.status}`);
383
+ }
384
+ const markdown = await res.text();
385
+ const parsed = parseServersFromReadme(markdown);
386
+ // Merge bundled servers with parsed ones — bundled take priority for known entries
387
+ const merged = [...BUNDLED_SERVERS];
388
+ for (const server of parsed) {
389
+ const exists = merged.some(s => s.package === server.package || s.name === server.name);
390
+ if (!exists) {
391
+ merged.push(server);
392
+ }
393
+ }
394
+ writeRegistryCache(merged);
395
+ return merged;
396
+ }
397
+ catch {
398
+ // Fall back to stale cache if available
399
+ if (cached) {
400
+ return cached.servers;
401
+ }
402
+ // Fall back to bundled list
403
+ return [...BUNDLED_SERVERS];
404
+ }
405
+ }
406
+ // ── Determine if an install target is git-based ──────────────────────────────
407
+ function isGitSource(target) {
408
+ return (target.includes('github.com') ||
409
+ target.includes('gitlab.com') ||
410
+ target.includes('bitbucket.org') ||
411
+ target.endsWith('.git') ||
412
+ // user/repo pattern without @ scope
413
+ (/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+$/.test(target) && !target.startsWith('@')));
414
+ }
415
+ // ── Derive a short server name ───────────────────────────────────────────────
416
+ function deriveServerName(target) {
417
+ // npm package: @scope/server-foo → foo
418
+ const scopedMatch = target.match(/@[^/]+\/(?:server-|mcp-server-?|mcp-)?(.+)$/);
419
+ if (scopedMatch)
420
+ return scopedMatch[1];
421
+ // Unscoped npm: server-foo → foo
422
+ const unscopedMatch = target.match(/^(?:server-|mcp-server-?|mcp-)?(.+)$/);
423
+ if (unscopedMatch)
424
+ return unscopedMatch[1];
425
+ // Git URL: extract repo name
426
+ const urlMatch = target.match(/\/([^/]+?)(?:\.git)?$/);
427
+ if (urlMatch) {
428
+ return urlMatch[1].replace(/^server-/, '').replace(/^mcp-server-?/, '').replace(/^mcp-/, '');
429
+ }
430
+ return target.replace(/[^a-zA-Z0-9-]/g, '-');
431
+ }
432
+ // ── Tool registration ────────────────────────────────────────────────────────
433
+ export function registerMcpMarketplaceTools() {
434
+ // ── mcp_search ─────────────────────────────────────────────────────────────
435
+ registerTool({
436
+ name: 'mcp_search',
437
+ description: 'Search the MCP server registry for available servers by keyword. Returns matching MCP servers with name, package, description, and source (official/community). Fetches from the official MCP servers repository and caches results for 24 hours.',
438
+ parameters: {
439
+ query: { type: 'string', description: 'Search keyword — matches server name, package, and description. Leave empty to list all known servers.', required: true },
440
+ },
441
+ tier: 'free',
442
+ async execute(args) {
443
+ const query = String(args.query || '').toLowerCase().trim();
444
+ try {
445
+ const servers = await fetchRegistry();
446
+ const results = query
447
+ ? servers.filter(s => {
448
+ return (s.name.toLowerCase().includes(query) ||
449
+ s.package.toLowerCase().includes(query) ||
450
+ s.description.toLowerCase().includes(query) ||
451
+ s.source.toLowerCase().includes(query));
452
+ })
453
+ : servers;
454
+ if (results.length === 0) {
455
+ return `No MCP servers found for "${query}". Try a broader search or leave the query empty to list all ${servers.length} known servers.`;
456
+ }
457
+ // Check which are already installed
458
+ const config = readMcpConfig();
459
+ const installedNames = new Set(Object.keys(config.servers));
460
+ const lines = [
461
+ `Found ${results.length} MCP server${results.length === 1 ? '' : 's'}:`,
462
+ '',
463
+ ];
464
+ for (const server of results) {
465
+ const installed = installedNames.has(server.name) ? ' [installed]' : '';
466
+ const badge = server.source === 'official' ? '[official]' : '[community]';
467
+ lines.push(` ${server.name} ${badge}${installed}`, ` Package: ${server.package}`, ` ${server.description}`, ` Install: mcp_install { "target": "${server.install}" }`, '');
468
+ }
469
+ return lines.join('\n');
470
+ }
471
+ catch (err) {
472
+ return `Error searching MCP registry: ${err instanceof Error ? err.message : String(err)}`;
473
+ }
474
+ },
475
+ });
476
+ // ── mcp_install ────────────────────────────────────────────────────────────
477
+ registerTool({
478
+ name: 'mcp_install',
479
+ description: 'Install an MCP server from npm or a git repository. For npm packages, installs globally. For git repos, clones to ~/.kbot/mcp-servers/<name>/ and runs npm install. After install, auto-adds to ~/.kbot/mcp-config.json so it can be connected via mcp_connect.',
480
+ parameters: {
481
+ target: { type: 'string', description: 'npm package name (e.g., "@modelcontextprotocol/server-github") or git repo URL / user/repo shorthand', required: true },
482
+ name: { type: 'string', description: 'Override the server name in config (default: derived from package name)' },
483
+ env: { type: 'object', description: 'Environment variables the server needs (e.g., {"GITHUB_TOKEN": "..."})', properties: {}, },
484
+ },
485
+ tier: 'free',
486
+ timeout: 180_000,
487
+ async execute(args) {
488
+ const target = String(args.target).trim();
489
+ if (!target) {
490
+ return 'Error: target is required. Provide an npm package name or git repository URL.';
491
+ }
492
+ const serverName = args.name ? String(args.name) : deriveServerName(target);
493
+ const envVars = args.env || {};
494
+ // Check if already installed
495
+ const config = readMcpConfig();
496
+ if (config.servers[serverName]) {
497
+ return `MCP server "${serverName}" is already installed. Use mcp_update to update it, or mcp_uninstall first to reinstall.`;
498
+ }
499
+ if (isGitSource(target)) {
500
+ // ── Git-based install ──
501
+ const cloneUrl = target.includes('://') ? target : `https://github.com/${target}.git`;
502
+ const serverDir = join(MCP_SERVERS_DIR, serverName);
503
+ ensureDir(MCP_SERVERS_DIR);
504
+ // Remove existing directory if present
505
+ if (existsSync(serverDir)) {
506
+ rmSync(serverDir, { recursive: true, force: true });
507
+ }
508
+ try {
509
+ await shell('git', ['clone', '--depth', '1', cloneUrl, serverDir], 120_000);
510
+ }
511
+ catch (err) {
512
+ return `Error cloning ${cloneUrl}: ${err instanceof Error ? err.message : String(err)}`;
513
+ }
514
+ // Install dependencies if package.json exists
515
+ const pkgJsonPath = join(serverDir, 'package.json');
516
+ if (existsSync(pkgJsonPath)) {
517
+ try {
518
+ await shell('npm', ['install', '--prefix', serverDir], 120_000);
519
+ }
520
+ catch (err) {
521
+ return `Cloned ${target} but npm install failed: ${err instanceof Error ? err.message : String(err)}. Server may not work correctly.`;
522
+ }
523
+ }
524
+ // Determine the entry point
525
+ let entryPoint = 'index.js';
526
+ if (existsSync(pkgJsonPath)) {
527
+ try {
528
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
529
+ if (pkg.main)
530
+ entryPoint = pkg.main;
531
+ else if (pkg.bin) {
532
+ const bins = typeof pkg.bin === 'string' ? { default: pkg.bin } : pkg.bin;
533
+ const firstBin = Object.values(bins)[0];
534
+ if (typeof firstBin === 'string')
535
+ entryPoint = firstBin;
536
+ }
537
+ }
538
+ catch {
539
+ // keep default
540
+ }
541
+ }
542
+ // Add to config
543
+ const serverConfig = {
544
+ command: 'node',
545
+ args: [join(serverDir, entryPoint)],
546
+ env: envVars,
547
+ };
548
+ addServerToConfig(serverName, serverConfig);
549
+ return [
550
+ `Installed MCP server "${serverName}" from git.`,
551
+ `Location: ${serverDir}`,
552
+ `Config added to ${MCP_CONFIG_PATH}`,
553
+ `Connect with: mcp_connect { "name": "${serverName}", "command": "${serverConfig.command} ${serverConfig.args.join(' ')}" }`,
554
+ ].join('\n');
555
+ }
556
+ else {
557
+ // ── npm-based install ──
558
+ try {
559
+ await shell('npm', ['install', '-g', target], 120_000);
560
+ }
561
+ catch (err) {
562
+ return `Error installing ${target} from npm: ${err instanceof Error ? err.message : String(err)}`;
563
+ }
564
+ // Add to config using npx -y for reliable invocation
565
+ const serverConfig = {
566
+ command: 'npx',
567
+ args: ['-y', target],
568
+ env: envVars,
569
+ };
570
+ addServerToConfig(serverName, serverConfig);
571
+ return [
572
+ `Installed MCP server "${serverName}" from npm (${target}).`,
573
+ `Config added to ${MCP_CONFIG_PATH}`,
574
+ `Connect with: mcp_connect { "name": "${serverName}", "command": "npx -y ${target}" }`,
575
+ ].join('\n');
576
+ }
577
+ },
578
+ });
579
+ // ── mcp_uninstall ──────────────────────────────────────────────────────────
580
+ registerTool({
581
+ name: 'mcp_uninstall',
582
+ description: 'Remove an installed MCP server. Removes it from ~/.kbot/mcp-config.json and optionally uninstalls the npm global package or deletes the cloned git repo.',
583
+ parameters: {
584
+ name: { type: 'string', description: 'The server name to uninstall (as shown in mcp_list)', required: true },
585
+ keep_package: { type: 'boolean', description: 'If true, only remove from config without uninstalling the npm package or deleting the repo (default: false)' },
586
+ },
587
+ tier: 'free',
588
+ async execute(args) {
589
+ const name = String(args.name).trim();
590
+ if (!name) {
591
+ return 'Error: name is required. Use mcp_list to see installed servers.';
592
+ }
593
+ const config = readMcpConfig();
594
+ const serverConfig = config.servers[name];
595
+ if (!serverConfig) {
596
+ return `MCP server "${name}" is not installed. Use mcp_list to see installed servers.`;
597
+ }
598
+ const keepPackage = Boolean(args.keep_package);
599
+ const messages = [];
600
+ // Remove from config first
601
+ removeServerFromConfig(name);
602
+ messages.push(`Removed "${name}" from ${MCP_CONFIG_PATH}`);
603
+ if (!keepPackage) {
604
+ // Determine if this was npm-based or git-based
605
+ const gitServerDir = join(MCP_SERVERS_DIR, name);
606
+ if (existsSync(gitServerDir)) {
607
+ // Git-based: remove the cloned directory
608
+ try {
609
+ rmSync(gitServerDir, { recursive: true, force: true });
610
+ messages.push(`Deleted cloned repo at ${gitServerDir}`);
611
+ }
612
+ catch (err) {
613
+ messages.push(`Warning: could not delete ${gitServerDir}: ${err instanceof Error ? err.message : String(err)}`);
614
+ }
615
+ }
616
+ else if (serverConfig.command === 'npx' && serverConfig.args.length >= 2) {
617
+ // npm-based: try to uninstall the global package
618
+ const pkg = serverConfig.args[serverConfig.args.length - 1];
619
+ try {
620
+ await shell('npm', ['uninstall', '-g', pkg], 60_000);
621
+ messages.push(`Uninstalled global npm package: ${pkg}`);
622
+ }
623
+ catch (err) {
624
+ messages.push(`Warning: npm uninstall -g ${pkg} failed: ${err instanceof Error ? err.message : String(err)}. Package may still be installed globally.`);
625
+ }
626
+ }
627
+ }
628
+ return messages.join('\n');
629
+ },
630
+ });
631
+ // ── mcp_list ───────────────────────────────────────────────────────────────
632
+ registerTool({
633
+ name: 'mcp_list',
634
+ description: 'List all installed MCP servers from ~/.kbot/mcp-config.json. Shows each server\'s name, command, arguments, and environment variables.',
635
+ parameters: {},
636
+ tier: 'free',
637
+ async execute() {
638
+ const config = readMcpConfig();
639
+ const names = Object.keys(config.servers);
640
+ if (names.length === 0) {
641
+ return [
642
+ 'No MCP servers installed.',
643
+ '',
644
+ 'Use mcp_search to discover available servers, then mcp_install to add them.',
645
+ 'Example: mcp_search { "query": "github" }',
646
+ ].join('\n');
647
+ }
648
+ const lines = [
649
+ `${names.length} MCP server${names.length === 1 ? '' : 's'} installed:`,
650
+ '',
651
+ ];
652
+ for (const name of names) {
653
+ const server = config.servers[name];
654
+ const fullCommand = [server.command, ...server.args].join(' ');
655
+ const envKeys = Object.keys(server.env || {});
656
+ const envInfo = envKeys.length > 0
657
+ ? `Env: ${envKeys.map(k => `${k}=${server.env[k] ? '***' : '(empty)'}`).join(', ')}`
658
+ : 'Env: (none)';
659
+ // Check if the server binary/directory is accessible
660
+ let status = 'ready';
661
+ if (server.command === 'node' && server.args.length > 0) {
662
+ const entryPath = server.args[0];
663
+ if (!existsSync(entryPath)) {
664
+ status = 'missing — entry point not found';
665
+ }
666
+ }
667
+ lines.push(` ${name} [${status}]`, ` Command: ${fullCommand}`, ` ${envInfo}`, '');
668
+ }
669
+ lines.push(`Config: ${MCP_CONFIG_PATH}`);
670
+ return lines.join('\n');
671
+ },
672
+ });
673
+ // ── mcp_update ─────────────────────────────────────────────────────────────
674
+ registerTool({
675
+ name: 'mcp_update',
676
+ description: 'Update an installed MCP server. For npm packages, runs npm update -g. For git repos, pulls latest changes and reinstalls dependencies.',
677
+ parameters: {
678
+ name: { type: 'string', description: 'The server name to update (as shown in mcp_list)', required: true },
679
+ },
680
+ tier: 'free',
681
+ timeout: 180_000,
682
+ async execute(args) {
683
+ const name = String(args.name).trim();
684
+ if (!name) {
685
+ return 'Error: name is required. Use mcp_list to see installed servers.';
686
+ }
687
+ const config = readMcpConfig();
688
+ const serverConfig = config.servers[name];
689
+ if (!serverConfig) {
690
+ return `MCP server "${name}" is not installed. Use mcp_list to see installed servers.`;
691
+ }
692
+ const gitServerDir = join(MCP_SERVERS_DIR, name);
693
+ if (existsSync(gitServerDir)) {
694
+ // Git-based: pull latest and reinstall
695
+ try {
696
+ const pullOutput = await shell('git', ['-C', gitServerDir, 'pull', '--ff-only'], 60_000);
697
+ // Reinstall dependencies
698
+ const pkgJsonPath = join(gitServerDir, 'package.json');
699
+ if (existsSync(pkgJsonPath)) {
700
+ try {
701
+ await shell('npm', ['install', '--prefix', gitServerDir], 120_000);
702
+ }
703
+ catch (err) {
704
+ return `Pulled latest for "${name}" but npm install failed: ${err instanceof Error ? err.message : String(err)}`;
705
+ }
706
+ }
707
+ // Read new version if available
708
+ let version = 'unknown';
709
+ const pkgPath = join(gitServerDir, 'package.json');
710
+ if (existsSync(pkgPath)) {
711
+ try {
712
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
713
+ version = pkg.version || 'unknown';
714
+ }
715
+ catch {
716
+ // keep unknown
717
+ }
718
+ }
719
+ return [
720
+ `Updated MCP server "${name}" (git).`,
721
+ `Version: ${version}`,
722
+ pullOutput.includes('Already up to date') ? 'Already up to date.' : `Changes pulled: ${pullOutput.split('\n')[0]}`,
723
+ ].join('\n');
724
+ }
725
+ catch (err) {
726
+ return `Error updating "${name}": ${err instanceof Error ? err.message : String(err)}`;
727
+ }
728
+ }
729
+ else if (serverConfig.command === 'npx' && serverConfig.args.length >= 2) {
730
+ // npm-based: update the global package
731
+ const pkg = serverConfig.args[serverConfig.args.length - 1];
732
+ try {
733
+ const output = await shell('npm', ['update', '-g', pkg], 120_000);
734
+ // Get the new version
735
+ let version = 'unknown';
736
+ try {
737
+ const viewOutput = await shell('npm', ['view', pkg, 'version'], 15_000);
738
+ version = viewOutput.trim();
739
+ }
740
+ catch {
741
+ // keep unknown
742
+ }
743
+ return [
744
+ `Updated MCP server "${name}" (npm: ${pkg}).`,
745
+ `Latest version: ${version}`,
746
+ output || 'Update complete.',
747
+ ].join('\n');
748
+ }
749
+ catch (err) {
750
+ return `Error updating "${name}": ${err instanceof Error ? err.message : String(err)}`;
751
+ }
752
+ }
753
+ else {
754
+ return `Cannot determine update method for "${name}". Command: ${serverConfig.command} ${serverConfig.args.join(' ')}. Try mcp_uninstall and mcp_install to reinstall.`;
755
+ }
756
+ },
757
+ });
758
+ }
759
+ //# sourceMappingURL=mcp-marketplace.js.map