mcpstore-gateway 1.3.1 → 2.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 CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * MCP Store Gateway v1.3.1
3
+ * MCP Store Gateway v2.0.0
4
4
  *
5
- * A single MCP server that gives Claude Code access to ALL your installed MCPs
6
- * from mcpclaudecode.com. Install MCPs on the website, they appear here instantly.
5
+ * A single MCP server that proxies ALL your installed MCPs from mcpclaudecode.com.
6
+ * Install MCPs on the website they appear automatically, no extra restart needed.
7
7
  *
8
- * Features:
9
- * - Hosted MCPs: tools proxied through our backend (e.g. AI Brain)
10
- * - External MCPs: auto-configured in ~/.mcp.json (e.g. GitHub, Playwright)
8
+ * Architecture:
9
+ * - Hosted MCPs: tools proxied through the backend API (debate, quick_review)
10
+ * - External MCPs: spawned as child processes, tools aggregated into this gateway
11
+ * - Periodic sync: re-checks every 60s, notifies Claude via tools/list_changed
11
12
  *
12
13
  * Setup (one command, that's it):
13
14
  * npx mcpstore-gateway --setup YOUR_API_KEY
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * MCP Store Gateway v1.3.1
3
+ * MCP Store Gateway v2.0.0
4
4
  *
5
- * A single MCP server that gives Claude Code access to ALL your installed MCPs
6
- * from mcpclaudecode.com. Install MCPs on the website, they appear here instantly.
5
+ * A single MCP server that proxies ALL your installed MCPs from mcpclaudecode.com.
6
+ * Install MCPs on the website they appear automatically, no extra restart needed.
7
7
  *
8
- * Features:
9
- * - Hosted MCPs: tools proxied through our backend (e.g. AI Brain)
10
- * - External MCPs: auto-configured in ~/.mcp.json (e.g. GitHub, Playwright)
8
+ * Architecture:
9
+ * - Hosted MCPs: tools proxied through the backend API (debate, quick_review)
10
+ * - External MCPs: spawned as child processes, tools aggregated into this gateway
11
+ * - Periodic sync: re-checks every 60s, notifies Claude via tools/list_changed
11
12
  *
12
13
  * Setup (one command, that's it):
13
14
  * npx mcpstore-gateway --setup YOUR_API_KEY
@@ -15,18 +16,166 @@
15
16
  * Runtime (called automatically by Claude Code):
16
17
  * npx mcpstore-gateway --api-key YOUR_API_KEY
17
18
  */
18
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
20
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
21
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
22
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
23
+ import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
20
24
  import * as fs from "node:fs";
21
25
  import * as path from "node:path";
22
26
  import * as os from "node:os";
23
27
  const GATEWAY_URL = process.env.MCPSTORE_GATEWAY_URL ||
24
28
  "https://rshsqjofouqyhzvzezos.supabase.co/functions/v1";
25
- // Prefix for auto-managed MCP entries in .mcp.json
29
+ // Prefix for auto-managed MCP entries (legacy, cleaned up on start)
26
30
  const MANAGED_PREFIX = "mcpstore-";
27
- async function syncExternalMcps(apiKey) {
31
+ const SYNC_INTERVAL_MS = 60_000; // Re-sync every 60 seconds
32
+ const SPAWN_TIMEOUT_MS = 30_000; // 30s timeout for child MCP connection
33
+ // ---------------------------------------------------------------------------
34
+ // Child MCP Management
35
+ // ---------------------------------------------------------------------------
36
+ const children = new Map();
37
+ /** Resolve ${VAR} templates in env values from process.env */
38
+ function resolveEnv(env) {
39
+ if (!env)
40
+ return {};
41
+ const resolved = {};
42
+ for (const [key, value] of Object.entries(env)) {
43
+ resolved[key] = value.replace(/\$\{(\w+)\}/g, (_, varName) => process.env[varName] || "");
44
+ }
45
+ return resolved;
46
+ }
47
+ /** Spawn a child MCP process and connect as client */
48
+ async function spawnChild(mcp) {
49
+ try {
50
+ let command = mcp.config.command;
51
+ let args = mcp.config.args || [];
52
+ // On Windows, wrap npx/node/uvx commands with cmd /c
53
+ if (os.platform() === "win32" &&
54
+ ["npx", "node", "uvx"].includes(command)) {
55
+ args = ["/c", command, ...args];
56
+ command = "cmd";
57
+ }
58
+ const env = {
59
+ ...Object.fromEntries(Object.entries(process.env).filter((e) => e[1] != null)),
60
+ ...resolveEnv(mcp.config.env),
61
+ };
62
+ const transport = new StdioClientTransport({
63
+ command,
64
+ args,
65
+ env,
66
+ stderr: "pipe", // Don't leak child stderr into our stdio
67
+ });
68
+ const client = new Client({ name: "mcpstore-gateway", version: "2.0.0" }, { capabilities: {} });
69
+ // Connect with timeout
70
+ await Promise.race([
71
+ client.connect(transport),
72
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Connection timeout")), SPAWN_TIMEOUT_MS)),
73
+ ]);
74
+ const { tools } = await client.listTools();
75
+ return {
76
+ slug: mcp.slug,
77
+ name: mcp.name,
78
+ client,
79
+ transport,
80
+ tools: tools || [],
81
+ };
82
+ }
83
+ catch (err) {
84
+ process.stderr.write(`[mcpstore] Failed to spawn ${mcp.slug}: ${err.message}\n`);
85
+ return null;
86
+ }
87
+ }
88
+ /** Kill a child MCP process */
89
+ async function killChild(slug) {
90
+ const child = children.get(slug);
91
+ if (!child)
92
+ return;
93
+ try {
94
+ await child.client.close();
95
+ }
96
+ catch {
97
+ // Best effort
98
+ }
99
+ children.delete(slug);
100
+ }
101
+ /** Kill all child MCP processes */
102
+ async function killAllChildren() {
103
+ for (const slug of [...children.keys()]) {
104
+ await killChild(slug);
105
+ }
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // Tool Aggregation
109
+ // ---------------------------------------------------------------------------
110
+ /** Build aggregated tool list from all child MCPs */
111
+ function getProxiedTools() {
112
+ const tools = [];
113
+ for (const [slug, child] of children) {
114
+ for (const tool of child.tools) {
115
+ tools.push({
116
+ ...tool,
117
+ name: `${slug}__${tool.name}`,
118
+ description: `[${child.name}] ${tool.description || ""}`,
119
+ });
120
+ }
121
+ }
122
+ return tools;
123
+ }
124
+ /** Find which child owns a prefixed tool name */
125
+ function findToolOwner(prefixedName) {
126
+ const sepIdx = prefixedName.indexOf("__");
127
+ if (sepIdx === -1)
128
+ return null;
129
+ const slug = prefixedName.slice(0, sepIdx);
130
+ const toolName = prefixedName.slice(sepIdx + 2);
131
+ const child = children.get(slug);
132
+ if (!child)
133
+ return null;
134
+ return { child, toolName };
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // Hosted Tools (Backend API proxy — debate, quick_review)
138
+ // ---------------------------------------------------------------------------
139
+ let hostedTools = [];
140
+ async function fetchHostedTools(apiKey) {
141
+ try {
142
+ const res = await fetch(`${GATEWAY_URL}/gateway-tools`, {
143
+ headers: {
144
+ Authorization: `Bearer ${apiKey}`,
145
+ "Content-Type": "application/json",
146
+ },
147
+ });
148
+ if (!res.ok)
149
+ return [];
150
+ const data = (await res.json());
151
+ return data.tools || [];
152
+ }
153
+ catch {
154
+ return [];
155
+ }
156
+ }
157
+ async function callHostedTool(apiKey, toolName, args) {
158
+ const res = await fetch(`${GATEWAY_URL}/gateway-call`, {
159
+ method: "POST",
160
+ headers: {
161
+ Authorization: `Bearer ${apiKey}`,
162
+ "Content-Type": "application/json",
163
+ },
164
+ body: JSON.stringify({ tool: toolName, arguments: args }),
165
+ });
166
+ if (!res.ok) {
167
+ const body = await res.json().catch(() => ({}));
168
+ throw new Error(body.error || `Gateway error: ${res.status}`);
169
+ }
170
+ const data = (await res.json());
171
+ const result = data.result ?? data;
172
+ return typeof result === "string" ? result : JSON.stringify(result, null, 2);
173
+ }
174
+ // ---------------------------------------------------------------------------
175
+ // Sync: fetch installed MCPs and manage child processes
176
+ // ---------------------------------------------------------------------------
177
+ async function fetchInstalledMcps(apiKey) {
28
178
  try {
29
- // 1. Fetch installed external MCPs from the API
30
179
  const res = await fetch(`${GATEWAY_URL}/gateway-sync`, {
31
180
  headers: {
32
181
  Authorization: `Bearer ${apiKey}`,
@@ -34,60 +183,73 @@ async function syncExternalMcps(apiKey) {
34
183
  },
35
184
  });
36
185
  if (!res.ok)
37
- return; // Silently fail — sync is best-effort
186
+ return [];
38
187
  const data = (await res.json());
39
- const installedSlugs = new Set(data.external_mcps.map((m) => m.slug));
40
- // 2. Read current ~/.mcp.json
41
- const mcpJsonPath = path.join(os.homedir(), ".mcp.json");
42
- let config = {};
43
- if (fs.existsSync(mcpJsonPath)) {
44
- try {
45
- config = JSON.parse(fs.readFileSync(mcpJsonPath, "utf-8"));
46
- }
47
- catch {
48
- return; // Don't mess with a broken config
49
- }
50
- }
51
- if (!config.mcpServers) {
52
- config.mcpServers = {};
188
+ return data.external_mcps || [];
189
+ }
190
+ catch {
191
+ return [];
192
+ }
193
+ }
194
+ /** Sync installed MCPs: spawn new, kill removed, notify if changed */
195
+ async function sync(apiKey, server) {
196
+ const installed = await fetchInstalledMcps(apiKey);
197
+ const installedSlugs = new Set(installed.map((m) => m.slug));
198
+ let changed = false;
199
+ // Remove uninstalled MCPs
200
+ for (const slug of [...children.keys()]) {
201
+ if (!installedSlugs.has(slug)) {
202
+ await killChild(slug);
203
+ changed = true;
53
204
  }
54
- let changed = false;
55
- // 3. Add or update external MCPs (prefixed with mcpstore-)
56
- // Always update so credentials from the vault get injected
57
- const isWindows = os.platform() === "win32";
58
- for (const mcp of data.external_mcps) {
59
- const key = `${MANAGED_PREFIX}${mcp.slug}`;
60
- // On Windows, wrap npx/node commands with cmd /c so Claude Code can spawn them
61
- const mcpConfig = { ...mcp.config };
62
- if (isWindows && (mcpConfig.command === "npx" || mcpConfig.command === "node")) {
63
- const originalCommand = mcpConfig.command;
64
- const originalArgs = mcpConfig.args || [];
65
- mcpConfig.command = "cmd";
66
- mcpConfig.args = ["/c", originalCommand, ...originalArgs];
67
- }
68
- const existing = config.mcpServers[key];
69
- if (!existing || JSON.stringify(existing) !== JSON.stringify(mcpConfig)) {
70
- config.mcpServers[key] = mcpConfig;
205
+ }
206
+ // Spawn newly installed MCPs (skip already running)
207
+ for (const mcp of installed) {
208
+ if (!children.has(mcp.slug)) {
209
+ const child = await spawnChild(mcp);
210
+ if (child) {
211
+ children.set(mcp.slug, child);
71
212
  changed = true;
72
213
  }
73
214
  }
74
- // 4. Remove uninstalled MCPs (only those with our prefix)
215
+ }
216
+ // Notify Claude Code if tool list changed
217
+ if (changed) {
218
+ try {
219
+ await server.sendToolListChanged();
220
+ }
221
+ catch {
222
+ // Best effort — server might not be connected yet
223
+ }
224
+ }
225
+ }
226
+ // ---------------------------------------------------------------------------
227
+ // Legacy cleanup: remove old mcpstore-* entries from .mcp.json
228
+ // (v1 wrote external MCPs to .mcp.json; v2 proxies them instead)
229
+ // ---------------------------------------------------------------------------
230
+ function cleanupLegacyEntries() {
231
+ try {
232
+ const mcpJsonPath = path.join(os.homedir(), ".mcp.json");
233
+ if (!fs.existsSync(mcpJsonPath))
234
+ return;
235
+ const raw = fs.readFileSync(mcpJsonPath, "utf-8");
236
+ const config = JSON.parse(raw);
237
+ if (!config.mcpServers)
238
+ return;
239
+ let changed = false;
75
240
  for (const key of Object.keys(config.mcpServers)) {
76
- if (key.startsWith(MANAGED_PREFIX)) {
77
- const slug = key.slice(MANAGED_PREFIX.length);
78
- if (!installedSlugs.has(slug)) {
79
- delete config.mcpServers[key];
80
- changed = true;
81
- }
241
+ // Remove mcpstore-* entries (but keep "mcpstore" — that's the gateway itself)
242
+ if (key.startsWith(MANAGED_PREFIX) && key !== "mcpstore") {
243
+ delete config.mcpServers[key];
244
+ changed = true;
82
245
  }
83
246
  }
84
- // 5. Write back if changed
85
247
  if (changed) {
86
248
  fs.writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
87
249
  }
88
250
  }
89
251
  catch {
90
- // Sync is best-effort, never block the server from starting
252
+ // Best effort
91
253
  }
92
254
  }
93
255
  // ---------------------------------------------------------------------------
@@ -133,6 +295,12 @@ async function runSetup(apiKey) {
133
295
  if (!config.mcpServers) {
134
296
  config.mcpServers = {};
135
297
  }
298
+ // Clean up legacy mcpstore-* entries from v1
299
+ for (const key of Object.keys(config.mcpServers)) {
300
+ if (key.startsWith(MANAGED_PREFIX) && key !== "mcpstore") {
301
+ delete config.mcpServers[key];
302
+ }
303
+ }
136
304
  // On Windows, wrap npx with cmd /c so Claude Code can spawn the process
137
305
  if (os.platform() === "win32") {
138
306
  config.mcpServers.mcpstore = {
@@ -161,102 +329,123 @@ async function runSetup(apiKey) {
161
329
  console.log(" +--------------------------------------------------+");
162
330
  console.log("");
163
331
  }
164
- async function fetchTools(apiKey) {
165
- const res = await fetch(`${GATEWAY_URL}/gateway-tools`, {
166
- headers: {
167
- Authorization: `Bearer ${apiKey}`,
168
- "Content-Type": "application/json",
169
- },
170
- });
171
- if (!res.ok) {
172
- const body = await res.json().catch(() => ({}));
173
- throw new Error(body.error || `Gateway error: ${res.status}`);
174
- }
175
- const data = (await res.json());
176
- return data.tools || [];
177
- }
178
- async function callTool(apiKey, toolName, args) {
179
- const res = await fetch(`${GATEWAY_URL}/gateway-call`, {
180
- method: "POST",
181
- headers: {
182
- Authorization: `Bearer ${apiKey}`,
183
- "Content-Type": "application/json",
184
- },
185
- body: JSON.stringify({ tool: toolName, arguments: args }),
186
- });
187
- if (!res.ok) {
188
- const body = await res.json().catch(() => ({}));
189
- throw new Error(body.error || `Gateway call error: ${res.status}`);
190
- }
191
- const data = (await res.json());
192
- return data.result ?? data;
193
- }
332
+ // ---------------------------------------------------------------------------
333
+ // Runtime mode: MCP server with proxy architecture
334
+ // ---------------------------------------------------------------------------
194
335
  async function runServer(apiKey) {
195
- // Sync external MCPs to ~/.mcp.json before starting
196
- await syncExternalMcps(apiKey);
197
- const server = new McpServer({
198
- name: "MCP Store",
199
- version: "1.3.1",
200
- });
201
- // Register hosted tools (proxied through our backend)
202
- try {
203
- const tools = await fetchTools(apiKey);
336
+ // 1. Clean up legacy mcpstore-* entries from .mcp.json (v1 → v2 migration)
337
+ cleanupLegacyEntries();
338
+ // 2. Fetch hosted tools (debate, quick_review — proxied through backend)
339
+ hostedTools = await fetchHostedTools(apiKey);
340
+ // 3. Create server with listChanged capability
341
+ const server = new Server({ name: "MCP Store", version: "2.0.0" }, { capabilities: { tools: { listChanged: true } } });
342
+ // 4. Initial spawn fetch installed MCPs and start child processes
343
+ const installed = await fetchInstalledMcps(apiKey);
344
+ for (const mcp of installed) {
345
+ const child = await spawnChild(mcp);
346
+ if (child) {
347
+ children.set(mcp.slug, child);
348
+ }
349
+ }
350
+ // 5. Handle tools/list
351
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
352
+ const tools = [];
353
+ // Hosted tools (backend API proxy)
354
+ for (const ht of hostedTools) {
355
+ tools.push({
356
+ name: ht.name,
357
+ description: ht.description,
358
+ inputSchema: ht.inputSchema,
359
+ });
360
+ }
361
+ // Proxied external tools (from child MCPs)
362
+ tools.push(...getProxiedTools());
363
+ // Fallback info tool if nothing installed
204
364
  if (tools.length === 0) {
205
- server.tool("mcpstore__info", "No MCPs installed yet. Visit https://www.mcpclaudecode.com/browse to install MCPs.", {}, async () => ({
365
+ tools.push({
366
+ name: "mcpstore__info",
367
+ description: "No MCPs installed yet. Visit https://www.mcpclaudecode.com/browse to install MCPs.",
368
+ inputSchema: { type: "object", properties: {} },
369
+ });
370
+ }
371
+ return { tools };
372
+ });
373
+ // 6. Handle tools/call
374
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
375
+ const { name, arguments: args = {} } = request.params;
376
+ // Info tool
377
+ if (name === "mcpstore__info") {
378
+ return {
206
379
  content: [
207
380
  {
208
381
  type: "text",
209
- text: "You have no MCPs installed. Visit https://www.mcpclaudecode.com/browse to browse and install MCPs. They will appear here automatically!",
382
+ text: "You have no MCPs installed yet. Visit https://www.mcpclaudecode.com/browse to browse and install MCPs. They will appear here automatically!",
210
383
  },
211
384
  ],
212
- }));
385
+ };
213
386
  }
214
- else {
215
- for (const tool of tools) {
216
- const shape = {};
217
- const schema = tool.inputSchema;
218
- if (schema?.properties) {
219
- for (const [key, value] of Object.entries(schema.properties)) {
220
- shape[key] = value;
221
- }
222
- }
223
- server.tool(tool.name, tool.description, shape, async (args) => {
224
- try {
225
- const result = await callTool(apiKey, tool.name, args);
226
- const text = typeof result === "string"
227
- ? result
228
- : JSON.stringify(result, null, 2);
229
- return { content: [{ type: "text", text }] };
230
- }
231
- catch (err) {
232
- return {
233
- content: [
234
- {
235
- type: "text",
236
- text: `Error: ${err.message}`,
237
- },
238
- ],
239
- isError: true,
240
- };
241
- }
242
- });
387
+ // Check hosted tools first
388
+ const hosted = hostedTools.find((t) => t.name === name);
389
+ if (hosted) {
390
+ try {
391
+ const text = await callHostedTool(apiKey, name, args);
392
+ return { content: [{ type: "text", text }] };
393
+ }
394
+ catch (err) {
395
+ return {
396
+ content: [
397
+ {
398
+ type: "text",
399
+ text: `Error: ${err.message}`,
400
+ },
401
+ ],
402
+ isError: true,
403
+ };
243
404
  }
244
405
  }
245
- }
246
- catch (err) {
247
- console.error("Failed to fetch tools from MCP Store:", err.message);
248
- server.tool("mcpstore__error", "MCP Store connection error", {}, async () => ({
249
- content: [
250
- {
251
- type: "text",
252
- text: `Failed to connect to MCP Store: ${err.message}\n\nPlease check your API key and try again.`,
253
- },
254
- ],
255
- isError: true,
256
- }));
257
- }
406
+ // Route to child MCP
407
+ const owner = findToolOwner(name);
408
+ if (!owner) {
409
+ return {
410
+ content: [
411
+ { type: "text", text: `Unknown tool: ${name}` },
412
+ ],
413
+ isError: true,
414
+ };
415
+ }
416
+ try {
417
+ const result = await owner.child.client.callTool({
418
+ name: owner.toolName,
419
+ arguments: args,
420
+ });
421
+ return result;
422
+ }
423
+ catch (err) {
424
+ return {
425
+ content: [
426
+ {
427
+ type: "text",
428
+ text: `Error calling ${name}: ${err.message}`,
429
+ },
430
+ ],
431
+ isError: true,
432
+ };
433
+ }
434
+ });
435
+ // 7. Connect to Claude Code via stdio
258
436
  const transport = new StdioServerTransport();
259
437
  await server.connect(transport);
438
+ // 8. Start periodic sync (after connection — can now send notifications)
439
+ setInterval(() => {
440
+ sync(apiKey, server).catch(() => { });
441
+ }, SYNC_INTERVAL_MS);
442
+ // 9. Cleanup on exit
443
+ const cleanup = async () => {
444
+ await killAllChildren();
445
+ process.exit(0);
446
+ };
447
+ process.on("SIGINT", cleanup);
448
+ process.on("SIGTERM", cleanup);
260
449
  }
261
450
  // ---------------------------------------------------------------------------
262
451
  // Entry point
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpstore-gateway",
3
- "version": "1.3.1",
3
+ "version": "2.0.0",
4
4
  "description": "MCP Store Gateway — One MCP server for all your installed MCPs from mcpclaudecode.com",
5
5
  "keywords": ["mcp", "claude", "claude-code", "ai", "gateway", "marketplace"],
6
6
  "license": "MIT",