@thehammer/danx-dashboard-mcp 0.1.247 → 0.1.248
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/dist/access-request.js +0 -2
- package/dist/index.js +4 -2
- package/dist/permission-request.js +11 -18
- package/dist/session-access.js +12 -11
- package/package.json +1 -1
package/dist/access-request.js
CHANGED
|
@@ -54,8 +54,6 @@ function isCreated(value) {
|
|
|
54
54
|
typeof v.secret === "string" &&
|
|
55
55
|
typeof v.confirmCode === "string" &&
|
|
56
56
|
typeof v.approvalUrl === "string" &&
|
|
57
|
-
typeof v.expiresAt === "string" &&
|
|
58
|
-
Number.isFinite(Date.parse(v.expiresAt)) &&
|
|
59
57
|
typeof v.pollMs === "number");
|
|
60
58
|
}
|
|
61
59
|
export async function createAccessRequest(fetchImpl, dashboardUrl, input) {
|
package/dist/index.js
CHANGED
|
@@ -515,7 +515,8 @@ strictTool("attach_file", "Attach a LOCAL file to a card. This server reads and
|
|
|
515
515
|
// ---------------- request_permission (DX-4434, parent DX-4432) ----------------
|
|
516
516
|
strictTool("request_permission", "Ask a person to grant this key ONE MORE permission. Call it when a call was refused `403 forbidden` naming a " +
|
|
517
517
|
"`permission`, or when you know you need one more. It returns AT ONCE with `{state, approvalUrl, confirmCode, " +
|
|
518
|
-
"
|
|
518
|
+
"instruction}`: show the person the code and URL, and keep working on anything else. It waits until this session " +
|
|
519
|
+
"ends; if expired, reconnect and ask again. If they approve, a " +
|
|
519
520
|
"later call simply succeeds with the same key (no new key, no re-auth). The same permissions asked again while " +
|
|
520
521
|
"pending return the pending request. A refusal (unknown permission, already held, rate limited) comes back as " +
|
|
521
522
|
"`{ok:false, status, body}`. If you are blocked without it, raise a problem on the card.", {
|
|
@@ -551,7 +552,8 @@ strictTool("plan_connect",
|
|
|
551
552
|
// DX-4390 — signed out, this is also how the session asks for access.
|
|
552
553
|
"Connect THIS session to a plan; one call is enough to start. Signed out, it requests access instead: " +
|
|
553
554
|
"the reply is `state: approval_required` or `approval_pending` with an `approvalUrl` and `confirmCode` " +
|
|
554
|
-
"(show the code, open the URL, then call again
|
|
555
|
+
"(show the code, open the URL, then call again; it waits until this session ends; a call re-asks an expired one). " +
|
|
556
|
+
"Signed in, the reply carries `missedEvents` (operator " +
|
|
555
557
|
"events on the plan that no session had been told about, oldest first: read them first), " +
|
|
556
558
|
"`browserInstruction` (do it now), `briefing` (the plan's goals, rules, caveats, architecture section " +
|
|
557
559
|
"titles and a first page of open cards) and `listenerHealth` (when its `state` is not `healthy`, do its " +
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defaultSleep } from "./session-access.js";
|
|
2
2
|
import { McpOutdatedError } from "./version-floor.js";
|
|
3
3
|
const PERMISSION_REQUESTS_BASE_PATH = "/api/permission-requests";
|
|
4
4
|
const APPROVAL_REQUIRED_INSTRUCTION = "Show the person the approval URL and the confirm code (they check the code matches, then approve). Keep working on anything " +
|
|
5
|
-
"else: this call has returned and nothing waits.
|
|
6
|
-
"
|
|
7
|
-
"the card; a person sees problems in Needs You.";
|
|
8
|
-
const APPROVAL_PENDING_INSTRUCTION = "This permission is already asked for and not yet decided
|
|
9
|
-
"working on anything else. A grant is picked up in the background and a later
|
|
10
|
-
"are blocked without it, raise a problem on the card.";
|
|
5
|
+
"else: this call has returned and nothing waits. The request stays open until this session ends, however long they take. If " +
|
|
6
|
+
"they approve, this process picks the grant up in the background and a later call simply succeeds with the same key (no new " +
|
|
7
|
+
"key, no re-auth). If you are blocked without it, raise a problem on the card; a person sees problems in Needs You.";
|
|
8
|
+
const APPROVAL_PENDING_INSTRUCTION = "This permission is already asked for and not yet decided; the request stays open until this session ends. Remind the person " +
|
|
9
|
+
"of the confirm code and the approval URL; keep working on anything else. A grant is picked up in the background and a later " +
|
|
10
|
+
"call simply succeeds with the same key. If you are blocked without it, raise a problem on the card.";
|
|
11
11
|
/** The dedupe set: sorted and de-duplicated, so the order the agent listed them in never matters. */
|
|
12
12
|
function setOf(permissions) {
|
|
13
13
|
return [...new Set(permissions)].sort();
|
|
@@ -19,8 +19,6 @@ function isCreated(value) {
|
|
|
19
19
|
return (typeof v.publicId === "string" &&
|
|
20
20
|
typeof v.confirmCode === "string" &&
|
|
21
21
|
typeof v.approvalUrl === "string" &&
|
|
22
|
-
typeof v.expiresAt === "string" &&
|
|
23
|
-
Number.isFinite(Date.parse(v.expiresAt)) &&
|
|
24
22
|
typeof v.pollMs === "number");
|
|
25
23
|
}
|
|
26
24
|
/** A 429's own `retryAfterMs`, else 0 (the poll then waits its normal interval). */
|
|
@@ -32,11 +30,9 @@ export class PermissionRequests {
|
|
|
32
30
|
deps;
|
|
33
31
|
pending = new Map();
|
|
34
32
|
queue = Promise.resolve();
|
|
35
|
-
now;
|
|
36
33
|
sleep;
|
|
37
34
|
constructor(deps) {
|
|
38
35
|
this.deps = deps;
|
|
39
|
-
this.now = deps.now ?? Date.now;
|
|
40
36
|
this.sleep = deps.sleep ?? defaultSleep;
|
|
41
37
|
}
|
|
42
38
|
ask(args) {
|
|
@@ -72,12 +68,11 @@ export class PermissionRequests {
|
|
|
72
68
|
state,
|
|
73
69
|
approvalUrl: request.approvalUrl,
|
|
74
70
|
confirmCode: request.confirmCode,
|
|
75
|
-
expiresAt: request.expiresAt,
|
|
76
71
|
instruction: state === "approval_required" ? APPROVAL_REQUIRED_INSTRUCTION : APPROVAL_PENDING_INSTRUCTION,
|
|
77
72
|
};
|
|
78
73
|
}
|
|
79
74
|
/**
|
|
80
|
-
* Polls until the request is decided or
|
|
75
|
+
* Polls until the request is decided or expired, then frees its set so a later call may ask again. Every ending is final: a
|
|
81
76
|
* halt (revoked, signed out, key lapsed) stops it silently, since the session no longer holds a key that could be granted.
|
|
82
77
|
*/
|
|
83
78
|
async pollUntilDecided(key, request) {
|
|
@@ -92,21 +87,19 @@ export class PermissionRequests {
|
|
|
92
87
|
this.pending.delete(key);
|
|
93
88
|
}
|
|
94
89
|
}
|
|
90
|
+
// DX-4530: no give-up time - the request is open until it is decided or the key dies (the `guard` halt).
|
|
95
91
|
async poll(request) {
|
|
96
|
-
const giveUpAt = Date.parse(request.expiresAt) + POLL_GRACE_MS;
|
|
97
92
|
let waitMs = request.pollMs;
|
|
98
93
|
for (;;) {
|
|
99
94
|
await this.sleep(waitMs);
|
|
100
95
|
waitMs = request.pollMs;
|
|
101
|
-
if (this.now() > giveUpAt)
|
|
102
|
-
return;
|
|
103
96
|
let guarded;
|
|
104
97
|
try {
|
|
105
98
|
// `guard` runs before EVERY claim (DX-4418): a revoked or signed-out session answers its halt without a request.
|
|
106
99
|
guarded = await this.deps.access.guard(() => this.deps.request({ method: "POST", basePath: PERMISSION_REQUESTS_BASE_PATH, path: `/${request.publicId}/claim` }));
|
|
107
100
|
}
|
|
108
101
|
catch (err) {
|
|
109
|
-
// An outdated MCP will never get a different answer; any other failure is retried until the request
|
|
102
|
+
// An outdated MCP will never get a different answer; any other failure is retried until the request ends.
|
|
110
103
|
if (err instanceof McpOutdatedError)
|
|
111
104
|
throw err;
|
|
112
105
|
this.deps.log?.(`[danx-dashboard-mcp] permission request poll failed, retrying: ${err.message}`);
|
|
@@ -120,7 +113,7 @@ export class PermissionRequests {
|
|
|
120
113
|
continue;
|
|
121
114
|
}
|
|
122
115
|
if (claim.status === 404)
|
|
123
|
-
return; // unknown or
|
|
116
|
+
return; // unknown or long since deleted: nothing left to wait for
|
|
124
117
|
if (!claim.ok) {
|
|
125
118
|
throw new Error(`the dashboard refused the claim (${claim.status}): ${JSON.stringify(claim.body)}`);
|
|
126
119
|
}
|
package/dist/session-access.js
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* the background, so the key is stored the moment the user approves, and the next `plan_connect`
|
|
19
19
|
* waits at most `awaitMs` (about 45s) for it before answering `approval_pending`. The secret the
|
|
20
20
|
* poll needs lives only in this process's memory; a restarted session asks again.
|
|
21
|
+
*
|
|
22
|
+
* DX-4530: a request has no deadline. It stays open for as long as this process keeps polling it, so it waits for a person
|
|
23
|
+
* however long they take, and it expires once the session ends (its polls stop). A poll that cannot reach the dashboard is
|
|
24
|
+
* retried; the dashboard answers `expired` if they stopped arriving for longer than its requester-gone window.
|
|
21
25
|
*/
|
|
22
26
|
import { existsSync } from "node:fs";
|
|
23
27
|
import { hostname } from "node:os";
|
|
@@ -28,8 +32,6 @@ import { deleteSessionKey, writeSessionKey } from "./session-key.js";
|
|
|
28
32
|
import { McpOutdatedError } from "./version-floor.js";
|
|
29
33
|
/** Longest a `plan_connect` call waits for an approval already asked for. */
|
|
30
34
|
export const AWAIT_APPROVAL_MS = 45_000;
|
|
31
|
-
/** The server reaps a lapsed request about a minute after expiry; polling past that finds nothing. */
|
|
32
|
-
export const POLL_GRACE_MS = 90_000;
|
|
33
35
|
/** A live server rewrites its key file this often, so an in-use key never ages out of the 7-day prune. */
|
|
34
36
|
const KEEP_KEY_FILE_EVERY_MS = 60 * 60 * 1000;
|
|
35
37
|
/** The request's title when the agent gave none: the session id's first characters. */
|
|
@@ -39,9 +41,10 @@ export const SIGN_IN_HALT = "Not signed in to the danxbot dashboard. Call `plan_
|
|
|
39
41
|
export const KEY_LAPSED_HALT = "The dashboard no longer accepts this session's key (it lapsed after a day unused), so this session is signed out. " +
|
|
40
42
|
SIGN_IN_HALT;
|
|
41
43
|
const APPROVAL_REQUIRED_INSTRUCTION = "Show the user the confirm code and open the approval URL in their browser; they check the code " +
|
|
42
|
-
"matches and approve. Then call `plan_connect` again: it waits up to about 45 seconds for the approval."
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
"matches and approve. Then call `plan_connect` again: it waits up to about 45 seconds for the approval. " +
|
|
45
|
+
"The request stays open until this session ends, however long the user takes.";
|
|
46
|
+
const APPROVAL_PENDING_INSTRUCTION = "Still waiting for the user to approve; the request stays open until this session ends. Remind them of the " +
|
|
47
|
+
"confirm code and the URL, then call `plan_connect` again.";
|
|
45
48
|
const DENIED_INSTRUCTION = "The user denied the request. Do not retry unless they ask; a new `plan_connect` makes a new request.";
|
|
46
49
|
const DASHBOARD_OUTDATED_ERROR = "this dashboard does not support request-and-approve yet (POST /api/access-requests answered 404): " +
|
|
47
50
|
"it must be updated before this session can sign in";
|
|
@@ -50,7 +53,8 @@ const unknownTeamError = (team) => `the dashboard has no team "${team}" (DANXBOT
|
|
|
50
53
|
const NO_SESSION_ID_ERROR = "this server has no CLAUDE_CODE_SESSION_ID, so it cannot request a session key";
|
|
51
54
|
const CLAIMED_LOST_ERROR = "the dashboard says this request was already claimed, but this process never received the key; " +
|
|
52
55
|
"start a new request";
|
|
53
|
-
const EXPIRED_NOTE = "The previous request expired before it was approved."
|
|
56
|
+
const EXPIRED_NOTE = "The previous request expired before it was approved: the dashboard stopped hearing from this session. This is a new " +
|
|
57
|
+
"request: show the user its new confirm code and URL.";
|
|
54
58
|
export const defaultSleep = (ms) => new Promise((resolve) => {
|
|
55
59
|
setTimeout(resolve, ms).unref();
|
|
56
60
|
});
|
|
@@ -233,24 +237,21 @@ export class SessionAccess {
|
|
|
233
237
|
state,
|
|
234
238
|
approvalUrl: request.approvalUrl,
|
|
235
239
|
confirmCode: request.confirmCode,
|
|
236
|
-
expiresAt: request.expiresAt,
|
|
237
240
|
instruction: state === "approval_required" ? APPROVAL_REQUIRED_INSTRUCTION : APPROVAL_PENDING_INSTRUCTION,
|
|
238
241
|
};
|
|
239
242
|
}
|
|
243
|
+
// DX-4530: no give-up time - the polls are what keep the request open, until the dashboard answers how it ended.
|
|
240
244
|
async pollUntilSettled(request) {
|
|
241
|
-
const giveUpAt = Date.parse(request.expiresAt) + POLL_GRACE_MS;
|
|
242
245
|
let waitMs = request.pollMs;
|
|
243
246
|
for (;;) {
|
|
244
247
|
await this.sleep(waitMs);
|
|
245
248
|
waitMs = request.pollMs;
|
|
246
|
-
if (this.now() > giveUpAt)
|
|
247
|
-
return { kind: "expired" };
|
|
248
249
|
let claim;
|
|
249
250
|
try {
|
|
250
251
|
claim = await claimAccessRequest(this.fetchImpl, this.deps.dashboardUrl, request.secret);
|
|
251
252
|
}
|
|
252
253
|
catch (err) {
|
|
253
|
-
// A claim the dashboard could not answer is retried until the request
|
|
254
|
+
// A claim the dashboard could not answer is retried until it answers how the request ended; an outdated
|
|
254
255
|
// MCP will never get a different answer, so that one ends it.
|
|
255
256
|
if (err instanceof McpOutdatedError)
|
|
256
257
|
return { kind: "failed", error: err.message };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thehammer/danx-dashboard-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.248",
|
|
4
4
|
"description": "Stdio MCP server fronting danxbot's dashboard API for agents (operator sessions and dispatched workers alike): the generic danxbot_api/danxbot_api_list/danxbot_api_spec tools reach any route the caller's credential may call, plus dedicated agent-specific tools (plan_connect, danxbot_complete, and other per-dispatch lifecycle callbacks) (DX-3465, PLN-11 R-25).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|