denpex-mcp 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/index.js +103 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,10 @@ straight off disk and hands back the root cause and the fix.
|
|
|
19
19
|
Sign in at **[denpex.com/dashboard](https://denpex.com/dashboard) → API Keys** and create a key (`dpx_…`).
|
|
20
20
|
The key unlocks authenticated cloud diagnosis, your diagnosis history, quota, and fleet
|
|
21
21
|
incidents. *Without a key, `diagnose_crash` uses the same canonical anonymous engine as
|
|
22
|
-
the homepage's three free diagnoses.
|
|
22
|
+
the homepage's three free diagnoses. Anonymous clarifications reuse the server-issued,
|
|
23
|
+
log-bound refinement token automatically (or accept it explicitly after an MCP restart),
|
|
24
|
+
so answering Denpex does not spend another diagnosis and cannot be forged with a public
|
|
25
|
+
boolean header. Set `DENPEX_LOCAL=1` explicitly for zero-egress;
|
|
23
26
|
`lookup_failure` and `analyze_cascade` remain available from the bundled offline data.*
|
|
24
27
|
|
|
25
28
|
### 2. Add it to your client
|
|
@@ -103,7 +106,7 @@ errors is cloud-only and simply unavailable offline.
|
|
|
103
106
|
|
|
104
107
|
| Tool | What it does | Needs |
|
|
105
108
|
|------|--------------|-------|
|
|
106
|
-
| `diagnose_crash` | Root cause + escalating fix from `logs` / `file` / `dir`. Also accepts `telemetry` (dmesg / nvidia-smi / ibstat), `codeContext` and `
|
|
109
|
+
| `diagnose_crash` | Root cause + escalating fix from `logs` / `file` / `dir`. Also accepts `telemetry` (dmesg / nvidia-smi / ibstat), `codeContext`, `topologyMap`, typed `operatorAnswers`, and (after an anonymous MCP restart) the prior `refinementToken`. | canonical cloud by default; explicit offline mode |
|
|
107
110
|
| `lookup_failure` | Search the bundled encyclopedia (300+ classes) by code/keyword/symptom. | works offline |
|
|
108
111
|
| `analyze_cascade` | Find the culprit rank in a distributed hang; route Xid faults. | works offline |
|
|
109
112
|
| `submit_fix_outcome` | Report whether a fix worked. **This is what makes the next diagnosis better.** | API key |
|
|
@@ -154,7 +157,7 @@ Two fields deserve attention when you write the agent loop:
|
|
|
154
157
|
```bash
|
|
155
158
|
npm install
|
|
156
159
|
npm run build # regenerates the offline corpus, then bundles dist/index.js
|
|
157
|
-
npm test #
|
|
160
|
+
npm test # 111 unit + protocol-integration tests
|
|
158
161
|
npm run inspector # interactive protocol inspector (MCP Inspector)
|
|
159
162
|
```
|
|
160
163
|
|
package/dist/index.js
CHANGED
|
@@ -106,6 +106,7 @@ var DenpexClient = class {
|
|
|
106
106
|
const headers = { Accept: "application/json" };
|
|
107
107
|
if (this.config.apiKey) headers["X-Denpex-Key"] = this.config.apiKey;
|
|
108
108
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
109
|
+
Object.assign(headers, opts.headers ?? {});
|
|
109
110
|
let res;
|
|
110
111
|
try {
|
|
111
112
|
res = await fetch(`${this.config.apiUrl}${path2}`, {
|
|
@@ -139,6 +140,7 @@ var DenpexClient = class {
|
|
|
139
140
|
|
|
140
141
|
// src/tools/diagnose.ts
|
|
141
142
|
import { z as z2 } from "zod";
|
|
143
|
+
import { createHash } from "crypto";
|
|
142
144
|
|
|
143
145
|
// src/logs.ts
|
|
144
146
|
import { promises as fs } from "fs";
|
|
@@ -697,6 +699,10 @@ function viewFromCloud(payload, note) {
|
|
|
697
699
|
steps: asArray(fix.steps),
|
|
698
700
|
evidence: asArray(fix.evidence),
|
|
699
701
|
clarifying: asArray(fix.clarifyingQuestions),
|
|
702
|
+
clarifyingChoices: objArray(fix.clarifyingChoices),
|
|
703
|
+
evidenceRequests: objArray(fix.evidenceRequests),
|
|
704
|
+
awaitingEvidence: fix.awaitingEvidence === true,
|
|
705
|
+
refinementToken: str(payload.refinementToken) ?? void 0,
|
|
700
706
|
advisories: [
|
|
701
707
|
...advisory.map((a) => `${(a.summary ?? "Advisory").trim()}${a.action ? ` \u2014 ${a.action}` : ""}`),
|
|
702
708
|
...advisoriesFromAdvanced(payload.advanced)
|
|
@@ -740,6 +746,9 @@ function viewFromLocal(r, note) {
|
|
|
740
746
|
steps: r.steps ?? [],
|
|
741
747
|
evidence: r.evidence ?? [],
|
|
742
748
|
clarifying: [],
|
|
749
|
+
clarifyingChoices: [],
|
|
750
|
+
evidenceRequests: [],
|
|
751
|
+
awaitingEvidence: false,
|
|
743
752
|
advisories: (r.detectors ?? []).map((d) => `${d.severity.toUpperCase()} \u2014 ${d.summary}`),
|
|
744
753
|
// The offline engine has no advisory array, no falsification contracts, no
|
|
745
754
|
// community/research grounding and no ranked-hypotheses ledger. Emit the fields as
|
|
@@ -844,7 +853,33 @@ function renderDiagnosis(v) {
|
|
|
844
853
|
}
|
|
845
854
|
if (v.priorIncident) lines.push("", `**You hit this before:** ${v.priorIncident}`);
|
|
846
855
|
if (v.costHeadline) lines.push("", `**Cost:** ${v.costHeadline}`);
|
|
847
|
-
if (v.
|
|
856
|
+
if (v.evidenceRequests.length) {
|
|
857
|
+
lines.push("", "**Collect this evidence next (the selection itself was not treated as evidence):**");
|
|
858
|
+
for (const request of v.evidenceRequests) {
|
|
859
|
+
if (request.label) lines.push(`- **${request.label}**${request.why ? ` \u2014 ${request.why}` : ""}`);
|
|
860
|
+
if (request.command) lines.push("```bash", request.command, "```");
|
|
861
|
+
}
|
|
862
|
+
lines.push("Paste the complete output back into the original log evidence and call `diagnose_crash` again.");
|
|
863
|
+
}
|
|
864
|
+
if (v.clarifyingChoices.length) {
|
|
865
|
+
lines.push("", "**To narrow it down, answer any of these:**");
|
|
866
|
+
for (const q of v.clarifyingChoices) {
|
|
867
|
+
if (!q?.question) continue;
|
|
868
|
+
lines.push("", `_${q.question}_`);
|
|
869
|
+
for (const o of q.options ?? []) {
|
|
870
|
+
if (!o?.label) continue;
|
|
871
|
+
const rec = o.recommended && q.kind === "action" ? " **(Recommended)**" : "";
|
|
872
|
+
const detail = o.detail ? ` \u2014 ${o.detail}` : "";
|
|
873
|
+
lines.push(`- \`${o.id ?? o.label}\` ${o.label}${rec}${detail}`);
|
|
874
|
+
if (o.command) lines.push(` - Evidence command: \`${o.command}\``);
|
|
875
|
+
}
|
|
876
|
+
if (q.kind === "fact") {
|
|
877
|
+
lines.push(` _Answer only from what was actually observed. If it is not known, say so rather than guessing \u2014 a wrong answer here sends the diagnosis the wrong way._`);
|
|
878
|
+
}
|
|
879
|
+
if (q.command) lines.push(` _Recommended check:_ \`${q.command}\``);
|
|
880
|
+
}
|
|
881
|
+
lines.push("", "_Call `diagnose_crash` again with the same original logs and an `operatorAnswers` array containing questionId, optionId, kind, question, and answer. Send the full answer history on every round. The anonymous refinement token is cached automatically; if the MCP process restarts, pass the `refinementToken` from structured output._");
|
|
882
|
+
} else if (v.clarifying.length) {
|
|
848
883
|
lines.push("", "**To narrow it down, tell me:**");
|
|
849
884
|
v.clarifying.forEach((q) => lines.push(`- ${q}`));
|
|
850
885
|
lines.push("", "_Use `ask_followup` to continue this conversation._");
|
|
@@ -903,6 +938,30 @@ var diagnosisOutputShape = {
|
|
|
903
938
|
steps: z.array(z.string()).describe("The full prescriptive fix ladder, in order."),
|
|
904
939
|
evidence: z.array(z.string()).describe("Literal lines quoted from the submitted log that support this verdict. An EMPTY array on a high-confidence verdict means the engine could not quote its own log \u2014 weigh it accordingly."),
|
|
905
940
|
clarifying: z.array(z.string()).describe("Discriminating questions. Answer them with `ask_followup` to narrow the verdict."),
|
|
941
|
+
clarifyingChoices: z.array(z.object({
|
|
942
|
+
id: z.string().optional(),
|
|
943
|
+
kind: z.enum(["fact", "action"]).optional().describe("`fact` = what was observed; `action` = what you could run. Only `action` questions carry a recommended option \u2014 a recommendation on a question of fact would bias the evidence the next verdict is built on."),
|
|
944
|
+
question: z.string().optional(),
|
|
945
|
+
options: z.array(z.object({
|
|
946
|
+
id: z.string().optional(),
|
|
947
|
+
label: z.string().optional(),
|
|
948
|
+
detail: z.string().optional(),
|
|
949
|
+
recommended: z.boolean().optional(),
|
|
950
|
+
command: z.string().optional()
|
|
951
|
+
})).optional(),
|
|
952
|
+
allowOther: z.boolean().optional(),
|
|
953
|
+
command: z.string().optional().describe("The exact command behind the recommended option.")
|
|
954
|
+
})).optional().describe("Structured form of `clarifying`: call `diagnose_crash` again with the same logs plus a typed `operatorAnswers` array. Answer FACT questions only from what was observed; selecting an ACTION is intent to collect evidence, not the check result."),
|
|
955
|
+
evidenceRequests: z.array(z.object({
|
|
956
|
+
questionId: z.string().optional(),
|
|
957
|
+
optionId: z.string().optional(),
|
|
958
|
+
label: z.string().optional(),
|
|
959
|
+
command: z.string().optional(),
|
|
960
|
+
why: z.string().optional(),
|
|
961
|
+
pasteBack: z.string().optional()
|
|
962
|
+
})).describe("Exact evidence checks selected by the operator. Run them and paste back the actual output; the selection was not counted as evidence."),
|
|
963
|
+
awaitingEvidence: z.boolean().optional(),
|
|
964
|
+
refinementToken: z.string().optional().describe("Anonymous-only capability for refining this exact original log. Normally cached by the MCP server; pass it back after a process restart."),
|
|
906
965
|
advisories: z.array(z.string()).describe("Human-readable one-liners, severity-sorted."),
|
|
907
966
|
advisory: z.array(advisorySchema).describe("Structured cross-cutting advisories, severity-sorted (critical first)."),
|
|
908
967
|
contradictions: z.array(z.string()).describe("What the engine ruled OUT and why \u2014 the falsification trail."),
|
|
@@ -958,6 +1017,19 @@ function fmtDate(ms) {
|
|
|
958
1017
|
}
|
|
959
1018
|
|
|
960
1019
|
// src/tools/diagnose.ts
|
|
1020
|
+
var anonymousRefinementTokens = /* @__PURE__ */ new Map();
|
|
1021
|
+
function refinementKey(logs) {
|
|
1022
|
+
return createHash("sha256").update(logs).digest("hex");
|
|
1023
|
+
}
|
|
1024
|
+
function rememberRefinement(key, token) {
|
|
1025
|
+
anonymousRefinementTokens.delete(key);
|
|
1026
|
+
anonymousRefinementTokens.set(key, token);
|
|
1027
|
+
while (anonymousRefinementTokens.size > 50) {
|
|
1028
|
+
const oldest = anonymousRefinementTokens.keys().next().value;
|
|
1029
|
+
if (!oldest) break;
|
|
1030
|
+
anonymousRefinementTokens.delete(oldest);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
961
1033
|
function registerDiagnose(server, ctx) {
|
|
962
1034
|
server.registerTool(
|
|
963
1035
|
"diagnose_crash",
|
|
@@ -982,17 +1054,29 @@ function registerDiagnose(server, ctx) {
|
|
|
982
1054
|
"Hardware telemetry captured from the failing node. Keys the engine reads: `dmesg` (output of `dmesg -T`), `nvidia_smi` (`nvidia-smi -q`), `ibstat`. Sending these turns an ambiguous NCCL timeout into a specific Xid/ECC verdict \u2014 and, just as importantly, lets the engine EXONERATE the GPU when the counters are clean."
|
|
983
1055
|
),
|
|
984
1056
|
codeContext: z2.record(z2.string()).optional().describe("Relevant source files as { path: contents }. Only send real files you read \u2014 the engine never fabricates code context, and neither should you."),
|
|
985
|
-
topologyMap: z2.record(z2.array(z2.string())).optional().describe("Rank \u2192 node placement as { nodeName: [rank, ...] }, e.g. from the launcher. Lets the engine map a failing rank to a physical node and GPU.")
|
|
1057
|
+
topologyMap: z2.record(z2.array(z2.string())).optional().describe("Rank \u2192 node placement as { nodeName: [rank, ...] }, e.g. from the launcher. Lets the engine map a failing rank to a physical node and GPU."),
|
|
1058
|
+
operatorAnswers: z2.array(z2.object({
|
|
1059
|
+
questionId: z2.string().optional(),
|
|
1060
|
+
optionId: z2.string().optional(),
|
|
1061
|
+
kind: z2.enum(["fact", "action"]).optional(),
|
|
1062
|
+
question: z2.string().optional(),
|
|
1063
|
+
answer: z2.string()
|
|
1064
|
+
})).max(12).optional().describe("Answers from a previous diagnose_crash result. Send the full answer history with the same original logs. Selecting an ACTION asks Denpex for that evidence; it is not treated as the result of the check."),
|
|
1065
|
+
refinementToken: z2.string().max(160).optional().describe("Anonymous-only capability returned by the prior diagnosis. Normally cached automatically in this MCP process; pass it explicitly after a process restart.")
|
|
986
1066
|
},
|
|
987
1067
|
outputSchema: diagnosisOutputShape
|
|
988
1068
|
},
|
|
989
|
-
async ({ logs, file, dir, jobName, runtimeHours, gpuRate, telemetry, codeContext, topologyMap }) => {
|
|
1069
|
+
async ({ logs, file, dir, jobName, runtimeHours, gpuRate, telemetry, codeContext, topologyMap, operatorAnswers, refinementToken }) => {
|
|
990
1070
|
try {
|
|
991
1071
|
const resolved = await resolveLogs({ logs, file, dir });
|
|
992
1072
|
assertEnoughLog(resolved.text);
|
|
993
1073
|
const prepared = applyPrivacy(condenseLogs(resolved.text), ctx.config.privacy);
|
|
1074
|
+
const preparedRefinementKey = refinementKey(prepared);
|
|
994
1075
|
const sourceNote = resolved.source === "inline" ? void 0 : `Read \`${resolved.source}\`.`;
|
|
995
1076
|
if (ctx.config.local) {
|
|
1077
|
+
if (operatorAnswers?.length) {
|
|
1078
|
+
return toolText("Offline mode cannot re-run the evidence-bound conversational reasoner. Run the selected check, append its actual output to the original logs, and call `diagnose_crash` again. No action selection was treated as evidence.", true);
|
|
1079
|
+
}
|
|
996
1080
|
const result = localDiagnose(prepared);
|
|
997
1081
|
const localView = viewFromLocal(result, sourceNote);
|
|
998
1082
|
return toolResult(renderDiagnosis(localView), structuredDiagnosis(localView));
|
|
@@ -1013,6 +1097,10 @@ function registerDiagnose(server, ctx) {
|
|
|
1013
1097
|
const preparedCodeContext = codeContext ? scrubKeys(codeContext, (v) => scrub(String(v))) : void 0;
|
|
1014
1098
|
const preparedTopologyMap = topologyMap ? scrubKeys(topologyMap, (ranks) => (Array.isArray(ranks) ? ranks : []).map((r) => scrub(String(r)))) : void 0;
|
|
1015
1099
|
const endpoint = ctx.config.apiKey ? "/api/diagnose" : "/api/diagnose/anon";
|
|
1100
|
+
const activeRefinementToken = refinementToken ?? anonymousRefinementTokens.get(preparedRefinementKey);
|
|
1101
|
+
if (operatorAnswers?.length && !ctx.config.apiKey && !activeRefinementToken) {
|
|
1102
|
+
return toolText("Anonymous clarification is missing the refinement token from the prior diagnosis. Re-run diagnose_crash with the same original logs, then answer the questions it returns. No answer was submitted or charged.", true);
|
|
1103
|
+
}
|
|
1016
1104
|
const payload = await ctx.client.request(endpoint, {
|
|
1017
1105
|
method: "POST",
|
|
1018
1106
|
body: {
|
|
@@ -1022,13 +1110,23 @@ function registerDiagnose(server, ctx) {
|
|
|
1022
1110
|
gpuRate,
|
|
1023
1111
|
...preparedTelemetry ? { telemetry: preparedTelemetry } : {},
|
|
1024
1112
|
...preparedCodeContext ? { codeContext: preparedCodeContext } : {},
|
|
1025
|
-
...preparedTopologyMap ? { topologyMap: preparedTopologyMap } : {}
|
|
1113
|
+
...preparedTopologyMap ? { topologyMap: preparedTopologyMap } : {},
|
|
1114
|
+
...operatorAnswers?.length ? { operatorAnswers } : {}
|
|
1026
1115
|
},
|
|
1116
|
+
...operatorAnswers?.length && !ctx.config.apiKey && activeRefinementToken ? {
|
|
1117
|
+
headers: {
|
|
1118
|
+
"X-Denpex-Followup": "1",
|
|
1119
|
+
"X-Denpex-Refinement": activeRefinementToken
|
|
1120
|
+
}
|
|
1121
|
+
} : {},
|
|
1027
1122
|
// The server budgets up to 290s for the deep/LLM path. Keep a small
|
|
1028
1123
|
// transport buffer so MCP never discards a result that completed inside
|
|
1029
1124
|
// the server's supported window.
|
|
1030
1125
|
timeoutMs: 32e4
|
|
1031
1126
|
});
|
|
1127
|
+
if (!ctx.config.apiKey && typeof payload.refinementToken === "string" && payload.refinementToken) {
|
|
1128
|
+
rememberRefinement(preparedRefinementKey, payload.refinementToken);
|
|
1129
|
+
}
|
|
1032
1130
|
const view = viewFromCloud(payload, sourceNote);
|
|
1033
1131
|
return toolResult(renderDiagnosis(view), structuredDiagnosis(view));
|
|
1034
1132
|
} catch (e) {
|
|
@@ -1726,7 +1824,7 @@ function registerPrompts(server) {
|
|
|
1726
1824
|
|
|
1727
1825
|
// src/version.ts
|
|
1728
1826
|
var SERVER_NAME = "denpex";
|
|
1729
|
-
var SERVER_VERSION = "0.2.
|
|
1827
|
+
var SERVER_VERSION = "0.2.7";
|
|
1730
1828
|
|
|
1731
1829
|
// src/server.ts
|
|
1732
1830
|
function createServer() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "denpex-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Denpex MCP server — diagnose GPU/ML training failures (CUDA OOM, NCCL timeouts, Xid faults, NaN loss) from Claude Desktop, Cursor, or Claude Code, with optional explicit zero-egress mode.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|