dsh-vscode-mode 0.17.0 → 0.18.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/README.md +6 -4
- package/lib/client.js +578 -49
- package/lib/client.js.map +1 -1
- package/lib/index.js +842 -244
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/dsh-vscodemode-mcp/SKILL.md +24 -7
- package/src/client/index.ts +15 -0
- package/src/client/mcpSessionState.ts +108 -0
- package/src/client/monaco/lsp/providers.ts +32 -3
- package/src/client/state/modelWarmer.ts +198 -0
- package/src/client/styles/editor.css +5 -1
- package/src/client/ui/EditorPane.ts +16 -4
- package/src/client/ui/EditorView.ts +76 -18
- package/src/client/ui/McpSessionStatus.ts +75 -0
- package/src/client/ui/McpSettings.ts +47 -13
- package/src/mcp.ts +1 -0
- package/src/mcpBridge.ts +163 -0
- package/src/mcpDiscover.ts +38 -0
- package/src/mcpPool.ts +245 -0
- package/src/mcpProject.ts +15 -12
- package/src/mcpRuntime.ts +299 -254
- package/src/rpc.ts +13 -1
- package/src/shared/mcp.ts +47 -0
- package/src/shared/rpc.ts +5 -1
package/lib/index.js
CHANGED
|
@@ -295,6 +295,469 @@ function installIsolation(ctx) {
|
|
|
295
295
|
syncAll();
|
|
296
296
|
}
|
|
297
297
|
//#endregion
|
|
298
|
+
//#region src/shared/mcp.ts
|
|
299
|
+
/** @public @author ddj 2026年10月20号 @param input Project-only policy. @returns Validated defaults. @throws Invalid explicit policy. */
|
|
300
|
+
function parseMcpPolicy(input) {
|
|
301
|
+
if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input))) throw new Error("vscodeMode 必须是对象");
|
|
302
|
+
const policy = input ?? {};
|
|
303
|
+
const startup = policy.startup ?? "on-demand";
|
|
304
|
+
const sharing = policy.sharing ?? "agent";
|
|
305
|
+
const startupTimeoutMs = policy.startupTimeoutMs ?? 6e4;
|
|
306
|
+
if (!["on-demand", "background"].includes(startup)) throw new Error("不支持的 MCP startup");
|
|
307
|
+
if (!["agent", "workspace"].includes(sharing)) throw new Error("不支持的 MCP sharing");
|
|
308
|
+
if (!Number.isInteger(startupTimeoutMs) || startupTimeoutMs < 1e3 || startupTimeoutMs > 3e5) throw new Error("MCP startupTimeoutMs 必须为 1000–300000 的整数");
|
|
309
|
+
return {
|
|
310
|
+
startup,
|
|
311
|
+
sharing,
|
|
312
|
+
startupTimeoutMs
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
//#endregion
|
|
316
|
+
//#region src/mcpBridge.ts
|
|
317
|
+
/** Private official-MCP registrations, never a global provider. @author ddj 2026年10月20号 */
|
|
318
|
+
/** Per-agent public surface; original definitions still run inside the normal registry pipeline. */
|
|
319
|
+
var McpBinding = class {
|
|
320
|
+
agent;
|
|
321
|
+
pool;
|
|
322
|
+
entry;
|
|
323
|
+
valid;
|
|
324
|
+
called;
|
|
325
|
+
removals = [];
|
|
326
|
+
unsubscribe;
|
|
327
|
+
closed = false;
|
|
328
|
+
/** @public @author ddj 2026年10月20号 @param agent Public scope. @param pool Connection owner. @param entry Exact generation. @param valid Authorization recheck. */
|
|
329
|
+
constructor(agent, pool, entry, valid, called) {
|
|
330
|
+
this.agent = agent;
|
|
331
|
+
this.pool = pool;
|
|
332
|
+
this.entry = entry;
|
|
333
|
+
this.valid = valid;
|
|
334
|
+
this.called = called;
|
|
335
|
+
}
|
|
336
|
+
/** @private @author ddj 2026年10月20号 @param execution Registry caller. @throws Stale generation or foreign caller. */
|
|
337
|
+
check(execution) {
|
|
338
|
+
if (this.closed || !this.valid() || !this.entry.live || execution && execution.agent !== this.agent) throw new Error("项目 MCP 作用域或配置已失效");
|
|
339
|
+
}
|
|
340
|
+
/** @public @author ddj 2026年10月20号 Reconciles exact definitions after official tool-list updates. */
|
|
341
|
+
sync() {
|
|
342
|
+
this.check();
|
|
343
|
+
const bridge = this.entry.bridge;
|
|
344
|
+
if (!bridge) return;
|
|
345
|
+
this.clear();
|
|
346
|
+
for (const [name, definition] of bridge.tools) {
|
|
347
|
+
/** @author ddj 2026年10月20号 @param args Registry-validated arguments. @param execution Unmodified caller context. @returns Official canonical output. */
|
|
348
|
+
const execute = async (args, execution) => {
|
|
349
|
+
this.check(execution);
|
|
350
|
+
return this.pool.run(this.entry, async () => {
|
|
351
|
+
this.check(execution);
|
|
352
|
+
if (bridge.tools.get(name) !== definition) throw new Error("MCP 工具定义已更新,请重新发现");
|
|
353
|
+
const startedAt = Date.now();
|
|
354
|
+
const result = await definition.execute(args, execution);
|
|
355
|
+
this.called?.(Date.now() - startedAt);
|
|
356
|
+
return result;
|
|
357
|
+
}, execution.signal);
|
|
358
|
+
};
|
|
359
|
+
this.removals.push(this.agent.ctx.get("tools").register({
|
|
360
|
+
...definition,
|
|
361
|
+
execute
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
for (const section of bridge.sections.values()) {
|
|
365
|
+
/** @author ddj 2026年10月20号 @param context Actual prompt assembly. @returns Only valid-generation instructions. */
|
|
366
|
+
const text = (context) => {
|
|
367
|
+
if (!this.valid() || this.closed) return "";
|
|
368
|
+
if (typeof section.text === "function") return section.text(context);
|
|
369
|
+
return section.text;
|
|
370
|
+
};
|
|
371
|
+
this.removals.push(this.agent.ctx.get("systemPrompt").section({
|
|
372
|
+
...section,
|
|
373
|
+
text
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
/** @author ddj 2026年10月20号 Reconciles dynamic official registrations only for a live subscriber. */
|
|
377
|
+
const update = () => {
|
|
378
|
+
if (!this.closed && this.valid() && this.entry.live) this.sync();
|
|
379
|
+
};
|
|
380
|
+
if (!this.unsubscribe) this.unsubscribe = bridge.subscribe(update);
|
|
381
|
+
}
|
|
382
|
+
/** @private @author ddj 2026年10月20号 Removes only this binding's exact scoped registrations. */
|
|
383
|
+
clear() {
|
|
384
|
+
for (const remove of this.removals.splice(0).reverse()) remove();
|
|
385
|
+
}
|
|
386
|
+
/** @public @author ddj 2026年10月20号 Revokes proxies synchronously before releasing a lease. */
|
|
387
|
+
dispose() {
|
|
388
|
+
this.closed = true;
|
|
389
|
+
this.unsubscribe?.();
|
|
390
|
+
this.unsubscribe = void 0;
|
|
391
|
+
this.clear();
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
/** A connection's private surface. Public consumers register scoped proxies, not this scope. */
|
|
395
|
+
var McpBridge = class McpBridge {
|
|
396
|
+
scope;
|
|
397
|
+
tools = /* @__PURE__ */ new Map();
|
|
398
|
+
resources = /* @__PURE__ */ new Map();
|
|
399
|
+
sections = /* @__PURE__ */ new Map();
|
|
400
|
+
listeners = /* @__PURE__ */ new Set();
|
|
401
|
+
closed = false;
|
|
402
|
+
closing;
|
|
403
|
+
/** @private @author ddj 2026年10月20号 @param scope Private lifetime and registration context. */
|
|
404
|
+
constructor(scope) {
|
|
405
|
+
this.scope = scope;
|
|
406
|
+
}
|
|
407
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host owner. @param make Optional real scope factory for installed-host tests. @returns Isolated registration adapter. */
|
|
408
|
+
static async create(ctx, make) {
|
|
409
|
+
const scope = (make ?? (await loadHostModule("@deepseek-ai/dsh-scope")).createScope)(ctx, {});
|
|
410
|
+
await scope.ctx.fiber?.await?.();
|
|
411
|
+
const bridge = new McpBridge(scope);
|
|
412
|
+
bridge.install();
|
|
413
|
+
return bridge;
|
|
414
|
+
}
|
|
415
|
+
/** @private @author ddj 2026年10月20号 Installs only private services; no project registration reaches the root. */
|
|
416
|
+
install() {
|
|
417
|
+
const privateCtx = this.scope.ctx.isolate("tools").isolate("mcpResources").isolate("systemPrompt");
|
|
418
|
+
privateCtx.provide("tools", { register: (definition) => this.register(this.tools, definition.name, definition) });
|
|
419
|
+
privateCtx.provide("mcpResources", { register: (name, provider) => this.register(this.resources, name, provider) });
|
|
420
|
+
privateCtx.provide("systemPrompt", {
|
|
421
|
+
getSectionOrder: () => 3100,
|
|
422
|
+
section: (section) => this.register(this.sections, section.name, section)
|
|
423
|
+
});
|
|
424
|
+
this.scope.ctx = privateCtx;
|
|
425
|
+
}
|
|
426
|
+
/** @private @author ddj 2026年10月20号 @param registry Private generation map. @param name Stable registration name. @param entry Exact value. @returns Exact-entry removal, safe after replacement. */
|
|
427
|
+
register(registry, name, entry) {
|
|
428
|
+
if (this.closed) throw new Error("MCP bridge is disposed");
|
|
429
|
+
if (registry.has(name)) throw new Error("Duplicate private MCP registration: " + name);
|
|
430
|
+
registry.set(name, entry);
|
|
431
|
+
this.changed();
|
|
432
|
+
return () => {
|
|
433
|
+
if (registry.get(name) !== entry) return;
|
|
434
|
+
registry.delete(name);
|
|
435
|
+
this.changed();
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
/** @public @author ddj 2026年10月20号 @param listener Surface change subscriber. @returns Subscription disposer. */
|
|
439
|
+
subscribe(listener) {
|
|
440
|
+
this.listeners.add(listener);
|
|
441
|
+
return () => {
|
|
442
|
+
this.listeners.delete(listener);
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/** @private @author ddj 2026年10月20号 Notifies consumers; observer errors cannot corrupt the official sync transaction. */
|
|
446
|
+
changed() {
|
|
447
|
+
for (const listener of this.listeners) try {
|
|
448
|
+
listener();
|
|
449
|
+
} catch {}
|
|
450
|
+
}
|
|
451
|
+
/** @public @author ddj 2026年10月20号 @returns Quiescent private-scope teardown. */
|
|
452
|
+
dispose() {
|
|
453
|
+
this.closed = true;
|
|
454
|
+
return this.closing ??= this.closeScope();
|
|
455
|
+
}
|
|
456
|
+
/** @private @author ddj 2026年10月20号 @returns Exact scope closure before forgetting registrations. */
|
|
457
|
+
async closeScope() {
|
|
458
|
+
await this.scope.dispose();
|
|
459
|
+
this.tools.clear();
|
|
460
|
+
this.resources.clear();
|
|
461
|
+
this.sections.clear();
|
|
462
|
+
this.changed();
|
|
463
|
+
this.listeners.clear();
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/mcpPool.ts
|
|
468
|
+
/** Connection ownership, bounded cold starts and workspace leases. @author ddj 2026年10月20号 */
|
|
469
|
+
/** @private @author ddj 2026年10月20号 @param input Plain effective configuration. @returns Stable secret-bearing identity used only as digest input. */
|
|
470
|
+
function stableConfig(input) {
|
|
471
|
+
if (Array.isArray(input)) return "[" + input.map(stableConfig).join(",") + "]";
|
|
472
|
+
if (input && typeof input === "object") {
|
|
473
|
+
const config = input;
|
|
474
|
+
return "{" + Object.keys(config).sort().map((key) => JSON.stringify(key) + ":" + stableConfig(config[key])).join(",") + "}";
|
|
475
|
+
}
|
|
476
|
+
return JSON.stringify(input) ?? "null";
|
|
477
|
+
}
|
|
478
|
+
/** @public @author ddj 2026年10月20号 @param task Shared work that must remain alive for other waiters. @param signal One waiter's cancellation. @returns Caller-local wait without abandoning owned pool work. */
|
|
479
|
+
function waitMcp(task, signal) {
|
|
480
|
+
if (!signal) return task;
|
|
481
|
+
return new Promise((resolve, reject) => {
|
|
482
|
+
/** @author ddj 2026年10月20号 Rejects only this waiter. */
|
|
483
|
+
const abort = () => {
|
|
484
|
+
reject(/* @__PURE__ */ new Error("MCP 等待已取消"));
|
|
485
|
+
};
|
|
486
|
+
if (signal.aborted) {
|
|
487
|
+
abort();
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
491
|
+
task.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
/** The pool owns private scopes independently from any first subscriber. */
|
|
495
|
+
var McpPool = class {
|
|
496
|
+
ctx;
|
|
497
|
+
load;
|
|
498
|
+
make;
|
|
499
|
+
entries = /* @__PURE__ */ new Map();
|
|
500
|
+
owners = /* @__PURE__ */ new WeakMap();
|
|
501
|
+
ownerSeq = 0;
|
|
502
|
+
running = 0;
|
|
503
|
+
queue = [];
|
|
504
|
+
jobs = /* @__PURE__ */ new Set();
|
|
505
|
+
stopped = false;
|
|
506
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host lifecycle owner. @param load Official plugin loader. @param make Private scope factory. */
|
|
507
|
+
constructor(ctx, load, make = () => McpBridge.create(ctx)) {
|
|
508
|
+
this.ctx = ctx;
|
|
509
|
+
this.load = load;
|
|
510
|
+
this.make = make;
|
|
511
|
+
}
|
|
512
|
+
/** @private @author ddj 2026年10月20号 @param spec Effective request. @returns Private digest; never emit configuration or secrets. */
|
|
513
|
+
keyOf(spec) {
|
|
514
|
+
let owner = 0;
|
|
515
|
+
if (spec.policy.sharing === "agent") {
|
|
516
|
+
if (!this.owners.has(spec.owner)) this.owners.set(spec.owner, ++this.ownerSeq);
|
|
517
|
+
owner = this.owners.get(spec.owner);
|
|
518
|
+
}
|
|
519
|
+
const workspace = process.platform === "win32" ? spec.workspace.replace(/\\/g, "/").toLowerCase() : spec.workspace;
|
|
520
|
+
return createHash("sha256").update(stableConfig([
|
|
521
|
+
workspace,
|
|
522
|
+
owner,
|
|
523
|
+
spec.config,
|
|
524
|
+
spec.policy
|
|
525
|
+
])).digest("hex");
|
|
526
|
+
}
|
|
527
|
+
/** @public @author ddj 2026年10月20号 @param spec Authorized connection request. @returns Reference-counted single-flight lease. */
|
|
528
|
+
acquire(spec) {
|
|
529
|
+
if (this.stopped) throw new Error("MCP runtime is disposed");
|
|
530
|
+
const key = this.keyOf(spec);
|
|
531
|
+
let entry = this.entries.get(key);
|
|
532
|
+
if (entry?.phase === "error" && entry.retryAt !== void 0 && Date.now() >= entry.retryAt && !entry.close) {
|
|
533
|
+
this.entries.delete(key);
|
|
534
|
+
entry = void 0;
|
|
535
|
+
}
|
|
536
|
+
if (entry && !entry.live) throw new Error(entry.error ?? "MCP 旧连接仍在释放,暂不能创建替代实例");
|
|
537
|
+
if (!entry) {
|
|
538
|
+
entry = {
|
|
539
|
+
key,
|
|
540
|
+
spec,
|
|
541
|
+
refs: 0,
|
|
542
|
+
phase: "queued",
|
|
543
|
+
queuedAt: Date.now(),
|
|
544
|
+
phaseAt: Date.now(),
|
|
545
|
+
ready: Promise.resolve(),
|
|
546
|
+
live: true,
|
|
547
|
+
calls: Promise.resolve()
|
|
548
|
+
};
|
|
549
|
+
this.entries.set(key, entry);
|
|
550
|
+
entry.ready = this.schedule(entry);
|
|
551
|
+
entry.ready.catch(() => void 0);
|
|
552
|
+
}
|
|
553
|
+
if (entry.idle) clearTimeout(entry.idle);
|
|
554
|
+
entry.idle = void 0;
|
|
555
|
+
entry.refs += 1;
|
|
556
|
+
let released = false;
|
|
557
|
+
return {
|
|
558
|
+
entry,
|
|
559
|
+
release: () => {
|
|
560
|
+
if (released) return;
|
|
561
|
+
released = true;
|
|
562
|
+
this.release(entry);
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/** @private @author ddj 2026年10月20号 @param entry New entry. @returns Queued initialization, bounded to two concurrent starts. */
|
|
567
|
+
schedule(entry) {
|
|
568
|
+
return new Promise((resolve, reject) => {
|
|
569
|
+
/** @author ddj 2026年10月20号 Initializes a still-owned queue item. */
|
|
570
|
+
const run = async () => {
|
|
571
|
+
try {
|
|
572
|
+
await this.start(entry);
|
|
573
|
+
resolve();
|
|
574
|
+
} catch (error) {
|
|
575
|
+
reject(error);
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
this.queue.push(run);
|
|
579
|
+
this.pump();
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/** @private @author ddj 2026年10月20号 Advances FIFO work; each task holds its slot until startup or cleanup quiesces. */
|
|
583
|
+
pump() {
|
|
584
|
+
while (this.running < 2 && this.queue.length) {
|
|
585
|
+
const run = this.queue.shift();
|
|
586
|
+
this.running += 1;
|
|
587
|
+
const job = run().finally(() => {
|
|
588
|
+
this.running -= 1;
|
|
589
|
+
this.jobs.delete(job);
|
|
590
|
+
this.pump();
|
|
591
|
+
});
|
|
592
|
+
this.jobs.add(job);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
/** @private @author ddj 2026年10月20号 @param entry Connection to initialize. @returns Initialization completion, or failure after owned cleanup. */
|
|
596
|
+
async start(entry) {
|
|
597
|
+
if (!entry.live || this.stopped) throw new Error("MCP 启动已取消");
|
|
598
|
+
entry.phase = "loading";
|
|
599
|
+
entry.phaseAt = Date.now();
|
|
600
|
+
entry.startAt = entry.phaseAt;
|
|
601
|
+
let timer;
|
|
602
|
+
const work = this.mount(entry);
|
|
603
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
604
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("MCP 初始化超时")), entry.spec.policy.startupTimeoutMs);
|
|
605
|
+
timer.unref?.();
|
|
606
|
+
});
|
|
607
|
+
try {
|
|
608
|
+
await Promise.race([work, deadline]);
|
|
609
|
+
if (!entry.live) throw new Error("MCP 启动已取消");
|
|
610
|
+
entry.phase = "ready";
|
|
611
|
+
entry.finishedAt = Date.now();
|
|
612
|
+
entry.connectedAt = entry.finishedAt;
|
|
613
|
+
} catch (error) {
|
|
614
|
+
entry.error = "MCP 初始化失败或超时,请查看 Host 日志";
|
|
615
|
+
this.ctx.logger?.warn?.("MCP initialization failed: " + String(error));
|
|
616
|
+
entry.live = false;
|
|
617
|
+
entry.phase = "stopping";
|
|
618
|
+
const closing = this.closeBridge(entry);
|
|
619
|
+
await Promise.allSettled([closing, work]);
|
|
620
|
+
entry.phase = "error";
|
|
621
|
+
entry.finishedAt = Date.now();
|
|
622
|
+
entry.retryAt = Date.now() + 5e3;
|
|
623
|
+
throw new Error(entry.error);
|
|
624
|
+
} finally {
|
|
625
|
+
if (timer) clearTimeout(timer);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
/** @private @author ddj 2026年10月20号 @param entry Owned generation. @returns Official initial sync; never publishes after invalidation. */
|
|
629
|
+
async mount(entry) {
|
|
630
|
+
const official = await this.load();
|
|
631
|
+
if (!entry.live || this.stopped) return;
|
|
632
|
+
const bridge = await this.make();
|
|
633
|
+
if (!entry.live || this.stopped) {
|
|
634
|
+
await bridge.dispose();
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
entry.bridge = bridge;
|
|
638
|
+
entry.phase = "starting";
|
|
639
|
+
entry.phaseAt = Date.now();
|
|
640
|
+
entry.fiber = bridge.scope.ctx.plugin(official, entry.spec.config);
|
|
641
|
+
await entry.fiber;
|
|
642
|
+
if (entry.fiber?.state !== void 0 && entry.fiber.state !== 2) throw new Error("MCP 插件未完成激活");
|
|
643
|
+
}
|
|
644
|
+
/** @private @author ddj 2026年10月20号 @param entry Connection to close. @returns Shared closure barrier; rejection retains a fenced entry. */
|
|
645
|
+
async closeBridge(entry) {
|
|
646
|
+
if (entry.close) return entry.close;
|
|
647
|
+
const close = entry.bridge?.dispose() ?? Promise.resolve();
|
|
648
|
+
entry.close = close;
|
|
649
|
+
try {
|
|
650
|
+
await close;
|
|
651
|
+
entry.bridge = void 0;
|
|
652
|
+
entry.fiber = void 0;
|
|
653
|
+
entry.close = void 0;
|
|
654
|
+
} catch (error) {
|
|
655
|
+
entry.error = "MCP 释放未确认,禁止重建,请查看 Host 日志";
|
|
656
|
+
this.ctx.logger?.warn?.("MCP cleanup failed: " + String(error));
|
|
657
|
+
throw error;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
/** @private @author ddj 2026年10月20号 @param entry One lease's owner. Arms workspace-only warm retention. */
|
|
661
|
+
release(entry) {
|
|
662
|
+
entry.refs = Math.max(0, entry.refs - 1);
|
|
663
|
+
if (entry.refs || !entry.live) return;
|
|
664
|
+
if (entry.spec.policy.sharing === "agent") {
|
|
665
|
+
this.retire(entry).catch(() => void 0);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
entry.idle = setTimeout(() => {
|
|
669
|
+
this.retire(entry).catch(() => void 0);
|
|
670
|
+
}, 6e4);
|
|
671
|
+
entry.idle.unref?.();
|
|
672
|
+
}
|
|
673
|
+
/** @public @author ddj 2026年10月20号 @param entry Invalid generation. @returns Quiescent close; callers may await before starting a replacement. */
|
|
674
|
+
async retire(entry) {
|
|
675
|
+
entry.live = false;
|
|
676
|
+
entry.phase = "stopping";
|
|
677
|
+
if (entry.idle) clearTimeout(entry.idle);
|
|
678
|
+
await this.closeBridge(entry);
|
|
679
|
+
await entry.ready.catch(() => void 0);
|
|
680
|
+
await entry.calls.catch(() => void 0);
|
|
681
|
+
if (this.entries.get(entry.key) === entry) this.entries.delete(entry.key);
|
|
682
|
+
}
|
|
683
|
+
/** @public @author ddj 2026年10月20号 @param entry Authorized connection. @param action Per-call work retaining original execution identity. @param signal Caller cancellation. @returns Serialized shared result; no direct registry bypass is introduced here. */
|
|
684
|
+
run(entry, action, signal) {
|
|
685
|
+
/** @author ddj 2026年10月20号 Checks cancellation and generation again at actual dispatch. */
|
|
686
|
+
const invoke = async () => {
|
|
687
|
+
await waitMcp(entry.ready, signal);
|
|
688
|
+
if (signal?.aborted || !entry.live || entry.phase !== "ready") throw new Error("MCP 连接不可用或调用已取消");
|
|
689
|
+
return action();
|
|
690
|
+
};
|
|
691
|
+
const result = entry.calls.then(invoke);
|
|
692
|
+
entry.calls = result.catch(() => void 0);
|
|
693
|
+
return result;
|
|
694
|
+
}
|
|
695
|
+
/** @public @author ddj 2026年10月20号 @returns Full disposal of entries, queue, timers and owned work. */
|
|
696
|
+
async dispose() {
|
|
697
|
+
this.stopped = true;
|
|
698
|
+
await Promise.all([...this.entries.values()].map((entry) => this.retire(entry)));
|
|
699
|
+
await Promise.all([...this.jobs]);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
//#endregion
|
|
703
|
+
//#region src/mcpDiscover.ts
|
|
704
|
+
/** @public @author ddj 2026年10月20号 @param runtime Authorized runtime. @param ctx Host registration context. @returns Global discovery cleanup; normal capability filters remain authoritative. */
|
|
705
|
+
function installDiscover(runtime, ctx) {
|
|
706
|
+
const tools = ctx.get("tools");
|
|
707
|
+
/** @author ddj 2026年10月20号 @param args Validated operation. @param execution Registry-owned caller identity. @returns Safe capability catalogue or real connected schemas. */
|
|
708
|
+
const execute = async (args, execution) => {
|
|
709
|
+
const agent = execution.agent;
|
|
710
|
+
if (!agent || !runtime.agents.has(agent)) throw new Error("会话未激活,不能使用项目 MCP 发现入口");
|
|
711
|
+
if (!args || typeof args !== "object" || Array.isArray(args) || Object.keys(args).some((key) => !["action", "serverName"].includes(key))) throw new Error("发现入口仅接受 action 和 serverName");
|
|
712
|
+
if (!["list", "connect"].includes(args.action)) throw new Error("action 必须为 list 或 connect");
|
|
713
|
+
if (args.action === "list") return runtime.catalogue(agent);
|
|
714
|
+
if (typeof args.serverName !== "string") throw new Error("请指定已配置的 serverName");
|
|
715
|
+
return runtime.connect(agent, args.serverName, execution.signal);
|
|
716
|
+
};
|
|
717
|
+
/** @author ddj 2026年10月20号 @param _args Unused. @param result Safe canonical JSON. @returns Native content without bypassing output validation. */
|
|
718
|
+
const render = (_args, result) => [{
|
|
719
|
+
type: "text",
|
|
720
|
+
text: JSON.stringify(result)
|
|
721
|
+
}];
|
|
722
|
+
return tools.register({
|
|
723
|
+
name: "discover_project_mcp",
|
|
724
|
+
description: "List this workspace’s configured project MCP services without starting them, or connect one to discover its real tools. After connecting, call discovered tools in the NEXT request/program; never guess schemas. Global/preset MCPs are not included.",
|
|
725
|
+
parameters: {
|
|
726
|
+
type: "object",
|
|
727
|
+
properties: {
|
|
728
|
+
action: {
|
|
729
|
+
type: "string",
|
|
730
|
+
enum: ["list", "connect"]
|
|
731
|
+
},
|
|
732
|
+
serverName: { type: "string" }
|
|
733
|
+
},
|
|
734
|
+
required: ["action"],
|
|
735
|
+
additionalProperties: false
|
|
736
|
+
},
|
|
737
|
+
output: {
|
|
738
|
+
schema: {},
|
|
739
|
+
render
|
|
740
|
+
},
|
|
741
|
+
execute
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
/** @public @author ddj 2026年10月20号 @param runtime Active owners. @param agent Scoped catalogue owner. @returns Scoped instruction cleanup; does not register a tool override. */
|
|
745
|
+
function discoverPrompt(runtime, agent) {
|
|
746
|
+
const prompt = agent.ctx.get("systemPrompt");
|
|
747
|
+
/** @author ddj 2026年10月20号 @returns Current enabled names only; no credential-bearing configuration. */
|
|
748
|
+
const text = () => {
|
|
749
|
+
const names = runtime.catalogue(agent).map((server) => server.serverName);
|
|
750
|
+
if (!names.length) return "";
|
|
751
|
+
return "Project MCP capabilities (may not be started): " + JSON.stringify(names) + ". Use discover_project_mcp action=list, then action=connect with serverName when needed. Only use returned real tool schemas. Native tools appear on the next model request; PTC must finish the current program and start a new program to refresh its SDK. Do not eval or bypass tool approval.";
|
|
752
|
+
};
|
|
753
|
+
return prompt.section({
|
|
754
|
+
name: "vscode-mode:project-mcp",
|
|
755
|
+
order: 3090,
|
|
756
|
+
text,
|
|
757
|
+
interpolate: false
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
//#endregion
|
|
298
761
|
//#region src/shared/diff.ts
|
|
299
762
|
/** 统一取对应 hunk;callHunk 只用于旧记录缺失 hunks 时兼容。 */
|
|
300
763
|
function preciseHunk(record, idx) {
|
|
@@ -2245,22 +2708,13 @@ async function archiveRecords(ctx, session, cwd, bucket, recs, reason, opts) {
|
|
|
2245
2708
|
}
|
|
2246
2709
|
//#endregion
|
|
2247
2710
|
//#region src/mcpRuntime.ts
|
|
2248
|
-
/**
|
|
2711
|
+
/** Project-owned MCP connections and scoped on-demand capabilities. @author ddj 2026年10月20号 */
|
|
2249
2712
|
const runtimes = /* @__PURE__ */ new WeakMap();
|
|
2250
|
-
/**
|
|
2251
|
-
* Load the official plugin from the running host's installation tree.
|
|
2252
|
-
* @private @author ddj 2026年09月28号
|
|
2253
|
-
* @returns Official MCP plugin namespace; rejects if the host package is absent.
|
|
2254
|
-
*/
|
|
2713
|
+
/** @private @author ddj 2026年09月28号 @returns Official namespace from the running Host installation. */
|
|
2255
2714
|
async function loadMcp() {
|
|
2256
2715
|
return loadHostModule(MCP_PACKAGE);
|
|
2257
2716
|
}
|
|
2258
|
-
/**
|
|
2259
|
-
* Resolve one runtime per host, shared by lifecycle and RPC contexts.
|
|
2260
|
-
* @public @author ddj 2026年09月28号
|
|
2261
|
-
* @param ctx Host context.
|
|
2262
|
-
* @returns Lazy runtime; allocating it does not start connections.
|
|
2263
|
-
*/
|
|
2717
|
+
/** @public @author ddj 2026年09月28号 @param ctx Host context. @returns Lazy per-host runtime, without opening connections. */
|
|
2264
2718
|
function runtimeOf(ctx) {
|
|
2265
2719
|
const owner = ctx.root ?? ctx;
|
|
2266
2720
|
let runtime = runtimes.get(owner);
|
|
@@ -2270,178 +2724,273 @@ function runtimeOf(ctx) {
|
|
|
2270
2724
|
}
|
|
2271
2725
|
return runtime;
|
|
2272
2726
|
}
|
|
2273
|
-
/**
|
|
2727
|
+
/** The pool owns transports; each agent owns only revocable public capabilities and leases. */
|
|
2274
2728
|
var McpRuntime = class {
|
|
2275
2729
|
ctx;
|
|
2276
2730
|
load;
|
|
2277
2731
|
projects = /* @__PURE__ */ new Map();
|
|
2278
2732
|
agents = /* @__PURE__ */ new Map();
|
|
2279
|
-
|
|
2733
|
+
pool;
|
|
2280
2734
|
stopped = false;
|
|
2735
|
+
offDiscover;
|
|
2281
2736
|
retired = /* @__PURE__ */ new WeakSet();
|
|
2282
2737
|
module;
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
* @param load Official module resolver, replaceable in targeted tests.
|
|
2288
|
-
*/
|
|
2289
|
-
constructor(ctx, load = loadMcp) {
|
|
2738
|
+
pending = /* @__PURE__ */ new Set();
|
|
2739
|
+
barriers = /* @__PURE__ */ new Map();
|
|
2740
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host services. @param load Official module resolver. @param make Optional controlled private-scope factory for tests. */
|
|
2741
|
+
constructor(ctx, load = loadMcp, make) {
|
|
2290
2742
|
this.ctx = ctx;
|
|
2291
2743
|
this.load = load;
|
|
2744
|
+
this.pool = new McpPool(ctx, () => this.getModule(), make);
|
|
2292
2745
|
}
|
|
2293
|
-
/**
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
* @returns Completion of this mutation, including official startup readiness.
|
|
2298
|
-
*/
|
|
2299
|
-
enqueue(action) {
|
|
2300
|
-
const run = async () => {
|
|
2301
|
-
if (!this.stopped) await action();
|
|
2302
|
-
};
|
|
2303
|
-
const next = this.tail.then(run);
|
|
2304
|
-
this.tail = next.catch(() => void 0);
|
|
2305
|
-
return next;
|
|
2746
|
+
/** @private @author ddj 2026年10月20号 @param task Owned background operation. Tracks failures without abandoning work. */
|
|
2747
|
+
track(task) {
|
|
2748
|
+
this.pending.add(task);
|
|
2749
|
+
task.catch((error) => this.ctx.logger?.warn?.("Project MCP task failed: " + String(error))).finally(() => this.pending.delete(task));
|
|
2306
2750
|
}
|
|
2307
|
-
/**
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2751
|
+
/** @private @author ddj 2026年10月20号 @param agent Scope owner. @returns Live record; retired agents cannot be resurrected. */
|
|
2752
|
+
recordOf(agent) {
|
|
2753
|
+
if (this.stopped || this.retired.has(agent)) return void 0;
|
|
2754
|
+
let record = this.agents.get(agent);
|
|
2755
|
+
if (!record) {
|
|
2756
|
+
record = {
|
|
2757
|
+
instances: /* @__PURE__ */ new Map(),
|
|
2758
|
+
resources: /* @__PURE__ */ new Map(),
|
|
2759
|
+
requested: /* @__PURE__ */ new Set(),
|
|
2760
|
+
live: true,
|
|
2761
|
+
tail: Promise.resolve(),
|
|
2762
|
+
startedAt: Date.now()
|
|
2763
|
+
};
|
|
2764
|
+
this.agents.set(agent, record);
|
|
2765
|
+
}
|
|
2766
|
+
return record;
|
|
2767
|
+
}
|
|
2768
|
+
/** @public @author ddj 2026年10月20号 @param agent Newly created scope. Starts only configuration timing. */
|
|
2769
|
+
prepare(agent) {
|
|
2770
|
+
this.recordOf(agent);
|
|
2771
|
+
}
|
|
2772
|
+
/** @public @author ddj 2026年10月20号 @param agent Owner. @param error Configuration failure, never sent raw to clients. */
|
|
2773
|
+
configError(agent, error) {
|
|
2774
|
+
const record = this.recordOf(agent);
|
|
2775
|
+
if (record) record.error = error === void 0 ? void 0 : "项目 MCP 配置读取或校验失败,保留最后有效配置;请查看 Host 日志";
|
|
2776
|
+
}
|
|
2777
|
+
/** @public @author ddj 2026年09月28号 @param agent Agent identity. @returns Longest registered workspace containing its cwd. */
|
|
2313
2778
|
workspace(agent) {
|
|
2314
2779
|
const paths = (this.ctx.get("workspaceRegistry")?.list?.() ?? []).map((ws) => ws.path);
|
|
2315
2780
|
return matchWorkspace(cwdOfSession(this.ctx, agent?.session) ?? void 0, paths);
|
|
2316
2781
|
}
|
|
2317
|
-
/**
|
|
2318
|
-
* Read configured server ownership, including disabled definitions.
|
|
2319
|
-
* @public @author ddj 2026年09月28号
|
|
2320
|
-
* @returns Server name to workspace mapping.
|
|
2321
|
-
*/
|
|
2782
|
+
/** @public @author ddj 2026年09月28号 @returns Configured namespaces including disabled definitions. */
|
|
2322
2783
|
owners() {
|
|
2323
2784
|
const names = /* @__PURE__ */ new Map();
|
|
2324
2785
|
for (const record of this.agents.values()) for (const [name, instance] of record.instances) names.set(name, instance.path);
|
|
2325
2786
|
for (const [path, defs] of this.projects) for (const name of defs.keys()) names.set(name, path);
|
|
2326
2787
|
return names;
|
|
2327
2788
|
}
|
|
2328
|
-
/**
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
*/
|
|
2789
|
+
/** @private @author ddj 2026年10月20号 @param def Effective definition. @returns Config and policy generation identity. */
|
|
2790
|
+
defKey(def) {
|
|
2791
|
+
return JSON.stringify([def.config, parseMcpPolicy(def.policy)]);
|
|
2792
|
+
}
|
|
2793
|
+
/** @private @author ddj 2026年09月28号 Refreshes existing isolation restrictions synchronously. */
|
|
2333
2794
|
changed() {
|
|
2334
2795
|
this.ctx.emit?.("vscode-mode/mcp-change");
|
|
2335
2796
|
}
|
|
2336
|
-
/**
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2797
|
+
/** @private @author ddj 2026年10月20号 @param instance Public lease to revoke. Revokes synchronously before lease release. */
|
|
2798
|
+
revoke(instance) {
|
|
2799
|
+
instance.binding.dispose();
|
|
2800
|
+
instance.lease.release();
|
|
2801
|
+
}
|
|
2802
|
+
/** @public @author ddj 2026年10月20号 @param path Owner workspace. @param defs Validated snapshot. @param refresh Optional explicitly invalidated service. @param wait Await configuration cleanup, never connection readiness. @returns Publication and optionally cleanup completion. */
|
|
2803
|
+
async replace(path, defs, refresh, wait = true) {
|
|
2804
|
+
if (this.stopped) return;
|
|
2805
|
+
const previous = this.projects.get(path);
|
|
2806
|
+
if (!refresh && previous && JSON.stringify([...previous]) === JSON.stringify([...defs])) return;
|
|
2807
|
+
this.projects.set(path, defs);
|
|
2808
|
+
const closing = /* @__PURE__ */ new Set();
|
|
2809
|
+
for (const [agent, record] of this.agents) for (const [name, instance] of record.instances) {
|
|
2810
|
+
const def = defs.get(name);
|
|
2811
|
+
if (instance.path !== path) continue;
|
|
2812
|
+
if (this.workspace(agent) === path && def?.enabled && this.defKey(def) === instance.key && name !== refresh) continue;
|
|
2813
|
+
record.instances.delete(name);
|
|
2814
|
+
this.revoke(instance);
|
|
2815
|
+
closing.add(instance.lease.entry);
|
|
2816
|
+
}
|
|
2817
|
+
for (const entry of this.pool.entries.values()) {
|
|
2818
|
+
if (entry.spec.workspace !== path) continue;
|
|
2819
|
+
const def = defs.get(entry.spec.config.serverName);
|
|
2820
|
+
const config = def && this.options(def.config, path);
|
|
2821
|
+
if (!def?.enabled || JSON.stringify(config) !== JSON.stringify(entry.spec.config) || JSON.stringify(parseMcpPolicy(def.policy)) !== JSON.stringify(entry.spec.policy) || entry.spec.config.serverName === refresh) closing.add(entry);
|
|
2822
|
+
}
|
|
2823
|
+
const prior = this.barriers.get(path) ?? Promise.resolve();
|
|
2824
|
+
const cleanup = Promise.all([...closing].map((entry) => this.pool.retire(entry))).then(() => void 0);
|
|
2825
|
+
const barrier = Promise.all([prior, cleanup]).then(() => void 0);
|
|
2826
|
+
this.barriers.set(path, barrier);
|
|
2827
|
+
this.track(barrier);
|
|
2828
|
+
this.changed();
|
|
2829
|
+
const updates = [...this.agents].map(([agent, record]) => this.enqueue(agent, record));
|
|
2830
|
+
if (wait) await Promise.all([barrier, ...updates]);
|
|
2831
|
+
}
|
|
2832
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Lightweight queue. @returns Scoped catalogue/resource update, not transport initialization. */
|
|
2833
|
+
enqueue(agent, record) {
|
|
2345
2834
|
const update = async () => {
|
|
2346
|
-
this.
|
|
2347
|
-
this.
|
|
2348
|
-
for (const [agent, record] of this.agents) await this.syncAgent(agent, record, this.workspace(agent) === path ? refresh : void 0);
|
|
2349
|
-
this.changed();
|
|
2835
|
+
if (!record.live || this.stopped) return;
|
|
2836
|
+
this.syncAgent(agent, record);
|
|
2350
2837
|
};
|
|
2351
|
-
|
|
2838
|
+
const task = record.tail.then(update);
|
|
2839
|
+
record.tail = task.catch(() => {
|
|
2840
|
+
record.error = "MCP 清理或入口更新失败,请查看 Host 日志";
|
|
2841
|
+
});
|
|
2842
|
+
this.track(task);
|
|
2843
|
+
return task;
|
|
2352
2844
|
}
|
|
2353
|
-
/**
|
|
2354
|
-
* Attach an agent once and await its project plugin startups.
|
|
2355
|
-
* @public @author ddj 2026年09月28号
|
|
2356
|
-
* @param agent Created or already-active agent.
|
|
2357
|
-
* @returns Completion suitable for the serial agent/created listener.
|
|
2358
|
-
*/
|
|
2845
|
+
/** @public @author ddj 2026年10月20号 @param agent Live agent. @returns Lightweight capability installation; never waits for MCP startup. */
|
|
2359
2846
|
attach(agent) {
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2847
|
+
const record = this.recordOf(agent);
|
|
2848
|
+
if (!record) return Promise.resolve();
|
|
2849
|
+
this.offDiscover ??= installDiscover(this, this.ctx);
|
|
2850
|
+
if (!record.remove) record.remove = discoverPrompt(this, agent);
|
|
2851
|
+
return this.enqueue(agent, record);
|
|
2852
|
+
}
|
|
2853
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Scope record. Updates lazy resources and schedules explicit background services. */
|
|
2854
|
+
syncAgent(agent, record) {
|
|
2855
|
+
const path = this.workspace(agent);
|
|
2856
|
+
const defs = this.projects.get(path ?? "") ?? /* @__PURE__ */ new Map();
|
|
2857
|
+
for (const [name, instance] of record.instances) {
|
|
2858
|
+
const def = defs.get(name);
|
|
2859
|
+
if (instance.path === path && def?.enabled && this.defKey(def) === instance.key) continue;
|
|
2860
|
+
record.instances.delete(name);
|
|
2861
|
+
this.revoke(instance);
|
|
2368
2862
|
}
|
|
2369
|
-
const
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2863
|
+
for (const remove of record.resources.values()) remove();
|
|
2864
|
+
record.resources.clear();
|
|
2865
|
+
for (const [name, def] of defs) {
|
|
2866
|
+
if (!def.enabled) continue;
|
|
2867
|
+
this.lazyResource(agent, record, name);
|
|
2868
|
+
if (!record.instances.has(name) && (parseMcpPolicy(def.policy).startup === "background" || record.requested.has(name))) this.track(this.connect(agent, name));
|
|
2869
|
+
}
|
|
2870
|
+
record.finishedAt ??= Date.now();
|
|
2871
|
+
this.changed();
|
|
2872
|
+
}
|
|
2873
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @param name Enabled namespace. Installs a resource-only on-demand entry without starting a connection. */
|
|
2874
|
+
lazyResource(agent, record, name) {
|
|
2875
|
+
const resources = agent.ctx.get("mcpResources");
|
|
2876
|
+
/** @author ddj 2026年10月20号 @param request Resource operation. @param execution Actual registry caller. @returns Official provider result after demand connection. */
|
|
2877
|
+
const request = async (request, execution) => {
|
|
2878
|
+
if (execution?.agent !== agent) throw new Error("不能借用其他会话的 MCP 资源");
|
|
2879
|
+
await this.connect(agent, name, execution.signal);
|
|
2880
|
+
const instance = record.instances.get(name);
|
|
2881
|
+
if (!instance || !this.valid(agent, record, name, instance)) throw new Error("项目 MCP 配置已失效");
|
|
2882
|
+
return this.pool.run(instance.lease.entry, async () => {
|
|
2883
|
+
if (!this.valid(agent, record, name, instance)) throw new Error("项目 MCP 配置已失效");
|
|
2884
|
+
const provider = instance.lease.entry.bridge?.resources.get(name);
|
|
2885
|
+
if (!provider) throw new Error("MCP 未提供资源入口");
|
|
2886
|
+
const startedAt = Date.now();
|
|
2887
|
+
const result = await provider.request(request, execution);
|
|
2888
|
+
instance.firstCallMs ??= Date.now() - startedAt;
|
|
2889
|
+
return result;
|
|
2890
|
+
}, execution.signal);
|
|
2373
2891
|
};
|
|
2374
|
-
|
|
2892
|
+
record.resources.set(name, resources.register(name, { request }));
|
|
2375
2893
|
}
|
|
2376
|
-
/**
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
*/
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2894
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @param name Namespace. @param instance Exact lease. @returns Whether a previously issued proxy can still execute. */
|
|
2895
|
+
valid(agent, record, name, instance) {
|
|
2896
|
+
const path = this.workspace(agent);
|
|
2897
|
+
const def = this.projects.get(path ?? "")?.get(name);
|
|
2898
|
+
return !this.stopped && record.live && record.instances.get(name) === instance && instance.path === path && def?.enabled === true && this.defKey(def) === instance.key;
|
|
2899
|
+
}
|
|
2900
|
+
/** @private @author ddj 2026年10月20号 @param config Official config. @param path Default working directory. @returns Detached effective config; project strategy is never forwarded. */
|
|
2901
|
+
options(config, path) {
|
|
2902
|
+
return config.transport === "stdio" ? {
|
|
2903
|
+
...config,
|
|
2904
|
+
cwd: config.cwd || path
|
|
2905
|
+
} : { ...config };
|
|
2906
|
+
}
|
|
2907
|
+
/** @public @author ddj 2026年10月20号 @param agent Actual execution owner. @returns Safe enabled catalogue without opening connections. */
|
|
2908
|
+
catalogue(agent) {
|
|
2909
|
+
const record = this.agents.get(agent);
|
|
2910
|
+
if (!record?.live || this.stopped) return [];
|
|
2911
|
+
return [...this.projects.get(this.workspace(agent) ?? "") ?? []].filter(([, def]) => def.enabled).map(([serverName]) => {
|
|
2912
|
+
const entry = record.instances.get(serverName)?.lease.entry;
|
|
2913
|
+
return {
|
|
2914
|
+
serverName,
|
|
2915
|
+
phase: entry?.fiber?.state === 3 ? "error" : entry?.phase ?? "idle"
|
|
2916
|
+
};
|
|
2917
|
+
});
|
|
2387
2918
|
}
|
|
2388
|
-
/**
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2919
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @param path Workspace. @param name Namespace. @param def Validated definition. @returns Synchronously reserved lease and public binding. */
|
|
2920
|
+
reserve(agent, record, path, name, def) {
|
|
2921
|
+
const lease = this.pool.acquire({
|
|
2922
|
+
workspace: path,
|
|
2923
|
+
owner: agent,
|
|
2924
|
+
config: this.options(def.config, path),
|
|
2925
|
+
policy: parseMcpPolicy(def.policy)
|
|
2926
|
+
});
|
|
2927
|
+
const instance = {
|
|
2928
|
+
key: this.defKey(def),
|
|
2929
|
+
path,
|
|
2930
|
+
lease,
|
|
2931
|
+
startedAt: Date.now(),
|
|
2932
|
+
reused: lease.entry.refs > 1 || lease.entry.phase === "ready"
|
|
2399
2933
|
};
|
|
2400
|
-
|
|
2934
|
+
const valid = () => this.valid(agent, record, name, instance);
|
|
2935
|
+
instance.binding = new McpBinding(agent, this.pool, lease.entry, valid, (elapsed) => {
|
|
2936
|
+
instance.firstCallMs ??= elapsed;
|
|
2937
|
+
});
|
|
2938
|
+
record.instances.set(name, instance);
|
|
2939
|
+
instance.ready = lease.entry.ready.then(() => {
|
|
2940
|
+
if (!valid()) throw new Error("项目 MCP 配置已失效");
|
|
2941
|
+
instance.binding.sync();
|
|
2942
|
+
this.changed();
|
|
2943
|
+
});
|
|
2944
|
+
this.track(instance.ready);
|
|
2945
|
+
return instance;
|
|
2401
2946
|
}
|
|
2402
|
-
/**
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2947
|
+
/** @public @author ddj 2026年10月20号 @param agent Actual active caller. @param name Enabled configured server. @param signal Caller-local cancellation. @returns Real tool schemas after private initial sync; no guessed tools. */
|
|
2948
|
+
async connect(agent, name, signal) {
|
|
2949
|
+
const record = this.agents.get(agent);
|
|
2950
|
+
if (!record?.live || this.stopped) throw new Error("会话未激活,不能连接项目 MCP");
|
|
2951
|
+
await waitMcp(record.tail, signal);
|
|
2952
|
+
const path = this.workspace(agent);
|
|
2953
|
+
await waitMcp(this.barriers.get(path ?? "") ?? Promise.resolve(), signal);
|
|
2954
|
+
if (!record.live || this.workspace(agent) !== path) throw new Error("项目 MCP 作用域已失效");
|
|
2955
|
+
const def = this.projects.get(path ?? "")?.get(name);
|
|
2956
|
+
if (!def?.enabled || !path) throw new Error("当前工作区未启用该项目 MCP");
|
|
2957
|
+
if (entriesOf(this.ctx).some((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? "")) && entry.options?.config?.serverName === name)) throw new Error("serverName 已被全局 MCP 使用");
|
|
2958
|
+
let instance = record.instances.get(name);
|
|
2959
|
+
if (instance?.lease.entry.phase === "error" && (instance.lease.entry.retryAt ?? Infinity) <= Date.now()) {
|
|
2960
|
+
record.instances.delete(name);
|
|
2961
|
+
this.revoke(instance);
|
|
2962
|
+
instance = void 0;
|
|
2963
|
+
}
|
|
2964
|
+
record.requested.add(name);
|
|
2965
|
+
instance ??= this.reserve(agent, record, path, name, def);
|
|
2966
|
+
await waitMcp(instance.ready, signal);
|
|
2967
|
+
if (!this.valid(agent, record, name, instance)) throw new Error("项目 MCP 配置已失效");
|
|
2968
|
+
const visible = agent.ctx.get("tools")?.view?.(agent)?.visible;
|
|
2969
|
+
return {
|
|
2970
|
+
serverName: name,
|
|
2971
|
+
tools: [...instance.lease.entry.bridge.tools.values()].filter((tool) => visible?.has(tool.name)).map((tool) => ({
|
|
2972
|
+
name: tool.name,
|
|
2973
|
+
description: tool.description,
|
|
2974
|
+
parameters: tool.parameters
|
|
2975
|
+
})),
|
|
2976
|
+
resources: instance.lease.entry.bridge.resources.has(name),
|
|
2977
|
+
nextRequest: "Use these tools in the next model request; PTC must start a new program to refresh its SDK."
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
/** @public @author ddj 2026年10月20号 @param agent Disposed owner. @returns Scoped teardown; shared transports remain owned by other leases. */
|
|
2408
2981
|
async detach(agent) {
|
|
2409
2982
|
this.retired.add(agent);
|
|
2410
2983
|
const record = this.agents.get(agent);
|
|
2411
2984
|
if (!record) return;
|
|
2412
2985
|
record.live = false;
|
|
2413
|
-
|
|
2986
|
+
record.remove?.();
|
|
2987
|
+
for (const remove of record.resources.values()) remove();
|
|
2988
|
+
for (const instance of record.instances.values()) this.revoke(instance);
|
|
2989
|
+
record.instances.clear();
|
|
2414
2990
|
this.agents.delete(agent);
|
|
2415
2991
|
this.changed();
|
|
2416
2992
|
}
|
|
2417
|
-
/**
|
|
2418
|
-
* Match the agent ledger to the current enabled definitions.
|
|
2419
|
-
* @private @author ddj 2026年09月28号
|
|
2420
|
-
* @param agent Scoped plugin owner.
|
|
2421
|
-
* @param record Agent ledger.
|
|
2422
|
-
* @param refresh Optional server to reconnect.
|
|
2423
|
-
* @returns Completion of removals and scoped mounts.
|
|
2424
|
-
*/
|
|
2425
|
-
async syncAgent(agent, record, refresh) {
|
|
2426
|
-
if (!record.live || this.stopped) return;
|
|
2427
|
-
const path = this.workspace(agent);
|
|
2428
|
-
const defs = this.projects.get(path ?? "") ?? /* @__PURE__ */ new Map();
|
|
2429
|
-
for (const [name, instance] of record.instances) {
|
|
2430
|
-
const def = defs.get(name);
|
|
2431
|
-
if (def?.enabled && instance.path === path && instance.key === JSON.stringify(def.config) && name !== refresh) continue;
|
|
2432
|
-
await this.stopOne(record, name);
|
|
2433
|
-
}
|
|
2434
|
-
for (const [name, def] of defs) {
|
|
2435
|
-
if (!record.live || this.stopped) break;
|
|
2436
|
-
if (!def.enabled || record.instances.has(name)) continue;
|
|
2437
|
-
await this.mount(agent, record, def.config, path);
|
|
2438
|
-
}
|
|
2439
|
-
}
|
|
2440
|
-
/**
|
|
2441
|
-
* Cache successful module loads while allowing explicit refresh after transient failures.
|
|
2442
|
-
* @private @author ddj 2026年10月08号
|
|
2443
|
-
* @returns Official plugin module; load failures propagate to the instance diagnostic.
|
|
2444
|
-
*/
|
|
2993
|
+
/** @private @author ddj 2026年10月08号 @returns Cached official module; failures are retryable. */
|
|
2445
2994
|
async getModule() {
|
|
2446
2995
|
const loading = this.module ??= this.load();
|
|
2447
2996
|
try {
|
|
@@ -2451,100 +3000,113 @@ var McpRuntime = class {
|
|
|
2451
3000
|
throw error;
|
|
2452
3001
|
}
|
|
2453
3002
|
}
|
|
2454
|
-
/**
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
* @param agent Scoped context owner.
|
|
2458
|
-
* @param record Agent ledger.
|
|
2459
|
-
* @param config Validated source config.
|
|
2460
|
-
* @param path Default stdio working directory.
|
|
2461
|
-
* @returns Settled startup; errors are retained for the project status view.
|
|
2462
|
-
*/
|
|
2463
|
-
async mount(agent, record, config, path) {
|
|
2464
|
-
const instance = {
|
|
2465
|
-
key: JSON.stringify(config),
|
|
2466
|
-
path
|
|
2467
|
-
};
|
|
2468
|
-
record.instances.set(config.serverName, instance);
|
|
2469
|
-
try {
|
|
2470
|
-
const officialMcp = await this.getModule();
|
|
2471
|
-
if (!record.live || this.stopped) return;
|
|
2472
|
-
if (entriesOf(this.ctx).find((entry) => !isProjectEntryId(String(entry.id ?? entry.options?.id ?? "")) && entry.options?.config?.serverName === config.serverName)) throw new Error("serverName 已被全局 MCP 使用:" + config.serverName);
|
|
2473
|
-
const options = config.transport === "stdio" ? {
|
|
2474
|
-
...config,
|
|
2475
|
-
cwd: config.cwd || path
|
|
2476
|
-
} : { ...config };
|
|
2477
|
-
instance.fiber = agent.ctx.plugin(officialMcp, options);
|
|
2478
|
-
await instance.fiber;
|
|
2479
|
-
} catch (error) {
|
|
2480
|
-
instance.error = String(error);
|
|
2481
|
-
await this.stopFiber(instance);
|
|
2482
|
-
instance.fiber = void 0;
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
/**
|
|
2486
|
-
* Build project status from exact agent scopes without assuming ACTIVE means connected.
|
|
2487
|
-
* @public @author ddj 2026年09月28号
|
|
2488
|
-
* @param path Workspace to summarize.
|
|
2489
|
-
* @returns Masked server views, including configured-only and disabled definitions.
|
|
2490
|
-
*/
|
|
2491
|
-
views(path) {
|
|
2492
|
-
return [...this.projects.get(path) ?? /* @__PURE__ */ new Map()].map(([name, def]) => this.serverView(path, name, def));
|
|
2493
|
-
}
|
|
2494
|
-
/**
|
|
2495
|
-
* Summarize one definition and its connection instances across matching agents.
|
|
2496
|
-
* @private @author ddj 2026年09月28号
|
|
2497
|
-
* @param path Owning workspace.
|
|
2498
|
-
* @param name Server name.
|
|
2499
|
-
* @param def Persisted definition.
|
|
2500
|
-
* @returns Honest status and union of tools visible to its agents.
|
|
2501
|
-
*/
|
|
2502
|
-
serverView(path, name, def) {
|
|
2503
|
-
const tools = /* @__PURE__ */ new Map();
|
|
2504
|
-
const instances = [];
|
|
3003
|
+
/** @public @author ddj 2026年10月20号 @returns Detached read-only active session statuses, without IO or activation. */
|
|
3004
|
+
sessionSnapshot() {
|
|
3005
|
+
const sessions = [];
|
|
2505
3006
|
for (const [agent, record] of this.agents) {
|
|
2506
|
-
if (
|
|
2507
|
-
const
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
3007
|
+
if (!record.live || typeof agent?.session?.id !== "string") continue;
|
|
3008
|
+
const servers = this.sessionServers(agent, record);
|
|
3009
|
+
const active = servers.find((server) => [
|
|
3010
|
+
"stopping",
|
|
3011
|
+
"starting",
|
|
3012
|
+
"loading",
|
|
3013
|
+
"queued"
|
|
3014
|
+
].includes(server.phase));
|
|
3015
|
+
let phase = "ready";
|
|
3016
|
+
if (record.error || servers.some((server) => server.phase === "error")) phase = "error";
|
|
3017
|
+
else if (active) phase = active.phase;
|
|
3018
|
+
else if (!record.finishedAt) phase = "preparing";
|
|
3019
|
+
else if (!servers.length) phase = "none";
|
|
3020
|
+
else if (servers.some((server) => server.phase === "idle")) phase = "idle";
|
|
3021
|
+
const attempts = servers.filter((server) => server.phase !== "idle");
|
|
3022
|
+
const startedAt = attempts.length ? Math.min(...attempts.map((server) => server.startedAt)) : record.startedAt;
|
|
3023
|
+
sessions.push({
|
|
3024
|
+
configMs: Math.max(0, (record.finishedAt ?? Date.now()) - record.startedAt),
|
|
3025
|
+
sessionId: agent.session.id,
|
|
3026
|
+
phase,
|
|
3027
|
+
startedAt,
|
|
3028
|
+
phaseAt: active?.phaseAt ?? startedAt,
|
|
3029
|
+
finishedAt: active ? void 0 : Math.max(record.finishedAt ?? record.startedAt, ...servers.map((server) => server.finishedAt ?? 0)),
|
|
3030
|
+
error: record.error,
|
|
3031
|
+
servers
|
|
3032
|
+
});
|
|
2511
3033
|
}
|
|
2512
|
-
const error = instances.find((item) => item.error)?.error;
|
|
2513
|
-
const fibers = instances.filter((item) => item.fiber);
|
|
2514
|
-
const failed = error || fibers.some((item) => item.fiber.state === 3);
|
|
2515
|
-
const status = !def.enabled ? "disabled" : failed ? "error" : fibers.length === 0 ? "configured" : fibers.some((item) => item.fiber.state === 1 || item.fiber.state === 0) ? "connecting" : "unverified";
|
|
2516
3034
|
return {
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
enabled: def.enabled,
|
|
2520
|
-
transport: def.config.transport,
|
|
2521
|
-
config: publicConfig(def.config),
|
|
2522
|
-
status,
|
|
2523
|
-
instanceCount: fibers.length,
|
|
2524
|
-
toolCount: tools.size,
|
|
2525
|
-
tools: [...tools.values()],
|
|
2526
|
-
error: error || (failed ? "MCP 插件未正常运行" : void 0)
|
|
3035
|
+
sessions,
|
|
3036
|
+
sampledAt: Date.now()
|
|
2527
3037
|
};
|
|
2528
3038
|
}
|
|
2529
|
-
/**
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
3039
|
+
/** @private @author ddj 2026年10月20号 @param agent Owner. @param record Ledger. @returns Server phases with caller-local timing, not pooled connection age. */
|
|
3040
|
+
sessionServers(agent, record) {
|
|
3041
|
+
return this.catalogue(agent).map(({ serverName, phase }) => {
|
|
3042
|
+
const instance = record.instances.get(serverName);
|
|
3043
|
+
if (!instance) return {
|
|
3044
|
+
serverName,
|
|
3045
|
+
phase: "idle",
|
|
3046
|
+
startedAt: record.finishedAt ?? record.startedAt,
|
|
3047
|
+
phaseAt: record.finishedAt ?? record.startedAt
|
|
3048
|
+
};
|
|
3049
|
+
const entry = instance.lease.entry;
|
|
3050
|
+
return {
|
|
3051
|
+
serverName,
|
|
3052
|
+
phase,
|
|
3053
|
+
startedAt: instance.startedAt,
|
|
3054
|
+
phaseAt: Math.max(instance.startedAt, entry.phaseAt),
|
|
3055
|
+
finishedAt: entry.finishedAt && Math.max(instance.startedAt, entry.finishedAt),
|
|
3056
|
+
error: entry.error,
|
|
3057
|
+
reused: instance.reused,
|
|
3058
|
+
queuedMs: Math.max(0, (entry.startAt ?? Date.now()) - instance.startedAt),
|
|
3059
|
+
connectMs: entry.connectedAt === void 0 ? void 0 : Math.max(0, entry.connectedAt - Math.max(instance.startedAt, entry.startAt ?? instance.startedAt)),
|
|
3060
|
+
firstCallMs: instance.firstCallMs
|
|
3061
|
+
};
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
/** @public @author ddj 2026年09月28号 @param path Workspace. @returns Masked configuration and pool/lease counts; initialization is not health. */
|
|
3065
|
+
views(path) {
|
|
3066
|
+
return [...this.projects.get(path) ?? []].map(([name, def]) => {
|
|
3067
|
+
const entries = [...this.pool.entries.values()].filter((entry) => entry.spec.workspace === path && entry.spec.config.serverName === name);
|
|
3068
|
+
const tools = /* @__PURE__ */ new Map();
|
|
3069
|
+
for (const [agent] of this.agents) if (this.workspace(agent) === path) for (const tool of toolsOf(this.ctx, name, agent)) tools.set(tool.name, tool);
|
|
3070
|
+
const failed = entries.some((entry) => entry.phase === "error" || entry.error);
|
|
3071
|
+
const busy = entries.some((entry) => [
|
|
3072
|
+
"queued",
|
|
3073
|
+
"loading",
|
|
3074
|
+
"starting",
|
|
3075
|
+
"stopping"
|
|
3076
|
+
].includes(entry.phase));
|
|
3077
|
+
let status = "configured";
|
|
3078
|
+
if (!def.enabled) status = "disabled";
|
|
3079
|
+
else if (failed) status = "error";
|
|
3080
|
+
else if (busy) status = "connecting";
|
|
3081
|
+
else if (entries.some((entry) => entry.phase === "ready")) status = "unverified";
|
|
3082
|
+
return {
|
|
3083
|
+
id: projectEntryId(path, name),
|
|
3084
|
+
serverName: name,
|
|
3085
|
+
enabled: def.enabled,
|
|
3086
|
+
transport: def.config.transport,
|
|
3087
|
+
config: publicConfig(def.config),
|
|
3088
|
+
policy: parseMcpPolicy(def.policy),
|
|
3089
|
+
status,
|
|
3090
|
+
instanceCount: entries.filter((entry) => entry.fiber).length,
|
|
3091
|
+
sessionCount: entries.reduce((total, entry) => total + entry.refs, 0),
|
|
3092
|
+
tools: [...tools.values()],
|
|
3093
|
+
toolCount: tools.size,
|
|
3094
|
+
error: failed ? "MCP 初始化或清理异常,请查看 Host 日志" : void 0
|
|
3095
|
+
};
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
/** @public @author ddj 2026年10月20号 @returns Complete owned transport and capability cleanup. */
|
|
2534
3099
|
async dispose() {
|
|
2535
3100
|
this.stopped = true;
|
|
3101
|
+
this.offDiscover?.();
|
|
2536
3102
|
await Promise.all([...this.agents.keys()].map((agent) => this.detach(agent)));
|
|
2537
|
-
await this.
|
|
3103
|
+
await this.pool.dispose();
|
|
3104
|
+
await Promise.allSettled([...this.pending]);
|
|
2538
3105
|
this.projects.clear();
|
|
2539
3106
|
this.module = void 0;
|
|
2540
3107
|
}
|
|
2541
3108
|
};
|
|
2542
|
-
/**
|
|
2543
|
-
* Remove every old global project entry before any agent-scoped mount is attempted.
|
|
2544
|
-
* @public @author ddj 2026年09月28号
|
|
2545
|
-
* @param ctx Host loader context.
|
|
2546
|
-
* @returns Completion of loader removal and connection teardown.
|
|
2547
|
-
*/
|
|
3109
|
+
/** @public @author ddj 2026年09月28号 @param ctx Host loader. @returns Removal of legacy global project entries before any scoped entry is installed. */
|
|
2548
3110
|
async function clearLegacy(ctx) {
|
|
2549
3111
|
const loader = ctx.get("loader");
|
|
2550
3112
|
for (const entry of entriesOf(ctx)) {
|
|
@@ -2552,35 +3114,41 @@ async function clearLegacy(ctx) {
|
|
|
2552
3114
|
await loader.remove(entry.id);
|
|
2553
3115
|
}
|
|
2554
3116
|
}
|
|
2555
|
-
/**
|
|
2556
|
-
* Install awaited creation hooks, migrate global entries, and attach existing agents.
|
|
2557
|
-
* @public @author ddj 2026年09月28号
|
|
2558
|
-
* @param ctx Host lifecycle context; installIsolation must already be installed.
|
|
2559
|
-
* @returns Readiness for index.apply to await before advertising startup completion.
|
|
2560
|
-
*/
|
|
3117
|
+
/** @public @author ddj 2026年10月20号 @param ctx Host lifecycle. @returns Config/catalogue readiness, independent from all project MCP handshakes. */
|
|
2561
3118
|
async function installMcpRuntime(ctx) {
|
|
2562
3119
|
const runtime = runtimeOf(ctx);
|
|
2563
3120
|
let ready;
|
|
3121
|
+
/** @author ddj 2026年10月20号 Loads validated snapshots before exposing lazy capabilities. */
|
|
2564
3122
|
const initialize = async () => {
|
|
2565
3123
|
await clearLegacy(ctx);
|
|
2566
|
-
await listProjects(ctx);
|
|
2567
|
-
for (const agent of ctx.get("agents")?.list?.() ?? [])
|
|
3124
|
+
const { projects } = await listProjects(ctx, false);
|
|
3125
|
+
for (const agent of ctx.get("agents")?.list?.() ?? []) {
|
|
3126
|
+
runtime.prepare(agent);
|
|
3127
|
+
const project = projects.find((item) => item.workspacePath === runtime.workspace(agent));
|
|
3128
|
+
if (project?.fileError) runtime.configError(agent, project.fileError);
|
|
3129
|
+
}
|
|
2568
3130
|
};
|
|
3131
|
+
/** @author ddj 2026年10月20号 @param event Actual created agent. Reads configuration but never waits for connection startup. */
|
|
2569
3132
|
const created = async ({ agent }) => {
|
|
3133
|
+
runtime.prepare(agent);
|
|
2570
3134
|
await ready;
|
|
2571
3135
|
const path = runtime.workspace(agent);
|
|
2572
3136
|
if (path) try {
|
|
2573
|
-
await reconcileProject(ctx, path);
|
|
3137
|
+
await reconcileProject(ctx, path, void 0, false);
|
|
3138
|
+
runtime.configError(agent);
|
|
2574
3139
|
} catch (error) {
|
|
3140
|
+
runtime.configError(agent, error);
|
|
2575
3141
|
ctx.logger?.warn?.("MCP configuration retained: " + String(error));
|
|
2576
3142
|
}
|
|
2577
3143
|
await runtime.attach(agent);
|
|
2578
3144
|
};
|
|
3145
|
+
/** @author ddj 2026年10月20号 @param event Actual disposed agent. Revokes public access synchronously. */
|
|
2579
3146
|
const disposed = ({ agent }) => {
|
|
2580
3147
|
runtime.detach(agent).catch((error) => ctx.logger?.warn?.("MCP disposal failed: " + String(error)));
|
|
2581
3148
|
};
|
|
2582
3149
|
const offCreate = ctx.on("agent/created", created);
|
|
2583
3150
|
const offDispose = ctx.on("agent/disposed", disposed);
|
|
3151
|
+
/** @author ddj 2026年10月20号 @returns Full lifecycle teardown. */
|
|
2584
3152
|
const cleanup = async () => {
|
|
2585
3153
|
offCreate?.();
|
|
2586
3154
|
offDispose?.();
|
|
@@ -2590,6 +3158,7 @@ async function installMcpRuntime(ctx) {
|
|
|
2590
3158
|
ctx.effect(() => cleanup, "vscode-mode:mcp-runtime");
|
|
2591
3159
|
ready = initialize();
|
|
2592
3160
|
await ready;
|
|
3161
|
+
await Promise.all((ctx.get("agents")?.list?.() ?? []).map((agent) => runtime.attach(agent)));
|
|
2593
3162
|
}
|
|
2594
3163
|
//#endregion
|
|
2595
3164
|
//#region src/mcpProject.ts
|
|
@@ -2773,9 +3342,10 @@ async function checkMcpName(ctx, serverName, workspacePath) {
|
|
|
2773
3342
|
* @param ctx Host services.
|
|
2774
3343
|
* @param workspacePath Registered workspace.
|
|
2775
3344
|
* @param refresh Optional unchanged server to reconnect.
|
|
2776
|
-
* @
|
|
3345
|
+
* @param wait Await connection synchronization for management, or only publication/scheduling for creation.
|
|
3346
|
+
* @returns Configuration publication, optionally including scoped fiber synchronization.
|
|
2777
3347
|
*/
|
|
2778
|
-
async function reconcileProject(ctx, workspacePath, refresh) {
|
|
3348
|
+
async function reconcileProject(ctx, workspacePath, refresh, wait = true) {
|
|
2779
3349
|
const data = await readProjectJson(ctx, workspacePath);
|
|
2780
3350
|
const defs = /* @__PURE__ */ new Map();
|
|
2781
3351
|
for (const [name, def] of Object.entries(serversOf(data))) {
|
|
@@ -2784,11 +3354,12 @@ async function reconcileProject(ctx, workspacePath, refresh) {
|
|
|
2784
3354
|
await checkMcpName(ctx, name, workspacePath);
|
|
2785
3355
|
defs.set(name, {
|
|
2786
3356
|
config,
|
|
2787
|
-
enabled: def.disabled !== true
|
|
3357
|
+
enabled: def.disabled !== true,
|
|
3358
|
+
policy: parseMcpPolicy(def.vscodeMode)
|
|
2788
3359
|
});
|
|
2789
3360
|
}
|
|
2790
3361
|
await clearLegacy(ctx);
|
|
2791
|
-
await runtimeOf(ctx).replace(workspacePath, defs, refresh);
|
|
3362
|
+
await runtimeOf(ctx).replace(workspacePath, defs, refresh, wait);
|
|
2792
3363
|
}
|
|
2793
3364
|
/**
|
|
2794
3365
|
* List one project, retaining the last good runtime view on file errors.
|
|
@@ -2798,7 +3369,7 @@ async function reconcileProject(ctx, workspacePath, refresh) {
|
|
|
2798
3369
|
* @param title Workspace title.
|
|
2799
3370
|
* @returns Configuration and honest runtime status with any read error attached.
|
|
2800
3371
|
*/
|
|
2801
|
-
async function projectOf(ctx, workspacePath, title) {
|
|
3372
|
+
async function projectOf(ctx, workspacePath, title, wait = true) {
|
|
2802
3373
|
let fileError;
|
|
2803
3374
|
let missingDir;
|
|
2804
3375
|
try {
|
|
@@ -2806,7 +3377,7 @@ async function projectOf(ctx, workspacePath, title) {
|
|
|
2806
3377
|
if (!fs) throw new Error("缺少 fs 服务");
|
|
2807
3378
|
const info = await fs.stat(await fs.resolve(workspacePath));
|
|
2808
3379
|
missingDir = !info || info.type !== "directory";
|
|
2809
|
-
if (!missingDir) await reconcileProject(ctx, workspacePath);
|
|
3380
|
+
if (!missingDir) await reconcileProject(ctx, workspacePath, void 0, wait);
|
|
2810
3381
|
} catch (error) {
|
|
2811
3382
|
missingDir = isMissingFile(error) || void 0;
|
|
2812
3383
|
fileError = String(error);
|
|
@@ -2833,9 +3404,9 @@ async function projectOf(ctx, workspacePath, title) {
|
|
|
2833
3404
|
* @param ctx Host services.
|
|
2834
3405
|
* @returns Project list; individual malformed files are reported on their project.
|
|
2835
3406
|
*/
|
|
2836
|
-
async function listProjects(ctx) {
|
|
3407
|
+
async function listProjects(ctx, wait = true) {
|
|
2837
3408
|
const projects = [];
|
|
2838
|
-
for (const ws of ctx.get("workspaceRegistry")?.list?.() ?? []) projects.push(await projectOf(ctx, ws.path, ws.title ?? ""));
|
|
3409
|
+
for (const ws of ctx.get("workspaceRegistry")?.list?.() ?? []) projects.push(await projectOf(ctx, ws.path, ws.title ?? "", wait));
|
|
2839
3410
|
return { projects };
|
|
2840
3411
|
}
|
|
2841
3412
|
/**
|
|
@@ -2881,11 +3452,12 @@ function mergeDefinition(previous, config) {
|
|
|
2881
3452
|
* @param config Submitted configuration.
|
|
2882
3453
|
* @returns Updated project view; invalid sources or conflicts reject before writing.
|
|
2883
3454
|
*/
|
|
2884
|
-
async function projectSave(ctx, workspacePath, serverName, config) {
|
|
2885
|
-
|
|
3455
|
+
async function projectSave(ctx, workspacePath, serverName, config, policy) {
|
|
3456
|
+
if (policy !== void 0) parseMcpPolicy(policy);
|
|
3457
|
+
return mcpEdit(ctx, () => saveProject(ctx, workspacePath, serverName, config, policy));
|
|
2886
3458
|
}
|
|
2887
3459
|
/** @private @author ddj 2026年10月08号 @param ctx Host services. @param workspacePath Owner. @param serverName Namespace. @param config Editable fields. @returns Persisted project status. */
|
|
2888
|
-
async function saveProject(ctx, workspacePath, serverName, config) {
|
|
3460
|
+
async function saveProject(ctx, workspacePath, serverName, config, policy) {
|
|
2889
3461
|
const workspace = requireWorkspace$2(ctx, workspacePath);
|
|
2890
3462
|
validateConfig({
|
|
2891
3463
|
...config,
|
|
@@ -2898,6 +3470,7 @@ async function saveProject(ctx, workspacePath, serverName, config) {
|
|
|
2898
3470
|
...config,
|
|
2899
3471
|
serverName
|
|
2900
3472
|
});
|
|
3473
|
+
if (policy !== void 0) servers[serverName].vscodeMode = { ...parseMcpPolicy(policy) };
|
|
2901
3474
|
await writeProjectJson(ctx, workspacePath, {
|
|
2902
3475
|
...data,
|
|
2903
3476
|
mcpServers: servers
|
|
@@ -3180,6 +3753,7 @@ async function saveMcp(ctx, config) {
|
|
|
3180
3753
|
}
|
|
3181
3754
|
/** @private @author ddj 2026年10月08号 @param ctx Host services. @param config Configuration. @returns Saved status; validation failures do not write. */
|
|
3182
3755
|
async function saveGlobal(ctx, config) {
|
|
3756
|
+
if (config && "vscodeMode" in config) throw new Error("vscodeMode 启动和共享策略仅适用于项目 MCP");
|
|
3183
3757
|
validateConfig(config);
|
|
3184
3758
|
await checkMcpName(ctx, config.serverName);
|
|
3185
3759
|
const loader = ctx.get("loader");
|
|
@@ -12850,6 +13424,10 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
12850
13424
|
ok: true,
|
|
12851
13425
|
...mcpSnapshot(ctx)
|
|
12852
13426
|
}),
|
|
13427
|
+
"mcp.sessionSnapshot": async () => ({
|
|
13428
|
+
ok: true,
|
|
13429
|
+
...runtimeOf(ctx).sessionSnapshot()
|
|
13430
|
+
}),
|
|
12853
13431
|
"mcp.save": async (args) => {
|
|
12854
13432
|
try {
|
|
12855
13433
|
return {
|
|
@@ -12913,11 +13491,31 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearch
|
|
|
12913
13491
|
};
|
|
12914
13492
|
}
|
|
12915
13493
|
},
|
|
13494
|
+
"mcp.projectConnect": async (args) => {
|
|
13495
|
+
const runtime = runtimeOf(ctx);
|
|
13496
|
+
const agent = [...runtime.agents.keys()].find((owner) => owner.session?.id === args.sessionId);
|
|
13497
|
+
if (!agent || runtime.workspace(agent) !== args.workspacePath) return {
|
|
13498
|
+
ok: false,
|
|
13499
|
+
error: "请先打开此项目的活跃会话;不会激活历史会话"
|
|
13500
|
+
};
|
|
13501
|
+
try {
|
|
13502
|
+
await runtime.connect(agent, args.serverName);
|
|
13503
|
+
return {
|
|
13504
|
+
ok: true,
|
|
13505
|
+
project: mcpSnapshot(ctx).projects.find((item) => item.workspacePath === args.workspacePath)
|
|
13506
|
+
};
|
|
13507
|
+
} catch {
|
|
13508
|
+
return {
|
|
13509
|
+
ok: false,
|
|
13510
|
+
error: "项目 MCP 连接失败,请查看会话状态和 Host 日志"
|
|
13511
|
+
};
|
|
13512
|
+
}
|
|
13513
|
+
},
|
|
12916
13514
|
"mcp.projectSave": async (args) => {
|
|
12917
13515
|
try {
|
|
12918
13516
|
return {
|
|
12919
13517
|
ok: true,
|
|
12920
|
-
project: await projectSave(ctx, args.workspacePath, args.serverName, args.config)
|
|
13518
|
+
project: await projectSave(ctx, args.workspacePath, args.serverName, args.config, args.policy)
|
|
12921
13519
|
};
|
|
12922
13520
|
} catch (error) {
|
|
12923
13521
|
return {
|