@yawlabs/mcp-connect 0.1.3 → 0.2.1

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 +539 -43
  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,81 @@ 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
+ for (let i = 0; i < 3 && buffer.length > 0; i++) {
122
+ await flush();
123
+ }
124
+ buffer.length = 0;
125
+ }
54
126
 
55
127
  // src/meta-tools.ts
56
128
  var META_TOOLS = {
@@ -59,7 +131,12 @@ var META_TOOLS = {
59
131
  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
132
  inputSchema: {
61
133
  type: "object",
62
- properties: {}
134
+ properties: {
135
+ context: {
136
+ type: "string",
137
+ description: "Optional: describe the current task or conversation context. Servers will be sorted by relevance to help you pick the right one."
138
+ }
139
+ }
63
140
  },
64
141
  annotations: {
65
142
  title: "Discover MCP Servers",
@@ -110,12 +187,50 @@ var META_TOOLS = {
110
187
  idempotentHint: true,
111
188
  openWorldHint: false
112
189
  }
190
+ },
191
+ import_config: {
192
+ name: "mcp_connect_import",
193
+ 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.",
194
+ inputSchema: {
195
+ type: "object",
196
+ properties: {
197
+ filepath: {
198
+ type: "string",
199
+ description: 'Path to the MCP config file (e.g., "~/.claude/claude_desktop_config.json", ".cursor/mcp.json")'
200
+ }
201
+ },
202
+ required: ["filepath"]
203
+ },
204
+ annotations: {
205
+ title: "Import MCP Config",
206
+ readOnlyHint: false,
207
+ destructiveHint: false,
208
+ idempotentHint: false,
209
+ openWorldHint: true
210
+ }
211
+ },
212
+ health: {
213
+ name: "mcp_connect_health",
214
+ description: "Show health stats for all active MCP server connections: total calls, error count, average latency, and last error.",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {}
218
+ },
219
+ annotations: {
220
+ title: "Connection Health",
221
+ readOnlyHint: true,
222
+ destructiveHint: false,
223
+ idempotentHint: true,
224
+ openWorldHint: false
225
+ }
113
226
  }
114
227
  };
115
228
  var META_TOOL_NAMES = /* @__PURE__ */ new Set([
116
229
  META_TOOLS.discover.name,
117
230
  META_TOOLS.activate.name,
118
- META_TOOLS.deactivate.name
231
+ META_TOOLS.deactivate.name,
232
+ META_TOOLS.import_config.name,
233
+ META_TOOLS.health.name
119
234
  ]);
120
235
 
121
236
  // src/proxy.ts
@@ -153,6 +268,89 @@ function buildToolRoutes(activeConnections) {
153
268
  }
154
269
  return routes;
155
270
  }
