mcp-proxy-conductor 0.1.1 → 0.1.3
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 +235 -56
- package/package.json +7 -6
package/dist/index.js
CHANGED
|
@@ -113,98 +113,185 @@ import {
|
|
|
113
113
|
ResourceListChangedNotificationSchema,
|
|
114
114
|
PromptListChangedNotificationSchema
|
|
115
115
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
116
|
+
|
|
117
|
+
// src/version.ts
|
|
118
|
+
var VERSION = "0.1.3";
|
|
119
|
+
|
|
120
|
+
// src/registry/connection.ts
|
|
121
|
+
var EMPTY = { tools: [], resources: [], prompts: [] };
|
|
116
122
|
var DownstreamConnection = class {
|
|
117
|
-
constructor(def, transportFactory,
|
|
123
|
+
constructor(def, transportFactory, opts = {}) {
|
|
118
124
|
this.def = def;
|
|
119
125
|
this.transportFactory = transportFactory;
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
126
|
+
this.capabilities = opts.cachedCaps ?? { ...EMPTY };
|
|
127
|
+
this.hydrated = opts.cachedCaps !== void 0;
|
|
128
|
+
this.idleMs = opts.idleMs ?? 0;
|
|
129
|
+
this.onChange = opts.onChange;
|
|
122
130
|
}
|
|
123
131
|
def;
|
|
124
132
|
transportFactory;
|
|
125
|
-
|
|
126
|
-
state = "connecting";
|
|
133
|
+
state = "idle";
|
|
127
134
|
error;
|
|
128
|
-
capabilities
|
|
135
|
+
capabilities;
|
|
136
|
+
hydrated;
|
|
137
|
+
// true while capabilities come from cache (not a live fetch)
|
|
138
|
+
idleMs;
|
|
139
|
+
onChange;
|
|
129
140
|
client;
|
|
130
|
-
|
|
141
|
+
idleTimer;
|
|
142
|
+
connecting;
|
|
143
|
+
// Connect if not already connected. Concurrent callers share one in-flight connect.
|
|
144
|
+
// Every call (re)starts the idle timer.
|
|
145
|
+
async ensureConnected() {
|
|
146
|
+
this.touch();
|
|
147
|
+
if (this.state === "connected") return;
|
|
148
|
+
if (!this.connecting) {
|
|
149
|
+
this.connecting = this.doConnect().finally(() => {
|
|
150
|
+
this.connecting = void 0;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return this.connecting;
|
|
154
|
+
}
|
|
155
|
+
async callTool(name, args) {
|
|
156
|
+
await this.ensureConnected();
|
|
157
|
+
this.touch();
|
|
158
|
+
return this.client.callTool({ name, arguments: args });
|
|
159
|
+
}
|
|
160
|
+
async readResource(uri) {
|
|
161
|
+
await this.ensureConnected();
|
|
162
|
+
this.touch();
|
|
163
|
+
return this.client.readResource({ uri });
|
|
164
|
+
}
|
|
165
|
+
async getPrompt(name, args) {
|
|
166
|
+
await this.ensureConnected();
|
|
167
|
+
this.touch();
|
|
168
|
+
return this.client.getPrompt({ name, arguments: args });
|
|
169
|
+
}
|
|
170
|
+
async refresh() {
|
|
171
|
+
const before = JSON.stringify(this.capabilities);
|
|
172
|
+
const caps = this.client.getServerCapabilities() ?? {};
|
|
173
|
+
this.capabilities = {
|
|
174
|
+
tools: caps.tools ? (await this.client.listTools()).tools : [],
|
|
175
|
+
resources: caps.resources ? (await this.client.listResources()).resources : [],
|
|
176
|
+
prompts: caps.prompts ? (await this.client.listPrompts()).prompts : []
|
|
177
|
+
};
|
|
178
|
+
return JSON.stringify(this.capabilities) !== before;
|
|
179
|
+
}
|
|
180
|
+
async close() {
|
|
181
|
+
this.clearIdle();
|
|
182
|
+
await this.client?.close().catch(() => void 0);
|
|
183
|
+
this.client = void 0;
|
|
184
|
+
if (this.state !== "error") this.state = "idle";
|
|
185
|
+
}
|
|
186
|
+
async doConnect() {
|
|
131
187
|
this.state = "connecting";
|
|
188
|
+
const client = new Client({ name: "ai-conductor", version: VERSION });
|
|
132
189
|
try {
|
|
133
190
|
const transport = await this.transportFactory(this.def);
|
|
134
|
-
await
|
|
135
|
-
this.client
|
|
136
|
-
|
|
191
|
+
await client.connect(transport);
|
|
192
|
+
this.client = client;
|
|
193
|
+
client.onclose = () => {
|
|
194
|
+
if (this.state === "connected") this.state = "idle";
|
|
137
195
|
this.onChange?.();
|
|
138
196
|
};
|
|
139
|
-
this.registerNotificationHandlers();
|
|
140
|
-
await this.refresh();
|
|
197
|
+
this.registerNotificationHandlers(client);
|
|
198
|
+
const changed = await this.refresh();
|
|
141
199
|
this.state = "connected";
|
|
200
|
+
this.error = void 0;
|
|
201
|
+
this.hydrated = false;
|
|
202
|
+
this.touch();
|
|
203
|
+
if (changed) this.onChange?.();
|
|
142
204
|
} catch (err) {
|
|
143
205
|
this.state = "error";
|
|
144
206
|
this.error = err instanceof Error ? err.message : String(err);
|
|
207
|
+
await client.close().catch(() => void 0);
|
|
208
|
+
this.client = void 0;
|
|
145
209
|
throw err;
|
|
146
210
|
}
|
|
147
211
|
}
|
|
148
|
-
registerNotificationHandlers() {
|
|
212
|
+
registerNotificationHandlers(client) {
|
|
149
213
|
const refreshAndNotify = async () => {
|
|
150
214
|
await this.refresh().catch(() => void 0);
|
|
151
215
|
this.onChange?.();
|
|
152
216
|
};
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
217
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema, refreshAndNotify);
|
|
218
|
+
client.setNotificationHandler(ResourceListChangedNotificationSchema, refreshAndNotify);
|
|
219
|
+
client.setNotificationHandler(PromptListChangedNotificationSchema, refreshAndNotify);
|
|
220
|
+
}
|
|
221
|
+
touch() {
|
|
222
|
+
this.clearIdle();
|
|
223
|
+
if (this.idleMs > 0) {
|
|
224
|
+
this.idleTimer = setTimeout(() => {
|
|
225
|
+
void this.close();
|
|
226
|
+
}, this.idleMs);
|
|
227
|
+
this.idleTimer.unref?.();
|
|
228
|
+
}
|
|
164
229
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
230
|
+
clearIdle() {
|
|
231
|
+
if (this.idleTimer) {
|
|
232
|
+
clearTimeout(this.idleTimer);
|
|
233
|
+
this.idleTimer = void 0;
|
|
234
|
+
}
|
|
168
235
|
}
|
|
169
236
|
};
|
|
170
237
|
|
|
171
238
|
// src/registry/manager.ts
|
|
172
239
|
var DownstreamManager = class {
|
|
173
|
-
constructor(transportFactory,
|
|
240
|
+
constructor(transportFactory, opts = {}) {
|
|
174
241
|
this.transportFactory = transportFactory;
|
|
175
|
-
this.onChange = onChange;
|
|
242
|
+
this.onChange = opts.onChange ?? (() => void 0);
|
|
243
|
+
this.cache = opts.cache;
|
|
244
|
+
this.idleMs = opts.idleMs ?? 0;
|
|
176
245
|
}
|
|
177
246
|
transportFactory;
|
|
178
|
-
onChange;
|
|
179
247
|
conns = /* @__PURE__ */ new Map();
|
|
180
|
-
|
|
181
|
-
|
|
248
|
+
onChange;
|
|
249
|
+
cache;
|
|
250
|
+
idleMs;
|
|
251
|
+
cacheMap = {};
|
|
252
|
+
// Non-blocking: create idle connections hydrated from cache; only servers with no cached
|
|
253
|
+
// capabilities are background-filled (fire-and-forget) so they can be advertised.
|
|
182
254
|
async start(defs) {
|
|
183
|
-
|
|
255
|
+
this.cacheMap = this.cache ? await this.cache.load() : {};
|
|
256
|
+
for (const def of defs.filter((d) => d.enabled)) {
|
|
257
|
+
const conn = this.createConn(def);
|
|
258
|
+
this.conns.set(def.id, conn);
|
|
259
|
+
if (!this.cacheMap[def.id]) {
|
|
260
|
+
void conn.ensureConnected().catch(() => void 0);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
184
263
|
}
|
|
185
|
-
async
|
|
186
|
-
|
|
264
|
+
async add(def) {
|
|
265
|
+
if (this.conns.has(def.id)) throw new Error(`server '${def.id}' already exists`);
|
|
266
|
+
const conn = this.createConn(def);
|
|
187
267
|
this.conns.set(def.id, conn);
|
|
188
|
-
await conn.
|
|
268
|
+
await conn.ensureConnected().catch(() => void 0);
|
|
269
|
+
if (conn.state === "connected" && this.cache) {
|
|
270
|
+
this.cacheMap[def.id] = {
|
|
271
|
+
tools: conn.capabilities.tools,
|
|
272
|
+
resources: conn.capabilities.resources,
|
|
273
|
+
prompts: conn.capabilities.prompts,
|
|
274
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
275
|
+
};
|
|
276
|
+
await this.cache.save(this.cacheMap).catch(() => void 0);
|
|
277
|
+
}
|
|
189
278
|
this.onChange();
|
|
190
279
|
return conn;
|
|
191
280
|
}
|
|
192
|
-
async add(def) {
|
|
193
|
-
if (this.conns.has(def.id)) throw new Error(`server '${def.id}' already exists`);
|
|
194
|
-
return this.connectOne(def);
|
|
195
|
-
}
|
|
196
281
|
async remove(id) {
|
|
197
282
|
const conn = this.conns.get(id);
|
|
198
283
|
if (!conn) return;
|
|
199
284
|
await conn.close();
|
|
200
285
|
this.conns.delete(id);
|
|
286
|
+
delete this.cacheMap[id];
|
|
287
|
+
if (this.cache) await this.cache.save(this.cacheMap).catch(() => void 0);
|
|
201
288
|
this.onChange();
|
|
202
289
|
}
|
|
203
290
|
get(id) {
|
|
204
291
|
return this.conns.get(id);
|
|
205
292
|
}
|
|
206
|
-
|
|
207
|
-
return [...this.conns.values()]
|
|
293
|
+
entries() {
|
|
294
|
+
return [...this.conns.values()];
|
|
208
295
|
}
|
|
209
296
|
list() {
|
|
210
297
|
return [...this.conns.values()].map((c) => ({
|
|
@@ -213,13 +300,36 @@ var DownstreamManager = class {
|
|
|
213
300
|
error: c.error,
|
|
214
301
|
toolCount: c.capabilities.tools.length,
|
|
215
302
|
resourceCount: c.capabilities.resources.length,
|
|
216
|
-
promptCount: c.capabilities.prompts.length
|
|
303
|
+
promptCount: c.capabilities.prompts.length,
|
|
304
|
+
cached: c.hydrated
|
|
217
305
|
}));
|
|
218
306
|
}
|
|
219
307
|
async closeAll() {
|
|
220
308
|
await Promise.all([...this.conns.values()].map((c) => c.close()));
|
|
221
309
|
this.conns.clear();
|
|
222
310
|
}
|
|
311
|
+
createConn(def) {
|
|
312
|
+
return new DownstreamConnection(def, this.transportFactory, {
|
|
313
|
+
idleMs: this.idleMs,
|
|
314
|
+
cachedCaps: this.cacheMap[def.id],
|
|
315
|
+
onChange: () => this.onConnChange(def.id)
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
// A connection reports a capability/state change: persist live caps to the cache and
|
|
319
|
+
// propagate listChanged upstream.
|
|
320
|
+
onConnChange(id) {
|
|
321
|
+
const conn = this.conns.get(id);
|
|
322
|
+
if (conn && conn.state === "connected") {
|
|
323
|
+
this.cacheMap[id] = {
|
|
324
|
+
tools: conn.capabilities.tools,
|
|
325
|
+
resources: conn.capabilities.resources,
|
|
326
|
+
prompts: conn.capabilities.prompts,
|
|
327
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
328
|
+
};
|
|
329
|
+
if (this.cache) void this.cache.save(this.cacheMap).catch(() => void 0);
|
|
330
|
+
}
|
|
331
|
+
this.onChange();
|
|
332
|
+
}
|
|
223
333
|
};
|
|
224
334
|
|
|
225
335
|
// src/aggregator/namespace.ts
|
|
@@ -252,38 +362,39 @@ var Aggregator = class {
|
|
|
252
362
|
this.manager = manager;
|
|
253
363
|
}
|
|
254
364
|
manager;
|
|
365
|
+
// Advertise from every known connection's capabilities (cached while idle, live when
|
|
366
|
+
// connected). No connection is established just to list.
|
|
255
367
|
async listTools() {
|
|
256
|
-
return this.manager.
|
|
368
|
+
return this.manager.entries().flatMap(
|
|
257
369
|
(c) => c.capabilities.tools.map((t) => ({ ...t, name: encodeName(c.def.id, t.name) }))
|
|
258
370
|
);
|
|
259
371
|
}
|
|
260
372
|
async listPrompts() {
|
|
261
|
-
return this.manager.
|
|
373
|
+
return this.manager.entries().flatMap(
|
|
262
374
|
(c) => c.capabilities.prompts.map((p) => ({ ...p, name: encodeName(c.def.id, p.name) }))
|
|
263
375
|
);
|
|
264
376
|
}
|
|
265
377
|
async listResources() {
|
|
266
|
-
return this.manager.
|
|
378
|
+
return this.manager.entries().flatMap(
|
|
267
379
|
(c) => c.capabilities.resources.map((r) => ({ ...r, uri: encodeUri(c.def.id, r.uri) }))
|
|
268
380
|
);
|
|
269
381
|
}
|
|
382
|
+
// Routing connects on demand via the connection's own methods (ensureConnected inside).
|
|
270
383
|
async callTool(qualifiedName, args) {
|
|
271
384
|
const { serverId, name } = decodeName(qualifiedName);
|
|
272
|
-
return this.connOrThrow(serverId).
|
|
385
|
+
return this.connOrThrow(serverId).callTool(name, args);
|
|
273
386
|
}
|
|
274
387
|
async getPrompt(qualifiedName, args) {
|
|
275
388
|
const { serverId, name } = decodeName(qualifiedName);
|
|
276
|
-
return this.connOrThrow(serverId).
|
|
389
|
+
return this.connOrThrow(serverId).getPrompt(name, args);
|
|
277
390
|
}
|
|
278
391
|
async readResource(qualifiedUri) {
|
|
279
392
|
const { serverId, uri } = decodeUri(qualifiedUri);
|
|
280
|
-
return this.connOrThrow(serverId).
|
|
393
|
+
return this.connOrThrow(serverId).readResource(uri);
|
|
281
394
|
}
|
|
282
395
|
connOrThrow(serverId) {
|
|
283
396
|
const conn = this.manager.get(serverId);
|
|
284
|
-
if (!conn
|
|
285
|
-
throw new Error(`downstream server '${serverId}' is not connected`);
|
|
286
|
-
}
|
|
397
|
+
if (!conn) throw new Error(`unknown downstream server '${serverId}'`);
|
|
287
398
|
return conn;
|
|
288
399
|
}
|
|
289
400
|
};
|
|
@@ -319,7 +430,7 @@ var MetaTools = class {
|
|
|
319
430
|
},
|
|
320
431
|
{
|
|
321
432
|
name: "list_servers",
|
|
322
|
-
description: "List managed downstream servers
|
|
433
|
+
description: "List managed downstream servers: connection state (idle/connected/error), whether capabilities are served from cache, and capability counts.",
|
|
323
434
|
inputSchema: { type: "object", properties: {} }
|
|
324
435
|
}
|
|
325
436
|
];
|
|
@@ -369,7 +480,7 @@ import {
|
|
|
369
480
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
370
481
|
function buildUpstreamServer(aggregator, meta) {
|
|
371
482
|
const server = new Server(
|
|
372
|
-
{ name: "ai-conductor", version:
|
|
483
|
+
{ name: "ai-conductor", version: VERSION },
|
|
373
484
|
{ capabilities: { tools: { listChanged: true }, resources: { listChanged: true }, prompts: { listChanged: true } } }
|
|
374
485
|
);
|
|
375
486
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -391,6 +502,51 @@ function buildUpstreamServer(aggregator, meta) {
|
|
|
391
502
|
return server;
|
|
392
503
|
}
|
|
393
504
|
|
|
505
|
+
// src/config/capability-cache.ts
|
|
506
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
507
|
+
import { dirname as dirname2 } from "path";
|
|
508
|
+
import envPaths2 from "env-paths";
|
|
509
|
+
import { z as z2 } from "zod";
|
|
510
|
+
var CachedCapabilitiesSchema = z2.object({
|
|
511
|
+
tools: z2.array(z2.any()),
|
|
512
|
+
resources: z2.array(z2.any()),
|
|
513
|
+
prompts: z2.array(z2.any()),
|
|
514
|
+
fetchedAt: z2.string()
|
|
515
|
+
});
|
|
516
|
+
var CacheMapSchema = z2.record(z2.string(), CachedCapabilitiesSchema);
|
|
517
|
+
function defaultCapabilityCachePath() {
|
|
518
|
+
return `${envPaths2("ai-conductor", { suffix: "" }).config}/capabilities.json`;
|
|
519
|
+
}
|
|
520
|
+
var CapabilityCache = class {
|
|
521
|
+
constructor(path = defaultCapabilityCachePath()) {
|
|
522
|
+
this.path = path;
|
|
523
|
+
}
|
|
524
|
+
path;
|
|
525
|
+
async load() {
|
|
526
|
+
let raw;
|
|
527
|
+
try {
|
|
528
|
+
raw = await readFile2(this.path, "utf8");
|
|
529
|
+
} catch (err) {
|
|
530
|
+
if (err.code === "ENOENT") return {};
|
|
531
|
+
throw err;
|
|
532
|
+
}
|
|
533
|
+
const parsed = CacheMapSchema.safeParse(JSON.parse(tryJson(raw)));
|
|
534
|
+
return parsed.success ? parsed.data : {};
|
|
535
|
+
}
|
|
536
|
+
async save(map) {
|
|
537
|
+
await mkdir2(dirname2(this.path), { recursive: true });
|
|
538
|
+
await writeFile2(this.path, JSON.stringify(map, null, 2), "utf8");
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
function tryJson(raw) {
|
|
542
|
+
try {
|
|
543
|
+
JSON.parse(raw);
|
|
544
|
+
return raw;
|
|
545
|
+
} catch {
|
|
546
|
+
return "null";
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
394
550
|
// src/index.ts
|
|
395
551
|
async function createConductor(opts = {}) {
|
|
396
552
|
const store = opts.store ?? new ConfigStore();
|
|
@@ -403,7 +559,10 @@ async function createConductor(opts = {}) {
|
|
|
403
559
|
server.sendResourceListChanged();
|
|
404
560
|
server.sendPromptListChanged();
|
|
405
561
|
};
|
|
406
|
-
const
|
|
562
|
+
const cache = opts.cache ?? new CapabilityCache();
|
|
563
|
+
const rawIdleMs = Number(process.env.CONDUCTOR_IDLE_TIMEOUT_MS ?? 3e5);
|
|
564
|
+
const idleMs = opts.idleMs ?? (Number.isFinite(rawIdleMs) ? rawIdleMs : 3e5);
|
|
565
|
+
const manager = new DownstreamManager(transportFactory, { onChange, cache, idleMs });
|
|
407
566
|
const aggregator = new Aggregator(manager);
|
|
408
567
|
const meta = new MetaTools(manager, store);
|
|
409
568
|
server = buildUpstreamServer(aggregator, meta);
|
|
@@ -417,6 +576,22 @@ async function createConductor(opts = {}) {
|
|
|
417
576
|
}
|
|
418
577
|
};
|
|
419
578
|
}
|
|
579
|
+
function installShutdownHandlers(conductor, proc = process) {
|
|
580
|
+
let shuttingDown = false;
|
|
581
|
+
const shutdown = async (signal) => {
|
|
582
|
+
if (shuttingDown) return;
|
|
583
|
+
shuttingDown = true;
|
|
584
|
+
try {
|
|
585
|
+
await conductor.manager.closeAll();
|
|
586
|
+
} catch (err) {
|
|
587
|
+
console.error(`ai-conductor shutdown error on ${signal}:`, err);
|
|
588
|
+
} finally {
|
|
589
|
+
proc.exit(0);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
proc.on("SIGINT", shutdown);
|
|
593
|
+
proc.on("SIGTERM", shutdown);
|
|
594
|
+
}
|
|
420
595
|
function isMainModule(argv1, importMetaUrl) {
|
|
421
596
|
if (!argv1) return false;
|
|
422
597
|
try {
|
|
@@ -426,12 +601,16 @@ function isMainModule(argv1, importMetaUrl) {
|
|
|
426
601
|
}
|
|
427
602
|
}
|
|
428
603
|
if (isMainModule(process.argv[1], import.meta.url)) {
|
|
429
|
-
createConductor().then((c) =>
|
|
604
|
+
createConductor().then(async (c) => {
|
|
605
|
+
installShutdownHandlers(c);
|
|
606
|
+
await c.start();
|
|
607
|
+
}).catch((err) => {
|
|
430
608
|
console.error("ai-conductor failed to start:", err);
|
|
431
609
|
process.exit(1);
|
|
432
610
|
});
|
|
433
611
|
}
|
|
434
612
|
export {
|
|
435
613
|
createConductor,
|
|
614
|
+
installShutdownHandlers,
|
|
436
615
|
isMainModule
|
|
437
616
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-proxy-conductor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Local MCP proxy that aggregates dynamically managed downstream MCP servers, managed at runtime without restarting the client.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,11 +32,12 @@
|
|
|
32
32
|
"node": ">=20"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"test:
|
|
39
|
-
"
|
|
35
|
+
"gen:version": "node scripts/gen-version.mjs",
|
|
36
|
+
"build": "pnpm gen:version && tsup",
|
|
37
|
+
"dev": "pnpm gen:version && tsx src/index.ts",
|
|
38
|
+
"test": "pnpm gen:version && vitest run",
|
|
39
|
+
"test:watch": "pnpm gen:version && vitest",
|
|
40
|
+
"typecheck": "pnpm gen:version && tsc --noEmit",
|
|
40
41
|
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|