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 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.0-rc.8`–`0.2.0` |
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.0-rc.8`–`0.2.0` |
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
+ [![Gitee](https://img.shields.io/badge/Gitee-mirror-c71d23?logo=gitee)](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.0-rc.8`–`0.2.0` |
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.0-rc.8`–`0.2.0` |
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
@@ -24,7 +24,7 @@
24
24
 
25
25
  | 维度 | 状态 |
26
26
  |---|---|
27
- | Harness | DeepSeek Harness `0.1.0-rc.8`–`0.2.0` |
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 + 浏览器) |
30
30
  | 模型 | 任意(面板只读;仅 `/mcp` 输出对模型可见) |
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.4.2"
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
- * Create the background-job hooks for one probe: cancel aborts the fetch, the
672
- * outcome settles `done` with sanitized detail only.
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 url - endpoint URL.
675
- * @param headers - configured headers (used, never rendered).
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(url, headers, timeoutMs) {
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 raw endpoint for probing. */
824
+ /** Resolve one configured server's probe spec (http endpoint or stdio launch). */
695
825
  function probeTarget(service, server) {
696
- return service.rawEndpoint(server);
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 MCP server as a background job. Results appear in the MCP settings panel only — they are not injected into model context.",
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 streamable-http MCP server (see /mcp for the list)."
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 streamable-http MCP server (see /mcp). stdio servers have no HTTP endpoint to probe.`);
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.url, target.headers, timeoutMs)
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 streamable-http
2035
- * server as an UNOWNED background job — panel-only, like the `mcp_probe`
2036
- * tool, but callable from the settings tab. Exported on the wire by the
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.rawEndpoint(serverName);
2044
- if (target === void 0) throw new Error(`dsh-mcp-panel: "${serverName}" is not a configured streamable-http MCP server`);
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.url, target.headers, this.config.probeTimeoutMs)
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 streamable-http server. */
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.rawEndpoint(serverName);
2316
+ const target = this.probeSpec(serverName);
2187
2317
  if (target === void 0) continue;
