@xuanling-rs/xuanling-dsh-tools 0.2.10 → 0.3.1

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-ZH.md CHANGED
@@ -6,19 +6,19 @@
6
6
  原生工具,并以 `mcp__xuanling__` 前缀增加 XuanLing 工具。
7
7
 
8
8
  ```sh
9
- dsh plugin --profile web add @xuanling-rs/xuanling-dsh-tools@0.2.10
9
+ dsh plugin --profile web add @xuanling-rs/xuanling-dsh-tools@0.3.1
10
10
  ```
11
11
 
12
12
  该命令会扩展 DSH 内置的 Web profile。内置 Headless profile 应使用 `--profile headless`。
13
13
  未知 profile 名称只包含 base bundle,本身不会提供可运行的 Web 或 Headless 应用。
14
14
 
15
- 精确版本的 `@xuanling-rs/xuanling-mcp@0.2.10` runtime 会安装在 profile 内,并通过带校验的 JS launcher
16
- 启动;不需要也不会使用全局 npm package。只有文件系统 capability root 与 DSH 工作目录不同时,
17
- 才需要设置 `XUANLING_WORKSPACE_ROOT`。
15
+ 精确版本的 `@xuanling-rs/xuanling-mcp@0.3.1` runtime 会安装在 profile 内,并通过带校验的 JS launcher
16
+ 启动;不需要也不会使用全局 npm package。启动 DSH 前必须把 `XUANLING_WORKSPACE_ROOT` 设置为
17
+ 已确认的绝对 workspace;缺失时 bundle 会启动失败,DSH 进程工作目录不会被当成用户授权。
18
18
 
19
- Bridge 会缓存 MCP 分页中的全部定义,但起初只暴露 `mcp_catalog__xuanling`。应按名称或描述检索并
20
- 激活精确 raw name;选中的定义会在下一次模型请求中作为 `mcp__xuanling__*` 工具出现。不要预先
21
- 激活完整目录。
19
+ Bundle 自带的 lazy wrapper 会让官方 bridge 缓存 MCP 分页中的全部定义,但起初只暴露
20
+ `mcp_catalog__xuanling`。应按名称或描述检索,并在每次调用中激活一个精确 raw name;选中的定义
21
+ 会在下一次模型请求中作为 `mcp__xuanling__*` 工具出现。不要预先激活完整目录。
22
22
 
23
23
  该 bundle 的 result adapter 会保留完整 Native 文本投影,只删除与 `structuredContent` 完全重复的
24
24
  意外文本块。
package/README.md CHANGED
@@ -7,22 +7,24 @@ keeps all Harness-native tools enabled and adds XuanLing tools under the
7
7
  `mcp__xuanling__` prefix.
8
8
 
9
9
  ```sh
10
- dsh plugin --profile web add @xuanling-rs/xuanling-dsh-tools@0.2.10
10
+ dsh plugin --profile web add @xuanling-rs/xuanling-dsh-tools@0.3.1
11
11
  ```
12
12
 
13
13
  This command augments DSH's shipped Web profile. Use `--profile headless` for
14
14
  the shipped Headless profile. Unknown profile names start with the base bundle
15
15
  only and do not provide a runnable Web or Headless application by themselves.
16
16
 
17
- The exact `@xuanling-rs/xuanling-mcp@0.2.10` runtime is installed in the profile and started
17
+ The exact `@xuanling-rs/xuanling-mcp@0.3.1` runtime is installed in the profile and started
18
18
  through its verified JS launcher. A global npm package is neither required nor
19
- used. Set `XUANLING_WORKSPACE_ROOT` only when the filesystem capability root
20
- must differ from the DSH working directory.
21
-
22
- The bridge caches every paginated MCP definition but initially exposes only
23
- `mcp_catalog__xuanling`. Search by name or description and activate exact raw
24
- names; the selected definitions appear as `mcp__xuanling__*` tools on the next
25
- model request. Do not activate the complete catalog speculatively.
19
+ used. Set `XUANLING_WORKSPACE_ROOT` to the confirmed absolute workspace before
20
+ starting DSH; the bundle fails startup when it is absent and never treats the
21
+ DSH process working directory as user authorization.
22
+
23
+ The bundle-owned lazy wrapper lets the official bridge cache every paginated
24
+ MCP definition but initially exposes only `mcp_catalog__xuanling`. Search by
25
+ name or description and activate one exact raw name per call; the selected
26
+ definition appears as an `mcp__xuanling__*` tool on the next model request. Do
27
+ not activate the complete catalog speculatively.
26
28
 
27
29
  The bundle's result adapter preserves the complete Native text projection and
28
30
  removes only accidental duplicate text blocks that exactly repeat