271
+ function buildResourceList(activeConnections) {
272
+ const resources = [];
273
+ for (const conn of activeConnections.values()) {
274
+ for (const r of conn.resources) {
275
+ resources.push({
276
+ uri: r.namespacedUri,
277
+ name: r.name,
278
+ description: r.description,
279
+ mimeType: r.mimeType
280
+ });
281
+ }
282
+ }
283
+ return resources;
284
+ }
285
+ function buildResourceRoutes(activeConnections) {
286
+ const routes = /* @__PURE__ */ new Map();
287
+ for (const conn of activeConnections.values()) {
288
+ for (const r of conn.resources) {
289
+ routes.set(r.namespacedUri, { namespace: conn.config.namespace, originalUri: r.uri });
290
+ }
291
+ }
292
+ return routes;
293
+ }
294
+ function buildPromptList(activeConnections) {
295
+ const prompts = [];
296
+ for (const conn of activeConnections.values()) {
297
+ for (const p of conn.prompts) {
298
+ prompts.push({
299
+ name: p.namespacedName,
300
+ description: p.description,
301
+ arguments: p.arguments
302
+ });
303
+ }
304
+ }
305
+ return prompts;
306
+ }
307
+ function buildPromptRoutes(activeConnections) {
308
+ const routes = /* @__PURE__ */ new Map();
309
+ for (const conn of activeConnections.values()) {
310
+ for (const p of conn.prompts) {
311
+ routes.set(p.namespacedName, { namespace: conn.config.namespace, originalName: p.name });
312
+ }
313
+ }
314
+ return routes;
315
+ }
316
+ async function routeResourceRead(uri, resourceRoutes, activeConnections) {
317
+ const route = resourceRoutes.get(uri);
318
+ if (!route) {
319
+ return { contents: [{ uri, text: "Unknown resource: " + uri }] };
320
+ }
321
+ const connection = activeConnections.get(route.namespace);
322
+ if (!connection || connection.status !== "connected") {
323
+ return { contents: [{ uri, text: 'Server "' + route.namespace + '" is not connected.' }] };
324
+ }
325
+ try {
326
+ const result = await connection.client.readResource({ uri: route.originalUri });
327
+ return result;
328
+ } catch (err) {
329
+ log("error", "Resource read failed", { uri, namespace: route.namespace, error: err.message });
330
+ return { contents: [{ uri, text: "Error: " + err.message }] };
331
+ }
332
+ }
333
+ async function routePromptGet(name, args, promptRoutes, activeConnections) {
334
+ const route = promptRoutes.get(name);
335
+ if (!route) {
336
+ return { messages: [{ role: "user", content: { type: "text", text: "Unknown prompt: " + name } }] };
337
+ }
338
+ const connection = activeConnections.get(route.namespace);
339
+ if (!connection || connection.status !== "connected") {
340
+ return {
341
+ messages: [
342
+ { role: "user", content: { type: "text", text: 'Server "' + route.namespace + '" is not connected.' } }
343
+ ]
344
+ };
345
+ }
346
+ try {
347
+ const result = await connection.client.getPrompt({ name: route.originalName, arguments: args });
348
+ return result;
349
+ } catch (err) {
350
+ log("error", "Prompt get failed", { name, namespace: route.namespace, error: err.message });
351
+ return { messages: [{ role: "user", content: { type: "text", text: "Error: " + err.message } }] };
352
+ }
353
+ }
156
354
  async function routeToolCall(toolName, args, toolRoutes, activeConnections) {
157
355
  const route = toolRoutes.get(toolName);
158
356
  if (!route) {
@@ -193,12 +391,31 @@ async function routeToolCall(toolName, args, toolRoutes, activeConnections) {
193
391
  }
194
392
  }
195
393
 
394
+ // src/relevance.ts
395
+ function scoreRelevance(context, server2, tools) {
396
+ const contextLower = context.toLowerCase();
397
+ const words = contextLower.split(/\s+/).filter((w) => w.length > 2).map((w) => w.replace(/[^a-z0-9]/g, "")).filter(Boolean);
398
+ if (words.length === 0) return 0;
399
+ let score = 0;
400
+ const nameLower = server2.name.toLowerCase();
401
+ const nsLower = server2.namespace.toLowerCase();
402
+ for (const word of words) {
403
+ if (nameLower.includes(word)) score += 3;
404
+ if (nsLower.includes(word)) score += 2;
405
+ for (const tool of tools) {
406
+ if (tool.name.toLowerCase().includes(word)) score += 2;
407
+ if (tool.description?.toLowerCase().includes(word)) score += 1;
408
+ }
409
+ }
410
+ return score;
411
+ }
412
+
196
413
  // src/upstream.ts
197
414
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
198
415
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
199
416
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
200
417
  var CONNECT_TIMEOUT = 15e3;
201
- async function connectToUpstream(config) {
418
+ async function connectToUpstream(config, onDisconnect) {
202
419
  const client = new Client({ name: "mcp-connect", version: "0.1.0" }, { capabilities: {} });
203
420
  let transport;
204
421
  if (config.type === "local") {
@@ -217,20 +434,45 @@ async function connectToUpstream(config) {
217
434
  }
218
435
  transport = new StreamableHTTPClientTransport(new URL(config.url));
219
436
  }
220
- const connectPromise = client.connect(transport);
221
- const timeoutPromise = new Promise(
222
- (_, reject) => setTimeout(() => reject(new Error("Connection timeout after " + CONNECT_TIMEOUT + "ms")), CONNECT_TIMEOUT)
223
- );
224
- await Promise.race([connectPromise, timeoutPromise]);
437
+ let timer;
438
+ const timeoutPromise = new Promise((_, reject) => {
439
+ timer = setTimeout(() => reject(new Error("Connection timeout after " + CONNECT_TIMEOUT + "ms")), CONNECT_TIMEOUT);
440
+ });
441
+ try {
442
+ await Promise.race([client.connect(transport), timeoutPromise]);
443
+ clearTimeout(timer);
444
+ } catch (err) {
445
+ clearTimeout(timer);
446
+ try {
447
+ await client.close();
448
+ } catch {
449
+ }
450
+ throw err;
451
+ }
225
452
  log("info", "Connected to upstream", { name: config.name, namespace: config.namespace, type: config.type });
453
+ const connection = {};
454
+ client.onclose = () => {
455
+ if (connection.status === "connected") {
456
+ connection.status = "error";
457
+ connection.error = "Upstream disconnected unexpectedly";
458
+ log("warn", "Upstream disconnected unexpectedly", { namespace: config.namespace });
459
+ if (onDisconnect) onDisconnect(config.namespace);
460
+ }
461
+ };
226
462
  const tools = await fetchToolsFromUpstream(client, config.namespace);
227
- return {
463
+ const resources = await fetchResourcesFromUpstream(client, config.namespace);
464
+ const prompts = await fetchPromptsFromUpstream(client, config.namespace);
465
+ Object.assign(connection, {
228
466
  config,
229
467
  client,
230
468
  transport,
231
469
  tools,
470
+ resources,
471
+ prompts,
472
+ health: { totalCalls: 0, errorCount: 0, totalLatencyMs: 0 },
232
473
  status: "connected"
233
- };
474
+ });
475
+ return connection;
234
476
  }
235
477
  async function disconnectFromUpstream(connection) {
236
478
  try {
@@ -244,6 +486,33 @@ async function disconnectFromUpstream(connection) {
244
486
  connection.status = "disconnected";
245
487
  log("info", "Disconnected from upstream", { namespace: connection.config.namespace });
246
488
  }
489
+ async function fetchResourcesFromUpstream(client, namespace) {
490
+ try {
491
+ const result = await client.listResources();
492
+ return (result.resources ?? []).map((r) => ({
493
+ uri: r.uri,
494
+ namespacedUri: "connect://" + namespace + "/" + r.uri,
495
+ name: r.name,
496
+ description: r.description,
497
+ mimeType: r.mimeType
498
+ }));
499
+ } catch {
500
+ return [];
501
+ }
502
+ }
503
+ async function fetchPromptsFromUpstream(client, namespace) {
504
+ try {
505
+ const result = await client.listPrompts();
506
+ return (result.prompts ?? []).map((p) => ({
507
+ name: p.name,
508
+ namespacedName: namespace + "_" + p.name,
509
+ description: p.description,
510
+ arguments: p.arguments
511
+ }));
512
+ } catch {
513
+ return [];
514
+ }
515
+ }
247
516
  async function fetchToolsFromUpstream(client, namespace) {
248
517
  const result = await client.listTools();
249
518
  return (result.tools ?? []).map((tool) => ({
@@ -258,12 +527,18 @@ async function fetchToolsFromUpstream(client, namespace) {
258
527
  // src/server.ts
259
528
  var POLL_INTERVAL = 6e4;
260
529
  var ConnectServer = class _ConnectServer {
261
- constructor(apiUrl2, token2) {
262
- this.apiUrl = apiUrl2;
263
- this.token = token2;
530
+ constructor(apiUrl3, token3) {
531
+ this.apiUrl = apiUrl3;
532
+ this.token = token3;
264
533
  this.server = new Server(
265
- { name: "mcp-connect", version: "0.1.0" },
266
- { capabilities: { tools: { listChanged: true } } }
534
+ { name: "mcp-connect", version: "0.2.0" },
535
+ {
536
+ capabilities: {
537
+ tools: { listChanged: true },
538
+ resources: { listChanged: true },
539
+ prompts: { listChanged: true }
540
+ }
541
+ }
267
542
  );
268
543
  this.setupHandlers();
269
544
  }
@@ -275,16 +550,50 @@ var ConnectServer = class _ConnectServer {
275
550
  configVersion = null;
276
551
  pollTimer = null;
277
552
  toolRoutes = /* @__PURE__ */ new Map();
553
+ resourceRoutes = /* @__PURE__ */ new Map();
554
+ promptRoutes = /* @__PURE__ */ new Map();
278
555
  idleCallCounts = /* @__PURE__ */ new Map();
279
556
  static IDLE_CALL_THRESHOLD = 10;
280
557
  setupHandlers() {
281
558
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
282
559
  tools: buildToolList(this.connections)
283
560
  }));
284
- this.server.setRequestHandler(CallToolRequestSchema, async (request2) => {
285
- const { name, arguments: args } = request2.params;
561
+ this.server.setRequestHandler(CallToolRequestSchema, async (request4) => {
562
+ const { name, arguments: args } = request4.params;
286
563
  return this.handleToolCall(name, args ?? {});
287
564
  });
565
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
566
+ resources: buildResourceList(this.connections)
567
+ }));
568
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request4) => {
569
+ return routeResourceRead(request4.params.uri, this.resourceRoutes, this.connections);
570
+ });
571
+ this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
572
+ prompts: buildPromptList(this.connections)
573
+ }));
574
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request4) => {
575
+ return routePromptGet(
576
+ request4.params.name,
577
+ request4.params.arguments,
578
+ this.promptRoutes,
579
+ this.connections
580
+ );
581
+ });
582
+ }
583
+ handleUpstreamDisconnect(namespace) {
584
+ log("warn", "Upstream disconnect detected, will auto-reconnect on next use", { namespace });
585
+ }
586
+ rebuildRoutes() {
587
+ this.toolRoutes = buildToolRoutes(this.connections);
588
+ this.resourceRoutes = buildResourceRoutes(this.connections);
589
+ this.promptRoutes = buildPromptRoutes(this.connections);
590
+ }
591
+ async notifyAllListsChanged() {
592
+ await this.server.sendToolListChanged();
593
+ await this.server.sendResourceListChanged().catch(() => {
594
+ });
595
+ await this.server.sendPromptListChanged().catch(() => {
596
+ });
288
597
  }
289
598
  async start() {
290
599
  try {
@@ -296,6 +605,7 @@ var ConnectServer = class _ConnectServer {
296
605
  log("warn", "Initial config fetch failed, starting with empty config", { error: err.message });
297
606
  this.config = { servers: [], configVersion: "" };
298
607
  }
608
+ initAnalytics(this.apiUrl, this.token);
299
609
  const transport = new StdioServerTransport();
300
610
  await this.server.connect(transport);
301
611
  this.startPolling();
@@ -306,22 +616,92 @@ var ConnectServer = class _ConnectServer {
306
616
  }
307
617
  async handleToolCall(name, args) {
308
618
  if (name === META_TOOLS.discover.name) {
309
- return this.handleDiscover();
619
+ recordConnectEvent({ namespace: null, toolName: null, action: "discover", latencyMs: null, success: true });
620
+ return this.handleDiscover(args.context);
310
621
  }
311
622
  if (name === META_TOOLS.activate.name) {
312
- return this.handleActivate(args.server);
623
+ const result2 = await this.handleActivate(args.server);
624
+ recordConnectEvent({
625
+ namespace: args.server || null,
626
+ toolName: null,
627
+ action: "activate",
628
+ latencyMs: null,
629
+ success: !result2.isError
630
+ });
631
+ return result2;
313
632
  }
314
633
  if (name === META_TOOLS.deactivate.name) {
315
- return this.handleDeactivate(args.server);
634
+ const result2 = await this.handleDeactivate(args.server);
635
+ recordConnectEvent({
636
+ namespace: args.server || null,
637
+ toolName: null,
638
+ action: "deactivate",
639
+ latencyMs: null,
640
+ success: !result2.isError
641
+ });
642
+ return result2;
643
+ }
644
+ if (name === META_TOOLS.import_config.name) {
645
+ return this.handleImport(args.filepath);
646
+ }
647
+ if (name === META_TOOLS.health.name) {
648
+ return this.handleHealth();
316
649
  }
317
650
  const route = this.toolRoutes.get(name);
651
+ if (route) {
652
+ const conn = this.connections.get(route.namespace);
653
+ if (conn && conn.status === "error") {
654
+ const serverConfig = this.config?.servers.find((s) => s.namespace === route.namespace);
655
+ if (serverConfig) {
656
+ try {
657
+ await disconnectFromUpstream(conn);
658
+ const newConn = await connectToUpstream(serverConfig, (ns) => this.handleUpstreamDisconnect(ns));
659
+ this.connections.set(route.namespace, newConn);
660
+ this.rebuildRoutes();
661
+ await this.notifyAllListsChanged();
662
+ log("info", "Auto-reconnected to upstream", { namespace: route.namespace });
663
+ } catch (err) {
664
+ log("error", "Auto-reconnect failed", { namespace: route.namespace, error: err.message });
665
+ return {
666
+ content: [
667
+ {
668
+ type: "text",
669
+ text: 'Server "' + route.namespace + '" disconnected and auto-reconnect failed: ' + err.message + '. Try mcp_connect_activate("' + route.namespace + '") to manually reconnect.'
670
+ }
671
+ ],
672
+ isError: true
673
+ };
674
+ }
675
+ }
676
+ }
677
+ }
678
+ const startMs = Date.now();
318
679
  const result = await routeToolCall(name, args, this.toolRoutes, this.connections);
680
+ const latencyMs = Date.now() - startMs;
319
681
  if (route) {
682
+ const conn = this.connections.get(route.namespace);
683
+ if (conn) {
684
+ conn.health.totalCalls++;
685
+ conn.health.totalLatencyMs += latencyMs;
686
+ if (result.isError) {
687
+ conn.health.errorCount++;
688
+ conn.health.lastErrorMessage = result.content[0]?.text;
689
+ conn.health.lastErrorAt = (/* @__PURE__ */ new Date()).toISOString();
690
+ }
691
+ }
692
+ recordConnectEvent({
693
+ namespace: route.namespace,
694
+ toolName: route.originalName,
695
+ action: "tool_call",
696
+ latencyMs,
697
+ success: !result.isError,
698
+ error: result.isError ? result.content[0]?.text : void 0
699
+ });
320
700
  await this.trackUsageAndAutoDeactivate(route.namespace);
321
701
  }
322
702
  return result;
323
703
  }
324
- handleDiscover() {
704
+ handleDiscover(context) {
325
705
  if (!this.config || this.config.servers.length === 0) {
326
706
  return {
327
707
  content: [
@@ -332,12 +712,26 @@ var ConnectServer = class _ConnectServer {
332
712
  ]
333
713
  };
334
714
  }
335
- const lines = ["Available MCP servers:\n"];
336
- for (const server2 of this.config.servers) {
337
- if (!server2.isActive) continue;
715
+ const activeServers = this.config.servers.filter((s) => s.isActive);
716
+ let sorted;
717
+ const scores = /* @__PURE__ */ new Map();
718
+ if (context) {
719
+ for (const server2 of activeServers) {
720
+ const connection = this.connections.get(server2.namespace);
721
+ const tools = connection?.tools ?? [];
722
+ scores.set(server2.namespace, scoreRelevance(context, server2, tools));
723
+ }
724
+ sorted = [...activeServers].sort((a, b) => (scores.get(b.namespace) ?? 0) - (scores.get(a.namespace) ?? 0));
725
+ } else {
726
+ sorted = activeServers;
727
+ }
728
+ const lines = [context ? "Servers ranked by relevance:\n" : "Available MCP servers:\n"];
729
+ for (const server2 of sorted) {
338
730
  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 + ")");
731
+ const status = connection ? connection.status === "error" ? "ERROR (disconnected, will auto-reconnect on use)" : "ACTIVE (" + connection.tools.length + " tools)" : "available";
732
+ const score = scores.get(server2.namespace);
733
+ const relevance = score && score > 0 ? " (relevance: " + score + ")" : "";
734
+ lines.push(" " + server2.namespace + " \u2014 " + server2.name + " [" + status + "] (" + server2.type + ")" + relevance);
341
735
  }
342
736
  const inactive = this.config.servers.filter((s) => !s.isActive);
343
737
  if (inactive.length > 0) {
@@ -386,11 +780,11 @@ var ConnectServer = class _ConnectServer {
386
780
  };
387
781
  }
388
782
  try {
389
- const connection = await connectToUpstream(serverConfig);
783
+ const connection = await connectToUpstream(serverConfig, (ns) => this.handleUpstreamDisconnect(ns));
390
784
  this.connections.set(namespace, connection);
391
785
  this.idleCallCounts.set(namespace, 0);
392
- this.toolRoutes = buildToolRoutes(this.connections);
393
- await this.server.sendToolListChanged();
786
+ this.rebuildRoutes();
787
+ await this.notifyAllListsChanged();
394
788
  const toolNames = connection.tools.map((t) => t.namespacedName).join(", ");
395
789
  return {
396
790
  content: [
@@ -425,8 +819,8 @@ var ConnectServer = class _ConnectServer {
425
819
  await disconnectFromUpstream(connection);
426
820
  this.connections.delete(namespace);
427
821
  this.idleCallCounts.delete(namespace);
428
- this.toolRoutes = buildToolRoutes(this.connections);
429
- await this.server.sendToolListChanged();
822
+ this.rebuildRoutes();
823
+ await this.notifyAllListsChanged();
430
824
  return {
431
825
  content: [{ type: "text", text: 'Deactivated "' + namespace + '". Tools removed.' }]
432
826
  };
@@ -454,8 +848,8 @@ var ConnectServer = class _ConnectServer {
454
848
  }
455
849
  }
456
850
  if (toDeactivate.length > 0) {
457
- this.toolRoutes = buildToolRoutes(this.connections);
458
- await this.server.sendToolListChanged();
851
+ this.rebuildRoutes();
852
+ await this.notifyAllListsChanged();
459
853
  }
460
854
  }
461
855
  async fetchAndApplyConfig() {
@@ -476,6 +870,7 @@ var ConnectServer = class _ConnectServer {
476
870
  log("info", "Server removed or disabled in config, deactivating", { namespace });
477
871
  await disconnectFromUpstream(connection);
478
872
  this.connections.delete(namespace);
873
+ this.idleCallCounts.delete(namespace);
479
874
  changed = true;
480
875
  continue;
481
876
  }
@@ -484,12 +879,13 @@ var ConnectServer = class _ConnectServer {
484
879
  log("info", "Server config changed, deactivating stale connection", { namespace });
485
880
  await disconnectFromUpstream(connection);
486
881
  this.connections.delete(namespace);
882
+ this.idleCallCounts.delete(namespace);
487
883
  changed = true;
488
884
  }
489
885
  }
490
886
  if (changed) {
491
- this.toolRoutes = buildToolRoutes(this.connections);
492
- await this.server.sendToolListChanged();
887
+ this.rebuildRoutes();
888
+ await this.notifyAllListsChanged();
493
889
  }
494
890
  }
495
891
  startPolling() {
@@ -504,12 +900,112 @@ var ConnectServer = class _ConnectServer {
504
900
  this.pollTimer.unref();
505
901
  }
506
902
  }
903
+ async handleImport(filepath) {
904
+ if (!filepath) {
905
+ return { content: [{ type: "text", text: "filepath is required." }], isError: true };
906
+ }
907
+ const ALLOWED_FILENAMES = ["claude_desktop_config.json", "mcp.json", "settings.json", "mcp_config.json"];
908
+ const basename = filepath.split(/[/\\]/).pop() || "";
909
+ if (!ALLOWED_FILENAMES.includes(basename)) {
910
+ return {
911
+ content: [
912
+ {
913
+ type: "text",
914
+ text: "Only MCP config files are allowed: " + ALLOWED_FILENAMES.join(", ") + ". Got: " + basename
915
+ }
916
+ ],
917
+ isError: true
918
+ };
919
+ }
920
+ try {
921
+ const resolved = filepath.startsWith("~/") ? resolve(homedir(), filepath.slice(2)) : resolve(filepath);
922
+ const raw = await readFile(resolved, "utf-8");
923
+ const parsed = JSON.parse(raw);
924
+ if (!parsed.mcpServers || typeof parsed.mcpServers !== "object") {
925
+ return {
926
+ content: [{ type: "text", text: "No mcpServers object found in " + resolved }],
927
+ isError: true
928
+ };
929
+ }
930
+ const mcpServers = parsed.mcpServers;
931
+ if (typeof mcpServers !== "object" || Array.isArray(mcpServers)) {
932
+ return { content: [{ type: "text", text: "No mcpServers object found in " + resolved }], isError: true };
933
+ }
934
+ const servers = [];
935
+ for (const [key, value] of Object.entries(mcpServers)) {
936
+ if (!value || typeof value !== "object") continue;
937
+ if (key === "mcp-connect") continue;
938
+ const namespace = key.toLowerCase().replace(/[^a-z0-9]/g, "_").replace(/^_+|_+$/g, "").slice(0, 30);
939
+ if (!namespace) continue;
940
+ const entry = {
941
+ name: key,
942
+ namespace,
943
+ type: value.url ? "remote" : "local"
944
+ };
945
+ if (value.command) entry.command = value.command;
946
+ if (value.args) entry.args = value.args;
947
+ if (value.url) entry.url = value.url;
948
+ servers.push(entry);
949
+ }
950
+ if (servers.length === 0) {
951
+ return { content: [{ type: "text", text: "No servers found in " + resolved }], isError: true };
952
+ }
953
+ const res = await request3(this.apiUrl.replace(/\/$/, "") + "/api/connect/import", {
954
+ method: "POST",
955
+ headers: {
956
+ Authorization: "Bearer " + this.token,
957
+ "Content-Type": "application/json"
958
+ },
959
+ body: JSON.stringify({ servers }),
960
+ headersTimeout: 15e3,
961
+ bodyTimeout: 15e3
962
+ });
963
+ const body = await res.body.json();
964
+ if (res.statusCode >= 400) {
965
+ return {
966
+ content: [{ type: "text", text: "Import failed: " + (body.error || "HTTP " + res.statusCode) }],
967
+ isError: true
968
+ };
969
+ }
970
+ await this.fetchAndApplyConfig().catch(() => {
971
+ });
972
+ return {
973
+ content: [
974
+ {
975
+ type: "text",
976
+ text: "Imported " + (body.imported || 0) + " servers" + (body.skipped ? ", " + body.skipped + " skipped (already exist)" : "") + " from " + resolved + ". Note: environment variables (API keys, tokens) were NOT imported for security \u2014 set them at mcp.hosting. Use mcp_connect_discover to see imported servers."
977
+ }
978
+ ]
979
+ };
980
+ } catch (err) {
981
+ return { content: [{ type: "text", text: "Import error: " + err.message }], isError: true };
982
+ }
983
+ }
984
+ handleHealth() {
985
+ if (this.connections.size === 0) {
986
+ return { content: [{ type: "text", text: "No active connections." }] };
987
+ }
988
+ const lines = ["Connection health:\n"];
989
+ for (const [namespace, conn] of this.connections) {
990
+ const h = conn.health;
991
+ const avgLatency = h.totalCalls > 0 ? Math.round(h.totalLatencyMs / h.totalCalls) : 0;
992
+ const errorRate = h.totalCalls > 0 ? Math.round(h.errorCount / h.totalCalls * 100) : 0;
993
+ lines.push(" " + namespace + " [" + conn.status + "]");
994
+ lines.push(" calls: " + h.totalCalls + ", errors: " + h.errorCount + " (" + errorRate + "%)");
995
+ lines.push(" avg latency: " + avgLatency + "ms");
996
+ if (h.lastErrorMessage) {
997
+ lines.push(" last error: " + h.lastErrorMessage + " at " + h.lastErrorAt);
998
+ }
999
+ }
1000
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1001
+ }
507
1002
  async shutdown() {
508
1003
  log("info", "Shutting down mcp-connect");
509
1004
  if (this.pollTimer) {
510
1005
  clearInterval(this.pollTimer);
511
1006
  this.pollTimer = null;
512
1007
  }
1008
+ await shutdownAnalytics();
513
1009
  const disconnects = Array.from(this.connections.values()).map((conn) => disconnectFromUpstream(conn));
514
1010
  await Promise.allSettled(disconnects);
515
1011
  this.connections.clear();
@@ -519,15 +1015,15 @@ var ConnectServer = class _ConnectServer {
519
1015
  };
520
1016
 
521
1017
  // 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) {
1018
+ var token2 = process.env.MCP_HOSTING_TOKEN;
1019
+ var apiUrl2 = process.env.MCP_HOSTING_URL ?? "https://mcp.hosting";
1020
+ if (!token2) {
525
1021
  process.stderr.write(
526
1022
  '\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
1023
  );
528
1024
  process.exit(1);
529
1025
  }
530
- var server = new ConnectServer(apiUrl, token);
1026
+ var server = new ConnectServer(apiUrl2, token2);
531
1027
  var shutdown = async () => {
532
1028
  await server.shutdown();
533
1029
  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.1",
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)",