@xuanling-rs/xuanling-dsh-tools 0.3.0 → 0.3.2
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 +5 -5
- package/README.md +7 -6
- package/cordis.patch.yml +6 -6
- package/lazy-mcp-client.mjs +234 -0
- package/package.json +5 -4
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.3.
|
|
9
|
+
dsh plugin --profile web add @xuanling-rs/xuanling-dsh-tools@0.3.2
|
|
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.3.
|
|
15
|
+
精确版本的 `@xuanling-rs/xuanling-mcp@0.3.2` runtime 会安装在 profile 内,并通过带校验的 JS launcher
|
|
16
16
|
启动;不需要也不会使用全局 npm package。启动 DSH 前必须把 `XUANLING_WORKSPACE_ROOT` 设置为
|
|
17
17
|
已确认的绝对 workspace;缺失时 bundle 会启动失败,DSH 进程工作目录不会被当成用户授权。
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
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,23 +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.3.
|
|
10
|
+
dsh plugin --profile web add @xuanling-rs/xuanling-dsh-tools@0.3.2
|
|
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.3.
|
|
17
|
+
The exact `@xuanling-rs/xuanling-mcp@0.3.2` runtime is installed in the profile and started
|
|
18
18
|
through its verified JS launcher. A global npm package is neither required nor
|
|
19
19
|
used. Set `XUANLING_WORKSPACE_ROOT` to the confirmed absolute workspace before
|
|
20
20
|
starting DSH; the bundle fails startup when it is absent and never treats the
|
|
21
21
|
DSH process working directory as user authorization.
|
|
22
22
|
|
|
23
|
-
The
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
27
28
|
|
|
28
29
|
The bundle's result adapter preserves the complete Native text projection and
|
|
29
30
|
removes only accidental duplicate text blocks that exactly repeat
|
package/cordis.patch.yml
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
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
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
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
|
|
@@ -19,10 +20,9 @@
|
|
|
19
20
|
|
|
20
21
|
- insert:
|
|
21
22
|
- id: xuanling-tools
|
|
22
|
-
name: '@
|
|
23
|
+
name: '@xuanling-rs/xuanling-dsh-tools/lazy-mcp-client.mjs'
|
|
23
24
|
config:
|
|
24
25
|
serverName: xuanling
|
|
25
|
-
toolExposure: lazy
|
|
26
26
|
transport: stdio
|
|
27
27
|
command: !!js process.execPath
|
|
28
28
|
args:
|
|
@@ -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.3.
|
|
4
|
-
"description": "DeepSeek Harness bundle: mount
|
|
3
|
+
"version": "0.3.2",
|
|
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.3.
|
|
9
|
+
"@xuanling-rs/xuanling-mcp": "0.3.2"
|
|
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": "
|
|
42
|
+
"sourceCommit": "2230b0f6f2ab933028953a5dae721b23aac0dfe3"
|
|
42
43
|
}
|
|
43
44
|
}
|