@yawlabs/mcp-connect 0.1.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 +508 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config.ts
|
|
4
|
+
import { request } from "undici";
|
|
5
|
+
|
|
6
|
+
// src/logger.ts
|
|
7
|
+
function log(level, msg, data) {
|
|
8
|
+
const entry = JSON.stringify({ level, msg, ts: (/* @__PURE__ */ new Date()).toISOString(), ...data });
|
|
9
|
+
process.stderr.write(entry + "\n");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
async function fetchConfig(apiUrl2, token2) {
|
|
14
|
+
const url = apiUrl2.replace(/\/$/, "") + "/api/connect/config";
|
|
15
|
+
const res = await request(url, {
|
|
16
|
+
method: "GET",
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: "Bearer " + token2,
|
|
19
|
+
Accept: "application/json"
|
|
20
|
+
},
|
|
21
|
+
headersTimeout: 1e4,
|
|
22
|
+
bodyTimeout: 1e4
|
|
23
|
+
});
|
|
24
|
+
if (res.statusCode === 401) {
|
|
25
|
+
throw new ConfigError("Invalid MCP_HOSTING_TOKEN \u2014 check your token at mcp.hosting", true);
|
|
26
|
+
}
|
|
27
|
+
if (res.statusCode === 403) {
|
|
28
|
+
throw new ConfigError("Access denied \u2014 your token may have expired", true);
|
|
29
|
+
}
|
|
30
|
+
if (res.statusCode !== 200) {
|
|
31
|
+
const body = await res.body.text().catch(() => "");
|
|
32
|
+
throw new ConfigError("Config fetch failed (HTTP " + res.statusCode + "): " + body, false);
|
|
33
|
+
}
|
|
34
|
+
const data = await res.body.json();
|
|
35
|
+
if (!data.servers || !Array.isArray(data.servers)) {
|
|
36
|
+
throw new ConfigError("Invalid config response from server", false);
|
|
37
|
+
}
|
|
38
|
+
log("info", "Config loaded", { serverCount: data.servers.length, version: data.configVersion });
|
|
39
|
+
return data;
|
|
40
|
+
}
|
|
41
|
+
var ConfigError = class extends Error {
|
|
42
|
+
constructor(message, fatal) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.fatal = fatal;
|
|
45
|
+
this.name = "ConfigError";
|
|
46
|
+
}
|
|
47
|
+
fatal;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/server.ts
|
|
51
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
52
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
53
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
54
|
+
|
|
55
|
+
// src/meta-tools.ts
|
|
56
|
+
var META_TOOLS = {
|
|
57
|
+
discover: {
|
|
58
|
+
name: "mcp_connect_discover",
|
|
59
|
+
description: "List all available MCP servers configured in your mcp.hosting account. Shows server names, namespaces, types, and whether they are currently active. Call this first to see what servers are available before activating them.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {}
|
|
63
|
+
},
|
|
64
|
+
annotations: {
|
|
65
|
+
title: "Discover MCP Servers",
|
|
66
|
+
readOnlyHint: true,
|
|
67
|
+
destructiveHint: false,
|
|
68
|
+
idempotentHint: true,
|
|
69
|
+
openWorldHint: false
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
activate: {
|
|
73
|
+
name: "mcp_connect_activate",
|
|
74
|
+
description: 'Activate an MCP server by its namespace. This connects to the server and makes its tools available. After activation, the tool list will update with the new tools prefixed by the namespace (e.g., "gh_create_issue" for namespace "gh").',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
server: {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: 'The namespace of the server to activate (e.g., "gh", "slack", "stripe")'
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
required: ["server"]
|
|
84
|
+
},
|
|
85
|
+
annotations: {
|
|
86
|
+
title: "Activate MCP Server",
|
|
87
|
+
readOnlyHint: false,
|
|
88
|
+
destructiveHint: false,
|
|
89
|
+
idempotentHint: true,
|
|
90
|
+
openWorldHint: false
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
deactivate: {
|
|
94
|
+
name: "mcp_connect_deactivate",
|
|
95
|
+
description: "Deactivate an MCP server by its namespace. This disconnects from the server and removes its tools from the available tool list. Use this to free up context when you no longer need a server.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
server: {
|
|
100
|
+
type: "string",
|
|
101
|
+
description: "The namespace of the server to deactivate"
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
required: ["server"]
|
|
105
|
+
},
|
|
106
|
+
annotations: {
|
|
107
|
+
title: "Deactivate MCP Server",
|
|
108
|
+
readOnlyHint: false,
|
|
109
|
+
destructiveHint: false,
|
|
110
|
+
idempotentHint: true,
|
|
111
|
+
openWorldHint: false
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var META_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
116
|
+
META_TOOLS.discover.name,
|
|
117
|
+
META_TOOLS.activate.name,
|
|
118
|
+
META_TOOLS.deactivate.name
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
// src/proxy.ts
|
|
122
|
+
function buildToolList(activeConnections) {
|
|
123
|
+
const tools = [];
|
|
124
|
+
for (const meta of Object.values(META_TOOLS)) {
|
|
125
|
+
tools.push({
|
|
126
|
+
name: meta.name,
|
|
127
|
+
description: meta.description,
|
|
128
|
+
inputSchema: meta.inputSchema,
|
|
129
|
+
annotations: meta.annotations
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
for (const conn of activeConnections.values()) {
|
|
133
|
+
for (const tool of conn.tools) {
|
|
134
|
+
tools.push({
|
|
135
|
+
name: tool.namespacedName,
|
|
136
|
+
description: tool.description,
|
|
137
|
+
inputSchema: tool.inputSchema,
|
|
138
|
+
annotations: tool.annotations
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return tools;
|
|
143
|
+
}
|
|
144
|
+
function buildToolRoutes(activeConnections) {
|
|
145
|
+
const routes = /* @__PURE__ */ new Map();
|
|
146
|
+
for (const conn of activeConnections.values()) {
|
|
147
|
+
for (const tool of conn.tools) {
|
|
148
|
+
routes.set(tool.namespacedName, {
|
|
149
|
+
namespace: conn.config.namespace,
|
|
150
|
+
originalName: tool.name
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return routes;
|
|
155
|
+
}
|
|
156
|
+
async function routeToolCall(toolName, args, toolRoutes, activeConnections) {
|
|
157
|
+
const route = toolRoutes.get(toolName);
|
|
158
|
+
if (!route) {
|
|
159
|
+
return {
|
|
160
|
+
content: [
|
|
161
|
+
{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: "Unknown tool: " + toolName + ". Use mcp_connect_discover to see available servers, then mcp_connect_activate to load tools."
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
isError: true
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const connection = activeConnections.get(route.namespace);
|
|
170
|
+
if (!connection || connection.status !== "connected") {
|
|
171
|
+
return {
|
|
172
|
+
content: [
|
|
173
|
+
{
|
|
174
|
+
type: "text",
|
|
175
|
+
text: 'Server "' + route.namespace + '" is no longer connected. Use mcp_connect_activate("' + route.namespace + '") to reconnect.'
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
isError: true
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const result = await connection.client.callTool({
|
|
183
|
+
name: route.originalName,
|
|
184
|
+
arguments: args
|
|
185
|
+
});
|
|
186
|
+
return result;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
log("error", "Tool call failed", { tool: toolName, namespace: route.namespace, error: err.message });
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: "text", text: "Error calling " + toolName + ": " + err.message }],
|
|
191
|
+
isError: true
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/upstream.ts
|
|
197
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
198
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
199
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
200
|
+
var CONNECT_TIMEOUT = 15e3;
|
|
201
|
+
async function connectToUpstream(config) {
|
|
202
|
+
const client = new Client({ name: "mcp-connect", version: "0.1.0" }, { capabilities: {} });
|
|
203
|
+
let transport;
|
|
204
|
+
if (config.type === "local") {
|
|
205
|
+
if (!config.command) {
|
|
206
|
+
throw new Error("command is required for local servers");
|
|
207
|
+
}
|
|
208
|
+
transport = new StdioClientTransport({
|
|
209
|
+
command: config.command,
|
|
210
|
+
args: config.args ?? [],
|
|
211
|
+
env: { ...process.env, ...config.env },
|
|
212
|
+
stderr: "pipe"
|
|
213
|
+
});
|
|
214
|
+
} else {
|
|
215
|
+
if (!config.url) {
|
|
216
|
+
throw new Error("url is required for remote servers");
|
|
217
|
+
}
|
|
218
|
+
transport = new StreamableHTTPClientTransport(new URL(config.url));
|
|
219
|
+
}
|
|
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]);
|
|
225
|
+
log("info", "Connected to upstream", { name: config.name, namespace: config.namespace, type: config.type });
|
|
226
|
+
const tools = await fetchToolsFromUpstream(client, config.namespace);
|
|
227
|
+
return {
|
|
228
|
+
config,
|
|
229
|
+
client,
|
|
230
|
+
transport,
|
|
231
|
+
tools,
|
|
232
|
+
status: "connected"
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
async function disconnectFromUpstream(connection) {
|
|
236
|
+
try {
|
|
237
|
+
await connection.client.close();
|
|
238
|
+
} catch (err) {
|
|
239
|
+
log("warn", "Error disconnecting from upstream", {
|
|
240
|
+
namespace: connection.config.namespace,
|
|
241
|
+
error: err.message
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
connection.status = "disconnected";
|
|
245
|
+
log("info", "Disconnected from upstream", { namespace: connection.config.namespace });
|
|
246
|
+
}
|
|
247
|
+
async function fetchToolsFromUpstream(client, namespace) {
|
|
248
|
+
const result = await client.listTools();
|
|
249
|
+
return (result.tools ?? []).map((tool) => ({
|
|
250
|
+
name: tool.name,
|
|
251
|
+
namespacedName: namespace + "_" + tool.name,
|
|
252
|
+
description: tool.description,
|
|
253
|
+
inputSchema: tool.inputSchema,
|
|
254
|
+
annotations: tool.annotations
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/server.ts
|
|
259
|
+
var POLL_INTERVAL = 6e4;
|
|
260
|
+
var ConnectServer = class {
|
|
261
|
+
constructor(apiUrl2, token2) {
|
|
262
|
+
this.apiUrl = apiUrl2;
|
|
263
|
+
this.token = token2;
|
|
264
|
+
this.server = new Server(
|
|
265
|
+
{ name: "mcp-connect", version: "0.1.0" },
|
|
266
|
+
{ capabilities: { tools: { listChanged: true } } }
|
|
267
|
+
);
|
|
268
|
+
this.setupHandlers();
|
|
269
|
+
}
|
|
270
|
+
apiUrl;
|
|
271
|
+
token;
|
|
272
|
+
server;
|
|
273
|
+
connections = /* @__PURE__ */ new Map();
|
|
274
|
+
config = null;
|
|
275
|
+
configVersion = null;
|
|
276
|
+
pollTimer = null;
|
|
277
|
+
toolRoutes = /* @__PURE__ */ new Map();
|
|
278
|
+
setupHandlers() {
|
|
279
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
280
|
+
tools: buildToolList(this.connections)
|
|
281
|
+
}));
|
|
282
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request2) => {
|
|
283
|
+
const { name, arguments: args } = request2.params;
|
|
284
|
+
return this.handleToolCall(name, args ?? {});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
async start() {
|
|
288
|
+
try {
|
|
289
|
+
await this.fetchAndApplyConfig();
|
|
290
|
+
} catch (err) {
|
|
291
|
+
if (err instanceof ConfigError && err.fatal) {
|
|
292
|
+
throw err;
|
|
293
|
+
}
|
|
294
|
+
log("warn", "Initial config fetch failed, starting with empty config", { error: err.message });
|
|
295
|
+
this.config = { servers: [], configVersion: "" };
|
|
296
|
+
}
|
|
297
|
+
const transport = new StdioServerTransport();
|
|
298
|
+
await this.server.connect(transport);
|
|
299
|
+
this.startPolling();
|
|
300
|
+
log("info", "mcp-connect started", {
|
|
301
|
+
apiUrl: this.apiUrl,
|
|
302
|
+
servers: this.config?.servers.length ?? 0
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
async handleToolCall(name, args) {
|
|
306
|
+
if (name === META_TOOLS.discover.name) {
|
|
307
|
+
return this.handleDiscover();
|
|
308
|
+
}
|
|
309
|
+
if (name === META_TOOLS.activate.name) {
|
|
310
|
+
return this.handleActivate(args.server);
|
|
311
|
+
}
|
|
312
|
+
if (name === META_TOOLS.deactivate.name) {
|
|
313
|
+
return this.handleDeactivate(args.server);
|
|
314
|
+
}
|
|
315
|
+
return routeToolCall(name, args, this.toolRoutes, this.connections);
|
|
316
|
+
}
|
|
317
|
+
handleDiscover() {
|
|
318
|
+
if (!this.config || this.config.servers.length === 0) {
|
|
319
|
+
return {
|
|
320
|
+
content: [
|
|
321
|
+
{
|
|
322
|
+
type: "text",
|
|
323
|
+
text: "No servers configured. Add servers at mcp.hosting to get started."
|
|
324
|
+
}
|
|
325
|
+
]
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
const lines = ["Available MCP servers:\n"];
|
|
329
|
+
for (const server2 of this.config.servers) {
|
|
330
|
+
if (!server2.isActive) continue;
|
|
331
|
+
const connection = this.connections.get(server2.namespace);
|
|
332
|
+
const status = connection ? "ACTIVE (" + connection.tools.length + " tools)" : "available";
|
|
333
|
+
lines.push(" " + server2.namespace + " \u2014 " + server2.name + " [" + status + "] (" + server2.type + ")");
|
|
334
|
+
}
|
|
335
|
+
const inactive = this.config.servers.filter((s) => !s.isActive);
|
|
336
|
+
if (inactive.length > 0) {
|
|
337
|
+
lines.push("\nDisabled servers:");
|
|
338
|
+
for (const server2 of inactive) {
|
|
339
|
+
lines.push(" " + server2.namespace + " \u2014 " + server2.name + " (disabled in dashboard)");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const activeCount = this.connections.size;
|
|
343
|
+
const totalTools = Array.from(this.connections.values()).reduce((sum, c) => sum + c.tools.length, 0);
|
|
344
|
+
lines.push("\n" + activeCount + " active, " + totalTools + " tools loaded.");
|
|
345
|
+
lines.push('Use mcp_connect_activate({ server: "namespace" }) to activate a server.');
|
|
346
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
347
|
+
}
|
|
348
|
+
async handleActivate(namespace) {
|
|
349
|
+
if (!namespace) {
|
|
350
|
+
return {
|
|
351
|
+
content: [
|
|
352
|
+
{ type: "text", text: "server namespace is required. Use mcp_connect_discover to see available servers." }
|
|
353
|
+
],
|
|
354
|
+
isError: true
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
const existing = this.connections.get(namespace);
|
|
358
|
+
if (existing && existing.status === "connected") {
|
|
359
|
+
const toolNames = existing.tools.map((t) => t.namespacedName).join(", ");
|
|
360
|
+
return {
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: 'Server "' + namespace + '" is already active with ' + existing.tools.length + " tools: " + toolNames
|
|
365
|
+
}
|
|
366
|
+
]
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
const serverConfig = this.config?.servers.find((s) => s.namespace === namespace && s.isActive);
|
|
370
|
+
if (!serverConfig) {
|
|
371
|
+
return {
|
|
372
|
+
content: [
|
|
373
|
+
{
|
|
374
|
+
type: "text",
|
|
375
|
+
text: 'Server "' + namespace + '" not found or disabled. Use mcp_connect_discover to see available servers.'
|
|
376
|
+
}
|
|
377
|
+
],
|
|
378
|
+
isError: true
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const connection = await connectToUpstream(serverConfig);
|
|
383
|
+
this.connections.set(namespace, connection);
|
|
384
|
+
this.toolRoutes = buildToolRoutes(this.connections);
|
|
385
|
+
await this.server.sendToolListChanged();
|
|
386
|
+
const toolNames = connection.tools.map((t) => t.namespacedName).join(", ");
|
|
387
|
+
return {
|
|
388
|
+
content: [
|
|
389
|
+
{
|
|
390
|
+
type: "text",
|
|
391
|
+
text: 'Activated "' + namespace + '" \u2014 ' + connection.tools.length + " tools available: " + toolNames
|
|
392
|
+
}
|
|
393
|
+
]
|
|
394
|
+
};
|
|
395
|
+
} catch (err) {
|
|
396
|
+
log("error", "Failed to activate upstream", { namespace, error: err.message });
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: "text", text: 'Failed to activate "' + namespace + '": ' + err.message }],
|
|
399
|
+
isError: true
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async handleDeactivate(namespace) {
|
|
404
|
+
if (!namespace) {
|
|
405
|
+
return {
|
|
406
|
+
content: [{ type: "text", text: "server namespace is required." }],
|
|
407
|
+
isError: true
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
const connection = this.connections.get(namespace);
|
|
411
|
+
if (!connection) {
|
|
412
|
+
return {
|
|
413
|
+
content: [{ type: "text", text: 'Server "' + namespace + '" is not active.' }],
|
|
414
|
+
isError: true
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
await disconnectFromUpstream(connection);
|
|
418
|
+
this.connections.delete(namespace);
|
|
419
|
+
this.toolRoutes = buildToolRoutes(this.connections);
|
|
420
|
+
await this.server.sendToolListChanged();
|
|
421
|
+
return {
|
|
422
|
+
content: [{ type: "text", text: 'Deactivated "' + namespace + '". Tools removed.' }]
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
async fetchAndApplyConfig() {
|
|
426
|
+
const newConfig = await fetchConfig(this.apiUrl, this.token);
|
|
427
|
+
if (newConfig.configVersion === this.configVersion) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
await this.reconcileConfig(newConfig);
|
|
431
|
+
this.config = newConfig;
|
|
432
|
+
this.configVersion = newConfig.configVersion;
|
|
433
|
+
}
|
|
434
|
+
async reconcileConfig(newConfig) {
|
|
435
|
+
const newNamespaces = new Set(newConfig.servers.map((s) => s.namespace));
|
|
436
|
+
let changed = false;
|
|
437
|
+
for (const [namespace, connection] of this.connections) {
|
|
438
|
+
const newServerConfig = newConfig.servers.find((s) => s.namespace === namespace);
|
|
439
|
+
if (!newServerConfig || !newServerConfig.isActive) {
|
|
440
|
+
log("info", "Server removed or disabled in config, deactivating", { namespace });
|
|
441
|
+
await disconnectFromUpstream(connection);
|
|
442
|
+
this.connections.delete(namespace);
|
|
443
|
+
changed = true;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const oldConfig = connection.config;
|
|
447
|
+
if (oldConfig.command !== newServerConfig.command || JSON.stringify(oldConfig.args) !== JSON.stringify(newServerConfig.args) || oldConfig.url !== newServerConfig.url || JSON.stringify(oldConfig.env) !== JSON.stringify(newServerConfig.env)) {
|
|
448
|
+
log("info", "Server config changed, deactivating stale connection", { namespace });
|
|
449
|
+
await disconnectFromUpstream(connection);
|
|
450
|
+
this.connections.delete(namespace);
|
|
451
|
+
changed = true;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (changed) {
|
|
455
|
+
this.toolRoutes = buildToolRoutes(this.connections);
|
|
456
|
+
await this.server.sendToolListChanged();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
startPolling() {
|
|
460
|
+
this.pollTimer = setInterval(async () => {
|
|
461
|
+
try {
|
|
462
|
+
await this.fetchAndApplyConfig();
|
|
463
|
+
} catch (err) {
|
|
464
|
+
log("warn", "Config poll failed", { error: err.message });
|
|
465
|
+
}
|
|
466
|
+
}, POLL_INTERVAL);
|
|
467
|
+
if (this.pollTimer.unref) {
|
|
468
|
+
this.pollTimer.unref();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async shutdown() {
|
|
472
|
+
log("info", "Shutting down mcp-connect");
|
|
473
|
+
if (this.pollTimer) {
|
|
474
|
+
clearInterval(this.pollTimer);
|
|
475
|
+
this.pollTimer = null;
|
|
476
|
+
}
|
|
477
|
+
const disconnects = Array.from(this.connections.values()).map((conn) => disconnectFromUpstream(conn));
|
|
478
|
+
await Promise.allSettled(disconnects);
|
|
479
|
+
this.connections.clear();
|
|
480
|
+
await this.server.close();
|
|
481
|
+
log("info", "mcp-connect shutdown complete");
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
// src/index.ts
|
|
486
|
+
var token = process.env.MCP_HOSTING_TOKEN;
|
|
487
|
+
var apiUrl = process.env.MCP_HOSTING_URL ?? "https://mcp.hosting";
|
|
488
|
+
if (!token) {
|
|
489
|
+
process.stderr.write(
|
|
490
|
+
'\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'
|
|
491
|
+
);
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
var server = new ConnectServer(apiUrl, token);
|
|
495
|
+
var shutdown = async () => {
|
|
496
|
+
await server.shutdown();
|
|
497
|
+
process.exit(0);
|
|
498
|
+
};
|
|
499
|
+
process.on("SIGTERM", shutdown);
|
|
500
|
+
process.on("SIGINT", shutdown);
|
|
501
|
+
server.start().catch((err) => {
|
|
502
|
+
if (err instanceof ConfigError && err.fatal) {
|
|
503
|
+
process.stderr.write("\n mcp-connect: " + err.message + "\n\n");
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
log("error", "Fatal startup error", { error: err.message });
|
|
507
|
+
process.exit(1);
|
|
508
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yawlabs/mcp-connect",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP orchestrator — one install, all your MCP servers, managed from the cloud",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Yaw Labs <contact@yaw.sh> (https://yaw.sh)",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcp-connect": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"!dist/**/*.test.*",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"lint": "biome check src/",
|
|
22
|
+
"lint:fix": "biome check --write src/",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test:ci": "npm run build && npm test",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"start": "node dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
30
|
+
"undici": "^7.8.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@biomejs/biome": "^1.9.4",
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
35
|
+
"tsup": "^8.4.0",
|
|
36
|
+
"typescript": "^5.8.3",
|
|
37
|
+
"vitest": "^3.1.1"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"mcp",
|
|
44
|
+
"model-context-protocol",
|
|
45
|
+
"orchestrator",
|
|
46
|
+
"mcp-server",
|
|
47
|
+
"ai",
|
|
48
|
+
"mcp-connect",
|
|
49
|
+
"mcp-hosting"
|
|
50
|
+
],
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/YawLabs/mcp-connect.git"
|
|
54
|
+
},
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/YawLabs/mcp-connect/issues"
|
|
57
|
+
},
|
|
58
|
+
"homepage": "https://mcp.hosting"
|
|
59
|
+
}
|