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/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
- /** Agent-owned project MCP fibers. @author ddj 2026年09月28号 */
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
- /** Agent -> server -> fiber ledger. Configuration alone never opens a connection. */
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
- tail = Promise.resolve();
2733
+ pool;
2280
2734
  stopped = false;
2735
+ offDiscover;
2281
2736
  retired = /* @__PURE__ */ new WeakSet();
2282
2737
  module;
2283
- /**
2284
- * Create an idle ledger with an injectable official-plugin resolver.
2285
- * @public @author ddj 2026年09月28号
2286
- * @param ctx Host services and lifecycle owner.
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
- * Serialize reconfiguration and creation; a failed operation cannot poison the queue.
2295
- * @private @author ddj 2026年09月28号
2296
- * @param action Mutation to execute while the runtime remains live.
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
- * Find the longest registered workspace containing an agent's cwd.
2309
- * @public @author ddj 2026年09月28号
2310
- * @param agent Live agent.
2311
- * @returns Registered workspace path, or undefined outside managed workspaces.
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
- * Publish ownership changes before any provider can become visible.
2330
- * @private @author ddj 2026年09月28号
2331
- * @sideEffects Refreshes agent restrictions and scoped prompt shadows synchronously.
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
- * Replace one validated file snapshot and update its active agents.
2338
- * @public @author ddj 2026年09月28号
2339
- * @param path Registered workspace path.
2340
- * @param defs Validated source definitions.
2341
- * @param refresh Optional server whose unchanged fiber must also be recreated.
2342
- * @returns Completion after old fibers close and replacement startups settle.
2343
- */
2344
- replace(path, defs, refresh) {
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.projects.set(path, defs);
2347
- this.changed();
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
- return this.enqueue(update);
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
- if (this.stopped || this.retired.has(agent)) return Promise.resolve();
2361
- let record = this.agents.get(agent);
2362
- if (!record) {
2363
- record = {
2364
- instances: /* @__PURE__ */ new Map(),
2365
- live: true
2366
- };
2367
- this.agents.set(agent, record);
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 target = record;
2370
- const attach = async () => {
2371
- await this.syncAgent(agent, target);
2372
- this.changed();
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
- return this.enqueue(attach);
2892
+ record.resources.set(name, resources.register(name, { request }));
2375
2893
  }
2376
- /**
2377
- * Dispose a single connection before releasing its ledger slot.
2378
- * @private @author ddj 2026年09月28号
2379
- * @param record Agent ledger.
2380
- * @param name Server name to stop.
2381
- * @returns Awaitable Cordis teardown; failures remain visible to the caller.
2382
- */
2383
- async stopOne(record, name) {
2384
- const instance = record.instances.get(name);
2385
- if (instance) await this.stopFiber(instance);
2386
- if (record.instances.get(name) === instance) record.instances.delete(name);
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
- * Await exact fiber teardown, including a disposer already claimed by its owning scope.
2390
- * @private @author ddj 2026年09月28号
2391
- * @param instance Connection ledger entry.
2392
- * @returns Shared completion of asynchronous Cordis effects.
2393
- */
2394
- stopFiber(instance) {
2395
- const close = async () => {
2396
- const fiber = instance.fiber;
2397
- await fiber?.dispose();
2398
- while (fiber?.inertia !== void 0) await fiber.inertia;
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
- return instance.stopping ??= close();
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
- * Cancel and drain every fiber owned by a disposed agent.
2404
- * @public @author ddj 2026年09月28号
2405
- * @param agent Disposed agent.
2406
- * @returns Completion of connection cleanup.
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
- await Promise.all([...record.instances.keys()].map((name) => this.stopOne(record, name)));
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
- * Mount the official plugin through agent.ctx; ACTIVE is not connection evidence.
2456
- * @private @author ddj 2026年09月28号
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 (this.workspace(agent) !== path) continue;
2507
- const instance = record.instances.get(name);
2508
- if (!instance) continue;
2509
- instances.push(instance);
2510
- for (const tool of toolsOf(this.ctx, name, agent)) tools.set(tool.name, tool);
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
- id: projectEntryId(path, name),
2518
- serverName: name,
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
- * Close every owned connection and prevent queued work from mounting replacements.
2531
- * @public @author ddj 2026年09月28号
2532
- * @returns Completion after connection disposal and pending startup work settle.
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.tail;
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?.() ?? []) await runtime.attach(agent);
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
- * @returns Completion after scoped fibers match the file, including disabled flags.
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
- return mcpEdit(ctx, () => saveProject(ctx, workspacePath, serverName, config));
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 {