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.
- package/dist/index.d.ts +2 -0
- package/dist/index.js +602 -0
- package/package.json +34 -0
- package/src/index.ts +748 -0
- package/tsconfig.json +16 -0
package/dist/index.d.ts
ADDED
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
|
+
}
|