package/cordis.patch.yml CHANGED
@@ -1,27 +1,28 @@
1
1
  # XuanLing tools for DeepSeek Harness — additive variant.
2
2
  #
3
3
  # Mounts the xuanling-mcp binary through the local result adapter and the
4
- # official @deepseek-ai/dsh-mcp-client bridge. The bridge caches every XuanLing
5
- # definition and exposes mcp_catalog__xuanling first; exact activations become
6
- # native mcp__xuanling__<raw> tools. It owns reconnect, re-sync, and generation rollback. Built-in tools
7
- # (read/write/edit/glob/grep/bash/…) stay registered — this variant only ADDS.
4
+ # official @deepseek-ai/dsh-mcp-client bridge through a bundle-owned lazy
5
+ # wrapper. The bridge caches every XuanLing definition; the wrapper exposes
6
+ # mcp_catalog__xuanling first and registers exact activations as native
7
+ # mcp__xuanling__<raw> tools. The official bridge still owns reconnect,
8
+ # re-sync, and generation rollback. Built-in tools stay registered.
8
9
  #
9
10
  # The bundle resolves its exact result adapter and xuanling-mcp dependency from
10
11
  # the DSH profile. The JS launcher then selects and verifies the matching native
11
12
  # package; the adapter preserves one complete text projection and structured value.
12
13
  #
13
- # The filesystem capability root defaults to the directory dsh was started
14
- # from; override with XUANLING_WORKSPACE_ROOT. Memory uses the shared default
14
+ # The filesystem capability root is explicit. XUANLING_WORKSPACE_ROOT must
15
+ # name the confirmed absolute workspace; missing configuration fails startup.
16
+ # Memory uses the shared default
15
17
  # database (~/.xuanling/memory.db) exactly like the ZCode plugin deployment.
16
18
  # The ZCode-only object-parameter compat shim stays OFF everywhere in this
17
19
  # integration: DeepSeek forwards model arguments as verbatim JSON.
18
20
 
19
21
  - insert:
20
22
  - id: xuanling-tools
21
- name: '@deepseek-ai/dsh-mcp-client'
23
+ name: '@xuanling-rs/xuanling-dsh-tools/lazy-mcp-client.mjs'
22
24
  config:
23
25
  serverName: xuanling
24
- toolExposure: lazy
25
26
  transport: stdio
26
27
  command: !!js process.execPath
27
28
  args:
@@ -31,5 +32,5 @@
31
32
  - '--'
32
33
  - !!js process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('@xuanling-rs/xuanling-mcp/bin/xuanling-mcp.js')
33
34
  - '--workspace-root'
34
- - !!js process.env.XUANLING_WORKSPACE_ROOT ?? process.cwd()
35
+ - !!js (() => { const root = process.env.XUANLING_WORKSPACE_ROOT; if (!root) throw new Error('XUANLING_WORKSPACE_ROOT is required for the XuanLing full-tools bundle'); return root })()
35
36
  toolCallTimeoutMs: 120000