2188
- const outcome = await probeEndpoint(target.url, target.headers, this.config.probeTimeoutMs, AbortSignal.timeout(this.config.probeTimeoutMs));
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 raw endpoint for the probe tool. Credentials stay
2200
- * inside this return value and are used for the request only — they never
2201
- * reach a snapshot, a log, or a display.
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 raw URL + configured headers, or `undefined` when the server
2205
- * is not a configured streamable-http row.
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
- rawEndpoint(serverName) {
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"] !== "streamable-http") return void 0;
2215
- const url = row["url"];
2216
- if (typeof url !== "string" || url === "") return void 0;
2217
- const headersValue = row["headers"];
2218
- const headers = {};
2219
- if (typeof headersValue === "object" && headersValue !== null && !Array.isArray(headersValue)) {
2220
- for (const [name, value] of Object.entries(headersValue)) if (typeof value === "string") headers[name] = value;
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
- return {
2223
- url,
2224
- headers
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");
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * The optional `mcp_probe` tool: a one-shot connectivity probe of one
3
- * configured Streamable HTTP endpoint, executed as an UNOWNED background job.
4
- * Probe results are panel-only — the tool returns just the job id and a
5
- * pointer to the settings tab, the job carries no owner (so no completion
6
- * notice is injected into the model), and the panel reads the sanitized
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 Streamable HTTP connectivity probe (panel-only results). */
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
- * Create the background-job hooks for one probe: cancel aborts the fetch, the
50
- * outcome settles `done` with sanitized detail only.
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 url - endpoint URL.
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(url: string, headers: Readonly<Record<string, string>>, timeoutMs: number): JobHooks;
91
+ export declare function probeJob(target: ProbeTarget, timeoutMs: number): JobHooks;
58
92
  /**
59
93
  * Build the `mcp_probe` tool definition.
60
94
  *
@@ -1 +1 @@
1
- {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAc,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAC9E,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,wEAAwE;QACxE,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;AAUD;;;;;;;;;;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;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,CAW5G;AAUD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,GAAG,cAAc,CA8C3G"}
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"}
@@ -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 streamable-http
109
- * server as an UNOWNED background job — panel-only, like the `mcp_probe`
110
- * tool, but callable from the settings tab. Exported on the wire by the
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 streamable-http server. */
163
+ /** One passive-probe sweep over every configured MCP server (both transports). */
163
164
  private runPassiveProbes;
164
165
  /**
165
- * Resolve one server's raw endpoint for the probe tool. Credentials stay
166
- * inside this return value and are used for the request only — they never
167
- * reach a snapshot, a log, or a display.
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":"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"}
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.4.2",
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.0-rc.8"
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.0-rc.8",
135
- "@deepseek-ai/dsh-app-boot": "0.1.0-rc.8",
136
- "@deepseek-ai/dsh-client-connection": "0.1.0-rc.8",
137
- "@deepseek-ai/dsh-client-locale": "0.1.0-rc.8",
138
- "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.8",
139
- "@deepseek-ai/dsh-client-ui-settings": "0.1.0-rc.8",
140
- "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.8",
141
- "@deepseek-ai/dsh-cmdline": "0.1.0-rc.8",
142
- "@deepseek-ai/dsh-commands": "0.1.0-rc.8",
143
- "@deepseek-ai/dsh-home-paths": "0.1.0-rc.8",
144
- "@deepseek-ai/dsh-jobs": "0.1.0-rc.8",
145
- "@deepseek-ai/dsh-session": "0.1.0-rc.8",
146
- "@deepseek-ai/dsh-system-prompt": "0.1.0-rc.8",
147
- "@deepseek-ai/dsh-tools": "0.1.0-rc.8",
148
- "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.8",
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 Streamable HTTP endpoint, executed as an UNOWNED background job.
4
- * Probe results are panel-only — the tool returns just the job id and a
5
- * pointer to the settings tab, the job carries no owner (so no completion
6
- * notice is injected into the model), and the panel reads the sanitized
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 Streamable HTTP connectivity probe (panel-only results). */
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.4.2' }
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
- * Create the background-job hooks for one probe: cancel aborts the fetch, the
116
- * outcome settles `done` with sanitized detail only.
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 url - endpoint URL.
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(url: string, headers: Readonly<Record<string, string>>, timeoutMs: number): JobHooks {
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> = probeEndpoint(url, headers, timeoutMs, controller.signal)
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 raw endpoint for probing. */
137
- function probeTarget(
138
- service: McpPanelService,
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 MCP server as a background job. '
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 streamable-http MCP server (see /mcp for the list).',
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 streamable-http MCP server (see /mcp). `
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.url, target.headers, timeoutMs),
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 { probeEndpoint, probeJob, PROBE_KIND } from './probe.ts'
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 streamable-http
316
- * server as an UNOWNED background job — panel-only, like the `mcp_probe`
317
- * tool, but callable from the settings tab. Exported on the wire by the
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.rawEndpoint(serverName)
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 MCP server`)
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.url, target.headers, this.config.probeTimeoutMs),
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 streamable-http server. */
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.rawEndpoint(serverName)
497
+ const target = this.probeSpec(serverName)
498
498
  if (target === undefined) continue
499
- const outcome = await probeEndpoint(target.url, target.headers, this.config.probeTimeoutMs, AbortSignal.timeout(this.config.probeTimeoutMs))
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 raw endpoint for the probe tool. Credentials stay
509
- * inside this return value and are used for the request only — they never
510
- * reach a snapshot, a log, or a display.
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 raw URL + configured headers, or `undefined` when the server
514
- * is not a configured streamable-http row.
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
- rawEndpoint(serverName: string): { url: string; headers: Record<string, string> } | undefined {
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'] !== 'streamable-http') return undefined
528
- const url = row['url']
529
- if (typeof url !== 'string' || url === '') return undefined
530
- const headersValue = row['headers']
531
- const headers: Record<string, string> = {}
532
- if (typeof headersValue === 'object' && headersValue !== null && !Array.isArray(headersValue)) {
533
- for (const [name, value] of Object.entries(headersValue as Record<string, unknown>)) {
534
- if (typeof value === 'string') headers[name] = value
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 { url, headers }
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')