@yawlabs/mcp-connect 0.1.3 → 0.2.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.
Files changed (2) hide show
  1. package/dist/index.js +502 -38
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -10,12 +10,12 @@ function log(level, msg, data) {
10
10
  }
11
11
 
12
12
  // src/config.ts
13
- async function fetchConfig(apiUrl2, token2) {
14
- const url = apiUrl2.replace(/\/$/, "") + "/api/connect/config";
13
+ async function fetchConfig(apiUrl3, token3) {
14
+ const url = apiUrl3.replace(/\/$/, "") + "/api/connect/config";
15
15
  const res = await request(url, {
16
16
  method: "GET",
17
17
  headers: {
18
- Authorization: "Bearer " + token2,
18
+ Authorization: "Bearer " + token3,
19
19
  Accept: "application/json"
20
20
  },
21
21
  headersTimeout: 1e4,
@@ -48,9 +48,80 @@ var ConfigError = class extends Error {
48
48
  };
49
49
 
50
50
  // src/server.ts
51
+ import { readFile } from "fs/promises";
52
+ import { homedir } from "os";
53
+ import { resolve } from "path";
51
54
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
52
55
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
53
- import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
56
+ import {
57
+ CallToolRequestSchema,
58
+ GetPromptRequestSchema,
59
+ ListPromptsRequestSchema,
60
+ ListResourcesRequestSchema,
61
+ ListToolsRequestSchema,
62
+ ReadResourceRequestSchema
63
+ } from "@modelcontextprotocol/sdk/types.js";
64
+ import { request as request3 } from "undici";
65
+
66
+ // src/analytics.ts
67
+ import { request as request2 } from "undici";
68
+ var FLUSH_INTERVAL = 3e4;
69
+ var FLUSH_SIZE = 50;
70
+ var MAX_BUFFER = 5e3;
71
+ var buffer = [];
72
+ var flushTimer = null;
73
+ var apiUrl = "";
74
+ var token = "";
75
+ function recordConnectEvent(event) {
76
+ if (buffer.length >= MAX_BUFFER) return;
77
+ buffer.push({ ...event, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
78
+ if (buffer.length >= FLUSH_SIZE) {
79
+ flush();
80
+ }
81
+ }
82
+ async function flush() {
83
+ if (buffer.length === 0 || !apiUrl || !token) return;
84
+ const events = buffer.splice(0, FLUSH_SIZE);
85
+ try {
86
+ const res = await request2(apiUrl.replace(/\/$/, "") + "/api/connect/analytics", {
87
+ method: "POST",
88
+ headers: {
89
+ Authorization: "Bearer " + token,
90
+ "Content-Type": "application/json"
91
+ },
92
+ body: JSON.stringify({ events }),
93
+ headersTimeout: 1e4,
94
+ bodyTimeout: 1e4
95
+ });
96
+ if (res.statusCode >= 400) {
97
+ const room = MAX_BUFFER - buffer.length;
98
+ if (room > 0) buffer.unshift(...events.slice(0, room));
99
+ log("warn", "Analytics flush failed", { status: res.statusCode });
100
+ }
101
+ await res.body.text().catch(() => {
102
+ });
103
+ } catch (err) {
104
+ const room = MAX_BUFFER - buffer.length;
105
+ if (room > 0) buffer.unshift(...events.slice(0, room));
106
+ log("warn", "Analytics flush error", { error: err.message });
107
+ }
108
+ }
109
+ function initAnalytics(url, tok) {
110
+ apiUrl = url;
111
+ token = tok;
112
+ flushTimer = setInterval(() => flush().catch(() => {
113
+ }), FLUSH_INTERVAL);
114
+ if (flushTimer.unref) flushTimer.unref();
115
+ }
116
+ async function shutdownAnalytics() {
117
+ if (flushTimer) {
118
+ clearInterval(flushTimer);
119
+ flushTimer = null;
120
+ }
121
+ while (buffer.length > 0) {
122
+ await flush();
123
+ }
124
+ }
54
125
 
55
126
  // src/meta-tools.ts
56
127
  var META_TOOLS = {
@@ -59,7 +130,12 @@ var META_TOOLS = {
59
130
  description: "List all available MCP servers. Call this FIRST before activating anything. Only activate servers you need for the CURRENT task \u2014 each one adds tools to your context. Shows server names, namespaces, tool counts, and activation status.",
60
131
  inputSchema: {
61
132
  type: "object",
62
- properties: {}
133
+ properties: {
134
+ context: {
135
+ type: "string",
136
+ description: "Optional: describe the current task or conversation context. Servers will be sorted by relevance to help you pick the right one."
137
+ }
138
+ }
63
139
  },
64
140
  annotations: {
65
141
  title: "Discover MCP Servers",
@@ -110,12 +186,50 @@ var META_TOOLS = {
110
186
  idempotentHint: true,
111
187
  openWorldHint: false
112
188
  }
189
+ },
190
+ import_config: {
191
+ name: "mcp_connect_import",
192
+ description: "Import MCP servers from an existing config file (Claude Desktop, Cursor, VS Code, etc.). Reads the file, parses the mcpServers section, and creates connect server entries in the cloud. Supported files: claude_desktop_config.json, mcp.json, settings.json.",
193
+ inputSchema: {
194
+ type: "object",
195
+ properties: {
196
+ filepath: {
197
+ type: "string",
198
+ description: 'Path to the MCP config file (e.g., "~/.claude/claude_desktop_config.json", ".cursor/mcp.json")'
199
+ }
200
+ },
201
+ required: ["filepath"]
202
+ },
203
+ annotations: {
204
+ title: "Import MCP Config",
205
+ readOnlyHint: false,
206
+ destructiveHint: false,
207
+ idempotentHint: false,
208
+ openWorldHint: true
209
+ }
210
+ },
211
+ health: {
212
+ name: "mcp_connect_health",
213
+ description: "Show health stats for all active MCP server connections: total calls, error count, average latency, and last error.",
214
+ inputSchema: {
215
+ type: "object",
216
+ properties: {}
217
+ },
218
+ annotations: {
219
+ title: "Connection Health",
220
+ readOnlyHint: true,
221
+ destructiveHint: false,
222
+ idempotentHint: true,
223
+ openWorldHint: false
224
+ }
113
225
  }
114
226
  };
115
227
  var META_TOOL_NAMES = /* @__PURE__ */ new Set([
116
228
  META_TOOLS.discover.name,
117
229
  META_TOOLS.activate.name,
118
- META_TOOLS.deactivate.name
230
+ META_TOOLS.deactivate.name,
231
+ META_TOOLS.import_config.name,
232
+ META_TOOLS.health.name
119
233
  ]);
120
234
 
121
235
  // src/proxy.ts
@@ -153,6 +267,89 @@ function buildToolRoutes(activeConnections) {
153
267
  }
154
268
  return routes;
155
269
  }
270
+ function buildResourceList(activeConnections) {
271
+ const resources = [];
272
+ for (const conn of activeConnections.values()) {
273
+ for (const r of conn.resources) {
274
+ resources.push({
275
+ uri: r.namespacedUri,
276
+ name: r.name,
277
+ description: r.description,
278
+ mimeType: r.mimeType
279
+ });
280
+ }
281
+ }
282
+ return resources;
283
+ }
284
+ function buildResourceRoutes(activeConnections) {
285
+ const routes = /* @__PURE__ */ new Map();
286
+ for (const conn of activeConnections.values()) {
287
+ for (const r of conn.resources) {
288
+ routes.set(r.namespacedUri, { namespace: conn.config.namespace, originalUri: r.uri });
289
+ }
290
+ }
291
+ return routes;
292
+ }
293
+ function buildPromptList(activeConnections) {
294
+ const prompts = [];
295
+ for (const conn of activeConnections.values()) {
296
+ for (const p of conn.prompts) {
297
+ prompts.push({
298
+ name: p.namespacedName,
299
+ description: p.description,
300
+ arguments: p.arguments
301
+ });
302
+ }
303
+ }
304
+ return prompts;
305
+ }
306
+ function buildPromptRoutes(activeConnections) {
307
+ const routes = /* @__PURE__ */ new Map();
308
+ for (const conn of activeConnections.values()) {
309
+ for (const p of conn.prompts) {
310
+ routes.set(p.namespacedName, { namespace: conn.config.namespace, originalName: p.name });
311
+ }
312
+ }
313
+ return routes;
314
+ }
315
+ async function routeResourceRead(uri, resourceRoutes, activeConnections) {
316
+ const route = resourceRoutes.get(uri);
317
+ if (!route) {
318
+ return { contents: [{ uri, text: "Unknown resource: " + uri }] };
319
+ }
320
+ const connection = activeConnections.get(route.namespace);
321
+ if (!connection || connection.status !== "connected") {
322
+ return { contents: [{ uri, text: 'Server "' + route.namespace + '" is not connected.' }] };
323
+ }
324
+ try {
325
+ const result = await connection.client.readResource({ uri: route.originalUri });
326
+ return result;
327
+ } catch (err) {
328
+ log("error", "Resource read failed", { uri, namespace: route.namespace, error: err.message });
329
+ return { contents: [{ uri, text: "Error: " + err.message }] };
330
+ }
331
+ }
332
+ async function routePromptGet(name, args, promptRoutes, activeConnections) {
333
+ const route = promptRoutes.get(name);
334
+ if (!route) {
335
+ return { messages: [{ role: "user", content: { type: "text", text: "Unknown prompt: " + name } }] };
336
+ }
337
+ const connection = activeConnections.get(route.namespace);
338
+ if (!connection || connection.status !== "connected") {
339
+ return {
340
+ messages: [
341
+ { role: "user", content: { type: "text", text: 'Server "' + route.namespace + '" is not connected.' } }
342
+ ]
343
+ };
344
+ }
345
+ try {
346
+ const result = await connection.client.getPrompt({ name: route.originalName, arguments: args });
347
+ return result;
348
+ } catch (err) {
349
+ log("error", "Prompt get failed", { name, namespace: route.namespace, error: err.message });
350
+ return { messages: [{ role: "user", content: { type: "text", text: "Error: " + err.message } }] };
351
+ }
352
+ }
156
353
  async function routeToolCall(toolName, args, toolRoutes, activeConnections) {
157
354
  const route = toolRoutes.get(toolName);
158
355
  if (!route) {
@@ -193,12 +390,31 @@ async function routeToolCall(toolName, args, toolRoutes, activeConnections) {
193
390
  }
194
391
  }
195
392
 
393
+ // src/relevance.ts
394
+ function scoreRelevance(context, server2, tools) {
395
+ const contextLower = context.toLowerCase();
396
+ const words = contextLower.split(/\s+/).filter((w) => w.length > 2).map((w) => w.replace(/[^a-z0-9]/g, "")).filter(Boolean);
397
+ if (words.length === 0) return 0;
398
+ let score = 0;
399
+ const nameLower = server2.name.toLowerCase();
400
+ const nsLower = server2.namespace.toLowerCase();
401
+ for (const word of words) {
402
+ if (nameLower.includes(word)) score += 3;
403
+ if (nsLower.includes(word)) score += 2;
404
+ for (const tool of tools) {
405
+ if (tool.name.toLowerCase().includes(word)) score += 2;
406
+ if (tool.description?.toLowerCase().includes(word)) score += 1;
407
+ }
408
+ }
409
+ return score;
410
+ }
411
+
196
412
  // src/upstream.ts
197
413
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
198
414
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
199
415
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
200
416
  var CONNECT_TIMEOUT = 15e3;
201
- async function connectToUpstream(config) {
417
+ async function connectToUpstream(config, onDisconnect) {
202
418
  const client = new Client({ name: "mcp-connect", version: "0.1.0" }, { capabilities: {} });
203
419
  let transport;
204
420
  if (config.type === "local") {
@@ -223,14 +439,29 @@ async function connectToUpstream(config) {
223
439
  );
224
440
  await Promise.race([connectPromise, timeoutPromise]);
225
441
  log("info", "Connected to upstream", { name: config.name, namespace: config.namespace, type: config.type });
442
+ const connection = {};
443
+ client.onclose = () => {
444
+ if (connection.status === "connected") {
445
+ connection.status = "error";
446
+ connection.error = "Upstream disconnected unexpectedly";
447
+ log("warn", "Upstream disconnected unexpectedly", { namespace: config.namespace });
448
+ if (onDisconnect) onDisconnect(config.namespace);
449
+ }
450
+ };
226
451
  const tools = await fetchToolsFromUpstream(client, config.namespace);
227
- return {
452
+ const resources = await fetchResourcesFromUpstream(client, config.namespace);
453
+ const prompts = await fetchPromptsFromUpstream(client, config.namespace);
454
+ Object.assign(connection, {
228
455
  config,
229
456
  client,
230
457
  transport,
231
458
  tools,
459
+ resources,
460
+ prompts,
461
+ health: { totalCalls: 0, errorCount: 0, totalLatencyMs: 0 },
232
462
  status: "connected"
233
- };
463
+ });
464
+ return connection;
234
465
  }
235
466
  async function disconnectFromUpstream(connection) {
236
467
  try {
@@ -244,6 +475,33 @@ async function disconnectFromUpstream(connection) {
244
475
  connection.status = "disconnected";
245
476
  log("info", "Disconnected from upstream", { namespace: connection.config.namespace });
246
477
  }
478
+ async function fetchResourcesFromUpstream(client, namespace) {
479
+ try {
480
+ const result = await client.listResources();
481
+ return (result.resources ?? []).map((r) => ({
482
+ uri: r.uri,
483
+ namespacedUri: "connect://" + namespace + "/" + r.uri,
484
+ name: r.name,
485
+ description: r.description,
486
+ mimeType: r.mimeType
487
+ }));
488
+ } catch {
489
+ return [];
490
+ }
491
+ }
492
+ async function fetchPromptsFromUpstream(client, namespace) {
493
+ try {
494
+ const result = await client.listPrompts();
495
+ return (result.prompts ?? []).map((p) => ({
496
+ name: p.name,
497
+ namespacedName: namespace + "_" + p.name,
498
+ description: p.description,
499
+ arguments: p.arguments
500
+ }));
501
+ } catch {
502
+ return [];
503
+ }
504
+ }
247
505
  async function fetchToolsFromUpstream(client, namespace) {
248
506
  const result = await client.listTools();
249
507
  return (result.tools ?? []).map((tool) => ({
@@ -258,12 +516,18 @@ async function fetchToolsFromUpstream(client, namespace) {
258
516
  // src/server.ts
259
517
  var POLL_INTERVAL = 6e4;
260
518
  var ConnectServer = class _ConnectServer {
261
- constructor(apiUrl2, token2) {
262
- this.apiUrl = apiUrl2;
263
- this.token = token2;
519
+ constructor(apiUrl3, token3) {
520
+ this.apiUrl = apiUrl3;
521
+ this.token = token3;
264
522
  this.server = new Server(
265
- { name: "mcp-connect", version: "0.1.0" },
266
- { capabilities: { tools: { listChanged: true } } }
523
+ { name: "mcp-connect", version: "0.2.0" },
524
+ {
525
+ capabilities: {
526
+ tools: { listChanged: true },
527
+ resources: { listChanged: true },
528
+ prompts: { listChanged: true }
529
+ }
530
+ }
267
531
  );
268
532
  this.setupHandlers();
269
533
  }
@@ -275,16 +539,50 @@ var ConnectServer = class _ConnectServer {
275
539
  configVersion = null;
276
540
  pollTimer = null;
277
541
  toolRoutes = /* @__PURE__ */ new Map();
542
+ resourceRoutes = /* @__PURE__ */ new Map();
543
+ promptRoutes = /* @__PURE__ */ new Map();
278
544
  idleCallCounts = /* @__PURE__ */ new Map();
279
545
  static IDLE_CALL_THRESHOLD = 10;
280
546
  setupHandlers() {
281
547
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
282
548
  tools: buildToolList(this.connections)
283
549
  }));
284
- this.server.setRequestHandler(CallToolRequestSchema, async (request2) => {
285
- const { name, arguments: args } = request2.params;
550
+ this.server.setRequestHandler(CallToolRequestSchema, async (request4) => {
551
+ const { name, arguments: args } = request4.params;
286
552
  return this.handleToolCall(name, args ?? {});
287
553
  });
554
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
555
+ resources: buildResourceList(this.connections)
556
+ }));
557
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request4) => {
558
+ return routeResourceRead(request4.params.uri, this.resourceRoutes, this.connections);
559
+ });
560
+ this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
561
+ prompts: buildPromptList(this.connections)
562
+ }));
563
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request4) => {
564
+ return routePromptGet(
565
+ request4.params.name,
566
+ request4.params.arguments,
567
+ this.promptRoutes,
568
+ this.connections
569
+ );
570
+ });
571
+ }
572
+ handleUpstreamDisconnect(namespace) {
573
+ log("warn", "Upstream disconnect detected, will auto-reconnect on next use", { namespace });
574
+ }
575
+ rebuildRoutes() {
576
+ this.toolRoutes = buildToolRoutes(this.connections);
577
+ this.resourceRoutes = buildResourceRoutes(this.connections);
578
+ this.promptRoutes = buildPromptRoutes(this.connections);
579
+ }
580
+ async notifyAllListsChanged() {
581
+ await this.server.sendToolListChanged();
582
+ await this.server.sendResourceListChanged().catch(() => {
583
+ });
584
+ await this.server.sendPromptListChanged().catch(() => {
585
+ });
288
586
  }
289
587
  async start() {
290
588
  try {
@@ -296,6 +594,7 @@ var ConnectServer = class _ConnectServer {
296
594
  log("warn", "Initial config fetch failed, starting with empty config", { error: err.message });
297
595
  this.config = { servers: [], configVersion: "" };
298
596
  }
597
+ initAnalytics(this.apiUrl, this.token);
299
598
  const transport = new StdioServerTransport();
300
599
  await this.server.connect(transport);
301
600
  this.startPolling();
@@ -306,22 +605,91 @@ var ConnectServer = class _ConnectServer {
306
605
  }
307
606
  async handleToolCall(name, args) {
308
607
  if (name === META_TOOLS.discover.name) {
309
- return this.handleDiscover();
608
+ recordConnectEvent({ namespace: null, toolName: null, action: "discover", latencyMs: null, success: true });
609
+ return this.handleDiscover(args.context);
310
610
  }
311
611
  if (name === META_TOOLS.activate.name) {
312
- return this.handleActivate(args.server);
612
+ const result2 = await this.handleActivate(args.server);
613
+ recordConnectEvent({
614
+ namespace: args.server || null,
615
+ toolName: null,
616
+ action: "activate",
617
+ latencyMs: null,
618
+ success: !result2.isError
619
+ });
620
+ return result2;
313
621
  }
314
622
  if (name === META_TOOLS.deactivate.name) {
315
- return this.handleDeactivate(args.server);
623
+ const result2 = await this.handleDeactivate(args.server);
624
+ recordConnectEvent({
625
+ namespace: args.server || null,
626
+ toolName: null,
627
+ action: "deactivate",
628
+ latencyMs: null,
629
+ success: !result2.isError
630
+ });
631
+ return result2;
632
+ }
633
+ if (name === META_TOOLS.import_config.name) {
634
+ return this.handleImport(args.filepath);
635
+ }
636
+ if (name === META_TOOLS.health.name) {
637
+ return this.handleHealth();
316
638
  }
317
639
  const route = this.toolRoutes.get(name);
640
+ if (route) {
641
+ const conn = this.connections.get(route.namespace);
642
+ if (conn && conn.status === "error") {
643
+ const serverConfig = this.config?.servers.find((s) => s.namespace === route.namespace);
644
+ if (serverConfig) {
645
+ try {
646
+ await disconnectFromUpstream(conn);
647
+ const newConn = await connectToUpstream(serverConfig, (ns) => this.handleUpstreamDisconnect(ns));
648
+ this.connections.set(route.namespace, newConn);
649
+ this.rebuildRoutes();
650
+ log("info", "Auto-reconnected to upstream", { namespace: route.namespace });
651
+ } catch (err) {
652
+ log("error", "Auto-reconnect failed", { namespace: route.namespace, error: err.message });
653
+ return {
654
+ content: [
655
+ {
656
+ type: "text",
657
+ text: 'Server "' + route.namespace + '" disconnected and auto-reconnect failed: ' + err.message + '. Try mcp_connect_activate("' + route.namespace + '") to manually reconnect.'
658
+ }
659
+ ],
660
+ isError: true
661
+ };
662
+ }
663
+ }
664
+ }
665
+ }
666
+ const startMs = Date.now();
318
667
  const result = await routeToolCall(name, args, this.toolRoutes, this.connections);
668
+ const latencyMs = Date.now() - startMs;
319
669
  if (route) {
670
+ const conn = this.connections.get(route.namespace);
671
+ if (conn) {
672
+ conn.health.totalCalls++;
673
+ conn.health.totalLatencyMs += latencyMs;
674
+ if (result.isError) {
675
+ conn.health.errorCount++;
676
+ conn.health.lastErrorMessage = result.content[0]?.text;
677
+ conn.health.lastErrorAt = (/* @__PURE__ */ new Date()).toISOString();
678
+ }
679
+ }
680
+ recordConnectEvent({
681
+ namespace: route.namespace,
682
+ toolName: route.originalName,
683
+ action: "tool_call",
684
+ latencyMs,
685
+ success: !result.isError,
686
+ error: result.isError ? result.content[0]?.text : void 0
687
+ });
320
688
  await this.trackUsageAndAutoDeactivate(route.namespace);
321
689
  }
322
690
  return result;
323
691
  }
324
- handleDiscover() {
692
+ handleDiscover(context) {
325
693
  if (!this.config || this.config.servers.length === 0) {
326
694
  return {
327
695
  content: [
@@ -332,12 +700,26 @@ var ConnectServer = class _ConnectServer {
332
700
  ]
333
701
  };
334
702
  }
335
- const lines = ["Available MCP servers:\n"];
336
- for (const server2 of this.config.servers) {
337
- if (!server2.isActive) continue;
703
+ const activeServers = this.config.servers.filter((s) => s.isActive);
704
+ let sorted;
705
+ const scores = /* @__PURE__ */ new Map();
706
+ if (context) {
707
+ for (const server2 of activeServers) {
708
+ const connection = this.connections.get(server2.namespace);
709
+ const tools = connection?.tools ?? [];
710
+ scores.set(server2.namespace, scoreRelevance(context, server2, tools));
711
+ }
712
+ sorted = [...activeServers].sort((a, b) => (scores.get(b.namespace) ?? 0) - (scores.get(a.namespace) ?? 0));
713
+ } else {
714
+ sorted = activeServers;
715
+ }
716
+ const lines = [context ? "Servers ranked by relevance:\n" : "Available MCP servers:\n"];
717
+ for (const server2 of sorted) {
338
718
  const connection = this.connections.get(server2.namespace);
339
- const status = connection ? "ACTIVE (" + connection.tools.length + " tools)" : "available";
340
- lines.push(" " + server2.namespace + " \u2014 " + server2.name + " [" + status + "] (" + server2.type + ")");
719
+ const status = connection ? connection.status === "error" ? "ERROR (disconnected, will auto-reconnect on use)" : "ACTIVE (" + connection.tools.length + " tools)" : "available";
720
+ const score = scores.get(server2.namespace);
721
+ const relevance = score && score > 0 ? " (relevance: " + score + ")" : "";
722
+ lines.push(" " + server2.namespace + " \u2014 " + server2.name + " [" + status + "] (" + server2.type + ")" + relevance);
341
723
  }
342
724
  const inactive = this.config.servers.filter((s) => !s.isActive);
343
725
  if (inactive.length > 0) {
@@ -386,11 +768,11 @@ var ConnectServer = class _ConnectServer {
386
768
  };
387
769
  }
388
770
  try {
389
- const connection = await connectToUpstream(serverConfig);
771
+ const connection = await connectToUpstream(serverConfig, (ns) => this.handleUpstreamDisconnect(ns));
390
772
  this.connections.set(namespace, connection);
391
773
  this.idleCallCounts.set(namespace, 0);
392
- this.toolRoutes = buildToolRoutes(this.connections);
393
- await this.server.sendToolListChanged();
774
+ this.rebuildRoutes();
775
+ await this.notifyAllListsChanged();
394
776
  const toolNames = connection.tools.map((t) => t.namespacedName).join(", ");
395
777
  return {
396
778
  content: [
@@ -425,8 +807,8 @@ var ConnectServer = class _ConnectServer {
425
807
  await disconnectFromUpstream(connection);
426
808
  this.connections.delete(namespace);
427
809
  this.idleCallCounts.delete(namespace);
428
- this.toolRoutes = buildToolRoutes(this.connections);
429
- await this.server.sendToolListChanged();
810
+ this.rebuildRoutes();
811
+ await this.notifyAllListsChanged();
430
812
  return {
431
813
  content: [{ type: "text", text: 'Deactivated "' + namespace + '". Tools removed.' }]
432
814
  };
@@ -454,8 +836,8 @@ var ConnectServer = class _ConnectServer {
454
836
  }
455
837
  }
456
838
  if (toDeactivate.length > 0) {
457
- this.toolRoutes = buildToolRoutes(this.connections);
458
- await this.server.sendToolListChanged();
839
+ this.rebuildRoutes();
840
+ await this.notifyAllListsChanged();
459
841
  }
460
842
  }
461
843
  async fetchAndApplyConfig() {
@@ -488,8 +870,8 @@ var ConnectServer = class _ConnectServer {
488
870
  }
489
871
  }
490
872
  if (changed) {
491
- this.toolRoutes = buildToolRoutes(this.connections);
492
- await this.server.sendToolListChanged();
873
+ this.rebuildRoutes();
874
+ await this.notifyAllListsChanged();
493
875
  }
494
876
  }
495
877
  startPolling() {
@@ -504,12 +886,94 @@ var ConnectServer = class _ConnectServer {
504
886
  this.pollTimer.unref();
505
887
  }
506
888
  }
889
+ async handleImport(filepath) {
890
+ if (!filepath) {
891
+ return { content: [{ type: "text", text: "filepath is required." }], isError: true };
892
+ }
893
+ try {
894
+ const resolved = filepath.startsWith("~") ? resolve(homedir(), filepath.slice(2)) : resolve(filepath);
895
+ const raw = await readFile(resolved, "utf-8");
896
+ const parsed = JSON.parse(raw);
897
+ const mcpServers = parsed.mcpServers || parsed;
898
+ if (typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
899
+ return { content: [{ type: "text", text: "No mcpServers object found in " + resolved }], isError: true };
900
+ }
901
+ const servers = [];
902
+ for (const [key, value] of Object.entries(mcpServers)) {
903
+ if (!value || typeof value !== "object") continue;
904
+ if (key === "mcp-connect") continue;
905
+ const namespace = key.toLowerCase().replace(/[^a-z0-9]/g, "_").replace(/^_+|_+$/g, "").slice(0, 30);
906
+ if (!namespace) continue;
907
+ const entry = {
908
+ name: key,
909
+ namespace,
910
+ type: value.url ? "remote" : "local"
911
+ };
912
+ if (value.command) entry.command = value.command;
913
+ if (value.args) entry.args = value.args;
914
+ if (value.env) entry.env = value.env;
915
+ if (value.url) entry.url = value.url;
916
+ servers.push(entry);
917
+ }
918
+ if (servers.length === 0) {
919
+ return { content: [{ type: "text", text: "No servers found in " + resolved }], isError: true };
920
+ }
921
+ const res = await request3(this.apiUrl.replace(/\/$/, "") + "/api/connect/import", {
922
+ method: "POST",
923
+ headers: {
924
+ Authorization: "Bearer " + this.token,
925
+ "Content-Type": "application/json"
926
+ },
927
+ body: JSON.stringify({ servers }),
928
+ headersTimeout: 15e3,
929
+ bodyTimeout: 15e3
930
+ });
931
+ const body = await res.body.json();
932
+ if (res.statusCode >= 400) {
933
+ return {
934
+ content: [{ type: "text", text: "Import failed: " + (body.error || "HTTP " + res.statusCode) }],
935
+ isError: true
936
+ };
937
+ }
938
+ await this.fetchAndApplyConfig().catch(() => {
939
+ });
940
+ return {
941
+ content: [
942
+ {
943
+ type: "text",
944
+ text: "Imported " + (body.imported || 0) + " servers" + (body.skipped ? ", " + body.skipped + " skipped (already exist)" : "") + " from " + resolved + ". Use mcp_connect_discover to see them."
945
+ }
946
+ ]
947
+ };
948
+ } catch (err) {
949
+ return { content: [{ type: "text", text: "Import error: " + err.message }], isError: true };
950
+ }
951
+ }
952
+ handleHealth() {
953
+ if (this.connections.size === 0) {
954
+ return { content: [{ type: "text", text: "No active connections." }] };
955
+ }
956
+ const lines = ["Connection health:\n"];
957
+ for (const [namespace, conn] of this.connections) {
958
+ const h = conn.health;
959
+ const avgLatency = h.totalCalls > 0 ? Math.round(h.totalLatencyMs / h.totalCalls) : 0;
960
+ const errorRate = h.totalCalls > 0 ? Math.round(h.errorCount / h.totalCalls * 100) : 0;
961
+ lines.push(" " + namespace + " [" + conn.status + "]");
962
+ lines.push(" calls: " + h.totalCalls + ", errors: " + h.errorCount + " (" + errorRate + "%)");
963
+ lines.push(" avg latency: " + avgLatency + "ms");
964
+ if (h.lastErrorMessage) {
965
+ lines.push(" last error: " + h.lastErrorMessage + " at " + h.lastErrorAt);
966
+ }
967
+ }
968
+ return { content: [{ type: "text", text: lines.join("\n") }] };
969
+ }
507
970
  async shutdown() {
508
971
  log("info", "Shutting down mcp-connect");
509
972
  if (this.pollTimer) {
510
973
  clearInterval(this.pollTimer);
511
974
  this.pollTimer = null;
512
975
  }
976
+ await shutdownAnalytics();
513
977
  const disconnects = Array.from(this.connections.values()).map((conn) => disconnectFromUpstream(conn));
514
978
  await Promise.allSettled(disconnects);
515
979
  this.connections.clear();
@@ -519,15 +983,15 @@ var ConnectServer = class _ConnectServer {
519
983
  };
520
984
 
521
985
  // src/index.ts
522
- var token = process.env.MCP_HOSTING_TOKEN;
523
- var apiUrl = process.env.MCP_HOSTING_URL ?? "https://mcp.hosting";
524
- if (!token) {
986
+ var token2 = process.env.MCP_HOSTING_TOKEN;
987
+ var apiUrl2 = process.env.MCP_HOSTING_URL ?? "https://mcp.hosting";
988
+ if (!token2) {
525
989
  process.stderr.write(
526
990
  '\n mcp-connect: MCP_HOSTING_TOKEN is required.\n\n 1. Create a token at https://mcp.hosting \u2192 Settings \u2192 API Tokens\n 2. Add it to your MCP client config:\n\n {\n "mcpServers": {\n "mcp-connect": {\n "command": "npx",\n "args": ["@yawlabs/mcp-connect"],\n "env": {\n "MCP_HOSTING_TOKEN": "mcp_pat_your_token_here"\n }\n }\n }\n }\n\n'
527
991
  );
528
992
  process.exit(1);
529
993
  }
530
- var server = new ConnectServer(apiUrl, token);
994
+ var server = new ConnectServer(apiUrl2, token2);
531
995
  var shutdown = async () => {
532
996
  await server.shutdown();
533
997
  process.exit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/mcp-connect",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "MCP orchestrator — one install, all your MCP servers, managed from the cloud",
5
5
  "license": "MIT",
6
6
  "author": "Yaw Labs <contact@yaw.sh> (https://yaw.sh)",