denpex-mcp 0.2.6 → 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 +73 -7
- package/package.json +2 -2
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";
|
|
@@ -698,6 +700,9 @@ function viewFromCloud(payload, note) {
|
|
|
698
700
|
evidence: asArray(fix.evidence),
|
|
699
701
|
clarifying: asArray(fix.clarifyingQuestions),
|
|
700
702
|
clarifyingChoices: objArray(fix.clarifyingChoices),
|
|
703
|
+
evidenceRequests: objArray(fix.evidenceRequests),
|
|
704
|
+
awaitingEvidence: fix.awaitingEvidence === true,
|
|
705
|
+
refinementToken: str(payload.refinementToken) ?? void 0,
|
|
701
706
|
advisories: [
|
|
702
707
|
...advisory.map((a) => `${(a.summary ?? "Advisory").trim()}${a.action ? ` \u2014 ${a.action}` : ""}`),
|
|
703
708
|
...advisoriesFromAdvanced(payload.advanced)
|
|
@@ -742,6 +747,8 @@ function viewFromLocal(r, note) {
|
|
|
742
747
|
evidence: r.evidence ?? [],
|
|
743
748
|
clarifying: [],
|
|
744
749
|
clarifyingChoices: [],
|
|
750
|
+
evidenceRequests: [],
|
|
751
|
+
awaitingEvidence: false,
|
|
745
752
|
advisories: (r.detectors ?? []).map((d) => `${d.severity.toUpperCase()} \u2014 ${d.summary}`),
|
|
746
753
|
// The offline engine has no advisory array, no falsification contracts, no
|
|
747
754
|
// community/research grounding and no ranked-hypotheses ledger. Emit the fields as
|
|
@@ -846,6 +853,14 @@ function renderDiagnosis(v) {
|
|
|
846
853
|
}
|
|
847
854
|
if (v.priorIncident) lines.push("", `**You hit this before:** ${v.priorIncident}`);
|
|
848
855
|
if (v.costHeadline) lines.push("", `**Cost:** ${v.costHeadline}`);
|
|
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
|
+
}
|
|
849
864
|
if (v.clarifyingChoices.length) {
|
|
850
865
|
lines.push("", "**To narrow it down, answer any of these:**");
|
|
851
866
|
for (const q of v.clarifyingChoices) {
|
|
@@ -856,13 +871,14 @@ function renderDiagnosis(v) {
|
|
|
856
871
|
const rec = o.recommended && q.kind === "action" ? " **(Recommended)**" : "";
|
|
857
872
|
const detail = o.detail ? ` \u2014 ${o.detail}` : "";
|
|
858
873
|
lines.push(`- \`${o.id ?? o.label}\` ${o.label}${rec}${detail}`);
|
|
874
|
+
if (o.command) lines.push(` - Evidence command: \`${o.command}\``);
|
|
859
875
|
}
|
|
860
876
|
if (q.kind === "fact") {
|
|
861
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._`);
|
|
862
878
|
}
|
|
863
879
|
if (q.command) lines.push(` _Recommended check:_ \`${q.command}\``);
|
|
864
880
|
}
|
|
865
|
-
lines.push("", "
|
|
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._");
|
|
866
882
|
} else if (v.clarifying.length) {
|
|
867
883
|
lines.push("", "**To narrow it down, tell me:**");
|
|
868
884
|
v.clarifying.forEach((q) => lines.push(`- ${q}`));
|
|
@@ -930,11 +946,22 @@ var diagnosisOutputShape = {
|
|
|
930
946
|
id: z.string().optional(),
|
|
931
947
|
label: z.string().optional(),
|
|
932
948
|
detail: z.string().optional(),
|
|
933
|
-
recommended: z.boolean().optional()
|
|
949
|
+
recommended: z.boolean().optional(),
|
|
950
|
+
command: z.string().optional()
|
|
934
951
|
})).optional(),
|
|
935
952
|
allowOther: z.boolean().optional(),
|
|
936
953
|
command: z.string().optional().describe("The exact command behind the recommended option.")
|
|
937
|
-
})).optional().describe("Structured form of `clarifying`:
|
|
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."),
|
|
938
965
|
advisories: z.array(z.string()).describe("Human-readable one-liners, severity-sorted."),
|
|
939
966
|
advisory: z.array(advisorySchema).describe("Structured cross-cutting advisories, severity-sorted (critical first)."),
|
|
940
967
|
contradictions: z.array(z.string()).describe("What the engine ruled OUT and why \u2014 the falsification trail."),
|
|
@@ -990,6 +1017,19 @@ function fmtDate(ms) {
|
|
|
990
1017
|
}
|
|
991
1018
|
|
|
992
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
|
+
}
|
|
993
1033
|
function registerDiagnose(server, ctx) {
|
|
994
1034
|
server.registerTool(
|
|
995
1035
|
"diagnose_crash",
|
|
@@ -1014,17 +1054,29 @@ function registerDiagnose(server, ctx) {
|
|
|
1014
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."
|
|
1015
1055
|
),
|
|
1016
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."),
|
|
1017
|
-
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.")
|
|
1018
1066
|
},
|
|
1019
1067
|
outputSchema: diagnosisOutputShape
|
|
1020
1068
|
},
|
|
1021
|
-
async ({ logs, file, dir, jobName, runtimeHours, gpuRate, telemetry, codeContext, topologyMap }) => {
|
|
1069
|
+
async ({ logs, file, dir, jobName, runtimeHours, gpuRate, telemetry, codeContext, topologyMap, operatorAnswers, refinementToken }) => {
|
|
1022
1070
|
try {
|
|
1023
1071
|
const resolved = await resolveLogs({ logs, file, dir });
|
|
1024
1072
|
assertEnoughLog(resolved.text);
|
|
1025
1073
|
const prepared = applyPrivacy(condenseLogs(resolved.text), ctx.config.privacy);
|
|
1074
|
+
const preparedRefinementKey = refinementKey(prepared);
|
|
1026
1075
|
const sourceNote = resolved.source === "inline" ? void 0 : `Read \`${resolved.source}\`.`;
|
|
1027
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
|
+
}
|
|
1028
1080
|
const result = localDiagnose(prepared);
|
|
1029
1081
|
const localView = viewFromLocal(result, sourceNote);
|
|
1030
1082
|
return toolResult(renderDiagnosis(localView), structuredDiagnosis(localView));
|
|
@@ -1045,6 +1097,10 @@ function registerDiagnose(server, ctx) {
|
|
|
1045
1097
|
const preparedCodeContext = codeContext ? scrubKeys(codeContext, (v) => scrub(String(v))) : void 0;
|
|
1046
1098
|
const preparedTopologyMap = topologyMap ? scrubKeys(topologyMap, (ranks) => (Array.isArray(ranks) ? ranks : []).map((r) => scrub(String(r)))) : void 0;
|
|
1047
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
|
+
}
|
|
1048
1104
|
const payload = await ctx.client.request(endpoint, {
|
|
1049
1105
|
method: "POST",
|
|
1050
1106
|
body: {
|
|
@@ -1054,13 +1110,23 @@ function registerDiagnose(server, ctx) {
|
|
|
1054
1110
|
gpuRate,
|
|
1055
1111
|
...preparedTelemetry ? { telemetry: preparedTelemetry } : {},
|
|
1056
1112
|
...preparedCodeContext ? { codeContext: preparedCodeContext } : {},
|
|
1057
|
-
...preparedTopologyMap ? { topologyMap: preparedTopologyMap } : {}
|
|
1113
|
+
...preparedTopologyMap ? { topologyMap: preparedTopologyMap } : {},
|
|
1114
|
+
...operatorAnswers?.length ? { operatorAnswers } : {}
|
|
1058
1115
|
},
|
|
1116
|
+
...operatorAnswers?.length && !ctx.config.apiKey && activeRefinementToken ? {
|
|
1117
|
+
headers: {
|
|
1118
|
+
"X-Denpex-Followup": "1",
|
|
1119
|
+
"X-Denpex-Refinement": activeRefinementToken
|
|
1120
|
+
}
|
|
1121
|
+
} : {},
|
|
1059
1122
|
// The server budgets up to 290s for the deep/LLM path. Keep a small
|
|
1060
1123
|
// transport buffer so MCP never discards a result that completed inside
|
|
1061
1124
|
// the server's supported window.
|
|
1062
1125
|
timeoutMs: 32e4
|
|
1063
1126
|
});
|
|
1127
|
+
if (!ctx.config.apiKey && typeof payload.refinementToken === "string" && payload.refinementToken) {
|
|
1128
|
+
rememberRefinement(preparedRefinementKey, payload.refinementToken);
|
|
1129
|
+
}
|
|
1064
1130
|
const view = viewFromCloud(payload, sourceNote);
|
|
1065
1131
|
return toolResult(renderDiagnosis(view), structuredDiagnosis(view));
|
|
1066
1132
|
} catch (e) {
|
|
@@ -1758,7 +1824,7 @@ function registerPrompts(server) {
|
|
|
1758
1824
|
|
|
1759
1825
|
// src/version.ts
|
|
1760
1826
|
var SERVER_NAME = "denpex";
|
|
1761
|
-
var SERVER_VERSION = "0.2.
|
|
1827
|
+
var SERVER_VERSION = "0.2.7";
|
|
1762
1828
|
|
|
1763
1829
|
// src/server.ts
|
|
1764
1830
|
function createServer() {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "denpex-mcp",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Denpex MCP server
|
|
3
|
+
"version": "0.2.7",
|
|
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",
|
|
7
7
|
"author": "Denpex",
|