@@ -0,0 +1,234 @@
1
+ const REQUIRED_SERVER_NAME = "xuanling";
2
+ const CATALOG_NAME = "mcp_catalog__xuanling";
3
+ const MAX_MATCHES = 8;
4
+
5
+ export const name = "xuanling-lazy-mcp-client";
6
+ export const inject = ["tools"];
7
+
8
+ function bindMember(target, property) {
9
+ const value = Reflect.get(target, property, target);
10
+ return typeof value === "function" ? value.bind(target) : value;
11
+ }
12
+
13
+ function rawNameFromPublicName(serverName, publicName) {
14
+ // The release contract proves every XuanLing raw name is already valid in
15
+ // DSH's public-name space, so the official bridge performs no lossy rewrite.
16
+ const prefix = `mcp__${serverName}__`;
17
+ if (typeof publicName !== "string" || !publicName.startsWith(prefix)) {
18
+ throw new Error(
19
+ `[XUANLING_LAZY_NAME_MISMATCH] bridge registered ${JSON.stringify(publicName)} outside ${prefix}`,
20
+ );
21
+ }
22
+ const rawName = publicName.slice(prefix.length);
23
+ if (rawName.length === 0 || `${prefix}${rawName}` !== publicName) {
24
+ throw new Error(
25
+ `[XUANLING_LAZY_NAME_MISMATCH] bridge public name is not reversibly qualified: ${publicName}`,
26
+ );
27
+ }
28
+ return rawName;
29
+ }
30
+
31
+ function createCatalogDefinition(state) {
32
+ return {
33
+ name: CATALOG_NAME,
34
+ description:
35
+ "Search cached XuanLing MCP tools by raw name or description and optionally activate one exact raw name for subsequent model turns.",
36
+ parameters: {
37
+ type: "object",
38
+ properties: {
39
+ query: {
40
+ type: "string",
41
+ description: "Case-insensitive search text. Empty text lists the first bounded page.",
42
+ },
43
+ activate: {
44
+ type: "string",
45
+ description: "Optional exact raw MCP tool name to activate. Identity matching is case-sensitive.",
46
+ },
47
+ },
48
+ required: ["query"],
49
+ additionalProperties: false,
50
+ },
51
+ output: {
52
+ schema: {
53
+ type: "object",
54
+ properties: {
55
+ query: { type: "string" },
56
+ total_tools: { type: "integer" },
57
+ matched_tools: { type: "integer" },
58
+ matches: {
59
+ type: "array",
60
+ items: {
61
+ type: "object",
62
+ properties: {
63
+ raw_name: { type: "string" },
64
+ public_name: { type: "string" },
65
+ description: { type: "string" },
66
+ active: { type: "boolean" },
67
+ },
68
+ required: ["raw_name", "public_name", "description", "active"],
69
+ additionalProperties: false,
70
+ },
71
+ },
72
+ active_tools: { type: "array", items: { type: "string" } },
73
+ activated: { type: "string" },
74
+ truncated: { type: "boolean" },
75
+ },
76
+ required: ["query", "total_tools", "matched_tools", "matches", "active_tools", "truncated"],
77
+ additionalProperties: false,
78
+ },
79
+ render(_arguments, value) {
80
+ return [{ type: "text", text: JSON.stringify(value) }];
81
+ },
82
+ },
83
+ async execute(argumentsValue) {
84
+ if (
85
+ typeof argumentsValue !== "object" ||
86
+ argumentsValue === null ||
87
+ Array.isArray(argumentsValue) ||
88
+ typeof argumentsValue.query !== "string" ||
89
+ (argumentsValue.activate !== undefined && typeof argumentsValue.activate !== "string")
90
+ ) {
91
+ throw new Error("[XUANLING_CATALOG_INVALID_ARGUMENTS] query must be a string and activate must be an optional string");
92
+ }
93
+
94
+ let activated;
95
+ if (argumentsValue.activate !== undefined) {
96
+ activated = state.activate(argumentsValue.activate);
97
+ }
98
+
99
+ const normalizedQuery = argumentsValue.query.trim().toLocaleLowerCase("en-US");
100
+ const allMatches = [...state.definitions.entries()]
101
+ .map(([rawName, record]) => ({
102
+ raw_name: rawName,
103
+ public_name: record.definition.name,
104
+ description: typeof record.definition.description === "string"
105
+ ? record.definition.description
106
+ : "",
107
+ active: state.isActive(rawName),
108
+ }))
109
+ .filter((entry) =>
110
+ normalizedQuery.length === 0 ||
111
+ entry.raw_name.toLocaleLowerCase("en-US").includes(normalizedQuery) ||
112
+ entry.description.toLocaleLowerCase("en-US").includes(normalizedQuery)
113
+ )
114
+ .sort((left, right) => left.raw_name.localeCompare(right.raw_name, "en-US"));
115
+
116
+ return {
117
+ query: argumentsValue.query,
118
+ total_tools: state.definitions.size,
119
+ matched_tools: allMatches.length,
120
+ matches: allMatches.slice(0, MAX_MATCHES),
121
+ active_tools: state.activeRawNames(),
122
+ ...(activated === undefined ? {} : { activated }),
123
+ truncated: allMatches.length > MAX_MATCHES,
124
+ };
125
+ },
126
+ };
127
+ }
128
+
129
+ export async function applyWithOfficialBridge(ctx, config, applyOfficialBridge) {
130
+ if (config?.serverName !== REQUIRED_SERVER_NAME) {
131
+ throw new Error(
132
+ `[XUANLING_LAZY_INVALID_SERVER] expected serverName ${REQUIRED_SERVER_NAME}, got ${JSON.stringify(config?.serverName)}`,
133
+ );
134
+ }
135
+ if (typeof applyOfficialBridge !== "function") {
136
+ throw new Error("[XUANLING_LAZY_INVALID_BRIDGE] official bridge apply export is unavailable");
137
+ }
138
+
139
+ const definitions = new Map();
140
+ const desiredActiveNames = new Set();
141
+ const registeredActive = new Map();
142
+
143
+ function registerActive(rawName, record) {
144
+ const previous = registeredActive.get(rawName);
145
+ if (previous !== undefined) previous.dispose();
146
+ const dispose = ctx.tools.register(record.definition);
147
+ registeredActive.set(rawName, { record, dispose });
148
+ }
149
+
150
+ function captureDefinition(definition) {
151
+ const rawName = rawNameFromPublicName(config.serverName, definition?.name);
152
+ if (definitions.has(rawName)) {
153
+ throw new Error(`[XUANLING_LAZY_DUPLICATE_TOOL] duplicate raw tool name: ${rawName}`);
154
+ }
155
+ const record = { definition };
156
+ definitions.set(rawName, record);
157
+ if (desiredActiveNames.has(rawName)) registerActive(rawName, record);
158
+
159
+ let disposed = false;
160
+ return () => {
161
+ if (disposed) return;
162
+ disposed = true;
163
+ if (definitions.get(rawName) !== record) return;
164
+ definitions.delete(rawName);
165
+ const active = registeredActive.get(rawName);
166
+ if (active?.record === record) {
167
+ active.dispose();
168
+ registeredActive.delete(rawName);
169
+ }
170
+ };
171
+ }
172
+
173
+ const projectedTools = new Proxy(ctx.tools, {
174
+ get(target, property) {
175
+ if (property === "register") return captureDefinition;
176
+ return bindMember(target, property);
177
+ },
178
+ });
179
+ const projectedContext = new Proxy(ctx, {
180
+ get(target, property) {
181
+ if (property === "tools") return projectedTools;
182
+ return bindMember(target, property);
183
+ },
184
+ });
185
+
186
+ await applyOfficialBridge(projectedContext, { ...config, failOnStartupError: true });
187
+ if (definitions.size === 0) {
188
+ throw new Error("[XUANLING_LAZY_EMPTY_CATALOG] official bridge discovered no XuanLing tools");
189
+ }
190
+
191
+ const state = {
192
+ definitions,
193
+ activate(rawName) {
194
+ const record = definitions.get(rawName);
195
+ if (record === undefined) {
196
+ throw new Error(`[XUANLING_CATALOG_UNKNOWN_TOOL] exact raw tool name not found: ${JSON.stringify(rawName)}`);
197
+ }
198
+ if (!desiredActiveNames.has(rawName)) {
199
+ desiredActiveNames.add(rawName);
200
+ try {
201
+ registerActive(rawName, record);
202
+ } catch (error) {
203
+ desiredActiveNames.delete(rawName);
204
+ throw error;
205
+ }
206
+ }
207
+ return rawName;
208
+ },
209
+ isActive(rawName) {
210
+ return registeredActive.has(rawName);
211
+ },
212
+ activeRawNames() {
213
+ return [...registeredActive.keys()].sort((left, right) => left.localeCompare(right, "en-US"));
214
+ },
215
+ };
216
+
217
+ ctx.effect(
218
+ () => () => {
219
+ desiredActiveNames.clear();
220
+ for (const active of [...registeredActive.values()].reverse()) active.dispose();
221
+ registeredActive.clear();
222
+ },
223
+ "xuanling.lazy-mcp-active-tools",
224
+ );
225
+ ctx.effect(
226
+ () => ctx.tools.register(createCatalogDefinition(state)),
227
+ "xuanling.lazy-mcp-catalog",
228
+ );
229
+ }
230
+
231
+ export async function apply(ctx, config) {
232
+ const bridge = await import("@deepseek-ai/dsh-mcp-client");
233
+ return applyWithOfficialBridge(ctx, config, bridge.apply);
234
+ }
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@xuanling-rs/xuanling-dsh-tools",
3
- "version": "0.2.10",
4
- "description": "DeepSeek Harness bundle: mount the XuanLing MCP server (xuanling-mcp binary) as native mcp__xuanling__* tools through the official @deepseek-ai/dsh-mcp-client bridge. Additive variant: built-in tools stay enabled.",
3
+ "version": "0.3.1",
4
+ "description": "DeepSeek Harness bundle: lazily mount XuanLing through a bundle-owned wrapper around the official MCP bridge. Additive variant: built-in tools stay enabled.",
5
5
  "license": "MIT",
6
6
  "private": false,
7
7
  "dependencies": {
8
8
  "@deepseek-ai/dsh-mcp-client": "^0.1.0-rc.5",
9
- "@xuanling-rs/xuanling-mcp": "0.2.10"
9
+ "@xuanling-rs/xuanling-mcp": "0.3.1"
10
10
  },
11
11
  "files": [
12
12
  "LICENSE",
13
13
  "README.md",
14
14
  "README-ZH.md",
15
15
  "cordis.patch.yml",
16
+ "lazy-mcp-client.mjs",
16
17
  "mcp-result-adapter.mjs"
17
18
  ],
18
19
  "dsh": {
@@ -38,6 +39,6 @@
38
39
  "registry": "https://registry.npmjs.org"
39
40
  },
40
41
  "xuanlingRelease": {
41
- "sourceCommit": "8f87bec7959a708109c3b10dee3539e0946c7282"
42
+ "sourceCommit": "df27d2e962d33731e4beba44f23ed1fd58adaae6"
42
43
  }
43
44
  }