@zhin.js/mcp-feature 1.0.6 → 1.0.9

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 CHANGED
@@ -47,7 +47,7 @@ old lease = 0: previous.stop()
47
47
  rollback: candidate.stop()
48
48
  ```
49
49
 
50
- MCP 没有入站 admission,因此候选连接不 quiesce 旧 client。旧 turn 可继续通过旧 projection 完成在途调用;新 turn 只看到已启动并已提交的新 client。要求排他连接时,应把连接资源提升到 Plugin Resource handoff。
50
+ MCP 候选连接不会触碰旧 client。旧 turn 继续通过旧 projection 与 snapshot lease 完成在途调用;新 turn 只看到已启动并已提交的新 client。要求排他连接时,连接必须由 Process Host 持有,并让 generation 只原子切换逻辑路由,不能在 shadow prepare 中抢占旧连接。
51
51
 
52
52
  `McpIndex` 对连接使用 owner inheritance。动态工具名由 client 返回;orchestrator 可投影为 `{connection}__{tool}`,但该模型命名不改变 Capability identity。
53
53
 
@@ -6,7 +6,7 @@ export interface McpToolDescriptor {
6
6
  readonly inputSchema?: unknown;
7
7
  }
8
8
  export interface McpClientInstance {
9
- start?(): void | Promise<void>;
9
+ start?(signal: AbortSignal): void | Promise<void>;
10
10
  stop?(): void | Promise<void>;
11
11
  listTools(): readonly McpToolDescriptor[] | Promise<readonly McpToolDescriptor[]>;
12
12
  callTool(name: string, input: unknown): unknown | Promise<unknown>;
@@ -10,10 +10,10 @@ export interface McpDescriptor {
10
10
  export declare class McpIndex {
11
11
  #private;
12
12
  private constructor();
13
- static create(slots: readonly Readonly<CapabilitySlot<McpDefinition>>[], snapshot: RuntimeSnapshot): Promise<McpIndex>;
13
+ static create(slots: readonly Readonly<CapabilitySlot<McpDefinition>>[], snapshot: RuntimeSnapshot, signal: AbortSignal): Promise<McpIndex>;
14
14
  list(): readonly McpDescriptor[];
15
15
  visible(requester: PluginId): readonly McpDescriptor[];
16
- start(): Promise<void>;
16
+ start(signal: AbortSignal): Promise<void>;
17
17
  stop(): Promise<void>;
18
18
  listTools(requester: PluginId, name: string): Promise<readonly McpToolDescriptor[]>;
19
19
  callTool<TResult = unknown>(requester: PluginId, connection: string, tool: string, input: unknown): Promise<TResult>;
package/lib/mcp-index.js CHANGED
@@ -11,12 +11,14 @@ export class McpIndex {
11
11
  for (const record of records)
12
12
  this.#records.set(record.entry.slot.id, record);
13
13
  }
14
- static async create(slots, snapshot) {
14
+ static async create(slots, snapshot, signal) {
15
15
  const index = new OwnerCapabilityIndex(slots, snapshot);
16
16
  const records = [];
17
17
  try {
18
18
  for (const entry of index.entries()) {
19
- const client = await entry.slot.definition.create(createCapabilityContext(snapshot, entry.owner));
19
+ signal.throwIfAborted();
20
+ const client = await entry.slot.definition.create(createCapabilityContext(snapshot, entry.owner, undefined, signal));
21
+ signal.throwIfAborted();
20
22
  assertMcpClient(client, entry.source);
21
23
  records.push({ entry, client, started: false, stopped: false });
22
24
  }
@@ -33,14 +35,16 @@ export class McpIndex {
33
35
  visible(requester) {
34
36
  return this.#index.visible(requester).map(toDescriptor);
35
37
  }
36
- async start() {
38
+ async start(signal) {
37
39
  const started = [];
38
40
  try {
39
41
  for (const record of this.#order) {
40
42
  if (record.started)
41
43
  continue;
44
+ signal.throwIfAborted();
42
45
  started.push(record);
43
- await record.client.start?.();
46
+ await record.client.start?.(signal);
47
+ signal.throwIfAborted();
44
48
  record.started = true;
45
49
  record.stopped = false;
46
50
  }
@@ -92,7 +96,7 @@ async function stopRecords(records, primary) {
92
96
  continue;
93
97
  stack.add(async () => {
94
98
  // 先 await stop 成功再标记 stopped:stop 失败时保持未标记,
95
- // 下次 stop()(或代际 resume 后的再次 quiesce)可重试,不会永久泄漏。
99
+ // 下次 stop() 可重试,不会因一次失败永久泄漏。
96
100
  await record.client.stop?.();
97
101
  record.started = false;
98
102
  record.stopped = true;
package/lib/provider.js CHANGED
@@ -17,29 +17,19 @@ const mcpFeature = defineFeatureProvider({
17
17
  },
18
18
  runtime: {
19
19
  async project(slots, context) {
20
- const index = await McpIndex.create(slots, context.snapshot);
21
- let previousIndex;
20
+ const index = await McpIndex.create(slots, context.snapshot, context.signal);
22
21
  return {
23
22
  value: index,
24
23
  dispose: () => index.stop(),
25
24
  handoff: {
26
- // 先停旧代连接再激活新代:独占端口型 server 热重载不会新旧并存。
27
- quiescePrevious(previous) {
28
- previousIndex = previousMcpIndex(previous);
29
- return previousIndex?.stop();
30
- },
31
- activateNext: () => index.start(),
25
+ // Candidate connections become ready before publication. The old
26
+ // index stays usable until its snapshot's final lease drains.
27
+ activateNext: (signal) => index.start(signal),
32
28
  deactivateNext: () => index.stop(),
33
- resumePrevious() {
34
- return previousIndex?.start();
35
- },
36
29
  },
37
30
  };
38
31
  },
39
32
  },
40
33
  });
41
- function previousMcpIndex(snapshot) {
42
- return snapshot.projections.get(mcpFeatureId);
43
- }
44
34
  export { mcpFeature };
45
35
  export default mcpFeature;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/mcp-feature",
3
- "version": "1.0.6",
3
+ "version": "1.0.9",
4
4
  "description": "Convention-based MCP client Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -17,8 +17,8 @@
17
17
  "src"
18
18
  ],
19
19
  "dependencies": {
20
- "@zhin.js/feature-kit": "1.0.6",
21
- "@zhin.js/plugin-runtime": "1.1.3"
20
+ "@zhin.js/feature-kit": "1.0.9",
21
+ "@zhin.js/plugin-runtime": "1.1.6"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^26.1.2",
package/src/definition.ts CHANGED
@@ -9,7 +9,7 @@ export interface McpToolDescriptor {
9
9
  }
10
10
 
11
11
  export interface McpClientInstance {
12
- start?(): void | Promise<void>;
12
+ start?(signal: AbortSignal): void | Promise<void>;
13
13
  stop?(): void | Promise<void>;
14
14
  listTools(): readonly McpToolDescriptor[] | Promise<readonly McpToolDescriptor[]>;
15
15
  callTool(name: string, input: unknown): unknown | Promise<unknown>;
package/src/mcp-index.ts CHANGED
@@ -48,14 +48,17 @@ export class McpIndex {
48
48
  static async create(
49
49
  slots: readonly Readonly<CapabilitySlot<McpDefinition>>[],
50
50
  snapshot: RuntimeSnapshot,
51
+ signal: AbortSignal,
51
52
  ): Promise<McpIndex> {
52
53
  const index = new OwnerCapabilityIndex(slots, snapshot);
53
54
  const records: McpRecord[] = [];
54
55
  try {
55
56
  for (const entry of index.entries()) {
57
+ signal.throwIfAborted();
56
58
  const client = await entry.slot.definition.create(
57
- createCapabilityContext(snapshot, entry.owner),
59
+ createCapabilityContext(snapshot, entry.owner, undefined, signal),
58
60
  );
61
+ signal.throwIfAborted();
59
62
  assertMcpClient(client, entry.source);
60
63
  records.push({ entry, client, started: false, stopped: false });
61
64
  }
@@ -74,13 +77,15 @@ export class McpIndex {
74
77
  return this.#index.visible(requester).map(toDescriptor);
75
78
  }
76
79
 
77
- async start(): Promise<void> {
80
+ async start(signal: AbortSignal): Promise<void> {
78
81
  const started: McpRecord[] = [];
79
82
  try {
80
83
  for (const record of this.#order) {
81
84
  if (record.started) continue;
85
+ signal.throwIfAborted();
82
86
  started.push(record);
83
- await record.client.start?.();
87
+ await record.client.start?.(signal);
88
+ signal.throwIfAborted();
84
89
  record.started = true;
85
90
  record.stopped = false;
86
91
  }
@@ -139,7 +144,7 @@ async function stopRecords(records: readonly McpRecord[], primary?: unknown): Pr
139
144
  if (record.stopped) continue;
140
145
  stack.add(async () => {
141
146
  // 先 await stop 成功再标记 stopped:stop 失败时保持未标记,
142
- // 下次 stop()(或代际 resume 后的再次 quiesce)可重试,不会永久泄漏。
147
+ // 下次 stop() 可重试,不会因一次失败永久泄漏。
143
148
  await record.client.stop?.();
144
149
  record.started = false;
145
150
  record.stopped = true;
package/src/provider.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { featureId, type RuntimeSnapshot } from '@zhin.js/plugin-runtime';
1
+ import { featureId } from '@zhin.js/plugin-runtime';
2
2
  import { defineFeatureProvider, typeScriptModules } from '@zhin.js/feature-kit';
3
3
  import { parseMcpDefinition } from './definition.js';
4
4
  import { McpIndex } from './mcp-index.js';
@@ -19,31 +19,20 @@ const mcpFeature = defineFeatureProvider({
19
19
  },
20
20
  runtime: {
21
21
  async project(slots, context) {
22
- const index = await McpIndex.create(slots, context.snapshot);
23
- let previousIndex: McpIndex | undefined;
22
+ const index = await McpIndex.create(slots, context.snapshot, context.signal);
24
23
  return {
25
24
  value: index,
26
25
  dispose: () => index.stop(),
27
26
  handoff: {
28
- // 先停旧代连接再激活新代:独占端口型 server 热重载不会新旧并存。
29
- quiescePrevious(previous) {
30
- previousIndex = previousMcpIndex(previous);
31
- return previousIndex?.stop();
32
- },
33
- activateNext: () => index.start(),
27
+ // Candidate connections become ready before publication. The old
28
+ // index stays usable until its snapshot's final lease drains.
29
+ activateNext: (signal) => index.start(signal),
34
30
  deactivateNext: () => index.stop(),
35
- resumePrevious() {
36
- return previousIndex?.start();
37
- },
38
31
  },
39
32
  };
40
33
  },
41
34
  },
42
35
  });
43
36
 
44
- function previousMcpIndex(snapshot: RuntimeSnapshot): McpIndex | undefined {
45
- return snapshot.projections.get(mcpFeatureId) as McpIndex | undefined;
46
- }
47
-
48
37
  export { mcpFeature };
49
38
  export default mcpFeature;