@zhin.js/mcp-feature 1.1.16 → 1.1.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/mcp-feature",
3
- "version": "1.1.16",
3
+ "version": "1.1.17",
4
4
  "description": "Convention-based MCP client Feature for Zhin Plugin Runtime",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -12,12 +12,11 @@
12
12
  }
13
13
  },
14
14
  "files": [
15
- "lib",
16
- "src"
15
+ "lib"
17
16
  ],
18
17
  "dependencies": {
19
- "@zhin.js/feature-kit": "1.1.15",
20
- "@zhin.js/plugin-runtime": "1.1.23"
18
+ "@zhin.js/plugin-runtime": "1.2.0",
19
+ "@zhin.js/feature-kit": "1.1.16"
21
20
  },
22
21
  "devDependencies": {
23
22
  "@types/node": "^26.1.2",
package/src/definition.ts DELETED
@@ -1,73 +0,0 @@
1
- import type { ConstructionContext } from '@zhin.js/feature-kit';
2
-
3
- const mcpBrand = 'zhin.mcp/1' as const;
4
-
5
- export interface McpToolDescriptor {
6
- readonly name: string;
7
- readonly description?: string;
8
- readonly inputSchema?: unknown;
9
- }
10
-
11
- export interface McpClientInstance {
12
- start?(signal: AbortSignal): void | Promise<void>;
13
- stop?(): void | Promise<void>;
14
- listTools(): readonly McpToolDescriptor[] | Promise<readonly McpToolDescriptor[]>;
15
- callTool(name: string, input: unknown): unknown | Promise<unknown>;
16
- }
17
-
18
- export interface McpDefinition<TConfig = unknown> {
19
- readonly $feature: typeof mcpBrand;
20
- readonly description?: string;
21
- create(context: ConstructionContext<TConfig>): McpClientInstance | Promise<McpClientInstance>;
22
- }
23
-
24
- declare module '@zhin.js/plugin-runtime' {
25
- interface PluginSetupContext<TConfig = unknown> {
26
- addMcp(localName: string, definition: McpDefinition<TConfig>): void;
27
- }
28
- }
29
-
30
- export type McpDefiner<TConfig> = (
31
- definition: Omit<McpDefinition<TConfig>, '$feature'>,
32
- ) => Readonly<McpDefinition<TConfig>>;
33
-
34
- export function defineMcp<TConfig = unknown>(
35
- definition: Omit<McpDefinition<TConfig>, '$feature'>,
36
- ): Readonly<McpDefinition<TConfig>> {
37
- if (typeof definition.create !== 'function') {
38
- throw new TypeError('MCP create must be a function');
39
- }
40
- return Object.freeze({ ...definition, $feature: mcpBrand });
41
- }
42
-
43
- /**
44
- * Fix the plugin config type first, then define the MCP client.
45
- */
46
- defineMcp.withConfig = function withConfig<TConfig = unknown>(): McpDefiner<TConfig> {
47
- return (definition) => defineMcp(definition);
48
- };
49
-
50
- export function parseMcpDefinition(value: unknown): McpDefinition {
51
- if (!value || typeof value !== 'object') throw invalidMcp();
52
- const definition = value as Partial<McpDefinition>;
53
- if (definition.$feature !== mcpBrand || typeof definition.create !== 'function') {
54
- throw invalidMcp();
55
- }
56
- return definition as McpDefinition;
57
- }
58
-
59
- export function assertMcpClient(value: unknown, source: string): asserts value is McpClientInstance {
60
- if (!value || typeof value !== 'object') throw invalidClient(source);
61
- const client = value as Partial<McpClientInstance>;
62
- if (typeof client.listTools !== 'function' || typeof client.callTool !== 'function') {
63
- throw invalidClient(source);
64
- }
65
- }
66
-
67
- function invalidMcp(): TypeError {
68
- return new TypeError('MCP module must default-export defineMcp(...)');
69
- }
70
-
71
- function invalidClient(source: string): TypeError {
72
- return new TypeError(`MCP ${source} create() must return a client instance`);
73
- }
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from './definition.js';
2
- export * from './mcp-index.js';
3
- export * from './provider.js';
4
- export { default } from './provider.js';
package/src/mcp-index.ts DELETED
@@ -1,172 +0,0 @@
1
- import {
2
- DisposeStack,
3
- type CapabilitySlot,
4
- type PluginId,
5
- type RuntimeSnapshot,
6
- } from '@zhin.js/plugin-runtime';
7
- import {
8
- OwnerCapabilityIndex,
9
- createConstructionContext,
10
- type OwnerCapabilityEntry,
11
- } from '@zhin.js/feature-kit';
12
- import {
13
- assertMcpClient,
14
- type McpClientInstance,
15
- type McpDefinition,
16
- type McpToolDescriptor,
17
- } from './definition.js';
18
-
19
- export interface McpDescriptor {
20
- readonly owner: PluginId;
21
- readonly name: string;
22
- readonly qualifiedName: string;
23
- readonly description?: string;
24
- readonly source: string;
25
- }
26
-
27
- interface McpRecord {
28
- readonly entry: OwnerCapabilityEntry<McpDefinition>;
29
- readonly client: McpClientInstance;
30
- started: boolean;
31
- stopped: boolean;
32
- }
33
-
34
- export class McpIndex {
35
- readonly $projection = 'zhin.mcp-index/1' as const;
36
- readonly #index: OwnerCapabilityIndex<McpDefinition>;
37
- readonly #records = new Map<string, McpRecord>();
38
- readonly #order: readonly McpRecord[];
39
-
40
- private constructor(
41
- index: OwnerCapabilityIndex<McpDefinition>,
42
- records: readonly McpRecord[],
43
- ) {
44
- this.#index = index;
45
- this.#order = Object.freeze([...records]);
46
- for (const record of records) this.#records.set(record.entry.slot.id, record);
47
- }
48
-
49
- static async create(
50
- slots: readonly Readonly<CapabilitySlot<McpDefinition>>[],
51
- snapshot: RuntimeSnapshot,
52
- signal: AbortSignal,
53
- ): Promise<McpIndex> {
54
- const index = new OwnerCapabilityIndex(slots, snapshot);
55
- const records: McpRecord[] = [];
56
- try {
57
- for (const entry of index.entries()) {
58
- signal.throwIfAborted();
59
- const client = await entry.slot.definition.create(
60
- createConstructionContext(snapshot, entry.owner, undefined, signal),
61
- );
62
- signal.throwIfAborted();
63
- assertMcpClient(client, entry.source);
64
- records.push({ entry, client, started: false, stopped: false });
65
- }
66
- return new McpIndex(index, records);
67
- } catch (error) {
68
- await stopRecords(records, error);
69
- throw error;
70
- }
71
- }
72
-
73
- list(): readonly McpDescriptor[] {
74
- return this.#index.entries().map(toDescriptor);
75
- }
76
-
77
- visible(requester: PluginId): readonly McpDescriptor[] {
78
- return this.#index.visible(requester).map(toDescriptor);
79
- }
80
-
81
- async start(signal: AbortSignal): Promise<void> {
82
- const started: McpRecord[] = [];
83
- try {
84
- for (const record of this.#order) {
85
- if (record.started) continue;
86
- signal.throwIfAborted();
87
- started.push(record);
88
- await record.client.start?.(signal);
89
- signal.throwIfAborted();
90
- record.started = true;
91
- record.stopped = false;
92
- }
93
- } catch (error) {
94
- await stopRecords(started, error);
95
- throw error;
96
- }
97
- }
98
-
99
- async stop(): Promise<void> {
100
- await stopRecords(this.#order);
101
- }
102
-
103
- async listTools(requester: PluginId, name: string): Promise<readonly McpToolDescriptor[]> {
104
- const record = this.#resolve(requester, name);
105
- return Object.freeze((await record.client.listTools()).map((tool) => {
106
- if (!tool.name.trim()) throw new TypeError(`MCP ${name} returned a Tool without a name`);
107
- return Object.freeze({ ...tool });
108
- }));
109
- }
110
-
111
- async callTool<TResult = unknown>(
112
- requester: PluginId,
113
- connection: string,
114
- tool: string,
115
- input: unknown,
116
- ): Promise<TResult> {
117
- const record = this.#resolve(requester, connection);
118
- return record.client.callTool(tool, input) as TResult | Promise<TResult>;
119
- }
120
-
121
- #resolve(requester: PluginId, name: string): McpRecord {
122
- const entry = this.#index.resolve(requester, name);
123
- if (!entry) throw new Error(`Unknown MCP connection ${name} for ${requester}`);
124
- const record = this.#records.get(entry.slot.id);
125
- if (!record || !record.started || record.stopped) {
126
- throw new Error(`MCP connection is not active: ${entry.qualifiedName}`);
127
- }
128
- return record;
129
- }
130
- }
131
-
132
- function toDescriptor(entry: OwnerCapabilityEntry<McpDefinition>): McpDescriptor {
133
- return Object.freeze({
134
- owner: entry.owner,
135
- name: entry.name,
136
- qualifiedName: entry.qualifiedName,
137
- description: entry.slot.definition.description,
138
- source: entry.source,
139
- });
140
- }
141
-
142
- async function stopRecords(records: readonly McpRecord[], primary?: unknown): Promise<void> {
143
- const stack = new DisposeStack();
144
- for (const record of records) {
145
- if (record.stopped) continue;
146
- stack.add(async () => {
147
- // 先 await stop 成功再标记 stopped:stop 失败时保持未标记,
148
- // 下次 stop() 可重试,不会因一次失败永久泄漏。
149
- await record.client.stop?.();
150
- record.started = false;
151
- record.stopped = true;
152
- });
153
- }
154
- try {
155
- await stack.dispose();
156
- } catch (stopError) {
157
- if (primary !== undefined) {
158
- throw new AggregateError(
159
- [primary, stopError],
160
- 'MCP prepare and client cleanup both failed',
161
- { cause: stopError },
162
- );
163
- }
164
- throw stopError;
165
- }
166
- }
167
-
168
- /** Recognize the versioned projection across generation module identities. */
169
- export function isMcpIndex(value: unknown): value is McpIndex {
170
- return !!value && typeof value === 'object'
171
- && (value as { readonly $projection?: unknown }).$projection === 'zhin.mcp-index/1';
172
- }
package/src/provider.ts DELETED
@@ -1,38 +0,0 @@
1
- import { featureId } from '@zhin.js/plugin-runtime';
2
- import { capture, captured, defineFeatureProvider, directoryModules } from '@zhin.js/feature-kit';
3
- import { parseMcpDefinition } from './definition.js';
4
- import { McpIndex } from './mcp-index.js';
5
-
6
- export const mcpFeatureId = featureId('zhin.mcp');
7
-
8
- const mcpFeature = defineFeatureProvider({
9
- protocol: 1,
10
- id: mcpFeatureId,
11
- authoring: {
12
- setupMethod: 'addMcp',
13
- conventions: [directoryModules({
14
- id: 'mcps-index',
15
- layouts: [{ segments: ['mcps', capture('name')], localName: (values) => captured(values, 'name') }],
16
- })],
17
- validate: parseMcpDefinition,
18
- },
19
- runtime: {
20
- retain: 'when-owner-unchanged',
21
- async project(slots, context) {
22
- const index = await McpIndex.create(slots, context.snapshot, context.signal);
23
- return {
24
- value: index,
25
- dispose: () => index.stop(),
26
- handoff: {
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),
30
- deactivateNext: () => index.stop(),
31
- },
32
- };
33
- },
34
- },
35
- });
36
-
37
- export { mcpFeature };
38
- export default mcpFeature;