@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.
- package/dist/index.js +539 -43
- 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(
|
|
14
|
-
const url =
|
|
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 " +
|
|
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 {
|
|
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
|
-
|
|
221
|
-
const timeoutPromise = new Promise(
|
|
222
|
-
|
|
223
|
-
);
|
|
224
|
-
|
|
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
|
-
|
|
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(
|
|
262
|
-
this.apiUrl =
|
|
263
|
-
this.token =
|
|
530
|
+
constructor(apiUrl3, token3) {
|
|
531
|
+
this.apiUrl = apiUrl3;
|
|
532
|
+
this.token = token3;
|
|
264
533
|
this.server = new Server(
|
|
265
|
-
{ name: "mcp-connect", version: "0.
|
|
266
|
-
{
|
|
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 (
|
|
285
|
-
const { name, arguments: args } =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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.
|
|
393
|
-
await this.
|
|
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.
|
|
429
|
-
await this.
|
|
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.
|
|
458
|
-
await this.
|
|
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.
|
|
492
|
-
await this.
|
|
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
|
|
523
|
-
var
|
|
524
|
-
if (!
|
|
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(
|
|
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