@yawlabs/mcp-connect 0.1.2 → 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.
- package/dist/index.js +502 -38
- 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,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 {
|
|
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
|
-
|
|
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(
|
|
262
|
-
this.apiUrl =
|
|
263
|
-
this.token =
|
|
519
|
+
constructor(apiUrl3, token3) {
|
|
520
|
+
this.apiUrl = apiUrl3;
|
|
521
|
+
this.token = token3;
|
|
264
522
|
this.server = new Server(
|
|
265
|
-
{ name: "mcp-connect", version: "0.
|
|
266
|
-
{
|
|
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 (
|
|
285
|
-
const { name, arguments: args } =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
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.
|
|
393
|
-
await this.
|
|
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.
|
|
429
|
-
await this.
|
|
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.
|
|
458
|
-
await this.
|
|
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.
|
|
492
|
-
await this.
|
|
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
|
|
523
|
-
var
|
|
524
|
-
if (!
|
|
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(
|
|
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