@xuanling-rs/xuanling-dsh-tools 0.2.3 → 0.2.5
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 +11 -2
- package/README.md +14 -2
- package/cordis.patch.yml +9 -4
- package/mcp-result-adapter.mjs +204 -0
- package/package.json +5 -4
package/README-ZH.md
CHANGED
|
@@ -6,13 +6,22 @@
|
|
|
6
6
|
原生工具,并以 `mcp__xuanling__` 前缀增加 XuanLing 工具。
|
|
7
7
|
|
|
8
8
|
```sh
|
|
9
|
-
dsh plugin --profile
|
|
9
|
+
dsh plugin --profile web add @xuanling-rs/xuanling-dsh-tools@0.2.5
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
该命令会扩展 DSH 内置的 Web profile。内置 Headless profile 应使用 `--profile headless`。
|
|
13
|
+
未知 profile 名称只包含 base bundle,本身不会提供可运行的 Web 或 Headless 应用。
|
|
14
|
+
|
|
15
|
+
精确版本的 `@xuanling-rs/xuanling-mcp@0.2.5` runtime 会安装在 profile 内,并通过带校验的 JS launcher
|
|
13
16
|
启动;不需要也不会使用全局 npm package。只有文件系统 capability root 与 DSH 工作目录不同时,
|
|
14
17
|
才需要设置 `XUANLING_WORKSPACE_ROOT`。
|
|
15
18
|
|
|
19
|
+
该 bundle 的 result adapter 会保留完整 Native 文本投影,只删除与 `structuredContent` 完全重复的
|
|
20
|
+
意外文本块。
|
|
21
|
+
|
|
22
|
+
Adapter 只接受子进程 JSON object frame。输出 malformed 或子进程正常退出时仍有未结算 request 会以非零状态
|
|
23
|
+
结束;Host 终止信号会被转发,子进程未在 500 ms grace 内退出时会被强制终止。
|
|
24
|
+
|
|
16
25
|
该包会与 DSH 原生文件、进程和项目工具产生能力重叠。日常配置建议使用目录更小的
|
|
17
26
|
`@xuanling-rs/xuanling-dsh-memory`。不要在同一 profile 内安装多个 XuanLing 工具 bundle。
|
|
18
27
|
|
package/README.md
CHANGED
|
@@ -7,14 +7,26 @@ 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
|
|
10
|
+
dsh plugin --profile web add @xuanling-rs/xuanling-dsh-tools@0.2.5
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
This command augments DSH's shipped Web profile. Use `--profile headless` for
|
|
14
|
+
the shipped Headless profile. Unknown profile names start with the base bundle
|
|
15
|
+
only and do not provide a runnable Web or Headless application by themselves.
|
|
16
|
+
|
|
17
|
+
The exact `@xuanling-rs/xuanling-mcp@0.2.5` runtime is installed in the profile and started
|
|
14
18
|
through its verified JS launcher. A global npm package is neither required nor
|
|
15
19
|
used. Set `XUANLING_WORKSPACE_ROOT` only when the filesystem capability root
|
|
16
20
|
must differ from the DSH working directory.
|
|
17
21
|
|
|
22
|
+
The bundle's result adapter preserves the complete Native text projection and
|
|
23
|
+
removes only accidental duplicate text blocks that exactly repeat
|
|
24
|
+
`structuredContent`.
|
|
25
|
+
|
|
26
|
+
The adapter accepts only JSON object frames from the child. Malformed output or
|
|
27
|
+
an unresolved request at clean child exit returns a nonzero status; host
|
|
28
|
+
termination is forwarded and force-terminated after a 500 ms grace when needed.
|
|
29
|
+
|
|
18
30
|
This package overlaps with native file, process, and project tools. Prefer
|
|
19
31
|
`@xuanling-rs/xuanling-dsh-memory` for the smaller daily tool surface. Do not install this
|
|
20
32
|
package with another XuanLing tool bundle in the same profile.
|
package/cordis.patch.yml
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# XuanLing tools for DeepSeek Harness — additive variant.
|
|
2
2
|
#
|
|
3
|
-
# Mounts the xuanling-mcp binary
|
|
4
|
-
# @deepseek-ai/dsh-mcp-client bridge. Every XuanLing tool becomes a native
|
|
3
|
+
# Mounts the xuanling-mcp binary through the local result adapter and the
|
|
4
|
+
# official @deepseek-ai/dsh-mcp-client bridge. Every XuanLing tool becomes a native
|
|
5
5
|
# harness tool named mcp__xuanling__<raw> (for example mcp__xuanling__fs_read_text);
|
|
6
6
|
# the bridge owns reconnect, re-sync, and generation rollback. Built-in tools
|
|
7
7
|
# (read/write/edit/glob/grep/bash/…) stay registered — this variant only ADDS.
|
|
8
8
|
#
|
|
9
|
-
# The bundle resolves its exact xuanling-mcp dependency from
|
|
10
|
-
# The JS launcher then selects and verifies the matching native
|
|
9
|
+
# The bundle resolves its exact result adapter and xuanling-mcp dependency from
|
|
10
|
+
# the DSH profile. The JS launcher then selects and verifies the matching native
|
|
11
|
+
# package; the adapter preserves one complete text projection and structured value.
|
|
11
12
|
#
|
|
12
13
|
# The filesystem capability root defaults to the directory dsh was started
|
|
13
14
|
# from; override with XUANLING_WORKSPACE_ROOT. Memory uses the shared default
|
|
@@ -23,6 +24,10 @@
|
|
|
23
24
|
transport: stdio
|
|
24
25
|
command: !!js process.execPath
|
|
25
26
|
args:
|
|
27
|
+
- !!js process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('@xuanling-rs/xuanling-dsh-tools/mcp-result-adapter.mjs')
|
|
28
|
+
- '--binary'
|
|
29
|
+
- !!js process.execPath
|
|
30
|
+
- '--'
|
|
26
31
|
- !!js process.getBuiltinModule('node:module').createRequire(baseUrl).resolve('@xuanling-rs/xuanling-mcp/bin/xuanling-mcp.js')
|
|
27
32
|
- '--workspace-root'
|
|
28
33
|
- !!js process.env.XUANLING_WORKSPACE_ROOT ?? process.cwd()
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const TEXT_RESULT_DUPLICATE_LIMIT = 1;
|
|
9
|
+
const CHILD_TERMINATION_GRACE_MS = 500;
|
|
10
|
+
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function requestIdKey(id) {
|
|
16
|
+
return `${typeof id}:${JSON.stringify(id)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function parseFrame(line) {
|
|
20
|
+
try {
|
|
21
|
+
const frame = JSON.parse(line);
|
|
22
|
+
return isRecord(frame) ? frame : undefined;
|
|
23
|
+
} catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validateChildFrame(line) {
|
|
29
|
+
if (!parseFrame(line)) throw new Error("invalid JSON or malformed child frame");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function requestMethodAndId(line) {
|
|
33
|
+
const frame = parseFrame(line);
|
|
34
|
+
if (!frame || typeof frame.method !== "string" || !Object.hasOwn(frame, "id")) return undefined;
|
|
35
|
+
return { id: requestIdKey(frame.id), method: frame.method };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parsedJsonText(text) {
|
|
39
|
+
if (typeof text !== "string") return { ok: false };
|
|
40
|
+
try {
|
|
41
|
+
return { ok: true, value: JSON.parse(text) };
|
|
42
|
+
} catch {
|
|
43
|
+
return { ok: false };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function matchesStructuredContent(block, structuredContent) {
|
|
48
|
+
if (!isRecord(block) || block.type !== "text") return false;
|
|
49
|
+
const parsed = parsedJsonText(block.text);
|
|
50
|
+
return parsed.ok && isDeepStrictEqual(parsed.value, structuredContent);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* DSH keeps the complete text projection for Native/model rendering. If a
|
|
55
|
+
* server or an intermediate bridge accidentally emits the same structured
|
|
56
|
+
* JSON text block more than once, retain one copy and preserve the canonical
|
|
57
|
+
* structured value for Code Mode and output validation.
|
|
58
|
+
*/
|
|
59
|
+
export function projectDshCallResult(result) {
|
|
60
|
+
if (!isRecord(result) || !Array.isArray(result.content) || !Object.hasOwn(result, "structuredContent")) {
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const matching = result.content.filter((block) =>
|
|
65
|
+
matchesStructuredContent(block, result.structuredContent),
|
|
66
|
+
);
|
|
67
|
+
if (matching.length <= TEXT_RESULT_DUPLICATE_LIMIT) return result;
|
|
68
|
+
|
|
69
|
+
let retained = false;
|
|
70
|
+
const content = result.content.filter((block) => {
|
|
71
|
+
if (!matchesStructuredContent(block, result.structuredContent)) return true;
|
|
72
|
+
if (retained) return false;
|
|
73
|
+
retained = true;
|
|
74
|
+
return true;
|
|
75
|
+
});
|
|
76
|
+
return { ...result, content };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function projectDshResponseFrame(line, pendingToolCallIds) {
|
|
80
|
+
const frame = parseFrame(line);
|
|
81
|
+
if (
|
|
82
|
+
!frame ||
|
|
83
|
+
!Object.hasOwn(frame, "id") ||
|
|
84
|
+
Object.hasOwn(frame, "method") ||
|
|
85
|
+
(!Object.hasOwn(frame, "result") && !Object.hasOwn(frame, "error"))
|
|
86
|
+
) return line;
|
|
87
|
+
const key = requestIdKey(frame.id);
|
|
88
|
+
if (!pendingToolCallIds.delete(key)) return line;
|
|
89
|
+
if (!isRecord(frame.result)) return line;
|
|
90
|
+
const projected = projectDshCallResult(frame.result);
|
|
91
|
+
return projected === frame.result ? line : JSON.stringify({ ...frame, result: projected });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function usageError(message) {
|
|
95
|
+
throw new Error(`${message}; usage: mcp-result-adapter.mjs --binary <server> -- [args...]`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseAdapterArgs(argv) {
|
|
99
|
+
if (argv[0] !== "--binary" || typeof argv[1] !== "string" || argv[1].length === 0) {
|
|
100
|
+
usageError("--binary is required");
|
|
101
|
+
}
|
|
102
|
+
if (argv[2] !== "--") usageError("expected -- before MCP server arguments");
|
|
103
|
+
return { binary: argv[1], childArgs: argv.slice(3) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function runAdapter(argv = process.argv.slice(2)) {
|
|
107
|
+
const { binary, childArgs } = parseAdapterArgs(argv);
|
|
108
|
+
const child = spawn(binary, childArgs, {
|
|
109
|
+
env: process.env,
|
|
110
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
});
|
|
113
|
+
const pendingToolCallIds = new Set();
|
|
114
|
+
let adapterFailed = false;
|
|
115
|
+
let forwardedSignal;
|
|
116
|
+
let terminationTimer;
|
|
117
|
+
|
|
118
|
+
child.stderr.pipe(process.stderr);
|
|
119
|
+
|
|
120
|
+
const clientLines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
121
|
+
const serverLines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
122
|
+
|
|
123
|
+
function childIsRunning() {
|
|
124
|
+
return child.exitCode === null && child.signalCode === null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function terminateChild(signal) {
|
|
128
|
+
if (!childIsRunning()) return;
|
|
129
|
+
child.kill(signal);
|
|
130
|
+
if (terminationTimer !== undefined) return;
|
|
131
|
+
terminationTimer = setTimeout(() => {
|
|
132
|
+
terminationTimer = undefined;
|
|
133
|
+
if (childIsRunning()) child.kill("SIGKILL");
|
|
134
|
+
}, CHILD_TERMINATION_GRACE_MS);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function failAdapter(error) {
|
|
138
|
+
if (adapterFailed) return;
|
|
139
|
+
adapterFailed = true;
|
|
140
|
+
process.exitCode = 1;
|
|
141
|
+
process.stderr.write(
|
|
142
|
+
`xuanling-dsh-result-adapter: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
143
|
+
);
|
|
144
|
+
clientLines.close();
|
|
145
|
+
serverLines.close();
|
|
146
|
+
terminateChild("SIGTERM");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
clientLines.on("line", (line) => {
|
|
150
|
+
const request = requestMethodAndId(line);
|
|
151
|
+
if (request?.method === "tools/call") pendingToolCallIds.add(request.id);
|
|
152
|
+
if (!child.stdin.write(`${line}\n`)) clientLines.pause();
|
|
153
|
+
});
|
|
154
|
+
clientLines.once("close", () => {
|
|
155
|
+
if (!child.stdin.destroyed) child.stdin.end();
|
|
156
|
+
});
|
|
157
|
+
child.stdin.on("drain", () => clientLines.resume());
|
|
158
|
+
child.stdin.on("error", (error) => {
|
|
159
|
+
if (child.exitCode === null && child.signalCode === null) failAdapter(error);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
serverLines.on("line", (line) => {
|
|
163
|
+
if (adapterFailed) return;
|
|
164
|
+
try {
|
|
165
|
+
validateChildFrame(line);
|
|
166
|
+
const projected = projectDshResponseFrame(line, pendingToolCallIds);
|
|
167
|
+
if (!process.stdout.write(`${projected}\n`)) serverLines.pause();
|
|
168
|
+
} catch (error) {
|
|
169
|
+
failAdapter(error);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
process.stdout.on("drain", () => serverLines.resume());
|
|
173
|
+
|
|
174
|
+
child.once("error", failAdapter);
|
|
175
|
+
child.once("close", (code, signal) => {
|
|
176
|
+
if (terminationTimer !== undefined) clearTimeout(terminationTimer);
|
|
177
|
+
clientLines.close();
|
|
178
|
+
serverLines.close();
|
|
179
|
+
if (adapterFailed) return;
|
|
180
|
+
if (forwardedSignal !== undefined) {
|
|
181
|
+
process.exitCode = 1;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (code !== 0 || signal !== null) {
|
|
185
|
+
process.exitCode = code ?? 1;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (pendingToolCallIds.size > 0) {
|
|
189
|
+
failAdapter(new Error(`child exited with ${pendingToolCallIds.size} unresolved tools/call request(s)`));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
194
|
+
process.once(signal, () => {
|
|
195
|
+
if (forwardedSignal !== undefined) return;
|
|
196
|
+
forwardedSignal = signal;
|
|
197
|
+
clientLines.close();
|
|
198
|
+
terminateChild(signal);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
204
|
+
if (isMain) runAdapter();
|
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xuanling-rs/xuanling-dsh-tools",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
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.",
|
|
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.
|
|
9
|
+
"@xuanling-rs/xuanling-mcp": "0.2.5"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"LICENSE",
|
|
13
13
|
"README.md",
|
|
14
14
|
"README-ZH.md",
|
|
15
|
-
"cordis.patch.yml"
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"mcp-result-adapter.mjs"
|
|
16
17
|
],
|
|
17
18
|
"dsh": {
|
|
18
19
|
"bundle": {
|
|
@@ -37,6 +38,6 @@
|
|
|
37
38
|
"registry": "https://registry.npmjs.org"
|
|
38
39
|
},
|
|
39
40
|
"xuanlingRelease": {
|
|
40
|
-
"sourceCommit": "
|
|
41
|
+
"sourceCommit": "3b5ec0f1addc6ae69ba89cfdac078d45c51a5aef"
|
|
41
42
|
}
|
|
42
43
|
}
|