dsh-mcp-panel 0.4.2 → 0.5.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/CHANGELOG.md +17 -0
- package/README.es.md +1 -1
- package/README.hi.md +1 -1
- package/README.md +2 -1
- package/README.pt.md +1 -1
- package/README.zh.md +1 -1
- package/lib/index.js +219 -40
- package/lib/types/probe.d.ts +45 -11
- package/lib/types/probe.d.ts.map +1 -1
- package/lib/types/service.d.ts +22 -8
- package/lib/types/service.d.ts.map +1 -1
- package/package.json +19 -18
- package/src/probe.ts +137 -24
- package/src/service.ts +68 -26
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.5.1] - 2026-08-22
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- All `@deepseek-ai/dsh-*` dependencies moved from the `0.1.0-rc.8` line to `0.1.1-rc.2`: the 15 devDependencies pin `0.1.1-rc.2` exactly, `@deepseek-ai/dsh-subprocess` (the one runtime dependency) pins `0.1.1-rc.2`, and the four peerDependencies (`dsh-commands` / `dsh-jobs` / `dsh-tools` / `dsh-typert-protocol`) keep their `>=0.1.0-rc.8 <0.2.0` range (no rc.2-exclusive API is used). `@deepseek-ai/cordis` and the non-dsh dependency lines are unchanged; the compatibility tables now claim `0.1.1-rc.2`–`0.2.0`, and the compat workflow pins the harness CLI and base/headless bundles to `0.1.1-rc.2`.
|
|
10
|
+
- The `/mcp` and `mcp_probe` surfaces are unchanged: rc.2 keeps the `commands.execute(agent, line, images, signal)` signature and the single-`CommandInvocation` handler shape this package already drives, so no call-site edits were needed.
|
|
11
|
+
|
|
12
|
+
### Engineering
|
|
13
|
+
|
|
14
|
+
- `pnpm-lock.yaml` regenerated against the rc.2 graph; `minimumReleaseAgeExclude` collapses the stale per-package rc.6 list to a single `@deepseek-ai/*` wildcard so fresh harness releases install without a release-age delay.
|
|
15
|
+
|
|
16
|
+
## [0.5.0] - 2026-08-21
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `mcp_probe`, the panel probe action, `/mcp <server> probe`, and the passive probe now support **stdio** MCP servers: a probe on a stdio row spawns the configured `command`/`args` under the same `scrubbedParentEnv` base the mcp-client bridge uses (credential-shaped and `DSH_*` names never leak into the child implicitly) plus the row's explicit `env`/`cwd`, completes one MCP `initialize` handshake over stdin/stdout, and records the sanitized server name/version or a sanitized failure detail — still panel-only, with the same unowned-job semantics (cancel, per-probe timeout, display cap). `ProbeTarget` is now a `kind`-discriminated union (`http` | `stdio`) resolved by `McpPanelService.probeSpec`; streamable-http rows are probed exactly as before.
|
|
21
|
+
|
|
5
22
|
## [0.4.2] - 2026-08-21
|
|
6
23
|
|
|
7
24
|
### Changed
|
package/README.es.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
| Superficie | Estado |
|
|
26
26
|
|---|---|
|
|
27
|
-
| Harness | DeepSeek Harness `0.1.
|
|
27
|
+
| Harness | DeepSeek Harness `0.1.1-rc.2`–`0.2.0` |
|
|
28
28
|
| Node | `^22.19.0 \|\| >=24.0.0` |
|
|
29
29
|
| Plataformas | Web GUI (doble cara: host + navegador) |
|
|
30
30
|
| Modelo | Cualquiera (el panel es de solo lectura; solo la salida de `/mcp` es legible por el modelo) |
|
package/README.hi.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
| सतह | स्थिति |
|
|
26
26
|
|---|---|
|
|
27
|
-
| Harness | DeepSeek Harness `0.1.
|
|
27
|
+
| Harness | DeepSeek Harness `0.1.1-rc.2`–`0.2.0` |
|
|
28
28
|
| Node | `^22.19.0 \|\| >=24.0.0` |
|
|
29
29
|
| प्लेटफ़ॉर्म | Web GUI (दोहरा चेहरा: host + browser) |
|
|
30
30
|
| मॉडल | कोई भी (पैनल रीड-ओनली है; केवल `/mcp` आउटपुट मॉडल-पठनीय है) |
|
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
3
|
# dsh-mcp-panel
|
|
4
|
+
[](https://gitee.com/perrylink/dsh-mcp-panel)
|
|
4
5
|
|
|
5
6
|
**The MCP management console for the official DeepSeek Harness MCP client — add, edit, remove, and trial-call MCP servers from a settings page, with honest status, health diagnostics, and safe, reversible profile writes.**
|
|
6
7
|
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
|
|
25
26
|
| Surface | Status |
|
|
26
27
|
|---|---|
|
|
27
|
-
| Harness | DeepSeek Harness `0.1.
|
|
28
|
+
| Harness | DeepSeek Harness `0.1.1-rc.2`–`0.2.0` |
|
|
28
29
|
| Node | `^22.19.0 \|\| >=24.0.0` |
|
|
29
30
|
| Platforms | Web GUI (dual-face: host + browser) |
|
|
30
31
|
| Model | Any (the panel is read-only; only `/mcp` output is model-readable) |
|
package/README.pt.md
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
| Superfície | Status |
|
|
26
26
|
|---|---|
|
|
27
|
-
| Harness | DeepSeek Harness `0.1.
|
|
27
|
+
| Harness | DeepSeek Harness `0.1.1-rc.2`–`0.2.0` |
|
|
28
28
|
| Node | `^22.19.0 \|\| >=24.0.0` |
|
|
29
29
|
| Plataformas | Web GUI (duas faces: host + navegador) |
|
|
30
30
|
| Modelo | Qualquer (o painel é somente leitura; só a saída de `/mcp` é legível pelo modelo) |
|
package/README.zh.md
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { s as string } from "./schemas-BhI7GrM5.js";
|
|
2
2
|
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
|
|
3
5
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
6
|
import { join } from "node:path";
|
|
5
7
|
import { fileURLToPath } from "node:url";
|
|
@@ -591,6 +593,21 @@ function sanitizeError(error) {
|
|
|
591
593
|
}
|
|
592
594
|
//#endregion
|
|
593
595
|
//#region src/probe.ts
|
|
596
|
+
/**
|
|
597
|
+
* The optional `mcp_probe` tool: a one-shot connectivity probe of one
|
|
598
|
+
* configured MCP server (streamable-http or stdio transport), executed as an
|
|
599
|
+
* UNOWNED background job. Probe results are panel-only — the tool returns
|
|
600
|
+
* just the job id and a pointer to the settings tab, the job carries no
|
|
601
|
+
* owner (so no completion notice is injected into the model), and the panel
|
|
602
|
+
* reads the sanitized snapshot back through `mcpPanel/status`.
|
|
603
|
+
*
|
|
604
|
+
* The stdio probe spawns the configured `command`/`args` with the same
|
|
605
|
+
* `scrubbedParentEnv` base the mcp-client bridge uses (credential-shaped and
|
|
606
|
+
* `DSH_*` names never leak into the child implicitly) plus the row's explicit
|
|
607
|
+
* `env`/`cwd`, and completes one MCP `initialize` handshake over stdin/stdout.
|
|
608
|
+
*
|
|
609
|
+
* @module dsh-mcp-panel/probe
|
|
610
|
+
*/
|
|
594
611
|
/** Producer kind; also the job-id prefix. */
|
|
595
612
|
const PROBE_KIND = "mcp-probe";
|
|
596
613
|
/** JSON-RPC MCP initialize request used by the probe (protocol constant). */
|
|
@@ -600,7 +617,7 @@ const INITIALIZE_PROTOCOL_VERSION = "2024-11-05";
|
|
|
600
617
|
* advertised version tracks the package version. */
|
|
601
618
|
const PROBE_CLIENT_INFO = {
|
|
602
619
|
name: "dsh-mcp-panel",
|
|
603
|
-
version: "0.
|
|
620
|
+
version: "0.5.1"
|
|
604
621
|
};
|
|
605
622
|
/** Display cap for server-reported name/version fields in probe details. */
|
|
606
623
|
const DISPLAY_LIMIT = 80;
|
|
@@ -668,15 +685,128 @@ async function probeEndpoint(url, headers, timeoutMs, signal) {
|
|
|
668
685
|
}
|
|
669
686
|
}
|
|
670
687
|
/**
|
|
671
|
-
*
|
|
672
|
-
* outcome
|
|
688
|
+
* Complete one MCP `initialize` handshake over a spawned stdio server's
|
|
689
|
+
* stdin/stdout and describe the outcome in one sanitized line. The child runs
|
|
690
|
+
* under the same `scrubbedParentEnv` base the mcp-client bridge uses —
|
|
691
|
+
* credential-shaped and `DSH_*` parent names never reach it implicitly — plus
|
|
692
|
+
* the row's explicit `env` (merged after the scrub) and optional `cwd`. The
|
|
693
|
+
* child's stderr is consumed and never rendered; the configured `command`/
|
|
694
|
+
* `args` are launch facts, not display content, but any error is sanitized
|
|
695
|
+
* before it lands in the outcome detail.
|
|
673
696
|
*
|
|
674
|
-
* @param
|
|
675
|
-
* @param
|
|
697
|
+
* @param target - resolved stdio launch spec.
|
|
698
|
+
* @param timeoutMs - probe deadline.
|
|
699
|
+
* @param signal - caller-owned abort (job kill or timeout).
|
|
700
|
+
* @returns the settled probe outcome.
|
|
701
|
+
*/
|
|
702
|
+
function probeStdio(target, timeoutMs, signal) {
|
|
703
|
+
const started = Date.now();
|
|
704
|
+
return new Promise((resolve) => {
|
|
705
|
+
let settled = false;
|
|
706
|
+
let child;
|
|
707
|
+
let buffer = "";
|
|
708
|
+
let timer;
|
|
709
|
+
const finish = (outcome) => {
|
|
710
|
+
if (settled) return;
|
|
711
|
+
settled = true;
|
|
712
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
713
|
+
try {
|
|
714
|
+
child?.kill();
|
|
715
|
+
} catch {}
|
|
716
|
+
resolve(outcome);
|
|
717
|
+
};
|
|
718
|
+
timer = setTimeout(() => {
|
|
719
|
+
if (!settled) finish({
|
|
720
|
+
status: "failed",
|
|
721
|
+
detail: `timeout after ${timeoutMs}ms or cancelled`
|
|
722
|
+
});
|
|
723
|
+
}, timeoutMs);
|
|
724
|
+
try {
|
|
725
|
+
child = spawn(target.command, [...target.args], {
|
|
726
|
+
stdio: [
|
|
727
|
+
"pipe",
|
|
728
|
+
"pipe",
|
|
729
|
+
"pipe"
|
|
730
|
+
],
|
|
731
|
+
env: {
|
|
732
|
+
...scrubbedParentEnv(),
|
|
733
|
+
...target.env
|
|
734
|
+
},
|
|
735
|
+
...target.cwd !== void 0 && target.cwd !== "" ? { cwd: target.cwd } : {},
|
|
736
|
+
windowsHide: true
|
|
737
|
+
});
|
|
738
|
+
} catch (error) {
|
|
739
|
+
finish({
|
|
740
|
+
status: "failed",
|
|
741
|
+
detail: sanitizeError(error)
|
|
742
|
+
});
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
child.on("error", (error) => finish({
|
|
746
|
+
status: "failed",
|
|
747
|
+
detail: sanitizeError(error)
|
|
748
|
+
}));
|
|
749
|
+
child.on("exit", (code, signalName) => {
|
|
750
|
+
if (!settled) finish({
|
|
751
|
+
status: "failed",
|
|
752
|
+
detail: `process exited before initialize response (code ${code ?? "null"}${signalName ? `, signal ${signalName}` : ""})`
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
child.stdout?.on("data", (chunk) => {
|
|
756
|
+
buffer += chunk.toString("utf8");
|
|
757
|
+
let index;
|
|
758
|
+
while ((index = buffer.indexOf("\n")) !== -1) {
|
|
759
|
+
const line = buffer.slice(0, index).trim();
|
|
760
|
+
buffer = buffer.slice(index + 1);
|
|
761
|
+
if (line === "") continue;
|
|
762
|
+
let message;
|
|
763
|
+
try {
|
|
764
|
+
message = JSON.parse(line);
|
|
765
|
+
} catch {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (message !== null && typeof message === "object" && message.id === 1 && message.result !== void 0) {
|
|
769
|
+
const serverInfo = message.result.serverInfo ?? {};
|
|
770
|
+
const name = typeof serverInfo.name === "string" && serverInfo.name !== "" ? boundedDisplay(sanitizeText(serverInfo.name)) : "unnamed";
|
|
771
|
+
const version = typeof serverInfo.version === "string" && serverInfo.version !== "" ? boundedDisplay(sanitizeText(serverInfo.version)) : "unknown version";
|
|
772
|
+
finish({
|
|
773
|
+
status: "completed",
|
|
774
|
+
detail: `MCP initialize ok over stdio (server ${name} ${version}) in ${Date.now() - started}ms`
|
|
775
|
+
});
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
child.stderr?.on("data", () => {});
|
|
781
|
+
signal.addEventListener("abort", () => {
|
|
782
|
+
if (!settled) finish({
|
|
783
|
+
status: "failed",
|
|
784
|
+
detail: `timeout after ${timeoutMs}ms or cancelled`
|
|
785
|
+
});
|
|
786
|
+
}, { once: true });
|
|
787
|
+
try {
|
|
788
|
+
child.stdin?.write(`${JSON.stringify({
|
|
789
|
+
jsonrpc: "2.0",
|
|
790
|
+
id: 1,
|
|
791
|
+
method: "initialize",
|
|
792
|
+
params: {
|
|
793
|
+
protocolVersion: INITIALIZE_PROTOCOL_VERSION,
|
|
794
|
+
capabilities: {},
|
|
795
|
+
clientInfo: PROBE_CLIENT_INFO
|
|
796
|
+
}
|
|
797
|
+
})}\n`);
|
|
798
|
+
} catch {}
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Create the background-job hooks for one probe: cancel aborts the fetch (or
|
|
803
|
+
* the spawned child), the outcome settles `done` with sanitized detail only.
|
|
804
|
+
*
|
|
805
|
+
* @param target - resolved probe spec (http endpoint or stdio launch).
|
|
676
806
|
* @param timeoutMs - probe deadline.
|
|
677
807
|
* @returns the registry hooks.
|
|
678
808
|
*/
|
|
679
|
-
function probeJob(
|
|
809
|
+
function probeJob(target, timeoutMs) {
|
|
680
810
|
const controller = new AbortController();
|
|
681
811
|
const timer = setTimeout(() => {
|
|
682
812
|
controller.abort();
|
|
@@ -686,14 +816,14 @@ function probeJob(url, headers, timeoutMs) {
|
|
|
686
816
|
cancel: () => {
|
|
687
817
|
controller.abort();
|
|
688
818
|
},
|
|
689
|
-
done: probeEndpoint(url, headers, timeoutMs, controller.signal).then((outcome) => ({ ...outcome })).finally(() => {
|
|
819
|
+
done: (target.kind === "stdio" ? probeStdio(target, timeoutMs, controller.signal) : probeEndpoint(target.url, target.headers, timeoutMs, controller.signal)).then((outcome) => ({ ...outcome })).finally(() => {
|
|
690
820
|
clearTimeout(timer);
|
|
691
821
|
})
|
|
692
822
|
};
|
|
693
823
|
}
|
|
694
|
-
/** Resolve one configured server's
|
|
824
|
+
/** Resolve one configured server's probe spec (http endpoint or stdio launch). */
|
|
695
825
|
function probeTarget(service, server) {
|
|
696
|
-
return service.
|
|
826
|
+
return service.probeSpec(server);
|
|
697
827
|
}
|
|
698
828
|
/**
|
|
699
829
|
* Build the `mcp_probe` tool definition.
|
|
@@ -706,11 +836,11 @@ function probeTarget(service, server) {
|
|
|
706
836
|
function mcpProbeTool(service, jobs, timeoutMs) {
|
|
707
837
|
return defineTool({
|
|
708
838
|
name: "mcp_probe",
|
|
709
|
-
description: "Run a one-shot connectivity probe of a configured streamable-http
|
|
839
|
+
description: "Run a one-shot connectivity probe of a configured MCP server (streamable-http or stdio) as a background job. Results appear in the MCP settings panel only — they are not injected into model context.",
|
|
710
840
|
parameters: { server: {
|
|
711
841
|
type: "string",
|
|
712
842
|
required: true,
|
|
713
|
-
description: "serverName of a configured
|
|
843
|
+
description: "serverName of a configured MCP server (see /mcp for the list); streamable-http and stdio transports are both supported."
|
|
714
844
|
} },
|
|
715
845
|
output: {
|
|
716
846
|
schema: {
|
|
@@ -734,12 +864,12 @@ function mcpProbeTool(service, jobs, timeoutMs) {
|
|
|
734
864
|
},
|
|
735
865
|
async execute(args) {
|
|
736
866
|
const target = probeTarget(service, args.server);
|
|
737
|
-
if (target === void 0) throw new Error(`mcp_probe: "${args.server}" is not a configured
|
|
867
|
+
if (target === void 0) throw new Error(`mcp_probe: "${args.server}" is not a configured MCP server (see /mcp).`);
|
|
738
868
|
return {
|
|
739
869
|
jobId: jobs.start({
|
|
740
870
|
kind: PROBE_KIND,
|
|
741
871
|
label: `mcp_probe ${args.server}`,
|
|
742
|
-
run: () => probeJob(target
|
|
872
|
+
run: () => probeJob(target, timeoutMs)
|
|
743
873
|
}),
|
|
744
874
|
note: "Probe results are panel-only: Settings → Plugins → MCP."
|
|
745
875
|
};
|
|
@@ -2031,24 +2161,24 @@ var McpPanelService = class extends TypertRemoteService {
|
|
|
2031
2161
|
});
|
|
2032
2162
|
}
|
|
2033
2163
|
/**
|
|
2034
|
-
* Start a one-shot connectivity probe of one configured
|
|
2035
|
-
*
|
|
2036
|
-
* tool, but callable from the settings tab. Exported
|
|
2037
|
-
* `mcpPanel/probe` invocation descriptor.
|
|
2164
|
+
* Start a one-shot connectivity probe of one configured MCP server
|
|
2165
|
+
* (streamable-http or stdio) as an UNOWNED background job — panel-only,
|
|
2166
|
+
* like the `mcp_probe` tool, but callable from the settings tab. Exported
|
|
2167
|
+
* on the wire by the `mcpPanel/probe` invocation descriptor.
|
|
2038
2168
|
*
|
|
2039
2169
|
* @param serverName - configured namespace.
|
|
2040
2170
|
* @returns the started job id and where the result lands.
|
|
2041
2171
|
*/
|
|
2042
2172
|
probe(serverName) {
|
|
2043
|
-
const target = this.
|
|
2044
|
-
if (target === void 0) throw new Error(`dsh-mcp-panel: "${serverName}" is not a configured streamable-http
|
|
2173
|
+
const target = this.probeSpec(serverName);
|
|
2174
|
+
if (target === void 0) throw new Error(`dsh-mcp-panel: "${serverName}" is not a configured MCP server (streamable-http or stdio)`);
|
|
2045
2175
|
const jobs = this.ctx.get("jobs");
|
|
2046
2176
|
if (jobs === void 0) throw new Error("dsh-mcp-panel: ctx.jobs is not composed — the panel probe action needs a background-job registry");
|
|
2047
2177
|
return {
|
|
2048
2178
|
jobId: jobs.start({
|
|
2049
2179
|
kind: PROBE_KIND,
|
|
2050
2180
|
label: `mcp_probe ${serverName}`,
|
|
2051
|
-
run: () => probeJob(target
|
|
2181
|
+
run: () => probeJob(target, this.config.probeTimeoutMs)
|
|
2052
2182
|
}),
|
|
2053
2183
|
note: "Probe results are panel-only: Settings → Plugins → MCP."
|
|
2054
2184
|
};
|
|
@@ -2175,7 +2305,7 @@ var McpPanelService = class extends TypertRemoteService {
|
|
|
2175
2305
|
issueError(resolution) {
|
|
2176
2306
|
return /* @__PURE__ */ new Error(`dsh-mcp-panel: invalid patch operation — ${resolution.issues.map((issue) => issue.text).join(" ")}`);
|
|
2177
2307
|
}
|
|
2178
|
-
/** One passive-probe sweep over every configured
|
|
2308
|
+
/** One passive-probe sweep over every configured MCP server (both transports). */
|
|
2179
2309
|
async runPassiveProbes() {
|
|
2180
2310
|
if (this.passiveRunning) return;
|
|
2181
2311
|
this.passiveRunning = true;
|
|
@@ -2183,9 +2313,9 @@ var McpPanelService = class extends TypertRemoteService {
|
|
|
2183
2313
|
for (const entry of this.ctx.loader.entries()) {
|
|
2184
2314
|
if (entry.options.name !== "@deepseek-ai/dsh-mcp-client") continue;
|
|
2185
2315
|
const serverName = serverNameOf(entry.options.config, `entry:${entry.options.id}`);
|
|
2186
|
-
const target = this.
|
|
2316
|
+
const target = this.probeSpec(serverName);
|
|
2187
2317
|
if (target === void 0) continue;
|
|
2188
|
-
const outcome = await
|
|
2318
|
+
const outcome = await probeJob(target, this.config.probeTimeoutMs).done;
|
|
2189
2319
|
this.probeStates.set(serverName, {
|
|
2190
2320
|
state: outcome.status === "completed" ? "reachable" : "unreachable",
|
|
2191
2321
|
checkedAt: Date.now()
|
|
@@ -2196,35 +2326,84 @@ var McpPanelService = class extends TypertRemoteService {
|
|
|
2196
2326
|
}
|
|
2197
2327
|
}
|
|
2198
2328
|
/**
|
|
2199
|
-
* Resolve one server's
|
|
2200
|
-
*
|
|
2201
|
-
*
|
|
2329
|
+
* Resolve one configured server's probe spec. Credentials stay inside the
|
|
2330
|
+
* returned value and are used for the probe only — they never reach a
|
|
2331
|
+
* snapshot, a log, or a display. Streamable-http rows yield their raw
|
|
2332
|
+
* endpoint + configured headers; stdio rows yield the launch spec
|
|
2333
|
+
* (`command`/`args`, explicit `env` merged after the scrub, optional
|
|
2334
|
+
* `cwd`) that the probe spawns.
|
|
2202
2335
|
*
|
|
2203
2336
|
* @param serverName - configured namespace.
|
|
2204
|
-
* @returns the
|
|
2205
|
-
* is not a configured
|
|
2337
|
+
* @returns the transport-tagged probe spec, or `undefined` when the server
|
|
2338
|
+
* is not a configured MCP row of either transport.
|
|
2206
2339
|
*/
|
|
2207
|
-
|
|
2340
|
+
probeSpec(serverName) {
|
|
2208
2341
|
for (const entry of this.ctx.loader.entries()) {
|
|
2209
2342
|
if (entry.options.name !== "@deepseek-ai/dsh-mcp-client") continue;
|
|
2210
2343
|
const config = entry.options.config;
|
|
2211
2344
|
if (serverNameOf(config, `entry:${entry.options.id}`) !== serverName) continue;
|
|
2212
2345
|
if (typeof config !== "object" || config === null || Array.isArray(config)) return void 0;
|
|
2213
2346
|
const row = config;
|
|
2214
|
-
if (row["transport"]
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2347
|
+
if (row["transport"] === "streamable-http") {
|
|
2348
|
+
const url = row["url"];
|
|
2349
|
+
if (typeof url !== "string" || url === "") return void 0;
|
|
2350
|
+
const headersValue = row["headers"];
|
|
2351
|
+
const headers = {};
|
|
2352
|
+
if (typeof headersValue === "object" && headersValue !== null && !Array.isArray(headersValue)) {
|
|
2353
|
+
for (const [name, value] of Object.entries(headersValue)) if (typeof value === "string") headers[name] = value;
|
|
2354
|
+
}
|
|
2355
|
+
return {
|
|
2356
|
+
kind: "http",
|
|
2357
|
+
url,
|
|
2358
|
+
headers
|
|
2359
|
+
};
|
|
2221
2360
|
}
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2361
|
+
if (row["transport"] === "stdio") {
|
|
2362
|
+
const command = row["command"];
|
|
2363
|
+
if (typeof command !== "string" || command === "") return void 0;
|
|
2364
|
+
const argsValue = row["args"];
|
|
2365
|
+
const args = [];
|
|
2366
|
+
if (Array.isArray(argsValue)) for (const value of argsValue) {
|
|
2367
|
+
if (typeof value !== "string") return void 0;
|
|
2368
|
+
args.push(value);
|
|
2369
|
+
}
|
|
2370
|
+
else if (argsValue !== void 0) return void 0;
|
|
2371
|
+
const envValue = row["env"];
|
|
2372
|
+
const env = {};
|
|
2373
|
+
if (typeof envValue === "object" && envValue !== null && !Array.isArray(envValue)) for (const [name, value] of Object.entries(envValue)) {
|
|
2374
|
+
if (typeof value !== "string") return void 0;
|
|
2375
|
+
env[name] = value;
|
|
2376
|
+
}
|
|
2377
|
+
else if (envValue !== void 0) return void 0;
|
|
2378
|
+
const cwd = row["cwd"];
|
|
2379
|
+
if (cwd !== void 0 && (typeof cwd !== "string" || cwd === "")) return void 0;
|
|
2380
|
+
return {
|
|
2381
|
+
kind: "stdio",
|
|
2382
|
+
command,
|
|
2383
|
+
args,
|
|
2384
|
+
env,
|
|
2385
|
+
...typeof cwd === "string" ? { cwd } : {}
|
|
2386
|
+
};
|
|
2387
|
+
}
|
|
2388
|
+
return;
|
|
2226
2389
|
}
|
|
2227
2390
|
}
|
|
2391
|
+
/**
|
|
2392
|
+
* Resolve one server's raw endpoint for the streamable-http probe path.
|
|
2393
|
+
* Credentials stay inside this return value and are used for the request
|
|
2394
|
+
* only — they never reach a snapshot, a log, or a display.
|
|
2395
|
+
*
|
|
2396
|
+
* @param serverName - configured namespace.
|
|
2397
|
+
* @returns the raw URL + configured headers, or `undefined` when the server
|
|
2398
|
+
* is not a configured streamable-http row.
|
|
2399
|
+
*/
|
|
2400
|
+
rawEndpoint(serverName) {
|
|
2401
|
+
const spec = this.probeSpec(serverName);
|
|
2402
|
+
return spec?.kind === "http" ? {
|
|
2403
|
+
url: spec.url,
|
|
2404
|
+
headers: { ...spec.headers }
|
|
2405
|
+
} : void 0;
|
|
2406
|
+
}
|
|
2228
2407
|
/** Unowned `mcp-probe` background jobs, newest first, sanitized for display. */
|
|
2229
2408
|
probeViews() {
|
|
2230
2409
|
const jobs = this.ctx.get("jobs");
|
package/lib/types/probe.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The optional `mcp_probe` tool: a one-shot connectivity probe of one
|
|
3
|
-
* configured
|
|
4
|
-
* Probe results are panel-only — the tool returns
|
|
5
|
-
* pointer to the settings tab, the job carries no
|
|
6
|
-
* notice is injected into the model), and the panel
|
|
7
|
-
* snapshot back through `mcpPanel/status`.
|
|
3
|
+
* configured MCP server (streamable-http or stdio transport), executed as an
|
|
4
|
+
* UNOWNED background job. Probe results are panel-only — the tool returns
|
|
5
|
+
* just the job id and a pointer to the settings tab, the job carries no
|
|
6
|
+
* owner (so no completion notice is injected into the model), and the panel
|
|
7
|
+
* reads the sanitized snapshot back through `mcpPanel/status`.
|
|
8
|
+
*
|
|
9
|
+
* The stdio probe spawns the configured `command`/`args` with the same
|
|
10
|
+
* `scrubbedParentEnv` base the mcp-client bridge uses (credential-shaped and
|
|
11
|
+
* `DSH_*` names never leak into the child implicitly) plus the row's explicit
|
|
12
|
+
* `env`/`cwd`, and completes one MCP `initialize` handshake over stdin/stdout.
|
|
8
13
|
*
|
|
9
14
|
* @module dsh-mcp-panel/probe
|
|
10
15
|
*/
|
|
@@ -15,7 +20,7 @@ import type { McpPanelService } from './service.js';
|
|
|
15
20
|
export declare const PROBE_KIND = "mcp-probe";
|
|
16
21
|
declare module '@deepseek-ai/dsh-jobs' {
|
|
17
22
|
interface JobKindMap {
|
|
18
|
-
/** One-shot
|
|
23
|
+
/** One-shot connectivity probe (streamable-http or stdio; panel-only results). */
|
|
19
24
|
'mcp-probe': 'mcp-probe';
|
|
20
25
|
}
|
|
21
26
|
}
|
|
@@ -33,6 +38,18 @@ export interface ProbeOutcome {
|
|
|
33
38
|
/** Sanitized one-line detail (HTTP status, latency, server info, or error). */
|
|
34
39
|
detail: string;
|
|
35
40
|
}
|
|
41
|
+
/** One configured server's probe spec: an HTTP endpoint or a stdio launch. */
|
|
42
|
+
export type ProbeTarget = {
|
|
43
|
+
readonly kind: 'http';
|
|
44
|
+
readonly url: string;
|
|
45
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
46
|
+
} | {
|
|
47
|
+
readonly kind: 'stdio';
|
|
48
|
+
readonly command: string;
|
|
49
|
+
readonly args: readonly string[];
|
|
50
|
+
readonly env: Readonly<Record<string, string>>;
|
|
51
|
+
readonly cwd?: string;
|
|
52
|
+
};
|
|
36
53
|
/**
|
|
37
54
|
* POST one MCP `initialize` request and describe the outcome in one sanitized
|
|
38
55
|
* line. Never sends or echoes credentials: the configured headers are used
|
|
@@ -46,15 +63,32 @@ export interface ProbeOutcome {
|
|
|
46
63
|
*/
|
|
47
64
|
export declare function probeEndpoint(url: string, headers: Readonly<Record<string, string>>, timeoutMs: number, signal: AbortSignal): Promise<ProbeOutcome>;
|
|
48
65
|
/**
|
|
49
|
-
*
|
|
50
|
-
* outcome
|
|
66
|
+
* Complete one MCP `initialize` handshake over a spawned stdio server's
|
|
67
|
+
* stdin/stdout and describe the outcome in one sanitized line. The child runs
|
|
68
|
+
* under the same `scrubbedParentEnv` base the mcp-client bridge uses —
|
|
69
|
+
* credential-shaped and `DSH_*` parent names never reach it implicitly — plus
|
|
70
|
+
* the row's explicit `env` (merged after the scrub) and optional `cwd`. The
|
|
71
|
+
* child's stderr is consumed and never rendered; the configured `command`/
|
|
72
|
+
* `args` are launch facts, not display content, but any error is sanitized
|
|
73
|
+
* before it lands in the outcome detail.
|
|
74
|
+
*
|
|
75
|
+
* @param target - resolved stdio launch spec.
|
|
76
|
+
* @param timeoutMs - probe deadline.
|
|
77
|
+
* @param signal - caller-owned abort (job kill or timeout).
|
|
78
|
+
* @returns the settled probe outcome.
|
|
79
|
+
*/
|
|
80
|
+
export declare function probeStdio(target: Extract<ProbeTarget, {
|
|
81
|
+
kind: 'stdio';
|
|
82
|
+
}>, timeoutMs: number, signal: AbortSignal): Promise<ProbeOutcome>;
|
|
83
|
+
/**
|
|
84
|
+
* Create the background-job hooks for one probe: cancel aborts the fetch (or
|
|
85
|
+
* the spawned child), the outcome settles `done` with sanitized detail only.
|
|
51
86
|
*
|
|
52
|
-
* @param
|
|
53
|
-
* @param headers - configured headers (used, never rendered).
|
|
87
|
+
* @param target - resolved probe spec (http endpoint or stdio launch).
|
|
54
88
|
* @param timeoutMs - probe deadline.
|
|
55
89
|
* @returns the registry hooks.
|
|
56
90
|
*/
|
|
57
|
-
export declare function probeJob(
|
|
91
|
+
export declare function probeJob(target: ProbeTarget, timeoutMs: number): JobHooks;
|
|
58
92
|
/**
|
|
59
93
|
* Build the `mcp_probe` tool definition.
|
|
60
94
|
*
|
package/lib/types/probe.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/probe.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAc,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAE9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAA;AAG5D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAEnD,6CAA6C;AAC7C,eAAO,MAAM,UAAU,cAAc,CAAA;AAErC,OAAO,QAAQ,uBAAuB,CAAC;IACrC,UAAU,UAAU;QAClB,kFAAkF;QAClF,WAAW,EAAE,WAAW,CAAA;KACzB;CACF;AAKD;;oDAEoD;AACpD,eAAO,MAAM,iBAAiB;IAAK,IAAI;IAAmB,OAAO;CAAW,CAAA;AAE5E,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B,yBAAyB;IACzB,MAAM,EAAE,WAAW,GAAG,QAAQ,CAAA;IAC9B,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAA;CACf;AAED,8EAA8E;AAC9E,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,GACnG;IACE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC9C,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAUL;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACzC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,YAAY,CAAC,CA4CvB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,OAAO,CAAC,WAAW,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,EAC/C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,YAAY,CAAC,CA6EvB;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,CAazE;AAOD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,cAAc,CA6C3G"}
|
package/lib/types/service.d.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import type { Context } from '@deepseek-ai/cordis';
|
|
28
28
|
import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
|
|
29
|
+
import { type ProbeTarget } from './probe.js';
|
|
29
30
|
import type { McpServerStatus } from './upstream.js';
|
|
30
31
|
import type { McpPanelSnapshot, McpTrialResultWire, PatchPreview, PatchWriteResult, ProbeStarted } from './wire.js';
|
|
31
32
|
declare module '@deepseek-ai/cordis' {
|
|
@@ -105,10 +106,10 @@ export declare class McpPanelService extends TypertRemoteService {
|
|
|
105
106
|
*/
|
|
106
107
|
status(): McpPanelSnapshot;
|
|
107
108
|
/**
|
|
108
|
-
* Start a one-shot connectivity probe of one configured
|
|
109
|
-
*
|
|
110
|
-
* tool, but callable from the settings tab. Exported
|
|
111
|
-
* `mcpPanel/probe` invocation descriptor.
|
|
109
|
+
* Start a one-shot connectivity probe of one configured MCP server
|
|
110
|
+
* (streamable-http or stdio) as an UNOWNED background job — panel-only,
|
|
111
|
+
* like the `mcp_probe` tool, but callable from the settings tab. Exported
|
|
112
|
+
* on the wire by the `mcpPanel/probe` invocation descriptor.
|
|
112
113
|
*
|
|
113
114
|
* @param serverName - configured namespace.
|
|
114
115
|
* @returns the started job id and where the result lands.
|
|
@@ -159,12 +160,25 @@ export declare class McpPanelService extends TypertRemoteService {
|
|
|
159
160
|
private resolveOp;
|
|
160
161
|
/** Render validation issues as one actionable error. */
|
|
161
162
|
private issueError;
|
|
162
|
-
/** One passive-probe sweep over every configured
|
|
163
|
+
/** One passive-probe sweep over every configured MCP server (both transports). */
|
|
163
164
|
private runPassiveProbes;
|
|
164
165
|
/**
|
|
165
|
-
* Resolve one server's
|
|
166
|
-
*
|
|
167
|
-
*
|
|
166
|
+
* Resolve one configured server's probe spec. Credentials stay inside the
|
|
167
|
+
* returned value and are used for the probe only — they never reach a
|
|
168
|
+
* snapshot, a log, or a display. Streamable-http rows yield their raw
|
|
169
|
+
* endpoint + configured headers; stdio rows yield the launch spec
|
|
170
|
+
* (`command`/`args`, explicit `env` merged after the scrub, optional
|
|
171
|
+
* `cwd`) that the probe spawns.
|
|
172
|
+
*
|
|
173
|
+
* @param serverName - configured namespace.
|
|
174
|
+
* @returns the transport-tagged probe spec, or `undefined` when the server
|
|
175
|
+
* is not a configured MCP row of either transport.
|
|
176
|
+
*/
|
|
177
|
+
probeSpec(serverName: string): ProbeTarget | undefined;
|
|
178
|
+
/**
|
|
179
|
+
* Resolve one server's raw endpoint for the streamable-http probe path.
|
|
180
|
+
* Credentials stay inside this return value and are used for the request
|
|
181
|
+
* only — they never reach a snapshot, a log, or a display.
|
|
168
182
|
*
|
|
169
183
|
* @param serverName - configured namespace.
|
|
170
184
|
* @returns the raw URL + configured headers, or `undefined` when the server
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAA;AAuBtE,OAAO,EAAwB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAA;AAEnE,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,eAAe,CAAA;AACpE,OAAO,KAAK,EAEV,gBAAgB,EAEhB,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACb,MAAM,WAAW,CAAA;AAElB,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,wEAAwE;QACxE,QAAQ,EAAE,eAAe,CAAA;KAC1B;CACF;AAoDD,gFAAgF;AAChF,MAAM,WAAW,qBAAqB;IACpC,yCAAyC;IACzC,cAAc,EAAE,MAAM,CAAA;IACtB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAA;IACjB,8DAA8D;IAC9D,iBAAiB,EAAE,MAAM,CAAA;IACzB,2CAA2C;IAC3C,mBAAmB,EAAE,OAAO,CAAA;IAC5B,8CAA8C;IAC9C,sBAAsB,EAAE,MAAM,CAAA;IAC9B,iDAAiD;IACjD,YAAY,EAAE,OAAO,CAAA;IACrB,mDAAmD;IACnD,cAAc,EAAE,MAAM,CAAA;IACtB,gDAAgD;IAChD,mBAAmB,EAAE,MAAM,CAAA;IAC3B,uDAAuD;IACvD,YAAY,EAAE,OAAO,CAAA;IACrB,kDAAkD;IAClD,WAAW,EAAE,MAAM,CAAA;CACpB;AAiCD,qFAAqF;AACrF,qBAAa,eAAgB,SAAQ,mBAAmB;IA2B5B,OAAO,CAAC,QAAQ,CAAC,MAAM;IA1BjD,MAAM,CAAC,MAAM,WAAsB;IAEnC,oDAAoD;IACpD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;IAC9D,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA4B;IACvD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA4B;IAC5D,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA4B;IACvD,8DAA8D;IAC9D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+E;IAE7G,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsB;IAChD,qDAAqD;IACrD,OAAO,CAAC,cAAc,CAAQ;IAE9B;;;OAGG;IACH,YAAY,GAAG,EAAE,OAAO,EAAmB,MAAM,GAAE,qBAA8C,EAQhG;IAED;;;;;;;;OAQG;IACH,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CAmCtC;IAED;;;;;;;;OAQG;IACH,MAAM,IAAI,gBAAgB,CAwCzB;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,YAAY,CAmBtC;IAED;;;;;;;OAOG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAQzC;IAED;;;;;;;;;;;;;;;;;;OAkBG;IACG,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAgD7G;IAED;;;;;;;;;;OAUG;IACG,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAmB9F;IAED,yDAAyD;IACzD,OAAO,CAAC,SAAS;IAoBjB,wDAAwD;IACxD,OAAO,CAAC,UAAU;IAIlB,kFAAkF;YACpE,gBAAgB;IAiB9B;;;;;;;;;;;OAWG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAiDrD;IAED;;;;;;;;OAQG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,SAAS,CAG5F;IAED,gFAAgF;IAChF,OAAO,CAAC,UAAU;IAoBlB,4EAA4E;IAC5E,OAAO,CAAC,SAAS;CAMlB;eASc,eAAe"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mcp-panel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "MCP management console for the official DeepSeek Harness MCP client: /mcp command with health diagnostics and pipeline trial calls, a Settings MCP tab with server CRUD (append-only profile-patch fragments, approval-gated writes with automatic backups), a tool trial console over the official tool pipeline, connection status, reconnect counts and sanitized tool inventory, plus an optional Streamable HTTP probe.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
],
|
|
82
82
|
"compatibility": {
|
|
83
83
|
"dshVersions": [
|
|
84
|
-
"0.1.
|
|
84
|
+
"0.1.1-rc.2"
|
|
85
85
|
]
|
|
86
86
|
},
|
|
87
87
|
"capability": {
|
|
@@ -125,27 +125,28 @@
|
|
|
125
125
|
"dependencies": {
|
|
126
126
|
"tsdown": "^0.22.14",
|
|
127
127
|
"typescript": "^7.0.0",
|
|
128
|
-
"zod": "^4.4.3"
|
|
128
|
+
"zod": "^4.4.3",
|
|
129
|
+
"@deepseek-ai/dsh-subprocess": "0.1.1-rc.2"
|
|
129
130
|
},
|
|
130
131
|
"devDependencies": {
|
|
131
132
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
132
133
|
"@deepseek-ai/cordis-plugin-include": "1.0.6",
|
|
133
134
|
"@deepseek-ai/cordis-plugin-loader": "^1.0.2",
|
|
134
|
-
"@deepseek-ai/dsh-agent": "0.1.
|
|
135
|
-
"@deepseek-ai/dsh-app-boot": "0.1.
|
|
136
|
-
"@deepseek-ai/dsh-client-connection": "0.1.
|
|
137
|
-
"@deepseek-ai/dsh-client-locale": "0.1.
|
|
138
|
-
"@deepseek-ai/dsh-client-runtime": "0.1.
|
|
139
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.1.
|
|
140
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.1.
|
|
141
|
-
"@deepseek-ai/dsh-cmdline": "0.1.
|
|
142
|
-
"@deepseek-ai/dsh-commands": "0.1.
|
|
143
|
-
"@deepseek-ai/dsh-home-paths": "0.1.
|
|
144
|
-
"@deepseek-ai/dsh-jobs": "0.1.
|
|
145
|
-
"@deepseek-ai/dsh-session": "0.1.
|
|
146
|
-
"@deepseek-ai/dsh-system-prompt": "0.1.
|
|
147
|
-
"@deepseek-ai/dsh-tools": "0.1.
|
|
148
|
-
"@deepseek-ai/dsh-typert-protocol": "0.1.
|
|
135
|
+
"@deepseek-ai/dsh-agent": "0.1.1-rc.2",
|
|
136
|
+
"@deepseek-ai/dsh-app-boot": "0.1.1-rc.2",
|
|
137
|
+
"@deepseek-ai/dsh-client-connection": "0.1.1-rc.2",
|
|
138
|
+
"@deepseek-ai/dsh-client-locale": "0.1.1-rc.2",
|
|
139
|
+
"@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
|
|
140
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.1.1-rc.2",
|
|
141
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
|
|
142
|
+
"@deepseek-ai/dsh-cmdline": "0.1.1-rc.2",
|
|
143
|
+
"@deepseek-ai/dsh-commands": "0.1.1-rc.2",
|
|
144
|
+
"@deepseek-ai/dsh-home-paths": "0.1.1-rc.2",
|
|
145
|
+
"@deepseek-ai/dsh-jobs": "0.1.1-rc.2",
|
|
146
|
+
"@deepseek-ai/dsh-session": "0.1.1-rc.2",
|
|
147
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
|
|
148
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.2",
|
|
149
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.1-rc.2",
|
|
149
150
|
"@deepseek-ai/schemastery": "^3.18.0",
|
|
150
151
|
"@types/node": "^22.19.0",
|
|
151
152
|
"@types/react": "18.3.31",
|
package/src/probe.ts
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The optional `mcp_probe` tool: a one-shot connectivity probe of one
|
|
3
|
-
* configured
|
|
4
|
-
* Probe results are panel-only — the tool returns
|
|
5
|
-
* pointer to the settings tab, the job carries no
|
|
6
|
-
* notice is injected into the model), and the panel
|
|
7
|
-
* snapshot back through `mcpPanel/status`.
|
|
3
|
+
* configured MCP server (streamable-http or stdio transport), executed as an
|
|
4
|
+
* UNOWNED background job. Probe results are panel-only — the tool returns
|
|
5
|
+
* just the job id and a pointer to the settings tab, the job carries no
|
|
6
|
+
* owner (so no completion notice is injected into the model), and the panel
|
|
7
|
+
* reads the sanitized snapshot back through `mcpPanel/status`.
|
|
8
|
+
*
|
|
9
|
+
* The stdio probe spawns the configured `command`/`args` with the same
|
|
10
|
+
* `scrubbedParentEnv` base the mcp-client bridge uses (credential-shaped and
|
|
11
|
+
* `DSH_*` names never leak into the child implicitly) plus the row's explicit
|
|
12
|
+
* `env`/`cwd`, and completes one MCP `initialize` handshake over stdin/stdout.
|
|
8
13
|
*
|
|
9
14
|
* @module dsh-mcp-panel/probe
|
|
10
15
|
*/
|
|
11
16
|
|
|
17
|
+
import { spawn } from 'node:child_process'
|
|
12
18
|
import type { JobHooks, JobOutcome, JobRegistry } from '@deepseek-ai/dsh-jobs'
|
|
19
|
+
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
|
|
13
20
|
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
|
14
21
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
15
22
|
import { sanitizeError, sanitizeText } from './sanitize.ts'
|
|
@@ -20,7 +27,7 @@ export const PROBE_KIND = 'mcp-probe'
|
|
|
20
27
|
|
|
21
28
|
declare module '@deepseek-ai/dsh-jobs' {
|
|
22
29
|
interface JobKindMap {
|
|
23
|
-
/** One-shot
|
|
30
|
+
/** One-shot connectivity probe (streamable-http or stdio; panel-only results). */
|
|
24
31
|
'mcp-probe': 'mcp-probe'
|
|
25
32
|
}
|
|
26
33
|
}
|
|
@@ -31,7 +38,7 @@ const INITIALIZE_PROTOCOL_VERSION = '2024-11-05'
|
|
|
31
38
|
/** MCP clientInfo facts; protocol constants, not configuration. Exported so the
|
|
32
39
|
* version-consistency tripwire (`tests/version.spec.ts`) can assert the
|
|
33
40
|
* advertised version tracks the package version. */
|
|
34
|
-
export const PROBE_CLIENT_INFO = { name: 'dsh-mcp-panel', version: '0.
|
|
41
|
+
export const PROBE_CLIENT_INFO = { name: 'dsh-mcp-panel', version: '0.5.1' }
|
|
35
42
|
|
|
36
43
|
/** One settled probe: outcome status plus a sanitized one-line detail. */
|
|
37
44
|
export interface ProbeOutcome {
|
|
@@ -41,6 +48,17 @@ export interface ProbeOutcome {
|
|
|
41
48
|
detail: string
|
|
42
49
|
}
|
|
43
50
|
|
|
51
|
+
/** One configured server's probe spec: an HTTP endpoint or a stdio launch. */
|
|
52
|
+
export type ProbeTarget =
|
|
53
|
+
| { readonly kind: 'http'; readonly url: string; readonly headers: Readonly<Record<string, string>> }
|
|
54
|
+
| {
|
|
55
|
+
readonly kind: 'stdio'
|
|
56
|
+
readonly command: string
|
|
57
|
+
readonly args: readonly string[]
|
|
58
|
+
readonly env: Readonly<Record<string, string>>
|
|
59
|
+
readonly cwd?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
/** Display cap for server-reported name/version fields in probe details. */
|
|
45
63
|
const DISPLAY_LIMIT = 80
|
|
46
64
|
|
|
@@ -112,19 +130,118 @@ export async function probeEndpoint(
|
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
/**
|
|
115
|
-
*
|
|
116
|
-
* outcome
|
|
133
|
+
* Complete one MCP `initialize` handshake over a spawned stdio server's
|
|
134
|
+
* stdin/stdout and describe the outcome in one sanitized line. The child runs
|
|
135
|
+
* under the same `scrubbedParentEnv` base the mcp-client bridge uses —
|
|
136
|
+
* credential-shaped and `DSH_*` parent names never reach it implicitly — plus
|
|
137
|
+
* the row's explicit `env` (merged after the scrub) and optional `cwd`. The
|
|
138
|
+
* child's stderr is consumed and never rendered; the configured `command`/
|
|
139
|
+
* `args` are launch facts, not display content, but any error is sanitized
|
|
140
|
+
* before it lands in the outcome detail.
|
|
141
|
+
*
|
|
142
|
+
* @param target - resolved stdio launch spec.
|
|
143
|
+
* @param timeoutMs - probe deadline.
|
|
144
|
+
* @param signal - caller-owned abort (job kill or timeout).
|
|
145
|
+
* @returns the settled probe outcome.
|
|
146
|
+
*/
|
|
147
|
+
export function probeStdio(
|
|
148
|
+
target: Extract<ProbeTarget, { kind: 'stdio' }>,
|
|
149
|
+
timeoutMs: number,
|
|
150
|
+
signal: AbortSignal,
|
|
151
|
+
): Promise<ProbeOutcome> {
|
|
152
|
+
const started = Date.now()
|
|
153
|
+
return new Promise<ProbeOutcome>((resolve) => {
|
|
154
|
+
let settled = false
|
|
155
|
+
let child: ReturnType<typeof spawn> | undefined
|
|
156
|
+
let buffer = ''
|
|
157
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
158
|
+
const finish = (outcome: ProbeOutcome): void => {
|
|
159
|
+
if (settled) return
|
|
160
|
+
settled = true
|
|
161
|
+
if (timer !== undefined) clearTimeout(timer)
|
|
162
|
+
try { child?.kill() } catch { /* already gone */ }
|
|
163
|
+
resolve(outcome)
|
|
164
|
+
}
|
|
165
|
+
timer = setTimeout(() => {
|
|
166
|
+
if (!settled) finish({ status: 'failed', detail: `timeout after ${timeoutMs}ms or cancelled` })
|
|
167
|
+
}, timeoutMs)
|
|
168
|
+
try {
|
|
169
|
+
child = spawn(target.command, [...target.args], {
|
|
170
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
171
|
+
env: { ...scrubbedParentEnv(), ...target.env },
|
|
172
|
+
...(target.cwd !== undefined && target.cwd !== '' ? { cwd: target.cwd } : {}),
|
|
173
|
+
windowsHide: true,
|
|
174
|
+
})
|
|
175
|
+
} catch (error) {
|
|
176
|
+
finish({ status: 'failed', detail: sanitizeError(error) })
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
child.on('error', (error) => finish({ status: 'failed', detail: sanitizeError(error) }))
|
|
180
|
+
child.on('exit', (code, signalName) => {
|
|
181
|
+
if (!settled) {
|
|
182
|
+
finish({ status: 'failed', detail: `process exited before initialize response (code ${code ?? 'null'}${signalName ? `, signal ${signalName}` : ''})` })
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
186
|
+
buffer += chunk.toString('utf8')
|
|
187
|
+
let index: number
|
|
188
|
+
while ((index = buffer.indexOf('\n')) !== -1) {
|
|
189
|
+
const line = buffer.slice(0, index).trim()
|
|
190
|
+
buffer = buffer.slice(index + 1)
|
|
191
|
+
if (line === '') continue
|
|
192
|
+
let message: { id?: unknown; result?: { serverInfo?: { name?: unknown; version?: unknown } } } | undefined
|
|
193
|
+
try {
|
|
194
|
+
message = JSON.parse(line) as typeof message
|
|
195
|
+
} catch {
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (message !== null && typeof message === 'object' && message.id === 1 && message.result !== undefined) {
|
|
199
|
+
const serverInfo = message.result.serverInfo ?? {}
|
|
200
|
+
const name = typeof serverInfo.name === 'string' && serverInfo.name !== ''
|
|
201
|
+
? boundedDisplay(sanitizeText(serverInfo.name))
|
|
202
|
+
: 'unnamed'
|
|
203
|
+
const version = typeof serverInfo.version === 'string' && serverInfo.version !== ''
|
|
204
|
+
? boundedDisplay(sanitizeText(serverInfo.version))
|
|
205
|
+
: 'unknown version'
|
|
206
|
+
finish({ status: 'completed', detail: `MCP initialize ok over stdio (server ${name} ${version}) in ${Date.now() - started}ms` })
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
child.stderr?.on('data', () => { /* consumed, never rendered */ })
|
|
212
|
+
signal.addEventListener('abort', () => {
|
|
213
|
+
if (!settled) finish({ status: 'failed', detail: `timeout after ${timeoutMs}ms or cancelled` })
|
|
214
|
+
}, { once: true })
|
|
215
|
+
try {
|
|
216
|
+
child.stdin?.write(`${JSON.stringify({
|
|
217
|
+
jsonrpc: '2.0',
|
|
218
|
+
id: 1,
|
|
219
|
+
method: 'initialize',
|
|
220
|
+
params: {
|
|
221
|
+
protocolVersion: INITIALIZE_PROTOCOL_VERSION,
|
|
222
|
+
capabilities: {},
|
|
223
|
+
clientInfo: PROBE_CLIENT_INFO,
|
|
224
|
+
},
|
|
225
|
+
})}\n`)
|
|
226
|
+
} catch { /* the exit/error paths settle the outcome */ }
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Create the background-job hooks for one probe: cancel aborts the fetch (or
|
|
232
|
+
* the spawned child), the outcome settles `done` with sanitized detail only.
|
|
117
233
|
*
|
|
118
|
-
* @param
|
|
119
|
-
* @param headers - configured headers (used, never rendered).
|
|
234
|
+
* @param target - resolved probe spec (http endpoint or stdio launch).
|
|
120
235
|
* @param timeoutMs - probe deadline.
|
|
121
236
|
* @returns the registry hooks.
|
|
122
237
|
*/
|
|
123
|
-
export function probeJob(
|
|
238
|
+
export function probeJob(target: ProbeTarget, timeoutMs: number): JobHooks {
|
|
124
239
|
const controller = new AbortController()
|
|
125
240
|
const timer = setTimeout(() => { controller.abort() }, timeoutMs)
|
|
126
241
|
timer.unref?.()
|
|
127
|
-
const done: Promise<JobOutcome> =
|
|
242
|
+
const done: Promise<JobOutcome> = (target.kind === 'stdio'
|
|
243
|
+
? probeStdio(target, timeoutMs, controller.signal)
|
|
244
|
+
: probeEndpoint(target.url, target.headers, timeoutMs, controller.signal))
|
|
128
245
|
.then(outcome => ({ ...outcome }))
|
|
129
246
|
.finally(() => { clearTimeout(timer) })
|
|
130
247
|
return {
|
|
@@ -133,12 +250,9 @@ export function probeJob(url: string, headers: Readonly<Record<string, string>>,
|
|
|
133
250
|
}
|
|
134
251
|
}
|
|
135
252
|
|
|
136
|
-
/** Resolve one configured server's
|
|
137
|
-
function probeTarget(
|
|
138
|
-
service
|
|
139
|
-
server: string,
|
|
140
|
-
): { url: string; headers: Record<string, string> } | undefined {
|
|
141
|
-
return service.rawEndpoint(server)
|
|
253
|
+
/** Resolve one configured server's probe spec (http endpoint or stdio launch). */
|
|
254
|
+
function probeTarget(service: McpPanelService, server: string): ProbeTarget | undefined {
|
|
255
|
+
return service.probeSpec(server)
|
|
142
256
|
}
|
|
143
257
|
|
|
144
258
|
/**
|
|
@@ -152,13 +266,13 @@ function probeTarget(
|
|
|
152
266
|
export function mcpProbeTool(service: McpPanelService, jobs: JobRegistry, timeoutMs: number): ToolDefinition {
|
|
153
267
|
return defineTool({
|
|
154
268
|
name: 'mcp_probe',
|
|
155
|
-
description: 'Run a one-shot connectivity probe of a configured streamable-http
|
|
269
|
+
description: 'Run a one-shot connectivity probe of a configured MCP server (streamable-http or stdio) as a background job. '
|
|
156
270
|
+ 'Results appear in the MCP settings panel only — they are not injected into model context.',
|
|
157
271
|
parameters: {
|
|
158
272
|
server: {
|
|
159
273
|
type: 'string',
|
|
160
274
|
required: true,
|
|
161
|
-
description: 'serverName of a configured
|
|
275
|
+
description: 'serverName of a configured MCP server (see /mcp for the list); streamable-http and stdio transports are both supported.',
|
|
162
276
|
},
|
|
163
277
|
},
|
|
164
278
|
output: {
|
|
@@ -179,15 +293,14 @@ export function mcpProbeTool(service: McpPanelService, jobs: JobRegistry, timeou
|
|
|
179
293
|
const target = probeTarget(service, args.server)
|
|
180
294
|
if (target === undefined) {
|
|
181
295
|
throw new Error(
|
|
182
|
-
`mcp_probe: "${args.server}" is not a configured
|
|
183
|
-
+ 'stdio servers have no HTTP endpoint to probe.',
|
|
296
|
+
`mcp_probe: "${args.server}" is not a configured MCP server (see /mcp).`,
|
|
184
297
|
)
|
|
185
298
|
}
|
|
186
299
|
const jobId = jobs.start({
|
|
187
300
|
kind: PROBE_KIND,
|
|
188
301
|
label: `mcp_probe ${args.server}`,
|
|
189
302
|
// Unowned: no model completion notice, readable by the panel only.
|
|
190
|
-
run: () => probeJob(target
|
|
303
|
+
run: () => probeJob(target, timeoutMs),
|
|
191
304
|
})
|
|
192
305
|
return {
|
|
193
306
|
jobId,
|
package/src/service.ts
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
} from './patch.ts'
|
|
52
52
|
import { appendPatchFragment } from './write.ts'
|
|
53
53
|
import { createTrialCaller, validateTrialRequest, type McpAgentRegistryFace, type McpTrialRequest } from './trial.ts'
|
|
54
|
-
import {
|
|
54
|
+
import { probeJob, PROBE_KIND, type ProbeTarget } from './probe.ts'
|
|
55
55
|
import { sanitizeText } from './sanitize.ts'
|
|
56
56
|
import type { McpServerStatus, McpStatusPhase } from './upstream.ts'
|
|
57
57
|
import type {
|
|
@@ -312,18 +312,18 @@ private readonly trialCaller = createTrialCaller()
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
/**
|
|
315
|
-
* Start a one-shot connectivity probe of one configured
|
|
316
|
-
*
|
|
317
|
-
* tool, but callable from the settings tab. Exported
|
|
318
|
-
* `mcpPanel/probe` invocation descriptor.
|
|
315
|
+
* Start a one-shot connectivity probe of one configured MCP server
|
|
316
|
+
* (streamable-http or stdio) as an UNOWNED background job — panel-only,
|
|
317
|
+
* like the `mcp_probe` tool, but callable from the settings tab. Exported
|
|
318
|
+
* on the wire by the `mcpPanel/probe` invocation descriptor.
|
|
319
319
|
*
|
|
320
320
|
* @param serverName - configured namespace.
|
|
321
321
|
* @returns the started job id and where the result lands.
|
|
322
322
|
*/
|
|
323
323
|
probe(serverName: string): ProbeStarted {
|
|
324
|
-
const target = this.
|
|
324
|
+
const target = this.probeSpec(serverName)
|
|
325
325
|
if (target === undefined) {
|
|
326
|
-
throw new Error(`dsh-mcp-panel: "${serverName}" is not a configured streamable-http
|
|
326
|
+
throw new Error(`dsh-mcp-panel: "${serverName}" is not a configured MCP server (streamable-http or stdio)`)
|
|
327
327
|
}
|
|
328
328
|
const jobs = this.ctx.get('jobs')
|
|
329
329
|
if (jobs === undefined) {
|
|
@@ -333,7 +333,7 @@ private readonly trialCaller = createTrialCaller()
|
|
|
333
333
|
kind: PROBE_KIND,
|
|
334
334
|
label: `mcp_probe ${serverName}`,
|
|
335
335
|
// Unowned: no model completion notice, readable by the panel only.
|
|
336
|
-
run: () => probeJob(target
|
|
336
|
+
run: () => probeJob(target, this.config.probeTimeoutMs),
|
|
337
337
|
})
|
|
338
338
|
return {
|
|
339
339
|
jobId,
|
|
@@ -486,7 +486,7 @@ private readonly trialCaller = createTrialCaller()
|
|
|
486
486
|
return new Error(`dsh-mcp-panel: invalid patch operation — ${resolution.issues.map(issue => issue.text).join(' ')}`)
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
-
/** One passive-probe sweep over every configured
|
|
489
|
+
/** One passive-probe sweep over every configured MCP server (both transports). */
|
|
490
490
|
private async runPassiveProbes(): Promise<void> {
|
|
491
491
|
if (this.passiveRunning) return
|
|
492
492
|
this.passiveRunning = true
|
|
@@ -494,9 +494,9 @@ private readonly trialCaller = createTrialCaller()
|
|
|
494
494
|
for (const entry of this.ctx.loader.entries()) {
|
|
495
495
|
if (entry.options.name !== MCP_CLIENT_MODULE) continue
|
|
496
496
|
const serverName = serverNameOf(entry.options.config, `entry:${entry.options.id}`)
|
|
497
|
-
const target = this.
|
|
497
|
+
const target = this.probeSpec(serverName)
|
|
498
498
|
if (target === undefined) continue
|
|
499
|
-
const outcome = await
|
|
499
|
+
const outcome = await probeJob(target, this.config.probeTimeoutMs).done
|
|
500
500
|
this.probeStates.set(serverName, { state: outcome.status === 'completed' ? 'reachable' : 'unreachable', checkedAt: Date.now() })
|
|
501
501
|
}
|
|
502
502
|
} finally {
|
|
@@ -505,15 +505,18 @@ private readonly trialCaller = createTrialCaller()
|
|
|
505
505
|
}
|
|
506
506
|
|
|
507
507
|
/**
|
|
508
|
-
* Resolve one server's
|
|
509
|
-
*
|
|
510
|
-
*
|
|
508
|
+
* Resolve one configured server's probe spec. Credentials stay inside the
|
|
509
|
+
* returned value and are used for the probe only — they never reach a
|
|
510
|
+
* snapshot, a log, or a display. Streamable-http rows yield their raw
|
|
511
|
+
* endpoint + configured headers; stdio rows yield the launch spec
|
|
512
|
+
* (`command`/`args`, explicit `env` merged after the scrub, optional
|
|
513
|
+
* `cwd`) that the probe spawns.
|
|
511
514
|
*
|
|
512
515
|
* @param serverName - configured namespace.
|
|
513
|
-
* @returns the
|
|
514
|
-
* is not a configured
|
|
516
|
+
* @returns the transport-tagged probe spec, or `undefined` when the server
|
|
517
|
+
* is not a configured MCP row of either transport.
|
|
515
518
|
*/
|
|
516
|
-
|
|
519
|
+
probeSpec(serverName: string): ProbeTarget | undefined {
|
|
517
520
|
for (const entry of this.ctx.loader.entries()) {
|
|
518
521
|
if (entry.options.name !== MCP_CLIENT_MODULE) continue
|
|
519
522
|
const config = entry.options.config
|
|
@@ -524,21 +527,60 @@ private readonly trialCaller = createTrialCaller()
|
|
|
524
527
|
if (serverNameOf(config, `entry:${entry.options.id}`) !== serverName) continue
|
|
525
528
|
if (typeof config !== 'object' || config === null || Array.isArray(config)) return undefined
|
|
526
529
|
const row = config as Record<string, unknown>
|
|
527
|
-
if (row['transport']
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
530
|
+
if (row['transport'] === 'streamable-http') {
|
|
531
|
+
const url = row['url']
|
|
532
|
+
if (typeof url !== 'string' || url === '') return undefined
|
|
533
|
+
const headersValue = row['headers']
|
|
534
|
+
const headers: Record<string, string> = {}
|
|
535
|
+
if (typeof headersValue === 'object' && headersValue !== null && !Array.isArray(headersValue)) {
|
|
536
|
+
for (const [name, value] of Object.entries(headersValue as Record<string, unknown>)) {
|
|
537
|
+
if (typeof value === 'string') headers[name] = value
|
|
538
|
+
}
|
|
535
539
|
}
|
|
540
|
+
return { kind: 'http', url, headers }
|
|
541
|
+
}
|
|
542
|
+
if (row['transport'] === 'stdio') {
|
|
543
|
+
const command = row['command']
|
|
544
|
+
if (typeof command !== 'string' || command === '') return undefined
|
|
545
|
+
const argsValue = row['args']
|
|
546
|
+
const args: string[] = []
|
|
547
|
+
if (Array.isArray(argsValue)) {
|
|
548
|
+
for (const value of argsValue) {
|
|
549
|
+
if (typeof value !== 'string') return undefined
|
|
550
|
+
args.push(value)
|
|
551
|
+
}
|
|
552
|
+
} else if (argsValue !== undefined) return undefined
|
|
553
|
+
const envValue = row['env']
|
|
554
|
+
const env: Record<string, string> = {}
|
|
555
|
+
if (typeof envValue === 'object' && envValue !== null && !Array.isArray(envValue)) {
|
|
556
|
+
for (const [name, value] of Object.entries(envValue as Record<string, unknown>)) {
|
|
557
|
+
if (typeof value !== 'string') return undefined
|
|
558
|
+
env[name] = value
|
|
559
|
+
}
|
|
560
|
+
} else if (envValue !== undefined) return undefined
|
|
561
|
+
const cwd = row['cwd']
|
|
562
|
+
if (cwd !== undefined && (typeof cwd !== 'string' || cwd === '')) return undefined
|
|
563
|
+
return { kind: 'stdio', command, args, env, ...(typeof cwd === 'string' ? { cwd } : {}) }
|
|
536
564
|
}
|
|
537
|
-
return
|
|
565
|
+
return undefined
|
|
538
566
|
}
|
|
539
567
|
return undefined
|
|
540
568
|
}
|
|
541
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Resolve one server's raw endpoint for the streamable-http probe path.
|
|
572
|
+
* Credentials stay inside this return value and are used for the request
|
|
573
|
+
* only — they never reach a snapshot, a log, or a display.
|
|
574
|
+
*
|
|
575
|
+
* @param serverName - configured namespace.
|
|
576
|
+
* @returns the raw URL + configured headers, or `undefined` when the server
|
|
577
|
+
* is not a configured streamable-http row.
|
|
578
|
+
*/
|
|
579
|
+
rawEndpoint(serverName: string): { url: string; headers: Record<string, string> } | undefined {
|
|
580
|
+
const spec = this.probeSpec(serverName)
|
|
581
|
+
return spec?.kind === 'http' ? { url: spec.url, headers: { ...spec.headers } } : undefined
|
|
582
|
+
}
|
|
583
|
+
|
|
542
584
|
/** Unowned `mcp-probe` background jobs, newest first, sanitized for display. */
|
|
543
585
|
private probeViews(): McpProbeView[] {
|
|
544
586
|
const jobs = this.ctx.get('jobs')
|