mcp-lazy 0.1.4
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/LICENSE +21 -0
- package/README.md +187 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1037 -0
- package/dist/index.js.map +1 -0
- package/docs/README.ko.md +173 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/utils/config.ts
|
|
13
|
+
var config_exports = {};
|
|
14
|
+
__export(config_exports, {
|
|
15
|
+
computeServerFingerprint: () => computeServerFingerprint,
|
|
16
|
+
convertUrlToMcpRemote: () => convertUrlToMcpRemote,
|
|
17
|
+
extractServersFromConfig: () => extractServersFromConfig,
|
|
18
|
+
extractServersFromOpencodeConfig: () => extractServersFromOpencodeConfig,
|
|
19
|
+
extractServersFromToml: () => extractServersFromToml,
|
|
20
|
+
loadServersBackup: () => loadServersBackup,
|
|
21
|
+
loadToolCache: () => loadToolCache,
|
|
22
|
+
saveServersBackup: () => saveServersBackup,
|
|
23
|
+
saveToolCache: () => saveToolCache
|
|
24
|
+
});
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
27
|
+
import { resolve, dirname } from "path";
|
|
28
|
+
import { homedir } from "os";
|
|
29
|
+
import { createHash } from "crypto";
|
|
30
|
+
function getBackupPath() {
|
|
31
|
+
return resolve(homedir(), ".mcp-lazy", "servers.json");
|
|
32
|
+
}
|
|
33
|
+
function saveServersBackup(servers) {
|
|
34
|
+
const existing = loadServersBackup();
|
|
35
|
+
const merged = { ...existing, ...servers };
|
|
36
|
+
delete merged["mcp-lazy"];
|
|
37
|
+
const dir = dirname(getBackupPath());
|
|
38
|
+
if (!existsSync(dir)) {
|
|
39
|
+
mkdirSync(dir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
writeFileSync(getBackupPath(), JSON.stringify({ servers: merged }, null, 2) + "\n");
|
|
42
|
+
}
|
|
43
|
+
function loadServersBackup() {
|
|
44
|
+
if (!existsSync(getBackupPath())) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const raw = JSON.parse(readFileSync(getBackupPath(), "utf-8"));
|
|
49
|
+
return raw.servers ?? {};
|
|
50
|
+
} catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function convertUrlToMcpRemote(url, headers) {
|
|
55
|
+
const args = ["-y", "mcp-remote", url];
|
|
56
|
+
if (headers) {
|
|
57
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
58
|
+
args.push("--header", `${key}:${value}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { command: "npx", args };
|
|
62
|
+
}
|
|
63
|
+
function extractServersFromToml(content) {
|
|
64
|
+
const servers = {};
|
|
65
|
+
const sectionRegex = /^\[mcp_servers\.([^\]]+)\]/gm;
|
|
66
|
+
let match;
|
|
67
|
+
while ((match = sectionRegex.exec(content)) !== null) {
|
|
68
|
+
const name = match[1];
|
|
69
|
+
if (name === "mcp-lazy") continue;
|
|
70
|
+
const sectionStart = match.index + match[0].length;
|
|
71
|
+
const nextSection = /^\[[^\]]+\]/m.exec(content.slice(sectionStart));
|
|
72
|
+
const sectionContent = nextSection ? content.slice(sectionStart, sectionStart + nextSection.index) : content.slice(sectionStart);
|
|
73
|
+
const cmdMatch = /^\s*command\s*=\s*"([^"]+)"/m.exec(sectionContent);
|
|
74
|
+
const command = cmdMatch ? cmdMatch[1] : void 0;
|
|
75
|
+
const urlMatch = /^\s*url\s*=\s*"([^"]*)"/m.exec(sectionContent);
|
|
76
|
+
const serverUrlMatch = /^\s*serverUrl\s*=\s*"([^"]*)"/m.exec(sectionContent);
|
|
77
|
+
const url = urlMatch ? urlMatch[1] : serverUrlMatch ? serverUrlMatch[1] : void 0;
|
|
78
|
+
const argsMatch = /^\s*args\s*=\s*\[([^\]]*)\]/m.exec(sectionContent);
|
|
79
|
+
const args = [];
|
|
80
|
+
if (argsMatch) {
|
|
81
|
+
const inner = argsMatch[1];
|
|
82
|
+
const itemRegex = /"([^"]*)"/g;
|
|
83
|
+
let item;
|
|
84
|
+
while ((item = itemRegex.exec(inner)) !== null) {
|
|
85
|
+
args.push(item[1]);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const env = {};
|
|
89
|
+
const envSectionRegex = new RegExp(`^\\[mcp_servers\\.${name}\\.env\\]`, "m");
|
|
90
|
+
if (envSectionRegex.test(content)) {
|
|
91
|
+
const envStart = content.indexOf(`[mcp_servers.${name}.env]`) + `[mcp_servers.${name}.env]`.length;
|
|
92
|
+
const envEnd = /^\[[^\]]+\]/m.exec(content.slice(envStart));
|
|
93
|
+
const envContent = envEnd ? content.slice(envStart, envStart + envEnd.index) : content.slice(envStart);
|
|
94
|
+
const envPairRegex = /^\s*(\w+)\s*=\s*"([^"]*)"/gm;
|
|
95
|
+
let envMatch;
|
|
96
|
+
while ((envMatch = envPairRegex.exec(envContent)) !== null) {
|
|
97
|
+
env[envMatch[1]] = envMatch[2];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const headers = {};
|
|
101
|
+
const headersSectionRegex = new RegExp(`^\\[mcp_servers\\.${name}\\.http_headers\\]`, "m");
|
|
102
|
+
if (headersSectionRegex.test(content)) {
|
|
103
|
+
const headersStart = content.indexOf(`[mcp_servers.${name}.http_headers]`) + `[mcp_servers.${name}.http_headers]`.length;
|
|
104
|
+
const headersEnd = /^\[[^\]]+\]/m.exec(content.slice(headersStart));
|
|
105
|
+
const headersContent = headersEnd ? content.slice(headersStart, headersStart + headersEnd.index) : content.slice(headersStart);
|
|
106
|
+
const headerPairRegex = /^\s*(\S+)\s*=\s*"([^"]*)"/gm;
|
|
107
|
+
let headerMatch;
|
|
108
|
+
while ((headerMatch = headerPairRegex.exec(headersContent)) !== null) {
|
|
109
|
+
headers[headerMatch[1]] = headerMatch[2];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const bearerMatch = /^\s*bearer_token_env_var\s*=\s*"([^"]*)"/m.exec(sectionContent);
|
|
113
|
+
if (bearerMatch) {
|
|
114
|
+
const envVarName = bearerMatch[1];
|
|
115
|
+
const token = process.env[envVarName];
|
|
116
|
+
if (token) {
|
|
117
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (command) {
|
|
121
|
+
servers[name] = { command, args, ...url && { url }, ...Object.keys(env).length > 0 && { env }, ...Object.keys(headers).length > 0 && { headers } };
|
|
122
|
+
} else if (url) {
|
|
123
|
+
servers[name] = { args, url, ...Object.keys(env).length > 0 && { env }, ...Object.keys(headers).length > 0 && { headers } };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return servers;
|
|
127
|
+
}
|
|
128
|
+
function extractServersFromConfig(configPath) {
|
|
129
|
+
if (!existsSync(configPath)) return {};
|
|
130
|
+
try {
|
|
131
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
132
|
+
const parsed = McpJsonSchema.safeParse(raw);
|
|
133
|
+
if (!parsed.success) return {};
|
|
134
|
+
const servers = { ...parsed.data.mcpServers };
|
|
135
|
+
delete servers["mcp-lazy"];
|
|
136
|
+
for (const config of Object.values(servers)) {
|
|
137
|
+
if (config.serverUrl && !config.url) {
|
|
138
|
+
config.url = config.serverUrl;
|
|
139
|
+
}
|
|
140
|
+
delete config.serverUrl;
|
|
141
|
+
}
|
|
142
|
+
return servers;
|
|
143
|
+
} catch {
|
|
144
|
+
return {};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function getToolCachePath() {
|
|
148
|
+
return resolve(homedir(), ".mcp-lazy", "tool-cache.json");
|
|
149
|
+
}
|
|
150
|
+
function computeServerFingerprint(servers) {
|
|
151
|
+
const parts = Object.keys(servers).sort().map((name) => {
|
|
152
|
+
const s = servers[name];
|
|
153
|
+
return `${name}:${s.command ?? ""}:${(s.args ?? []).join(",")}`;
|
|
154
|
+
});
|
|
155
|
+
return createHash("sha256").update(parts.join("|")).digest("hex");
|
|
156
|
+
}
|
|
157
|
+
function loadToolCache() {
|
|
158
|
+
const cachePath = getToolCachePath();
|
|
159
|
+
if (!existsSync(cachePath)) return null;
|
|
160
|
+
try {
|
|
161
|
+
const raw = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
162
|
+
if (typeof raw.fingerprint === "string" && Array.isArray(raw.tools)) {
|
|
163
|
+
return raw;
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function saveToolCache(fingerprint, tools) {
|
|
171
|
+
const cachePath = getToolCachePath();
|
|
172
|
+
const dir = dirname(cachePath);
|
|
173
|
+
if (!existsSync(dir)) {
|
|
174
|
+
mkdirSync(dir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
writeFileSync(cachePath, JSON.stringify({ fingerprint, tools }, null, 2) + "\n");
|
|
177
|
+
}
|
|
178
|
+
function extractServersFromOpencodeConfig(configPath) {
|
|
179
|
+
if (!existsSync(configPath)) return {};
|
|
180
|
+
try {
|
|
181
|
+
const raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
182
|
+
const mcpSection = raw.mcp;
|
|
183
|
+
if (!mcpSection || typeof mcpSection !== "object") return {};
|
|
184
|
+
const servers = {};
|
|
185
|
+
for (const [name, config] of Object.entries(mcpSection)) {
|
|
186
|
+
if (name === "mcp-lazy") continue;
|
|
187
|
+
const cfg = config;
|
|
188
|
+
if (cfg.type === "local" && Array.isArray(cfg.command) && cfg.command.length > 0) {
|
|
189
|
+
servers[name] = {
|
|
190
|
+
command: cfg.command[0],
|
|
191
|
+
args: cfg.command.slice(1),
|
|
192
|
+
...cfg.environment && { env: cfg.environment }
|
|
193
|
+
};
|
|
194
|
+
} else if (cfg.type === "remote" && cfg.url) {
|
|
195
|
+
servers[name] = {
|
|
196
|
+
url: cfg.url,
|
|
197
|
+
args: [],
|
|
198
|
+
...cfg.headers && { headers: cfg.headers }
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return servers;
|
|
203
|
+
} catch {
|
|
204
|
+
return {};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
var ServerConfigSchema, McpJsonSchema;
|
|
208
|
+
var init_config = __esm({
|
|
209
|
+
"src/utils/config.ts"() {
|
|
210
|
+
"use strict";
|
|
211
|
+
ServerConfigSchema = z.object({
|
|
212
|
+
command: z.string().optional(),
|
|
213
|
+
args: z.array(z.string()).default([]),
|
|
214
|
+
url: z.string().optional(),
|
|
215
|
+
serverUrl: z.string().optional(),
|
|
216
|
+
headers: z.record(z.string()).optional(),
|
|
217
|
+
env: z.record(z.string()).optional(),
|
|
218
|
+
description: z.string().optional()
|
|
219
|
+
});
|
|
220
|
+
McpJsonSchema = z.object({
|
|
221
|
+
mcpServers: z.record(ServerConfigSchema)
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// src/version.ts
|
|
227
|
+
var VERSION;
|
|
228
|
+
var init_version = __esm({
|
|
229
|
+
"src/version.ts"() {
|
|
230
|
+
"use strict";
|
|
231
|
+
VERSION = "0.1.0";
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// src/proxy/registry.ts
|
|
236
|
+
var registry_exports = {};
|
|
237
|
+
__export(registry_exports, {
|
|
238
|
+
ToolRegistry: () => ToolRegistry,
|
|
239
|
+
extractKeywords: () => extractKeywords
|
|
240
|
+
});
|
|
241
|
+
function extractKeywords(name, description) {
|
|
242
|
+
const text = `${name} ${description}`;
|
|
243
|
+
const words = text.replace(/[_-]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
244
|
+
return [...new Set(words)];
|
|
245
|
+
}
|
|
246
|
+
var ToolRegistry;
|
|
247
|
+
var init_registry = __esm({
|
|
248
|
+
"src/proxy/registry.ts"() {
|
|
249
|
+
"use strict";
|
|
250
|
+
ToolRegistry = class {
|
|
251
|
+
tools = [];
|
|
252
|
+
addTool(entry) {
|
|
253
|
+
this.tools.push(entry);
|
|
254
|
+
}
|
|
255
|
+
addTools(entries) {
|
|
256
|
+
this.tools.push(...entries);
|
|
257
|
+
}
|
|
258
|
+
getToolCount() {
|
|
259
|
+
return this.tools.length;
|
|
260
|
+
}
|
|
261
|
+
getAllTools() {
|
|
262
|
+
return [...this.tools];
|
|
263
|
+
}
|
|
264
|
+
getServerNames() {
|
|
265
|
+
return [...new Set(this.tools.map((t) => t.server))];
|
|
266
|
+
}
|
|
267
|
+
getToolsByServer(serverName) {
|
|
268
|
+
return this.tools.filter((t) => t.server === serverName);
|
|
269
|
+
}
|
|
270
|
+
findTool(toolName, serverName) {
|
|
271
|
+
return this.tools.find(
|
|
272
|
+
(t) => t.name === toolName && t.server === serverName
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
search(query, limit = 5) {
|
|
276
|
+
const queryLower = query.toLowerCase();
|
|
277
|
+
const queryTokens = queryLower.split(/\s+/).filter(Boolean);
|
|
278
|
+
const scored = [];
|
|
279
|
+
for (const entry of this.tools) {
|
|
280
|
+
let score = 0;
|
|
281
|
+
const nameLower = entry.name.toLowerCase();
|
|
282
|
+
const descLower = entry.description.toLowerCase();
|
|
283
|
+
const serverDescLower = entry.serverDescription.toLowerCase();
|
|
284
|
+
if (nameLower === queryLower) {
|
|
285
|
+
score += 1;
|
|
286
|
+
} else if (nameLower.includes(queryLower) || queryTokens.some((t) => nameLower.includes(t))) {
|
|
287
|
+
score += 0.8;
|
|
288
|
+
}
|
|
289
|
+
for (const token of queryTokens) {
|
|
290
|
+
if (descLower.includes(token)) {
|
|
291
|
+
score += 0.6;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (const token of queryTokens) {
|
|
295
|
+
if (serverDescLower.includes(token)) {
|
|
296
|
+
score += 0.4;
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (const token of queryTokens) {
|
|
301
|
+
if (entry.keywords.some((k) => k.toLowerCase().includes(token))) {
|
|
302
|
+
score += 0.3;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (score > 0) {
|
|
306
|
+
scored.push({ entry, score });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return scored.sort((a, b) => b.score - a.score).slice(0, limit).map(({ entry, score }) => ({
|
|
310
|
+
tool_name: entry.name,
|
|
311
|
+
server_name: entry.server,
|
|
312
|
+
description: entry.description,
|
|
313
|
+
relevance_score: Math.round(score * 100) / 100
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
clear() {
|
|
317
|
+
this.tools = [];
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// src/proxy/loader.ts
|
|
324
|
+
var loader_exports = {};
|
|
325
|
+
__export(loader_exports, {
|
|
326
|
+
ServerLoader: () => ServerLoader
|
|
327
|
+
});
|
|
328
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
329
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
330
|
+
var ServerLoader;
|
|
331
|
+
var init_loader = __esm({
|
|
332
|
+
"src/proxy/loader.ts"() {
|
|
333
|
+
"use strict";
|
|
334
|
+
init_version();
|
|
335
|
+
ServerLoader = class {
|
|
336
|
+
servers = /* @__PURE__ */ new Map();
|
|
337
|
+
serverConfigs;
|
|
338
|
+
loading = /* @__PURE__ */ new Map();
|
|
339
|
+
constructor(serverConfigs) {
|
|
340
|
+
this.serverConfigs = serverConfigs;
|
|
341
|
+
}
|
|
342
|
+
async getClient(serverName) {
|
|
343
|
+
const existing = this.servers.get(serverName);
|
|
344
|
+
if (existing) {
|
|
345
|
+
return existing.client;
|
|
346
|
+
}
|
|
347
|
+
const pendingLoad = this.loading.get(serverName);
|
|
348
|
+
if (pendingLoad) {
|
|
349
|
+
return pendingLoad;
|
|
350
|
+
}
|
|
351
|
+
const loadPromise = this.loadServer(serverName);
|
|
352
|
+
this.loading.set(serverName, loadPromise);
|
|
353
|
+
try {
|
|
354
|
+
const client = await loadPromise;
|
|
355
|
+
return client;
|
|
356
|
+
} finally {
|
|
357
|
+
this.loading.delete(serverName);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async loadServer(serverName) {
|
|
361
|
+
const config = this.serverConfigs[serverName];
|
|
362
|
+
if (!config) {
|
|
363
|
+
throw new Error(`Unknown server: ${serverName}`);
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
return await this.attemptConnect(serverName, config);
|
|
367
|
+
} catch (error) {
|
|
368
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
369
|
+
if (message.includes("timed out")) {
|
|
370
|
+
try {
|
|
371
|
+
return await this.attemptConnect(serverName, config);
|
|
372
|
+
} catch (retryError) {
|
|
373
|
+
throw retryError;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
throw error;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
async attemptConnect(serverName, config) {
|
|
380
|
+
const timeoutMs = 3e4;
|
|
381
|
+
if (!config.command) {
|
|
382
|
+
throw new Error(`Server ${serverName} has no command configured`);
|
|
383
|
+
}
|
|
384
|
+
const client = new Client({
|
|
385
|
+
name: `mcp-lazy-proxy/${serverName}`,
|
|
386
|
+
version: VERSION
|
|
387
|
+
});
|
|
388
|
+
const env = { ...process.env, ...config.env };
|
|
389
|
+
const transport = new StdioClientTransport({
|
|
390
|
+
command: config.command,
|
|
391
|
+
args: config.args,
|
|
392
|
+
env
|
|
393
|
+
});
|
|
394
|
+
const connectPromise = client.connect(transport);
|
|
395
|
+
const timeoutPromise = new Promise(
|
|
396
|
+
(_, reject) => setTimeout(() => reject(new Error(`Server ${serverName} timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
397
|
+
);
|
|
398
|
+
await Promise.race([connectPromise, timeoutPromise]);
|
|
399
|
+
this.servers.set(serverName, {
|
|
400
|
+
client,
|
|
401
|
+
transport,
|
|
402
|
+
loadedAt: /* @__PURE__ */ new Date()
|
|
403
|
+
});
|
|
404
|
+
return client;
|
|
405
|
+
}
|
|
406
|
+
getLoadedServers() {
|
|
407
|
+
return [...this.servers.keys()];
|
|
408
|
+
}
|
|
409
|
+
isLoaded(serverName) {
|
|
410
|
+
return this.servers.has(serverName);
|
|
411
|
+
}
|
|
412
|
+
async closeServer(serverName) {
|
|
413
|
+
const server = this.servers.get(serverName);
|
|
414
|
+
if (server) {
|
|
415
|
+
await server.client.close();
|
|
416
|
+
this.servers.delete(serverName);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
async closeAll() {
|
|
420
|
+
const closePromises = [...this.servers.keys()].map(
|
|
421
|
+
(name) => this.closeServer(name)
|
|
422
|
+
);
|
|
423
|
+
await Promise.allSettled(closePromises);
|
|
424
|
+
}
|
|
425
|
+
hasConfig(serverName) {
|
|
426
|
+
return serverName in this.serverConfigs;
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// src/proxy/server.ts
|
|
433
|
+
var server_exports = {};
|
|
434
|
+
__export(server_exports, {
|
|
435
|
+
createProxyServer: () => createProxyServer,
|
|
436
|
+
startProxyServer: () => startProxyServer
|
|
437
|
+
});
|
|
438
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
439
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
440
|
+
import { z as z2 } from "zod";
|
|
441
|
+
async function createProxyServer(registry, loader) {
|
|
442
|
+
const server = new McpServer({
|
|
443
|
+
name: "mcp-lazy",
|
|
444
|
+
version: VERSION
|
|
445
|
+
});
|
|
446
|
+
server.tool(
|
|
447
|
+
"mcp_search_tools",
|
|
448
|
+
`Search available MCP tools by keyword.
|
|
449
|
+
Use this BEFORE calling any MCP tool.
|
|
450
|
+
Returns matching tool names, server names, and descriptions.
|
|
451
|
+
Example: mcp_search_tools("query database") \u2192 postgres-mcp.query_database`,
|
|
452
|
+
{
|
|
453
|
+
query: z2.string().describe("What you want to do in natural language"),
|
|
454
|
+
limit: z2.number().optional().default(5).describe("Max results to return (default: 5)")
|
|
455
|
+
},
|
|
456
|
+
async ({ query, limit }) => {
|
|
457
|
+
const results = registry.search(query, limit);
|
|
458
|
+
if (results.length === 0) {
|
|
459
|
+
const allServers = registry.getServerNames();
|
|
460
|
+
return {
|
|
461
|
+
content: [
|
|
462
|
+
{
|
|
463
|
+
type: "text",
|
|
464
|
+
text: JSON.stringify({
|
|
465
|
+
results: [],
|
|
466
|
+
suggestion: `No tools found for "${query}". Available servers: ${allServers.join(", ")}. Try different keywords.`
|
|
467
|
+
})
|
|
468
|
+
}
|
|
469
|
+
]
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return {
|
|
473
|
+
content: [
|
|
474
|
+
{
|
|
475
|
+
type: "text",
|
|
476
|
+
text: JSON.stringify({ results })
|
|
477
|
+
}
|
|
478
|
+
]
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
);
|
|
482
|
+
server.tool(
|
|
483
|
+
"mcp_execute_tool",
|
|
484
|
+
`Execute a specific MCP tool.
|
|
485
|
+
Use tool_name and server_name from mcp_search_tools results.`,
|
|
486
|
+
{
|
|
487
|
+
tool_name: z2.string().describe("Tool name from mcp_search_tools"),
|
|
488
|
+
server_name: z2.string().describe("Server name from mcp_search_tools"),
|
|
489
|
+
arguments: z2.record(z2.unknown()).optional().describe("Tool arguments")
|
|
490
|
+
},
|
|
491
|
+
async ({ tool_name, server_name, arguments: args }) => {
|
|
492
|
+
const tool = registry.findTool(tool_name, server_name);
|
|
493
|
+
if (!tool) {
|
|
494
|
+
return {
|
|
495
|
+
content: [
|
|
496
|
+
{
|
|
497
|
+
type: "text",
|
|
498
|
+
text: JSON.stringify({
|
|
499
|
+
error: `Tool "${tool_name}" not found in server "${server_name}". Use mcp_search_tools first.`
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
],
|
|
503
|
+
isError: true
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (!loader.hasConfig(server_name)) {
|
|
507
|
+
return {
|
|
508
|
+
content: [
|
|
509
|
+
{
|
|
510
|
+
type: "text",
|
|
511
|
+
text: JSON.stringify({
|
|
512
|
+
error: `Server "${server_name}" is not configured.`
|
|
513
|
+
})
|
|
514
|
+
}
|
|
515
|
+
],
|
|
516
|
+
isError: true
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
const client = await loader.getClient(server_name);
|
|
521
|
+
const result = await client.callTool({
|
|
522
|
+
name: tool_name,
|
|
523
|
+
arguments: args ?? {}
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
content: [
|
|
527
|
+
{
|
|
528
|
+
type: "text",
|
|
529
|
+
text: JSON.stringify(result)
|
|
530
|
+
}
|
|
531
|
+
]
|
|
532
|
+
};
|
|
533
|
+
} catch (error) {
|
|
534
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
535
|
+
const alternatives = registry.search(tool_name, 3);
|
|
536
|
+
return {
|
|
537
|
+
content: [
|
|
538
|
+
{
|
|
539
|
+
type: "text",
|
|
540
|
+
text: JSON.stringify({
|
|
541
|
+
error: `Failed to execute ${tool_name} on ${server_name}: ${message}`,
|
|
542
|
+
alternatives: alternatives.length > 0 ? alternatives : void 0
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
],
|
|
546
|
+
isError: true
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
);
|
|
551
|
+
return server;
|
|
552
|
+
}
|
|
553
|
+
async function startProxyServer(registry, loader) {
|
|
554
|
+
const server = await createProxyServer(registry, loader);
|
|
555
|
+
const transport = new StdioServerTransport();
|
|
556
|
+
await server.connect(transport);
|
|
557
|
+
process.on("SIGINT", async () => {
|
|
558
|
+
await loader.closeAll();
|
|
559
|
+
process.exit(0);
|
|
560
|
+
});
|
|
561
|
+
process.on("SIGTERM", async () => {
|
|
562
|
+
await loader.closeAll();
|
|
563
|
+
process.exit(0);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
var init_server = __esm({
|
|
567
|
+
"src/proxy/server.ts"() {
|
|
568
|
+
"use strict";
|
|
569
|
+
init_version();
|
|
570
|
+
}
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// src/utils/mcp-client.ts
|
|
574
|
+
var mcp_client_exports = {};
|
|
575
|
+
__export(mcp_client_exports, {
|
|
576
|
+
callServerTool: () => callServerTool,
|
|
577
|
+
connectToServer: () => connectToServer,
|
|
578
|
+
disconnectServer: () => disconnectServer,
|
|
579
|
+
listServerTools: () => listServerTools
|
|
580
|
+
});
|
|
581
|
+
import { Client as Client2 } from "@modelcontextprotocol/sdk/client/index.js";
|
|
582
|
+
import { StdioClientTransport as StdioClientTransport2 } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
583
|
+
async function connectToServer(command, args, env) {
|
|
584
|
+
const client = new Client2({
|
|
585
|
+
name: "mcp-lazy-proxy",
|
|
586
|
+
version: VERSION
|
|
587
|
+
});
|
|
588
|
+
const mergedEnv = { ...process.env, ...env };
|
|
589
|
+
const transport = new StdioClientTransport2({
|
|
590
|
+
command,
|
|
591
|
+
args,
|
|
592
|
+
env: mergedEnv
|
|
593
|
+
});
|
|
594
|
+
await client.connect(transport);
|
|
595
|
+
return { client, transport };
|
|
596
|
+
}
|
|
597
|
+
async function listServerTools(client) {
|
|
598
|
+
const allTools = [];
|
|
599
|
+
let cursor;
|
|
600
|
+
do {
|
|
601
|
+
const result = await client.listTools({ cursor });
|
|
602
|
+
for (const tool of result.tools) {
|
|
603
|
+
allTools.push({
|
|
604
|
+
name: tool.name,
|
|
605
|
+
description: tool.description,
|
|
606
|
+
inputSchema: tool.inputSchema
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
cursor = result.nextCursor;
|
|
610
|
+
} while (cursor);
|
|
611
|
+
return allTools;
|
|
612
|
+
}
|
|
613
|
+
async function callServerTool(client, toolName, args) {
|
|
614
|
+
const result = await client.callTool({
|
|
615
|
+
name: toolName,
|
|
616
|
+
arguments: args
|
|
617
|
+
});
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
async function disconnectServer(connection) {
|
|
621
|
+
await connection.client.close();
|
|
622
|
+
}
|
|
623
|
+
var init_mcp_client = __esm({
|
|
624
|
+
"src/utils/mcp-client.ts"() {
|
|
625
|
+
"use strict";
|
|
626
|
+
init_version();
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// src/index.ts
|
|
631
|
+
import { Command } from "commander";
|
|
632
|
+
|
|
633
|
+
// src/agents/index.ts
|
|
634
|
+
init_config();
|
|
635
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
636
|
+
import { resolve as resolve2, dirname as dirname2 } from "path";
|
|
637
|
+
import { homedir as homedir2 } from "os";
|
|
638
|
+
function resolvePath(p) {
|
|
639
|
+
const home = homedir2();
|
|
640
|
+
const cwd = process.cwd();
|
|
641
|
+
return p.startsWith("~") ? resolve2(home, p.slice(2)) : resolve2(cwd, p);
|
|
642
|
+
}
|
|
643
|
+
var AGENTS = [
|
|
644
|
+
{
|
|
645
|
+
name: "cursor",
|
|
646
|
+
displayName: "Cursor",
|
|
647
|
+
configPaths: ["~/.cursor/mcp.json"]
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
name: "opencode",
|
|
651
|
+
displayName: "Opencode",
|
|
652
|
+
configPaths: ["~/.config/opencode/config.json"],
|
|
653
|
+
format: "opencode"
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
name: "antigravity",
|
|
657
|
+
displayName: "Antigravity",
|
|
658
|
+
configPaths: ["~/.gemini/antigravity/mcp_config.json"]
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: "codex",
|
|
662
|
+
displayName: "Codex",
|
|
663
|
+
configPaths: ["~/.codex/config.toml"],
|
|
664
|
+
format: "toml"
|
|
665
|
+
}
|
|
666
|
+
];
|
|
667
|
+
function getAgentByName(name) {
|
|
668
|
+
return AGENTS.find((a) => a.name === name);
|
|
669
|
+
}
|
|
670
|
+
function findAgentConfig(agent) {
|
|
671
|
+
for (const p of agent.configPaths) {
|
|
672
|
+
const resolved = resolvePath(p);
|
|
673
|
+
if (existsSync2(resolved)) {
|
|
674
|
+
return resolved;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
function generateProxyEntry() {
|
|
680
|
+
return {
|
|
681
|
+
command: "npx",
|
|
682
|
+
args: ["-y", "mcp-lazy", "serve"]
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
function registerProxy(agent) {
|
|
686
|
+
const existing = findAgentConfig(agent);
|
|
687
|
+
const fallback = agent.configPaths[0];
|
|
688
|
+
let targetPath = existing ?? resolvePath(fallback);
|
|
689
|
+
let created = false;
|
|
690
|
+
let serverCount = 0;
|
|
691
|
+
if (agent.format === "opencode") {
|
|
692
|
+
if (existsSync2(targetPath)) {
|
|
693
|
+
try {
|
|
694
|
+
const fullConfig = JSON.parse(readFileSync2(targetPath, "utf-8"));
|
|
695
|
+
const mcpSection = fullConfig.mcp || {};
|
|
696
|
+
const serversToBackup = {};
|
|
697
|
+
for (const [name, cfg] of Object.entries(mcpSection)) {
|
|
698
|
+
if (name === "mcp-lazy") continue;
|
|
699
|
+
const c = cfg;
|
|
700
|
+
if (c.type === "remote" && c.url) {
|
|
701
|
+
serversToBackup[name] = convertUrlToMcpRemote(c.url, c.headers);
|
|
702
|
+
} else if (c.type === "local" && Array.isArray(c.command) && c.command.length > 0) {
|
|
703
|
+
serversToBackup[name] = {
|
|
704
|
+
command: c.command[0],
|
|
705
|
+
args: c.command.slice(1),
|
|
706
|
+
...c.environment && { env: c.environment }
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
serverCount = Object.keys(serversToBackup).length;
|
|
711
|
+
if (serverCount > 0) {
|
|
712
|
+
saveServersBackup(serversToBackup);
|
|
713
|
+
}
|
|
714
|
+
fullConfig.mcp = {
|
|
715
|
+
"mcp-lazy": {
|
|
716
|
+
type: "local",
|
|
717
|
+
command: ["npx", "-y", "mcp-lazy", "serve"]
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
writeFileSync2(targetPath, JSON.stringify(fullConfig, null, 2) + "\n");
|
|
721
|
+
} catch {
|
|
722
|
+
const config = { mcp: { "mcp-lazy": { type: "local", command: ["npx", "-y", "mcp-lazy", "serve"] } } };
|
|
723
|
+
writeFileSync2(targetPath, JSON.stringify(config, null, 2) + "\n");
|
|
724
|
+
}
|
|
725
|
+
} else {
|
|
726
|
+
created = true;
|
|
727
|
+
const dir = dirname2(targetPath);
|
|
728
|
+
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
729
|
+
const config = { mcp: { "mcp-lazy": { type: "local", command: ["npx", "-y", "mcp-lazy", "serve"] } } };
|
|
730
|
+
writeFileSync2(targetPath, JSON.stringify(config, null, 2) + "\n");
|
|
731
|
+
}
|
|
732
|
+
return { configPath: targetPath, created, serverCount };
|
|
733
|
+
}
|
|
734
|
+
if (agent.format === "toml") {
|
|
735
|
+
if (!existsSync2(targetPath)) {
|
|
736
|
+
created = true;
|
|
737
|
+
const dir = dirname2(targetPath);
|
|
738
|
+
if (!existsSync2(dir)) {
|
|
739
|
+
mkdirSync2(dir, { recursive: true });
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const existingContent = existsSync2(targetPath) ? readFileSync2(targetPath, "utf-8") : "";
|
|
743
|
+
const allServers = extractServersFromToml(existingContent);
|
|
744
|
+
const serversToBackup = {};
|
|
745
|
+
for (const [name, cfg] of Object.entries(allServers)) {
|
|
746
|
+
if (cfg.command) {
|
|
747
|
+
serversToBackup[name] = cfg;
|
|
748
|
+
} else if (cfg.url) {
|
|
749
|
+
serversToBackup[name] = convertUrlToMcpRemote(cfg.url, cfg.headers);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
serverCount = Object.keys(serversToBackup).length;
|
|
753
|
+
if (serverCount > 0) {
|
|
754
|
+
saveServersBackup(serversToBackup);
|
|
755
|
+
}
|
|
756
|
+
let cleaned = existingContent.replace(
|
|
757
|
+
/\n?\[mcp_servers\.[^\]]+\](?:\n(?!\[)[^\n]*)*/g,
|
|
758
|
+
""
|
|
759
|
+
).trimEnd();
|
|
760
|
+
const tomlBlock = `
|
|
761
|
+
|
|
762
|
+
[mcp_servers.mcp-lazy]
|
|
763
|
+
command = "npx"
|
|
764
|
+
args = ["-y", "mcp-lazy", "serve"]
|
|
765
|
+
`;
|
|
766
|
+
writeFileSync2(targetPath, cleaned + tomlBlock);
|
|
767
|
+
return { configPath: targetPath, created, serverCount };
|
|
768
|
+
}
|
|
769
|
+
if (existsSync2(targetPath)) {
|
|
770
|
+
let rawConfig = {};
|
|
771
|
+
try {
|
|
772
|
+
rawConfig = JSON.parse(readFileSync2(targetPath, "utf-8"));
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
const rawServers = rawConfig.mcpServers ?? {};
|
|
776
|
+
const serversToBackup = {};
|
|
777
|
+
const normalizedServers = extractServersFromConfig(targetPath);
|
|
778
|
+
for (const [name, serverCfg] of Object.entries(rawServers)) {
|
|
779
|
+
if (name === "mcp-lazy") continue;
|
|
780
|
+
const cfg = serverCfg;
|
|
781
|
+
if (cfg.url || cfg.serverUrl) {
|
|
782
|
+
const url = cfg.url || cfg.serverUrl;
|
|
783
|
+
const headers = cfg.headers;
|
|
784
|
+
serversToBackup[name] = convertUrlToMcpRemote(url, headers);
|
|
785
|
+
} else if (cfg.command && normalizedServers[name]) {
|
|
786
|
+
serversToBackup[name] = normalizedServers[name];
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
serverCount = Object.keys(serversToBackup).length;
|
|
790
|
+
if (serverCount > 0) {
|
|
791
|
+
saveServersBackup(serversToBackup);
|
|
792
|
+
}
|
|
793
|
+
const config = {
|
|
794
|
+
mcpServers: {
|
|
795
|
+
"mcp-lazy": generateProxyEntry()
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
writeFileSync2(targetPath, JSON.stringify(config, null, 2) + "\n");
|
|
799
|
+
} else {
|
|
800
|
+
created = true;
|
|
801
|
+
const dir = dirname2(targetPath);
|
|
802
|
+
if (!existsSync2(dir)) {
|
|
803
|
+
mkdirSync2(dir, { recursive: true });
|
|
804
|
+
}
|
|
805
|
+
const config = {
|
|
806
|
+
mcpServers: {
|
|
807
|
+
"mcp-lazy": generateProxyEntry()
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
writeFileSync2(targetPath, JSON.stringify(config, null, 2) + "\n");
|
|
811
|
+
}
|
|
812
|
+
return { configPath: targetPath, created, serverCount };
|
|
813
|
+
}
|
|
814
|
+
function isProxyRegistered(agent) {
|
|
815
|
+
const configPath = findAgentConfig(agent);
|
|
816
|
+
if (!configPath) return false;
|
|
817
|
+
if (agent.format === "opencode") {
|
|
818
|
+
try {
|
|
819
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
820
|
+
return !!(config.mcp && config.mcp["mcp-lazy"]);
|
|
821
|
+
} catch {
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
if (agent.format === "toml") {
|
|
826
|
+
try {
|
|
827
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
828
|
+
return content.includes("[mcp_servers.mcp-lazy]");
|
|
829
|
+
} catch {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
835
|
+
return !!(config.mcpServers && config.mcpServers["mcp-lazy"]);
|
|
836
|
+
} catch {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// src/cli/add.ts
|
|
842
|
+
var BANNER = `
|
|
843
|
+
\x1B[36m\x1B[1m \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557
|
|
844
|
+
\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D
|
|
845
|
+
\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2588\u2554\u255D \u255A\u2588\u2588\u2588\u2588\u2554\u255D
|
|
846
|
+
\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2588\u2554\u255D \u255A\u2588\u2588\u2554\u255D
|
|
847
|
+
\u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551
|
|
848
|
+
\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D\x1B[0m
|
|
849
|
+
`;
|
|
850
|
+
async function runAdd(options) {
|
|
851
|
+
console.log(BANNER);
|
|
852
|
+
let targets = [];
|
|
853
|
+
if (options.all) {
|
|
854
|
+
targets = [...AGENTS];
|
|
855
|
+
} else {
|
|
856
|
+
const flagMap = {
|
|
857
|
+
cursor: options.cursor,
|
|
858
|
+
opencode: options.opencode,
|
|
859
|
+
antigravity: options.antigravity,
|
|
860
|
+
codex: options.codex
|
|
861
|
+
};
|
|
862
|
+
for (const [name, enabled] of Object.entries(flagMap)) {
|
|
863
|
+
if (enabled) {
|
|
864
|
+
const agent = getAgentByName(name);
|
|
865
|
+
if (agent) {
|
|
866
|
+
targets.push(agent);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (targets.length === 0) {
|
|
872
|
+
console.log(" No agent specified. Use one of:");
|
|
873
|
+
console.log(" mcp-lazy add --cursor");
|
|
874
|
+
console.log(" mcp-lazy add --opencode");
|
|
875
|
+
console.log(" mcp-lazy add --antigravity");
|
|
876
|
+
console.log(" mcp-lazy add --codex");
|
|
877
|
+
console.log(" mcp-lazy add --all\n");
|
|
878
|
+
process.exit(1);
|
|
879
|
+
}
|
|
880
|
+
console.log(" Registering mcp-lazy proxy...\n");
|
|
881
|
+
for (const agent of targets) {
|
|
882
|
+
try {
|
|
883
|
+
const { configPath, created, serverCount } = registerProxy(agent);
|
|
884
|
+
const action = created ? "created" : "updated";
|
|
885
|
+
const servers = serverCount > 0 ? ` (${serverCount} servers captured)` : "";
|
|
886
|
+
console.log(` \u2713 ${agent.displayName}: ${action} ${configPath}${servers}`);
|
|
887
|
+
} catch (err) {
|
|
888
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
889
|
+
console.log(` \u2717 ${agent.displayName}: failed - ${message}`);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
console.log("\n Done! Restart your agents to activate mcp-lazy.\n");
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/cli/doctor.ts
|
|
896
|
+
init_config();
|
|
897
|
+
var TOKENS_PER_TOOL = 650;
|
|
898
|
+
var PROXY_BASE_TOKENS = 350;
|
|
899
|
+
async function runDoctor() {
|
|
900
|
+
let hasIssues = false;
|
|
901
|
+
console.log("\nmcp-lazy status check\n");
|
|
902
|
+
const nodeVersion = process.versions.node;
|
|
903
|
+
const major = parseInt(nodeVersion.split(".")[0], 10);
|
|
904
|
+
if (major >= 18) {
|
|
905
|
+
console.log(` \u2713 Node.js ${nodeVersion}`);
|
|
906
|
+
} else {
|
|
907
|
+
console.log(` \u2717 Node.js ${nodeVersion} (requires >= 18)`);
|
|
908
|
+
hasIssues = true;
|
|
909
|
+
}
|
|
910
|
+
const servers = loadServersBackup();
|
|
911
|
+
const serverCount = Object.keys(servers).length;
|
|
912
|
+
if (serverCount > 0) {
|
|
913
|
+
const serverNames = Object.keys(servers);
|
|
914
|
+
console.log(` \u2713 ${serverCount} MCP server(s) registered`);
|
|
915
|
+
for (const name of serverNames) {
|
|
916
|
+
console.log(` - ${name}`);
|
|
917
|
+
}
|
|
918
|
+
} else {
|
|
919
|
+
console.log(` \u2717 No MCP servers registered -> run 'mcp-lazy add --<agent>'`);
|
|
920
|
+
hasIssues = true;
|
|
921
|
+
}
|
|
922
|
+
console.log("");
|
|
923
|
+
let registeredCount = 0;
|
|
924
|
+
for (const agent of AGENTS) {
|
|
925
|
+
const registered = isProxyRegistered(agent);
|
|
926
|
+
if (registered) {
|
|
927
|
+
console.log(` \u2713 ${agent.displayName} registered`);
|
|
928
|
+
registeredCount++;
|
|
929
|
+
} else {
|
|
930
|
+
console.log(` - ${agent.displayName} not registered -> mcp-lazy add --${agent.name}`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (registeredCount === 0) {
|
|
934
|
+
console.log("\n No agents registered. Run 'mcp-lazy add --<agent>' to register.");
|
|
935
|
+
hasIssues = true;
|
|
936
|
+
}
|
|
937
|
+
if (serverCount > 0) {
|
|
938
|
+
const estimatedTools = serverCount * 15;
|
|
939
|
+
const estimatedTokens = estimatedTools * TOKENS_PER_TOOL;
|
|
940
|
+
const savings = estimatedTokens > 0 ? Math.round((estimatedTokens - PROXY_BASE_TOKENS) / estimatedTokens * 100) : 0;
|
|
941
|
+
console.log(`
|
|
942
|
+
Token savings estimate:`);
|
|
943
|
+
console.log(` ${serverCount} server(s) registered`);
|
|
944
|
+
console.log(` Without mcp-lazy: ~${estimatedTokens.toLocaleString()} tokens`);
|
|
945
|
+
console.log(` With mcp-lazy: ${PROXY_BASE_TOKENS.toLocaleString()} tokens`);
|
|
946
|
+
console.log(` Estimated savings: ${savings}%`);
|
|
947
|
+
}
|
|
948
|
+
console.log("");
|
|
949
|
+
if (!hasIssues) {
|
|
950
|
+
console.log(" All checks passed.\n");
|
|
951
|
+
} else {
|
|
952
|
+
console.log(" Some issues found. See above for details.\n");
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// src/index.ts
|
|
957
|
+
init_version();
|
|
958
|
+
var program = new Command();
|
|
959
|
+
program.name("mcp-lazy").description("MCP lazy loading proxy - reduce context window token usage by 90%+").version(VERSION);
|
|
960
|
+
program.command("add").description("Register mcp-lazy proxy with an agent").option("--cursor", "Register with Cursor").option("--opencode", "Register with Opencode").option("--antigravity", "Register with Antigravity").option("--codex", "Register with Codex").option("--all", "Register with all agents").action(async (options) => {
|
|
961
|
+
await runAdd(options);
|
|
962
|
+
});
|
|
963
|
+
program.command("doctor").description("Check installation status and token savings").action(async () => {
|
|
964
|
+
await runDoctor();
|
|
965
|
+
});
|
|
966
|
+
program.command("serve").description("Start the mcp-lazy proxy server (stdio mode)").action(async () => {
|
|
967
|
+
await runServe();
|
|
968
|
+
});
|
|
969
|
+
async function runServe() {
|
|
970
|
+
const { loadServersBackup: loadServersBackup2, computeServerFingerprint: computeServerFingerprint2, loadToolCache: loadToolCache2, saveToolCache: saveToolCache2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
971
|
+
const { ToolRegistry: ToolRegistry2, extractKeywords: extractKeywords2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
|
|
972
|
+
const { ServerLoader: ServerLoader2 } = await Promise.resolve().then(() => (init_loader(), loader_exports));
|
|
973
|
+
const { startProxyServer: startProxyServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
974
|
+
const { connectToServer: connectToServer2, listServerTools: listServerTools2, disconnectServer: disconnectServer2 } = await Promise.resolve().then(() => (init_mcp_client(), mcp_client_exports));
|
|
975
|
+
const servers = loadServersBackup2();
|
|
976
|
+
const serverNames = Object.keys(servers);
|
|
977
|
+
if (serverNames.length === 0) {
|
|
978
|
+
console.error("No MCP servers found. Check your MCP configurations.");
|
|
979
|
+
process.exit(1);
|
|
980
|
+
}
|
|
981
|
+
const registry = new ToolRegistry2();
|
|
982
|
+
const startMs = Date.now();
|
|
983
|
+
const fingerprint = computeServerFingerprint2(servers);
|
|
984
|
+
const cached = loadToolCache2();
|
|
985
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
986
|
+
for (const entry of cached.tools) {
|
|
987
|
+
registry.addTool(entry);
|
|
988
|
+
}
|
|
989
|
+
const elapsed = Date.now() - startMs;
|
|
990
|
+
console.error(`mcp-lazy: loaded ${registry.getToolCount()} tools from cache in ${elapsed}ms`);
|
|
991
|
+
} else {
|
|
992
|
+
const results = await Promise.allSettled(
|
|
993
|
+
serverNames.map(async (name) => {
|
|
994
|
+
const serverConfig = servers[name];
|
|
995
|
+
if (!serverConfig.command) {
|
|
996
|
+
console.error(`Warning: ${name} has no command configured, skipping`);
|
|
997
|
+
return [];
|
|
998
|
+
}
|
|
999
|
+
const conn = await connectToServer2(serverConfig.command, serverConfig.args, serverConfig.env);
|
|
1000
|
+
const tools = await listServerTools2(conn.client);
|
|
1001
|
+
await disconnectServer2(conn);
|
|
1002
|
+
return tools.map((tool) => ({
|
|
1003
|
+
name: tool.name,
|
|
1004
|
+
description: tool.description ?? "",
|
|
1005
|
+
server: name,
|
|
1006
|
+
serverDescription: serverConfig.description ?? "",
|
|
1007
|
+
inputSchema: tool.inputSchema,
|
|
1008
|
+
keywords: extractKeywords2(tool.name, tool.description ?? "")
|
|
1009
|
+
}));
|
|
1010
|
+
})
|
|
1011
|
+
);
|
|
1012
|
+
let successCount = 0;
|
|
1013
|
+
for (let i = 0; i < results.length; i++) {
|
|
1014
|
+
const result = results[i];
|
|
1015
|
+
if (result.status === "fulfilled") {
|
|
1016
|
+
for (const entry of result.value) {
|
|
1017
|
+
registry.addTool(entry);
|
|
1018
|
+
}
|
|
1019
|
+
if (result.value.length > 0) successCount++;
|
|
1020
|
+
} else {
|
|
1021
|
+
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
1022
|
+
console.error(`Warning: could not connect to ${serverNames[i]}: ${message}`);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
const elapsed = Date.now() - startMs;
|
|
1026
|
+
console.error(`mcp-lazy: discovered ${registry.getToolCount()} tools from ${successCount} servers in ${elapsed}ms`);
|
|
1027
|
+
saveToolCache2(fingerprint, registry.getAllTools());
|
|
1028
|
+
}
|
|
1029
|
+
if (registry.getToolCount() === 0) {
|
|
1030
|
+
console.error("No tools discovered from any server. Check your MCP configurations.");
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
const loader = new ServerLoader2(servers);
|
|
1034
|
+
await startProxyServer2(registry, loader);
|
|
1035
|
+
}
|
|
1036
|
+
program.parse();
|
|
1037
|
+
//# sourceMappingURL=index.js.map
|