opencode-mcp-marketplace 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,602 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { execFileSync } from "child_process";
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+ import * as os from "os";
9
+ // Cross-platform paths
10
+ const HOME = os.homedir();
11
+ const CONFIG_DIR = path.join(HOME, ".config", "opencode");
12
+ const MCPS_DIR = path.join(CONFIG_DIR, "mcps");
13
+ const OPENCODE_CONFIG = path.join(CONFIG_DIR, "opencode.json");
14
+ const MARKETPLACE_STATE = path.join(CONFIG_DIR, "marketplace-state.json");
15
+ // GitHub config
16
+ const GITHUB_USER = "schwarztim";
17
+ const GITHUB_API = "https://api.github.com";
18
+ // Safe command execution helper
19
+ function runCommand(command, args, cwd) {
20
+ try {
21
+ const output = execFileSync(command, args, {
22
+ cwd,
23
+ encoding: "utf-8",
24
+ stdio: ["pipe", "pipe", "pipe"],
25
+ });
26
+ return { success: true, output };
27
+ }
28
+ catch (error) {
29
+ const err = error;
30
+ return { success: false, output: err.stderr || err.message || "Unknown error" };
31
+ }
32
+ }
33
+ // Ensure directories exist
34
+ function ensureDirectories() {
35
+ if (!fs.existsSync(CONFIG_DIR)) {
36
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
37
+ }
38
+ if (!fs.existsSync(MCPS_DIR)) {
39
+ fs.mkdirSync(MCPS_DIR, { recursive: true });
40
+ }
41
+ }
42
+ // Load marketplace state
43
+ function loadState() {
44
+ if (fs.existsSync(MARKETPLACE_STATE)) {
45
+ return JSON.parse(fs.readFileSync(MARKETPLACE_STATE, "utf-8"));
46
+ }
47
+ return { installed: {} };
48
+ }
49
+ // Save marketplace state
50
+ function saveState(state) {
51
+ fs.writeFileSync(MARKETPLACE_STATE, JSON.stringify(state, null, 2));
52
+ }
53
+ // Load opencode config
54
+ function loadOpencodeConfig() {
55
+ if (fs.existsSync(OPENCODE_CONFIG)) {
56
+ return JSON.parse(fs.readFileSync(OPENCODE_CONFIG, "utf-8"));
57
+ }
58
+ return {};
59
+ }
60
+ // Save opencode config
61
+ function saveOpencodeConfig(config) {
62
+ fs.writeFileSync(OPENCODE_CONFIG, JSON.stringify(config, null, 2));
63
+ }
64
+ // Validate MCP name (alphanumeric, hyphens, underscores only)
65
+ function isValidMcpName(name) {
66
+ return /^[a-zA-Z0-9_-]+$/.test(name);
67
+ }
68
+ // Fetch repos from GitHub
69
+ async function fetchGitHubRepos() {
70
+ const repos = [];
71
+ let page = 1;
72
+ const perPage = 100;
73
+ while (true) {
74
+ const response = await fetch(`${GITHUB_API}/users/${GITHUB_USER}/repos?per_page=${perPage}&page=${page}&sort=updated`);
75
+ if (!response.ok) {
76
+ throw new Error(`GitHub API error: ${response.status}`);
77
+ }
78
+ const data = (await response.json());
79
+ if (data.length === 0)
80
+ break;
81
+ for (const repo of data) {
82
+ // Filter for MCP repos
83
+ if (repo.name.toLowerCase().includes("mcp") ||
84
+ repo.name.toLowerCase().endsWith("-mcp")) {
85
+ repos.push({
86
+ name: repo.name,
87
+ description: repo.description || "No description",
88
+ url: repo.html_url,
89
+ stars: repo.stargazers_count,
90
+ updated: repo.updated_at,
91
+ });
92
+ }
93
+ }
94
+ if (data.length < perPage)
95
+ break;
96
+ page++;
97
+ }
98
+ return repos;
99
+ }
100
+ // Detect package manager
101
+ function detectPackageManager(mcpPath) {
102
+ if (fs.existsSync(path.join(mcpPath, "bun.lockb")))
103
+ return "bun";
104
+ if (fs.existsSync(path.join(mcpPath, "pnpm-lock.yaml")))
105
+ return "pnpm";
106
+ return "npm";
107
+ }
108
+ // Parse .env.example or README for env vars
109
+ function parseEnvVars(mcpPath) {
110
+ const envVars = [];
111
+ // Try .env.example first
112
+ const envExamplePath = path.join(mcpPath, ".env.example");
113
+ if (fs.existsSync(envExamplePath)) {
114
+ const content = fs.readFileSync(envExamplePath, "utf-8");
115
+ const lines = content.split("\n");
116
+ for (const line of lines) {
117
+ const trimmed = line.trim();
118
+ if (trimmed && !trimmed.startsWith("#")) {
119
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=/);
120
+ if (match) {
121
+ // Look for comment on previous line
122
+ const idx = lines.indexOf(line);
123
+ let desc = "";
124
+ if (idx > 0 && lines[idx - 1].trim().startsWith("#")) {
125
+ desc = lines[idx - 1].trim().replace(/^#\s*/, "");
126
+ }
127
+ envVars.push({ name: match[1], description: desc || match[1] });
128
+ }
129
+ }
130
+ }
131
+ }
132
+ // Fallback to README parsing
133
+ if (envVars.length === 0) {
134
+ const readmePath = path.join(mcpPath, "README.md");
135
+ if (fs.existsSync(readmePath)) {
136
+ const content = fs.readFileSync(readmePath, "utf-8");
137
+ // Look for env var patterns
138
+ const matches = content.matchAll(/`([A-Z_][A-Z0-9_]*)`/g);
139
+ const seen = new Set();
140
+ for (const match of matches) {
141
+ if (!seen.has(match[1])) {
142
+ seen.add(match[1]);
143
+ envVars.push({ name: match[1], description: `From README: ${match[1]}` });
144
+ }
145
+ }
146
+ }
147
+ }
148
+ return envVars;
149
+ }
150
+ // Get entry point from package.json
151
+ function getEntryPoint(mcpPath) {
152
+ const pkgPath = path.join(mcpPath, "package.json");
153
+ if (fs.existsSync(pkgPath)) {
154
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
155
+ if (pkg.main)
156
+ return pkg.main;
157
+ if (pkg.bin) {
158
+ if (typeof pkg.bin === "string")
159
+ return pkg.bin;
160
+ return Object.values(pkg.bin)[0];
161
+ }
162
+ }
163
+ // Default to common patterns
164
+ if (fs.existsSync(path.join(mcpPath, "dist", "index.js"))) {
165
+ return "dist/index.js";
166
+ }
167
+ return "index.js";
168
+ }
169
+ // Tools definition
170
+ const tools = [
171
+ {
172
+ name: "list_available",
173
+ description: "List all available MCPs from GitHub that can be installed. Shows name, description, and install status.",
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ refresh: {
178
+ type: "boolean",
179
+ description: "Force refresh from GitHub (default: false, uses cache if recent)",
180
+ },
181
+ },
182
+ },
183
+ },
184
+ {
185
+ name: "list_installed",
186
+ description: "List all locally installed MCPs with their status and paths.",
187
+ inputSchema: {
188
+ type: "object",
189
+ properties: {},
190
+ },
191
+ },
192
+ {
193
+ name: "install",
194
+ description: "Install an MCP from GitHub. Clones the repo, builds it, and configures it for Opencode.",
195
+ inputSchema: {
196
+ type: "object",
197
+ properties: {
198
+ name: {
199
+ type: "string",
200
+ description: "Name of the MCP to install (e.g., 'elastic-mcp', 'crowdstrike-mcp')",
201
+ },
202
+ env_vars: {
203
+ type: "object",
204
+ description: "Environment variables to configure (key-value pairs). If not provided, will list required vars.",
205
+ additionalProperties: { type: "string" },
206
+ },
207
+ },
208
+ required: ["name"],
209
+ },
210
+ },
211
+ {
212
+ name: "update",
213
+ description: "Update an installed MCP to the latest version from GitHub.",
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: {
217
+ name: {
218
+ type: "string",
219
+ description: "Name of the MCP to update",
220
+ },
221
+ },
222
+ required: ["name"],
223
+ },
224
+ },
225
+ {
226
+ name: "update_all",
227
+ description: "Update all installed MCPs to their latest versions.",
228
+ inputSchema: {
229
+ type: "object",
230
+ properties: {},
231
+ },
232
+ },
233
+ {
234
+ name: "uninstall",
235
+ description: "Uninstall an MCP - removes the files and Opencode configuration.",
236
+ inputSchema: {
237
+ type: "object",
238
+ properties: {
239
+ name: {
240
+ type: "string",
241
+ description: "Name of the MCP to uninstall",
242
+ },
243
+ },
244
+ required: ["name"],
245
+ },
246
+ },
247
+ {
248
+ name: "configure",
249
+ description: "Reconfigure environment variables for an installed MCP.",
250
+ inputSchema: {
251
+ type: "object",
252
+ properties: {
253
+ name: {
254
+ type: "string",
255
+ description: "Name of the MCP to configure",
256
+ },
257
+ env_vars: {
258
+ type: "object",
259
+ description: "Environment variables to set (key-value pairs)",
260
+ additionalProperties: { type: "string" },
261
+ },
262
+ },
263
+ required: ["name", "env_vars"],
264
+ },
265
+ },
266
+ {
267
+ name: "get_required_env",
268
+ description: "Get the required environment variables for an MCP (installed or available).",
269
+ inputSchema: {
270
+ type: "object",
271
+ properties: {
272
+ name: {
273
+ type: "string",
274
+ description: "Name of the MCP",
275
+ },
276
+ },
277
+ required: ["name"],
278
+ },
279
+ },
280
+ ];
281
+ // Tool handlers
282
+ async function handleListAvailable(args) {
283
+ ensureDirectories();
284
+ const state = loadState();
285
+ const repos = await fetchGitHubRepos();
286
+ const result = repos.map((repo) => ({
287
+ ...repo,
288
+ installed: !!state.installed[repo.name],
289
+ localPath: state.installed[repo.name]?.path,
290
+ }));
291
+ const installed = result.filter((r) => r.installed);
292
+ const available = result.filter((r) => !r.installed);
293
+ let output = `## Available MCPs (${available.length})\n\n`;
294
+ for (const mcp of available) {
295
+ output += `- **${mcp.name}** - ${mcp.description}\n`;
296
+ output += ` Stars: ${mcp.stars} | Updated: ${new Date(mcp.updated).toLocaleDateString()}\n\n`;
297
+ }
298
+ output += `\n## Already Installed (${installed.length})\n\n`;
299
+ for (const mcp of installed) {
300
+ output += `- **${mcp.name}** (${mcp.localPath})\n`;
301
+ }
302
+ return output;
303
+ }
304
+ async function handleListInstalled() {
305
+ ensureDirectories();
306
+ const state = loadState();
307
+ if (Object.keys(state.installed).length === 0) {
308
+ return "No MCPs installed yet. Use `install` to add some!";
309
+ }
310
+ let output = "## Installed MCPs\n\n";
311
+ for (const [name, info] of Object.entries(state.installed)) {
312
+ output += `### ${name}\n`;
313
+ output += `- Path: ${info.path}\n`;
314
+ output += `- Installed: ${new Date(info.installedAt).toLocaleDateString()}\n`;
315
+ if (info.envVars.length > 0) {
316
+ output += `- Configured env vars: ${info.envVars.join(", ")}\n`;
317
+ }
318
+ output += "\n";
319
+ }
320
+ return output;
321
+ }
322
+ async function handleInstall(args) {
323
+ ensureDirectories();
324
+ const state = loadState();
325
+ const { name, env_vars } = args;
326
+ // Validate name
327
+ if (!isValidMcpName(name)) {
328
+ return `Invalid MCP name: ${name}. Names can only contain letters, numbers, hyphens, and underscores.`;
329
+ }
330
+ // Check if already installed
331
+ if (state.installed[name]) {
332
+ return `${name} is already installed at ${state.installed[name].path}. Use \`update\` to get the latest version.`;
333
+ }
334
+ const mcpPath = path.join(MCPS_DIR, name);
335
+ const repoUrl = `https://github.com/${GITHUB_USER}/${name}.git`;
336
+ // Step 1: Clone
337
+ if (fs.existsSync(mcpPath)) {
338
+ fs.rmSync(mcpPath, { recursive: true });
339
+ }
340
+ const cloneResult = runCommand("git", ["clone", repoUrl, mcpPath]);
341
+ if (!cloneResult.success) {
342
+ return `Failed to clone ${name}. Make sure the repo exists at ${repoUrl}\nError: ${cloneResult.output}`;
343
+ }
344
+ // Step 2: Check required env vars
345
+ const requiredEnvVars = parseEnvVars(mcpPath);
346
+ if (requiredEnvVars.length > 0 && !env_vars) {
347
+ let output = `## ${name} requires configuration\n\n`;
348
+ output += `The following environment variables are needed:\n\n`;
349
+ for (const v of requiredEnvVars) {
350
+ output += `- **${v.name}**: ${v.description}\n`;
351
+ }
352
+ output += `\nPlease call \`install\` again with the \`env_vars\` parameter containing these values.`;
353
+ return output;
354
+ }
355
+ // Step 3: Write .env file if env_vars provided
356
+ if (env_vars && Object.keys(env_vars).length > 0) {
357
+ const envContent = Object.entries(env_vars)
358
+ .map(([k, v]) => `${k}=${v}`)
359
+ .join("\n");
360
+ fs.writeFileSync(path.join(mcpPath, ".env"), envContent);
361
+ }
362
+ // Step 4: Install dependencies and build
363
+ const pm = detectPackageManager(mcpPath);
364
+ const installResult = runCommand(pm, ["install"], mcpPath);
365
+ if (!installResult.success) {
366
+ return `Failed to install dependencies for ${name}. Error: ${installResult.output}`;
367
+ }
368
+ // Check if build script exists
369
+ const pkgPath = path.join(mcpPath, "package.json");
370
+ if (fs.existsSync(pkgPath)) {
371
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
372
+ if (pkg.scripts?.build) {
373
+ const buildResult = runCommand(pm, ["run", "build"], mcpPath);
374
+ if (!buildResult.success) {
375
+ return `Failed to build ${name}. Error: ${buildResult.output}`;
376
+ }
377
+ }
378
+ }
379
+ // Step 5: Add to Opencode config
380
+ const config = loadOpencodeConfig();
381
+ if (!config.mcp) {
382
+ config.mcp = {};
383
+ }
384
+ const entryPoint = getEntryPoint(mcpPath);
385
+ const fullEntryPath = path.join(mcpPath, entryPoint);
386
+ config.mcp[name] = {
387
+ command: "node",
388
+ args: [fullEntryPath],
389
+ env: env_vars || {},
390
+ };
391
+ saveOpencodeConfig(config);
392
+ // Step 6: Update marketplace state
393
+ state.installed[name] = {
394
+ name,
395
+ path: mcpPath,
396
+ installedAt: new Date().toISOString(),
397
+ envVars: env_vars ? Object.keys(env_vars) : [],
398
+ };
399
+ saveState(state);
400
+ return `## Successfully installed ${name}!\n\n- Location: ${mcpPath}\n- Added to Opencode config\n\nRestart Opencode to use the new MCP.`;
401
+ }
402
+ async function handleUpdate(args) {
403
+ ensureDirectories();
404
+ const state = loadState();
405
+ const { name } = args;
406
+ if (!isValidMcpName(name)) {
407
+ return `Invalid MCP name: ${name}`;
408
+ }
409
+ if (!state.installed[name]) {
410
+ return `${name} is not installed. Use \`list_installed\` to see installed MCPs.`;
411
+ }
412
+ const mcpPath = state.installed[name].path;
413
+ // Pull latest
414
+ const pullResult = runCommand("git", ["pull"], mcpPath);
415
+ if (!pullResult.success) {
416
+ return `Failed to pull updates for ${name}: ${pullResult.output}`;
417
+ }
418
+ // Rebuild
419
+ const pm = detectPackageManager(mcpPath);
420
+ const installResult = runCommand(pm, ["install"], mcpPath);
421
+ if (!installResult.success) {
422
+ return `Failed to install dependencies for ${name}: ${installResult.output}`;
423
+ }
424
+ const pkgPath = path.join(mcpPath, "package.json");
425
+ if (fs.existsSync(pkgPath)) {
426
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
427
+ if (pkg.scripts?.build) {
428
+ const buildResult = runCommand(pm, ["run", "build"], mcpPath);
429
+ if (!buildResult.success) {
430
+ return `Failed to build ${name}: ${buildResult.output}`;
431
+ }
432
+ }
433
+ }
434
+ return `## Successfully updated ${name}!\n\nRestart Opencode to use the updated MCP.`;
435
+ }
436
+ async function handleUpdateAll() {
437
+ ensureDirectories();
438
+ const state = loadState();
439
+ if (Object.keys(state.installed).length === 0) {
440
+ return "No MCPs installed to update.";
441
+ }
442
+ const results = [];
443
+ for (const name of Object.keys(state.installed)) {
444
+ const result = await handleUpdate({ name });
445
+ results.push(`### ${name}\n${result}`);
446
+ }
447
+ return `## Update Results\n\n${results.join("\n\n")}`;
448
+ }
449
+ async function handleUninstall(args) {
450
+ ensureDirectories();
451
+ const state = loadState();
452
+ const { name } = args;
453
+ if (!isValidMcpName(name)) {
454
+ return `Invalid MCP name: ${name}`;
455
+ }
456
+ if (!state.installed[name]) {
457
+ return `${name} is not installed.`;
458
+ }
459
+ const mcpPath = state.installed[name].path;
460
+ // Remove from Opencode config
461
+ const config = loadOpencodeConfig();
462
+ if (config.mcp && config.mcp[name]) {
463
+ delete config.mcp[name];
464
+ saveOpencodeConfig(config);
465
+ }
466
+ // Remove files
467
+ if (fs.existsSync(mcpPath)) {
468
+ fs.rmSync(mcpPath, { recursive: true });
469
+ }
470
+ // Update state
471
+ delete state.installed[name];
472
+ saveState(state);
473
+ return `## Uninstalled ${name}\n\n- Removed from Opencode config\n- Deleted files at ${mcpPath}`;
474
+ }
475
+ async function handleConfigure(args) {
476
+ ensureDirectories();
477
+ const state = loadState();
478
+ const { name, env_vars } = args;
479
+ if (!isValidMcpName(name)) {
480
+ return `Invalid MCP name: ${name}`;
481
+ }
482
+ if (!state.installed[name]) {
483
+ return `${name} is not installed. Install it first.`;
484
+ }
485
+ const mcpPath = state.installed[name].path;
486
+ // Update .env file
487
+ const envContent = Object.entries(env_vars)
488
+ .map(([k, v]) => `${k}=${v}`)
489
+ .join("\n");
490
+ fs.writeFileSync(path.join(mcpPath, ".env"), envContent);
491
+ // Update Opencode config
492
+ const config = loadOpencodeConfig();
493
+ if (config.mcp && config.mcp[name]) {
494
+ config.mcp[name].env = env_vars;
495
+ saveOpencodeConfig(config);
496
+ }
497
+ // Update state
498
+ state.installed[name].envVars = Object.keys(env_vars);
499
+ saveState(state);
500
+ return `## Updated configuration for ${name}\n\nRestart Opencode to apply changes.`;
501
+ }
502
+ async function handleGetRequiredEnv(args) {
503
+ ensureDirectories();
504
+ const state = loadState();
505
+ const { name } = args;
506
+ if (!isValidMcpName(name)) {
507
+ return `Invalid MCP name: ${name}`;
508
+ }
509
+ let mcpPath;
510
+ if (state.installed[name]) {
511
+ mcpPath = state.installed[name].path;
512
+ }
513
+ else {
514
+ // Need to fetch from GitHub temporarily
515
+ const tempPath = path.join(os.tmpdir(), `mcp-check-${name}`);
516
+ const repoUrl = `https://github.com/${GITHUB_USER}/${name}.git`;
517
+ if (fs.existsSync(tempPath)) {
518
+ fs.rmSync(tempPath, { recursive: true });
519
+ }
520
+ const cloneResult = runCommand("git", ["clone", "--depth", "1", repoUrl, tempPath]);
521
+ if (!cloneResult.success) {
522
+ return `Could not find ${name} on GitHub.`;
523
+ }
524
+ mcpPath = tempPath;
525
+ }
526
+ const envVars = parseEnvVars(mcpPath);
527
+ if (envVars.length === 0) {
528
+ return `No environment variables detected for ${name}.`;
529
+ }
530
+ let output = `## Required Environment Variables for ${name}\n\n`;
531
+ for (const v of envVars) {
532
+ output += `- **${v.name}**: ${v.description}\n`;
533
+ }
534
+ return output;
535
+ }
536
+ // Main server setup
537
+ const server = new Server({
538
+ name: "mcp-marketplace",
539
+ version: "1.0.0",
540
+ }, {
541
+ capabilities: {
542
+ tools: {},
543
+ },
544
+ });
545
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
546
+ tools,
547
+ }));
548
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
549
+ const { name, arguments: args } = request.params;
550
+ try {
551
+ let result;
552
+ switch (name) {
553
+ case "list_available":
554
+ result = await handleListAvailable(args);
555
+ break;
556
+ case "list_installed":
557
+ result = await handleListInstalled();
558
+ break;
559
+ case "install":
560
+ result = await handleInstall(args);
561
+ break;
562
+ case "update":
563
+ result = await handleUpdate(args);
564
+ break;
565
+ case "update_all":
566
+ result = await handleUpdateAll();
567
+ break;
568
+ case "uninstall":
569
+ result = await handleUninstall(args);
570
+ break;
571
+ case "configure":
572
+ result = await handleConfigure(args);
573
+ break;
574
+ case "get_required_env":
575
+ result = await handleGetRequiredEnv(args);
576
+ break;
577
+ default:
578
+ result = `Unknown tool: ${name}`;
579
+ }
580
+ return {
581
+ content: [{ type: "text", text: result }],
582
+ };
583
+ }
584
+ catch (error) {
585
+ return {
586
+ content: [
587
+ {
588
+ type: "text",
589
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
590
+ },
591
+ ],
592
+ isError: true,
593
+ };
594
+ }
595
+ });
596
+ // Start server
597
+ async function main() {
598
+ const transport = new StdioServerTransport();
599
+ await server.connect(transport);
600
+ console.error("MCP Marketplace server running");
601
+ }
602
+ main().catch(console.error);
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "opencode-mcp-marketplace",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for discovering, installing, and managing other MCPs for Opencode",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "opencode-mcp-marketplace": "./dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "tsc && node dist/index.js"
14
+ },
15
+ "keywords": [
16
+ "mcp",
17
+ "opencode",
18
+ "marketplace",
19
+ "model-context-protocol"
20
+ ],
21
+ "author": "schwarztim",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/schwarztim/mcp-marketplace.git"
26
+ },
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0",
32
+ "typescript": "^5.0.0"
33
+ }
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,748 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ Tool,
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { execFileSync } from "child_process";
11
+ import * as fs from "fs";
12
+ import * as path from "path";
13
+ import * as os from "os";
14
+
15
+ // Cross-platform paths
16
+ const HOME = os.homedir();
17
+ const CONFIG_DIR = path.join(HOME, ".config", "opencode");
18
+ const MCPS_DIR = path.join(CONFIG_DIR, "mcps");
19
+ const OPENCODE_CONFIG = path.join(CONFIG_DIR, "opencode.json");
20
+ const MARKETPLACE_STATE = path.join(CONFIG_DIR, "marketplace-state.json");
21
+
22
+ // GitHub config
23
+ const GITHUB_USER = "schwarztim";
24
+ const GITHUB_API = "https://api.github.com";
25
+
26
+ interface McpInfo {
27
+ name: string;
28
+ description: string;
29
+ url: string;
30
+ stars: number;
31
+ updated: string;
32
+ installed?: boolean;
33
+ localPath?: string;
34
+ }
35
+
36
+ interface InstalledMcp {
37
+ name: string;
38
+ path: string;
39
+ installedAt: string;
40
+ version?: string;
41
+ envVars: string[];
42
+ }
43
+
44
+ interface MarketplaceState {
45
+ installed: Record<string, InstalledMcp>;
46
+ }
47
+
48
+ // Safe command execution helper
49
+ function runCommand(command: string, args: string[], cwd?: string): { success: boolean; output: string } {
50
+ try {
51
+ const output = execFileSync(command, args, {
52
+ cwd,
53
+ encoding: "utf-8",
54
+ stdio: ["pipe", "pipe", "pipe"],
55
+ });
56
+ return { success: true, output };
57
+ } catch (error: unknown) {
58
+ const err = error as { stderr?: string; message?: string };
59
+ return { success: false, output: err.stderr || err.message || "Unknown error" };
60
+ }
61
+ }
62
+
63
+ // Ensure directories exist
64
+ function ensureDirectories() {
65
+ if (!fs.existsSync(CONFIG_DIR)) {
66
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
67
+ }
68
+ if (!fs.existsSync(MCPS_DIR)) {
69
+ fs.mkdirSync(MCPS_DIR, { recursive: true });
70
+ }
71
+ }
72
+
73
+ // Load marketplace state
74
+ function loadState(): MarketplaceState {
75
+ if (fs.existsSync(MARKETPLACE_STATE)) {
76
+ return JSON.parse(fs.readFileSync(MARKETPLACE_STATE, "utf-8"));
77
+ }
78
+ return { installed: {} };
79
+ }
80
+
81
+ // Save marketplace state
82
+ function saveState(state: MarketplaceState) {
83
+ fs.writeFileSync(MARKETPLACE_STATE, JSON.stringify(state, null, 2));
84
+ }
85
+
86
+ // Load opencode config
87
+ function loadOpencodeConfig(): Record<string, unknown> {
88
+ if (fs.existsSync(OPENCODE_CONFIG)) {
89
+ return JSON.parse(fs.readFileSync(OPENCODE_CONFIG, "utf-8"));
90
+ }
91
+ return {};
92
+ }
93
+
94
+ // Save opencode config
95
+ function saveOpencodeConfig(config: Record<string, unknown>) {
96
+ fs.writeFileSync(OPENCODE_CONFIG, JSON.stringify(config, null, 2));
97
+ }
98
+
99
+ // Validate MCP name (alphanumeric, hyphens, underscores only)
100
+ function isValidMcpName(name: string): boolean {
101
+ return /^[a-zA-Z0-9_-]+$/.test(name);
102
+ }
103
+
104
+ // Fetch repos from GitHub
105
+ async function fetchGitHubRepos(): Promise<McpInfo[]> {
106
+ const repos: McpInfo[] = [];
107
+ let page = 1;
108
+ const perPage = 100;
109
+
110
+ while (true) {
111
+ const response = await fetch(
112
+ `${GITHUB_API}/users/${GITHUB_USER}/repos?per_page=${perPage}&page=${page}&sort=updated`
113
+ );
114
+
115
+ if (!response.ok) {
116
+ throw new Error(`GitHub API error: ${response.status}`);
117
+ }
118
+
119
+ const data = (await response.json()) as Array<{
120
+ name: string;
121
+ description: string | null;
122
+ html_url: string;
123
+ stargazers_count: number;
124
+ updated_at: string;
125
+ }>;
126
+
127
+ if (data.length === 0) break;
128
+
129
+ for (const repo of data) {
130
+ // Filter for MCP repos
131
+ if (
132
+ repo.name.toLowerCase().includes("mcp") ||
133
+ repo.name.toLowerCase().endsWith("-mcp")
134
+ ) {
135
+ repos.push({
136
+ name: repo.name,
137
+ description: repo.description || "No description",
138
+ url: repo.html_url,
139
+ stars: repo.stargazers_count,
140
+ updated: repo.updated_at,
141
+ });
142
+ }
143
+ }
144
+
145
+ if (data.length < perPage) break;
146
+ page++;
147
+ }
148
+
149
+ return repos;
150
+ }
151
+
152
+ // Detect package manager
153
+ function detectPackageManager(mcpPath: string): string {
154
+ if (fs.existsSync(path.join(mcpPath, "bun.lockb"))) return "bun";
155
+ if (fs.existsSync(path.join(mcpPath, "pnpm-lock.yaml"))) return "pnpm";
156
+ return "npm";
157
+ }
158
+
159
+ // Parse .env.example or README for env vars
160
+ function parseEnvVars(mcpPath: string): { name: string; description: string }[] {
161
+ const envVars: { name: string; description: string }[] = [];
162
+
163
+ // Try .env.example first
164
+ const envExamplePath = path.join(mcpPath, ".env.example");
165
+ if (fs.existsSync(envExamplePath)) {
166
+ const content = fs.readFileSync(envExamplePath, "utf-8");
167
+ const lines = content.split("\n");
168
+
169
+ for (const line of lines) {
170
+ const trimmed = line.trim();
171
+ if (trimmed && !trimmed.startsWith("#")) {
172
+ const match = trimmed.match(/^([A-Z_][A-Z0-9_]*)=/);
173
+ if (match) {
174
+ // Look for comment on previous line
175
+ const idx = lines.indexOf(line);
176
+ let desc = "";
177
+ if (idx > 0 && lines[idx - 1].trim().startsWith("#")) {
178
+ desc = lines[idx - 1].trim().replace(/^#\s*/, "");
179
+ }
180
+ envVars.push({ name: match[1], description: desc || match[1] });
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ // Fallback to README parsing
187
+ if (envVars.length === 0) {
188
+ const readmePath = path.join(mcpPath, "README.md");
189
+ if (fs.existsSync(readmePath)) {
190
+ const content = fs.readFileSync(readmePath, "utf-8");
191
+ // Look for env var patterns
192
+ const matches = content.matchAll(/`([A-Z_][A-Z0-9_]*)`/g);
193
+ const seen = new Set<string>();
194
+ for (const match of matches) {
195
+ if (!seen.has(match[1])) {
196
+ seen.add(match[1]);
197
+ envVars.push({ name: match[1], description: `From README: ${match[1]}` });
198
+ }
199
+ }
200
+ }
201
+ }
202
+
203
+ return envVars;
204
+ }
205
+
206
+ // Get entry point from package.json
207
+ function getEntryPoint(mcpPath: string): string {
208
+ const pkgPath = path.join(mcpPath, "package.json");
209
+ if (fs.existsSync(pkgPath)) {
210
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
211
+ if (pkg.main) return pkg.main;
212
+ if (pkg.bin) {
213
+ if (typeof pkg.bin === "string") return pkg.bin;
214
+ return Object.values(pkg.bin)[0] as string;
215
+ }
216
+ }
217
+ // Default to common patterns
218
+ if (fs.existsSync(path.join(mcpPath, "dist", "index.js"))) {
219
+ return "dist/index.js";
220
+ }
221
+ return "index.js";
222
+ }
223
+
224
+ // Tools definition
225
+ const tools: Tool[] = [
226
+ {
227
+ name: "list_available",
228
+ description:
229
+ "List all available MCPs from GitHub that can be installed. Shows name, description, and install status.",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: {
233
+ refresh: {
234
+ type: "boolean",
235
+ description: "Force refresh from GitHub (default: false, uses cache if recent)",
236
+ },
237
+ },
238
+ },
239
+ },
240
+ {
241
+ name: "list_installed",
242
+ description: "List all locally installed MCPs with their status and paths.",
243
+ inputSchema: {
244
+ type: "object",
245
+ properties: {},
246
+ },
247
+ },
248
+ {
249
+ name: "install",
250
+ description:
251
+ "Install an MCP from GitHub. Clones the repo, builds it, and configures it for Opencode.",
252
+ inputSchema: {
253
+ type: "object",
254
+ properties: {
255
+ name: {
256
+ type: "string",
257
+ description: "Name of the MCP to install (e.g., 'elastic-mcp', 'crowdstrike-mcp')",
258
+ },
259
+ env_vars: {
260
+ type: "object",
261
+ description:
262
+ "Environment variables to configure (key-value pairs). If not provided, will list required vars.",
263
+ additionalProperties: { type: "string" },
264
+ },
265
+ },
266
+ required: ["name"],
267
+ },
268
+ },
269
+ {
270
+ name: "update",
271
+ description: "Update an installed MCP to the latest version from GitHub.",
272
+ inputSchema: {
273
+ type: "object",
274
+ properties: {
275
+ name: {
276
+ type: "string",
277
+ description: "Name of the MCP to update",
278
+ },
279
+ },
280
+ required: ["name"],
281
+ },
282
+ },
283
+ {
284
+ name: "update_all",
285
+ description: "Update all installed MCPs to their latest versions.",
286
+ inputSchema: {
287
+ type: "object",
288
+ properties: {},
289
+ },
290
+ },
291
+ {
292
+ name: "uninstall",
293
+ description: "Uninstall an MCP - removes the files and Opencode configuration.",
294
+ inputSchema: {
295
+ type: "object",
296
+ properties: {
297
+ name: {
298
+ type: "string",
299
+ description: "Name of the MCP to uninstall",
300
+ },
301
+ },
302
+ required: ["name"],
303
+ },
304
+ },
305
+ {
306
+ name: "configure",
307
+ description: "Reconfigure environment variables for an installed MCP.",
308
+ inputSchema: {
309
+ type: "object",
310
+ properties: {
311
+ name: {
312
+ type: "string",
313
+ description: "Name of the MCP to configure",
314
+ },
315
+ env_vars: {
316
+ type: "object",
317
+ description: "Environment variables to set (key-value pairs)",
318
+ additionalProperties: { type: "string" },
319
+ },
320
+ },
321
+ required: ["name", "env_vars"],
322
+ },
323
+ },
324
+ {
325
+ name: "get_required_env",
326
+ description: "Get the required environment variables for an MCP (installed or available).",
327
+ inputSchema: {
328
+ type: "object",
329
+ properties: {
330
+ name: {
331
+ type: "string",
332
+ description: "Name of the MCP",
333
+ },
334
+ },
335
+ required: ["name"],
336
+ },
337
+ },
338
+ ];
339
+
340
+ // Tool handlers
341
+ async function handleListAvailable(args: { refresh?: boolean }): Promise<string> {
342
+ ensureDirectories();
343
+ const state = loadState();
344
+
345
+ const repos = await fetchGitHubRepos();
346
+
347
+ const result = repos.map((repo) => ({
348
+ ...repo,
349
+ installed: !!state.installed[repo.name],
350
+ localPath: state.installed[repo.name]?.path,
351
+ }));
352
+
353
+ const installed = result.filter((r) => r.installed);
354
+ const available = result.filter((r) => !r.installed);
355
+
356
+ let output = `## Available MCPs (${available.length})\n\n`;
357
+ for (const mcp of available) {
358
+ output += `- **${mcp.name}** - ${mcp.description}\n`;
359
+ output += ` Stars: ${mcp.stars} | Updated: ${new Date(mcp.updated).toLocaleDateString()}\n\n`;
360
+ }
361
+
362
+ output += `\n## Already Installed (${installed.length})\n\n`;
363
+ for (const mcp of installed) {
364
+ output += `- **${mcp.name}** (${mcp.localPath})\n`;
365
+ }
366
+
367
+ return output;
368
+ }
369
+
370
+ async function handleListInstalled(): Promise<string> {
371
+ ensureDirectories();
372
+ const state = loadState();
373
+
374
+ if (Object.keys(state.installed).length === 0) {
375
+ return "No MCPs installed yet. Use `install` to add some!";
376
+ }
377
+
378
+ let output = "## Installed MCPs\n\n";
379
+ for (const [name, info] of Object.entries(state.installed)) {
380
+ output += `### ${name}\n`;
381
+ output += `- Path: ${info.path}\n`;
382
+ output += `- Installed: ${new Date(info.installedAt).toLocaleDateString()}\n`;
383
+ if (info.envVars.length > 0) {
384
+ output += `- Configured env vars: ${info.envVars.join(", ")}\n`;
385
+ }
386
+ output += "\n";
387
+ }
388
+
389
+ return output;
390
+ }
391
+
392
+ async function handleInstall(args: {
393
+ name: string;
394
+ env_vars?: Record<string, string>;
395
+ }): Promise<string> {
396
+ ensureDirectories();
397
+ const state = loadState();
398
+ const { name, env_vars } = args;
399
+
400
+ // Validate name
401
+ if (!isValidMcpName(name)) {
402
+ return `Invalid MCP name: ${name}. Names can only contain letters, numbers, hyphens, and underscores.`;
403
+ }
404
+
405
+ // Check if already installed
406
+ if (state.installed[name]) {
407
+ return `${name} is already installed at ${state.installed[name].path}. Use \`update\` to get the latest version.`;
408
+ }
409
+
410
+ const mcpPath = path.join(MCPS_DIR, name);
411
+ const repoUrl = `https://github.com/${GITHUB_USER}/${name}.git`;
412
+
413
+ // Step 1: Clone
414
+ if (fs.existsSync(mcpPath)) {
415
+ fs.rmSync(mcpPath, { recursive: true });
416
+ }
417
+
418
+ const cloneResult = runCommand("git", ["clone", repoUrl, mcpPath]);
419
+ if (!cloneResult.success) {
420
+ return `Failed to clone ${name}. Make sure the repo exists at ${repoUrl}\nError: ${cloneResult.output}`;
421
+ }
422
+
423
+ // Step 2: Check required env vars
424
+ const requiredEnvVars = parseEnvVars(mcpPath);
425
+
426
+ if (requiredEnvVars.length > 0 && !env_vars) {
427
+ let output = `## ${name} requires configuration\n\n`;
428
+ output += `The following environment variables are needed:\n\n`;
429
+ for (const v of requiredEnvVars) {
430
+ output += `- **${v.name}**: ${v.description}\n`;
431
+ }
432
+ output += `\nPlease call \`install\` again with the \`env_vars\` parameter containing these values.`;
433
+ return output;
434
+ }
435
+
436
+ // Step 3: Write .env file if env_vars provided
437
+ if (env_vars && Object.keys(env_vars).length > 0) {
438
+ const envContent = Object.entries(env_vars)
439
+ .map(([k, v]) => `${k}=${v}`)
440
+ .join("\n");
441
+ fs.writeFileSync(path.join(mcpPath, ".env"), envContent);
442
+ }
443
+
444
+ // Step 4: Install dependencies and build
445
+ const pm = detectPackageManager(mcpPath);
446
+
447
+ const installResult = runCommand(pm, ["install"], mcpPath);
448
+ if (!installResult.success) {
449
+ return `Failed to install dependencies for ${name}. Error: ${installResult.output}`;
450
+ }
451
+
452
+ // Check if build script exists
453
+ const pkgPath = path.join(mcpPath, "package.json");
454
+ if (fs.existsSync(pkgPath)) {
455
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
456
+ if (pkg.scripts?.build) {
457
+ const buildResult = runCommand(pm, ["run", "build"], mcpPath);
458
+ if (!buildResult.success) {
459
+ return `Failed to build ${name}. Error: ${buildResult.output}`;
460
+ }
461
+ }
462
+ }
463
+
464
+ // Step 5: Add to Opencode config
465
+ const config = loadOpencodeConfig();
466
+ if (!config.mcp) {
467
+ config.mcp = {};
468
+ }
469
+
470
+ const entryPoint = getEntryPoint(mcpPath);
471
+ const fullEntryPath = path.join(mcpPath, entryPoint);
472
+
473
+ (config.mcp as Record<string, unknown>)[name] = {
474
+ command: "node",
475
+ args: [fullEntryPath],
476
+ env: env_vars || {},
477
+ };
478
+
479
+ saveOpencodeConfig(config);
480
+
481
+ // Step 6: Update marketplace state
482
+ state.installed[name] = {
483
+ name,
484
+ path: mcpPath,
485
+ installedAt: new Date().toISOString(),
486
+ envVars: env_vars ? Object.keys(env_vars) : [],
487
+ };
488
+ saveState(state);
489
+
490
+ return `## Successfully installed ${name}!\n\n- Location: ${mcpPath}\n- Added to Opencode config\n\nRestart Opencode to use the new MCP.`;
491
+ }
492
+
493
+ async function handleUpdate(args: { name: string }): Promise<string> {
494
+ ensureDirectories();
495
+ const state = loadState();
496
+ const { name } = args;
497
+
498
+ if (!isValidMcpName(name)) {
499
+ return `Invalid MCP name: ${name}`;
500
+ }
501
+
502
+ if (!state.installed[name]) {
503
+ return `${name} is not installed. Use \`list_installed\` to see installed MCPs.`;
504
+ }
505
+
506
+ const mcpPath = state.installed[name].path;
507
+
508
+ // Pull latest
509
+ const pullResult = runCommand("git", ["pull"], mcpPath);
510
+ if (!pullResult.success) {
511
+ return `Failed to pull updates for ${name}: ${pullResult.output}`;
512
+ }
513
+
514
+ // Rebuild
515
+ const pm = detectPackageManager(mcpPath);
516
+
517
+ const installResult = runCommand(pm, ["install"], mcpPath);
518
+ if (!installResult.success) {
519
+ return `Failed to install dependencies for ${name}: ${installResult.output}`;
520
+ }
521
+
522
+ const pkgPath = path.join(mcpPath, "package.json");
523
+ if (fs.existsSync(pkgPath)) {
524
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
525
+ if (pkg.scripts?.build) {
526
+ const buildResult = runCommand(pm, ["run", "build"], mcpPath);
527
+ if (!buildResult.success) {
528
+ return `Failed to build ${name}: ${buildResult.output}`;
529
+ }
530
+ }
531
+ }
532
+
533
+ return `## Successfully updated ${name}!\n\nRestart Opencode to use the updated MCP.`;
534
+ }
535
+
536
+ async function handleUpdateAll(): Promise<string> {
537
+ ensureDirectories();
538
+ const state = loadState();
539
+
540
+ if (Object.keys(state.installed).length === 0) {
541
+ return "No MCPs installed to update.";
542
+ }
543
+
544
+ const results: string[] = [];
545
+
546
+ for (const name of Object.keys(state.installed)) {
547
+ const result = await handleUpdate({ name });
548
+ results.push(`### ${name}\n${result}`);
549
+ }
550
+
551
+ return `## Update Results\n\n${results.join("\n\n")}`;
552
+ }
553
+
554
+ async function handleUninstall(args: { name: string }): Promise<string> {
555
+ ensureDirectories();
556
+ const state = loadState();
557
+ const { name } = args;
558
+
559
+ if (!isValidMcpName(name)) {
560
+ return `Invalid MCP name: ${name}`;
561
+ }
562
+
563
+ if (!state.installed[name]) {
564
+ return `${name} is not installed.`;
565
+ }
566
+
567
+ const mcpPath = state.installed[name].path;
568
+
569
+ // Remove from Opencode config
570
+ const config = loadOpencodeConfig();
571
+ if (config.mcp && (config.mcp as Record<string, unknown>)[name]) {
572
+ delete (config.mcp as Record<string, unknown>)[name];
573
+ saveOpencodeConfig(config);
574
+ }
575
+
576
+ // Remove files
577
+ if (fs.existsSync(mcpPath)) {
578
+ fs.rmSync(mcpPath, { recursive: true });
579
+ }
580
+
581
+ // Update state
582
+ delete state.installed[name];
583
+ saveState(state);
584
+
585
+ return `## Uninstalled ${name}\n\n- Removed from Opencode config\n- Deleted files at ${mcpPath}`;
586
+ }
587
+
588
+ async function handleConfigure(args: {
589
+ name: string;
590
+ env_vars: Record<string, string>;
591
+ }): Promise<string> {
592
+ ensureDirectories();
593
+ const state = loadState();
594
+ const { name, env_vars } = args;
595
+
596
+ if (!isValidMcpName(name)) {
597
+ return `Invalid MCP name: ${name}`;
598
+ }
599
+
600
+ if (!state.installed[name]) {
601
+ return `${name} is not installed. Install it first.`;
602
+ }
603
+
604
+ const mcpPath = state.installed[name].path;
605
+
606
+ // Update .env file
607
+ const envContent = Object.entries(env_vars)
608
+ .map(([k, v]) => `${k}=${v}`)
609
+ .join("\n");
610
+ fs.writeFileSync(path.join(mcpPath, ".env"), envContent);
611
+
612
+ // Update Opencode config
613
+ const config = loadOpencodeConfig();
614
+ if (config.mcp && (config.mcp as Record<string, unknown>)[name]) {
615
+ ((config.mcp as Record<string, unknown>)[name] as Record<string, unknown>).env = env_vars;
616
+ saveOpencodeConfig(config);
617
+ }
618
+
619
+ // Update state
620
+ state.installed[name].envVars = Object.keys(env_vars);
621
+ saveState(state);
622
+
623
+ return `## Updated configuration for ${name}\n\nRestart Opencode to apply changes.`;
624
+ }
625
+
626
+ async function handleGetRequiredEnv(args: { name: string }): Promise<string> {
627
+ ensureDirectories();
628
+ const state = loadState();
629
+ const { name } = args;
630
+
631
+ if (!isValidMcpName(name)) {
632
+ return `Invalid MCP name: ${name}`;
633
+ }
634
+
635
+ let mcpPath: string;
636
+
637
+ if (state.installed[name]) {
638
+ mcpPath = state.installed[name].path;
639
+ } else {
640
+ // Need to fetch from GitHub temporarily
641
+ const tempPath = path.join(os.tmpdir(), `mcp-check-${name}`);
642
+ const repoUrl = `https://github.com/${GITHUB_USER}/${name}.git`;
643
+
644
+ if (fs.existsSync(tempPath)) {
645
+ fs.rmSync(tempPath, { recursive: true });
646
+ }
647
+
648
+ const cloneResult = runCommand("git", ["clone", "--depth", "1", repoUrl, tempPath]);
649
+ if (!cloneResult.success) {
650
+ return `Could not find ${name} on GitHub.`;
651
+ }
652
+ mcpPath = tempPath;
653
+ }
654
+
655
+ const envVars = parseEnvVars(mcpPath);
656
+
657
+ if (envVars.length === 0) {
658
+ return `No environment variables detected for ${name}.`;
659
+ }
660
+
661
+ let output = `## Required Environment Variables for ${name}\n\n`;
662
+ for (const v of envVars) {
663
+ output += `- **${v.name}**: ${v.description}\n`;
664
+ }
665
+
666
+ return output;
667
+ }
668
+
669
+ // Main server setup
670
+ const server = new Server(
671
+ {
672
+ name: "mcp-marketplace",
673
+ version: "1.0.0",
674
+ },
675
+ {
676
+ capabilities: {
677
+ tools: {},
678
+ },
679
+ }
680
+ );
681
+
682
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
683
+ tools,
684
+ }));
685
+
686
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
687
+ const { name, arguments: args } = request.params;
688
+
689
+ try {
690
+ let result: string;
691
+
692
+ switch (name) {
693
+ case "list_available":
694
+ result = await handleListAvailable(args as { refresh?: boolean });
695
+ break;
696
+ case "list_installed":
697
+ result = await handleListInstalled();
698
+ break;
699
+ case "install":
700
+ result = await handleInstall(
701
+ args as { name: string; env_vars?: Record<string, string> }
702
+ );
703
+ break;
704
+ case "update":
705
+ result = await handleUpdate(args as { name: string });
706
+ break;
707
+ case "update_all":
708
+ result = await handleUpdateAll();
709
+ break;
710
+ case "uninstall":
711
+ result = await handleUninstall(args as { name: string });
712
+ break;
713
+ case "configure":
714
+ result = await handleConfigure(
715
+ args as { name: string; env_vars: Record<string, string> }
716
+ );
717
+ break;
718
+ case "get_required_env":
719
+ result = await handleGetRequiredEnv(args as { name: string });
720
+ break;
721
+ default:
722
+ result = `Unknown tool: ${name}`;
723
+ }
724
+
725
+ return {
726
+ content: [{ type: "text", text: result }],
727
+ };
728
+ } catch (error) {
729
+ return {
730
+ content: [
731
+ {
732
+ type: "text",
733
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
734
+ },
735
+ ],
736
+ isError: true,
737
+ };
738
+ }
739
+ });
740
+
741
+ // Start server
742
+ async function main() {
743
+ const transport = new StdioServerTransport();
744
+ await server.connect(transport);
745
+ console.error("MCP Marketplace server running");
746
+ }
747
+
748
+ main().catch(console.error);
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "declaration": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }