kojee-mcp 0.5.3 → 0.5.6
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 +112 -5
- package/dist/{chunk-YEC7IHIG.js → chunk-2BDAM3TH.js} +92 -523
- package/dist/chunk-2MIISF2W.js +35 -0
- package/dist/chunk-3XDJOHMZ.js +223 -0
- package/dist/{chunk-ZW4SW7LJ.js → chunk-64EOLZNI.js} +14 -5
- package/dist/chunk-6SK6ITFE.js +142 -0
- package/dist/chunk-GI2CKKBL.js +46 -0
- package/dist/chunk-HIZ4NDWN.js +141 -0
- package/dist/chunk-LDZXU3DW.js +170 -0
- package/dist/{resubscribe-SLZNA76S.js → chunk-OT2GILXC.js} +1 -0
- package/dist/{chunk-WBMX4CHB.js → chunk-UEGQGXPY.js} +57 -40
- package/dist/chunk-V5VZPYMZ.js +185 -0
- package/dist/{chunk-C6GZ2L2W.js → chunk-X672ZN7V.js} +5 -2
- package/dist/cli.js +47 -24
- package/dist/{codex-stop-hook-JOTBCS5K.js → codex-stop-hook-SWA53ECG.js} +1 -1
- package/dist/control-token-4BUCTYQB.js +13 -0
- package/dist/{doctor-TSHOMT5X.js → doctor-QCQDFLEH.js} +30 -17
- package/dist/{doctor-codex-BMI5JOO6.js → doctor-codex-NZ53ROQA.js} +12 -5
- package/dist/ensure-join-7AEDJMPE.js +96 -0
- package/dist/gateway-client-93P1E0CZ.d.ts +92 -0
- package/dist/{hook-server-QF5JVUHV.js → hook-server-37E2LUKJ.js} +91 -0
- package/dist/index.d.ts +18 -15
- package/dist/index.js +9 -3
- package/dist/lib.d.ts +427 -0
- package/dist/lib.js +44 -0
- package/dist/reconnect-scheduler-JSXCJKQP.js +26 -0
- package/dist/resubscribe-G5OGDZJD.js +6 -0
- package/dist/send-cli-C2F4WTBN.js +72 -0
- package/dist/{stop-hook-SEPWWETV.js → stop-hook-TRAMQYNE.js} +16 -8
- package/dist/{tail-stream-BYKO4DW6.js → tail-stream-VUZBYKXS.js} +4 -3
- package/dist/{user-prompt-submit-hook-ARPEO6FF.js → user-prompt-submit-hook-ZD2XKN7U.js} +7 -1
- package/dist/webhook-config-O4WMQ532.js +20 -0
- package/dist/{webhook-sink-7OYZBWXA.js → webhook-sink-NWGCUDGY.js} +28 -5
- package/dist/{wizard-7KHD5JT4.js → wizard-OSOAY4GO.js} +64 -27
- package/package.json +11 -2
- package/dist/chunk-F7L25L2J.js +0 -60
- package/dist/webhook-config-5TLLX7RA.js +0 -10
|
@@ -1,518 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildCatchUpNote,
|
|
3
|
+
buildMonitorSpawn,
|
|
4
|
+
buildReplyRecipe
|
|
5
|
+
} from "./chunk-X672ZN7V.js";
|
|
1
6
|
import {
|
|
2
7
|
deriveDiscoveryKey,
|
|
3
8
|
findClaudeAncestorPid
|
|
4
9
|
} from "./chunk-BJMASMKX.js";
|
|
5
10
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
AuthModule
|
|
12
|
+
} from "./chunk-6SK6ITFE.js";
|
|
13
|
+
import {
|
|
14
|
+
GatewayClient
|
|
15
|
+
} from "./chunk-3XDJOHMZ.js";
|
|
10
16
|
import {
|
|
11
|
-
MCP_SESSION_ID,
|
|
12
|
-
createDPoPProof,
|
|
13
17
|
startEventStream
|
|
14
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-UEGQGXPY.js";
|
|
15
19
|
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
} from "./chunk-BLEGIR35.js";
|
|
20
|
+
translateToolCallResult
|
|
21
|
+
} from "./chunk-LDZXU3DW.js";
|
|
19
22
|
|
|
20
23
|
// src/index.ts
|
|
21
|
-
import
|
|
22
|
-
import os2 from "os";
|
|
23
|
-
import path3 from "path";
|
|
24
|
-
|
|
25
|
-
// src/auth/auth-module.ts
|
|
26
|
-
import { calculateJwkThumbprint } from "jose";
|
|
27
|
-
import crypto from "crypto";
|
|
28
|
-
|
|
29
|
-
// src/auth/keystore.ts
|
|
30
|
-
import { importJWK, exportJWK, generateKeyPair } from "jose";
|
|
31
|
-
import fs from "fs";
|
|
24
|
+
import fs2 from "fs";
|
|
32
25
|
import os from "os";
|
|
33
|
-
import
|
|
34
|
-
var DEFAULT_PATH = path.join(os.homedir(), ".kojee", "keypair.json");
|
|
35
|
-
async function loadKeystore(keystorePath = DEFAULT_PATH, expectedBrokerUrl) {
|
|
36
|
-
if (!fs.existsSync(keystorePath)) {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
const raw = fs.readFileSync(keystorePath, "utf-8");
|
|
40
|
-
const data = JSON.parse(raw);
|
|
41
|
-
if (expectedBrokerUrl && data.broker_url !== expectedBrokerUrl) {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
const privateKey = await importJWK(data.private_key_jwk, "ES256");
|
|
45
|
-
return {
|
|
46
|
-
privateKey,
|
|
47
|
-
publicJwk: data.public_jwk,
|
|
48
|
-
kid: data.kid,
|
|
49
|
-
data
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
async function saveKeystore(privateKey, publicJwk, kid, brokerUrl, keystorePath = DEFAULT_PATH) {
|
|
53
|
-
const dir = path.dirname(keystorePath);
|
|
54
|
-
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
55
|
-
secureDir(dir);
|
|
56
|
-
const privateJwk = await exportJWK(privateKey);
|
|
57
|
-
const data = {
|
|
58
|
-
private_key_jwk: privateJwk,
|
|
59
|
-
kid,
|
|
60
|
-
broker_url: brokerUrl,
|
|
61
|
-
public_jwk: publicJwk,
|
|
62
|
-
enrolled_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
63
|
-
};
|
|
64
|
-
fs.writeFileSync(keystorePath, JSON.stringify(data, null, 2), {
|
|
65
|
-
mode: 384
|
|
66
|
-
});
|
|
67
|
-
secureFile(keystorePath);
|
|
68
|
-
}
|
|
69
|
-
async function generateES256KeyPair() {
|
|
70
|
-
const { privateKey, publicKey } = await generateKeyPair("ES256");
|
|
71
|
-
const publicJwk = await exportJWK(publicKey);
|
|
72
|
-
publicJwk.kty = "EC";
|
|
73
|
-
publicJwk.crv = "P-256";
|
|
74
|
-
return { privateKey, publicJwk };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// src/auth/registration.ts
|
|
78
|
-
async function registerKey(brokerUrl, token, publicJwk) {
|
|
79
|
-
const url = `${brokerUrl}/api/v1/bots/keys/register/`;
|
|
80
|
-
const response = await fetch(url, {
|
|
81
|
-
method: "POST",
|
|
82
|
-
headers: {
|
|
83
|
-
"Content-Type": "application/json",
|
|
84
|
-
Authorization: `Bearer ${token}`
|
|
85
|
-
},
|
|
86
|
-
body: JSON.stringify({ public_jwk: publicJwk })
|
|
87
|
-
});
|
|
88
|
-
if (!response.ok) {
|
|
89
|
-
const body = await response.text();
|
|
90
|
-
throw new Error(
|
|
91
|
-
`Key registration failed (${response.status}): ${body}`
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
return await response.json();
|
|
95
|
-
}
|
|
96
|
-
async function confirmKey(brokerUrl, token, botKeyId, challenge, signature) {
|
|
97
|
-
const url = `${brokerUrl}/api/v1/bots/keys/confirm/`;
|
|
98
|
-
const response = await fetch(url, {
|
|
99
|
-
method: "POST",
|
|
100
|
-
headers: {
|
|
101
|
-
"Content-Type": "application/json",
|
|
102
|
-
Authorization: `Bearer ${token}`
|
|
103
|
-
},
|
|
104
|
-
body: JSON.stringify({
|
|
105
|
-
bot_key_id: botKeyId,
|
|
106
|
-
challenge,
|
|
107
|
-
signature
|
|
108
|
-
})
|
|
109
|
-
});
|
|
110
|
-
if (!response.ok) {
|
|
111
|
-
const body = await response.text();
|
|
112
|
-
throw new Error(
|
|
113
|
-
`Key confirmation failed (${response.status}): ${body}`
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
return await response.json();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// src/auth/auth-module.ts
|
|
120
|
-
async function signChallengeRaw(privateKey, data) {
|
|
121
|
-
const signer = crypto.createSign("SHA256");
|
|
122
|
-
signer.update(data);
|
|
123
|
-
signer.end();
|
|
124
|
-
const derSignature = signer.sign(
|
|
125
|
-
privateKey
|
|
126
|
-
);
|
|
127
|
-
return derSignature.toString("base64url");
|
|
128
|
-
}
|
|
129
|
-
var AuthModule = class {
|
|
130
|
-
constructor(token, brokerUrl, keystorePath) {
|
|
131
|
-
this.token = token;
|
|
132
|
-
this.brokerUrl = brokerUrl;
|
|
133
|
-
this.keystorePath = keystorePath;
|
|
134
|
-
}
|
|
135
|
-
token;
|
|
136
|
-
brokerUrl;
|
|
137
|
-
keystorePath;
|
|
138
|
-
privateKey = null;
|
|
139
|
-
publicJwk = null;
|
|
140
|
-
kid = null;
|
|
141
|
-
/**
|
|
142
|
-
* Ensure we have an enrolled keypair. Either loads from disk or
|
|
143
|
-
* performs the full enrollment flow.
|
|
144
|
-
*/
|
|
145
|
-
async ensureEnrolled() {
|
|
146
|
-
const existing = await loadKeystore(this.keystorePath, this.brokerUrl);
|
|
147
|
-
if (existing) {
|
|
148
|
-
this.privateKey = existing.privateKey;
|
|
149
|
-
this.publicJwk = existing.publicJwk;
|
|
150
|
-
this.kid = existing.kid;
|
|
151
|
-
console.error("[auth] Loaded existing keypair from keystore");
|
|
152
|
-
return {
|
|
153
|
-
privateKey: existing.privateKey,
|
|
154
|
-
publicJwk: existing.publicJwk,
|
|
155
|
-
kid: existing.kid
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
console.error("[auth] No valid keystore found, enrolling new keypair...");
|
|
159
|
-
const { privateKey, publicJwk } = await generateES256KeyPair();
|
|
160
|
-
const regResult = await registerKey(this.brokerUrl, this.token, publicJwk);
|
|
161
|
-
console.error(`[auth] Key registered: ${regResult.bot_key_id}`);
|
|
162
|
-
const thumbprint = await calculateJwkThumbprint(publicJwk, "sha256");
|
|
163
|
-
const challengeData = `${regResult.challenge}.${thumbprint}`;
|
|
164
|
-
const signature = await signChallengeRaw(privateKey, challengeData);
|
|
165
|
-
const confirmResult = await confirmKey(
|
|
166
|
-
this.brokerUrl,
|
|
167
|
-
this.token,
|
|
168
|
-
regResult.bot_key_id,
|
|
169
|
-
regResult.challenge,
|
|
170
|
-
signature
|
|
171
|
-
);
|
|
172
|
-
if (!confirmResult.key_confirmed) {
|
|
173
|
-
throw new Error("Key enrollment failed: confirmation was rejected");
|
|
174
|
-
}
|
|
175
|
-
console.error("[auth] Key enrollment confirmed");
|
|
176
|
-
await saveKeystore(
|
|
177
|
-
privateKey,
|
|
178
|
-
publicJwk,
|
|
179
|
-
regResult.bot_key_id,
|
|
180
|
-
this.brokerUrl,
|
|
181
|
-
this.keystorePath
|
|
182
|
-
);
|
|
183
|
-
this.privateKey = privateKey;
|
|
184
|
-
this.publicJwk = publicJwk;
|
|
185
|
-
this.kid = regResult.bot_key_id;
|
|
186
|
-
return {
|
|
187
|
-
privateKey,
|
|
188
|
-
publicJwk,
|
|
189
|
-
kid: regResult.bot_key_id
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
getPrivateKey() {
|
|
193
|
-
if (!this.privateKey) throw new Error("Not enrolled yet");
|
|
194
|
-
return this.privateKey;
|
|
195
|
-
}
|
|
196
|
-
getPublicJwk() {
|
|
197
|
-
if (!this.publicJwk) throw new Error("Not enrolled yet");
|
|
198
|
-
return this.publicJwk;
|
|
199
|
-
}
|
|
200
|
-
getKid() {
|
|
201
|
-
if (!this.kid) throw new Error("Not enrolled yet");
|
|
202
|
-
return this.kid;
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
|
|
206
|
-
// src/gateway-client.ts
|
|
207
|
-
import crypto2 from "crypto";
|
|
208
|
-
|
|
209
|
-
// src/error-translator.ts
|
|
210
|
-
function translateGovernanceResult(result) {
|
|
211
|
-
const governance = result._meta?.governance;
|
|
212
|
-
if (!governance) return result;
|
|
213
|
-
if (governance.decision === "deny") {
|
|
214
|
-
return formatDenied(governance);
|
|
215
|
-
}
|
|
216
|
-
if (governance.decision === "require_approval") {
|
|
217
|
-
return formatApprovalRequired(governance);
|
|
218
|
-
}
|
|
219
|
-
return result;
|
|
220
|
-
}
|
|
221
|
-
function formatApprovalRequired(governance) {
|
|
222
|
-
const rules = formatRules(governance.triggered_guardrails);
|
|
223
|
-
const text = `APPROVAL REQUIRED: This action needs user approval before executing. Approval ID: ${governance.approval_id}. Expires: ${governance.expires_at ?? "unknown"}. Triggered rules: ${rules}. The user has been notified. Call kojee_check_approval with this approval_id to check the status.`;
|
|
224
|
-
return {
|
|
225
|
-
content: [{ type: "text", text }],
|
|
226
|
-
isError: true
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
function formatDenied(governance) {
|
|
230
|
-
const rules = formatRules(governance.triggered_guardrails);
|
|
231
|
-
const text = `DENIED: This action was blocked by governance policy. Triggered rules: ${rules}. This cannot proceed \u2014 modify your request or ask the user to adjust governance rules.`;
|
|
232
|
-
return {
|
|
233
|
-
content: [{ type: "text", text }],
|
|
234
|
-
isError: true
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
function translateHttpError(status, errorCode, trigger) {
|
|
238
|
-
if (status === 401) {
|
|
239
|
-
if (errorCode === "use_dpop_nonce") {
|
|
240
|
-
return null;
|
|
241
|
-
}
|
|
242
|
-
if (errorCode === "invalid_dpop_proof") {
|
|
243
|
-
return makeError(
|
|
244
|
-
"Authentication failed. The proxy will attempt to re-enroll. If this persists, regenerate your gateway token."
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
if (errorCode === "key_enrollment_required") {
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
return makeError(
|
|
251
|
-
"Gateway token is invalid or expired. Generate a new one."
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
if (status === 403 && errorCode === "step_up_required") {
|
|
255
|
-
const reason = trigger ? ` (reason: ${trigger})` : "";
|
|
256
|
-
return makeError(
|
|
257
|
-
`Device re-authorization required${reason}. This action can't proceed until the user re-authorizes this device in the Kojee dashboard.`
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
if (status === 429) {
|
|
261
|
-
return makeError(
|
|
262
|
-
"Rate limit exceeded. Wait before making more requests."
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
if (status >= 500) {
|
|
266
|
-
return makeError(
|
|
267
|
-
"Kojee gateway encountered an error. Try again."
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
var TANDEM_ERROR_MESSAGES = {
|
|
273
|
-
[-32003]: () => "This Tandem is hardened to owner-only membership; you can't join.",
|
|
274
|
-
[-32004]: () => "You aren't a member of that Tandem. Use tandem_join(join_link) first.",
|
|
275
|
-
[-32006]: (data) => {
|
|
276
|
-
const retry = data?.["retry_after_seconds"] ?? "a moment";
|
|
277
|
-
return `Rate limit hit on this Tandem. Retry after ${retry} seconds.`;
|
|
278
|
-
},
|
|
279
|
-
[-32007]: (data) => {
|
|
280
|
-
const rule = data?.["rule"] ?? "policy";
|
|
281
|
-
return `Message rejected by Tandem policy (${rule}).`;
|
|
282
|
-
},
|
|
283
|
-
[-32011]: (data) => {
|
|
284
|
-
const id = data?.["tandem_id"] ?? "unknown";
|
|
285
|
-
return `Tandem ${id} doesn't exist or isn't visible to you.`;
|
|
286
|
-
},
|
|
287
|
-
[-32015]: (data) => {
|
|
288
|
-
const candidates = data?.["candidates"] ?? [];
|
|
289
|
-
return `An @-mention matched multiple members and is ambiguous. Retry with explicit mentions[]. Candidates: ${candidates.join(", ")}.`;
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
function translateJsonRpcError(error) {
|
|
293
|
-
const msg = error.message ?? "";
|
|
294
|
-
const msgLower = msg.toLowerCase();
|
|
295
|
-
const tandemMessage = TANDEM_ERROR_MESSAGES[error.code];
|
|
296
|
-
if (tandemMessage) {
|
|
297
|
-
return {
|
|
298
|
-
content: [
|
|
299
|
-
{
|
|
300
|
-
type: "text",
|
|
301
|
-
text: tandemMessage(error.data)
|
|
302
|
-
}
|
|
303
|
-
],
|
|
304
|
-
isError: true
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
switch (error.code) {
|
|
308
|
-
case -32601:
|
|
309
|
-
return makeError(
|
|
310
|
-
`Tool not available. It may have been removed or is not connected. Check your connected services in the Kojee dashboard.`
|
|
311
|
-
);
|
|
312
|
-
case -32602:
|
|
313
|
-
return makeError(msg || "Invalid parameters for this tool call.");
|
|
314
|
-
case -32603: {
|
|
315
|
-
if (msgLower.includes("multiple accounts connected")) {
|
|
316
|
-
return makeError(msg);
|
|
317
|
-
}
|
|
318
|
-
if (msgLower.includes("not connected")) {
|
|
319
|
-
return makeError(
|
|
320
|
-
"The service is not connected. Connect it in the Kojee dashboard."
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
if (msgLower.includes("scope") && msgLower.includes("access")) {
|
|
324
|
-
return makeError(
|
|
325
|
-
"Token doesn't have access to this tool. Update scopes in the Kojee dashboard."
|
|
326
|
-
);
|
|
327
|
-
}
|
|
328
|
-
if (msgLower.includes("invalid_grant") || msgLower.includes("token refresh failed") || msgLower.includes("re-authorization") || msgLower.includes("reauthorization")) {
|
|
329
|
-
const serviceMatch = msg.match(
|
|
330
|
-
/(?:for|connected for)\s+(\w[\w-]*)/i
|
|
331
|
-
);
|
|
332
|
-
const service = serviceMatch ? serviceMatch[1] : "service";
|
|
333
|
-
return makeError(
|
|
334
|
-
`The ${service} connection needs re-authorization. Ask the user to reconnect it in the Kojee dashboard.`
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
return makeError(msg || "An internal error occurred on the gateway.");
|
|
338
|
-
}
|
|
339
|
-
case -32600:
|
|
340
|
-
return makeError(
|
|
341
|
-
"Unexpected response from gateway. This may be a temporary issue."
|
|
342
|
-
);
|
|
343
|
-
case -32e3:
|
|
344
|
-
return makeError(
|
|
345
|
-
"Rate limit exceeded. Wait before making more requests."
|
|
346
|
-
);
|
|
347
|
-
default:
|
|
348
|
-
return makeError(msg || "An unknown error occurred.");
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
function translateNetworkError(_error) {
|
|
352
|
-
return makeError(
|
|
353
|
-
"Cannot reach Kojee gateway. Check your connection."
|
|
354
|
-
);
|
|
355
|
-
}
|
|
356
|
-
function translateToolCallResult(result) {
|
|
357
|
-
if (result._meta?.governance) {
|
|
358
|
-
return translateGovernanceResult(result);
|
|
359
|
-
}
|
|
360
|
-
return result;
|
|
361
|
-
}
|
|
362
|
-
function formatRules(guardrails) {
|
|
363
|
-
if (!guardrails || guardrails.length === 0) return "unknown";
|
|
364
|
-
return guardrails.join(", ");
|
|
365
|
-
}
|
|
366
|
-
function makeError(text) {
|
|
367
|
-
return {
|
|
368
|
-
content: [{ type: "text", text }],
|
|
369
|
-
isError: true
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// src/gateway-client.ts
|
|
374
|
-
var GatewayClient = class {
|
|
375
|
-
constructor(brokerUrl, token, privateKey, kid, sessionId) {
|
|
376
|
-
this.brokerUrl = brokerUrl;
|
|
377
|
-
this.token = token;
|
|
378
|
-
this.privateKey = privateKey;
|
|
379
|
-
this.kid = kid;
|
|
380
|
-
this.endpoint = `${brokerUrl}/mcp/messages/${sessionId}/`;
|
|
381
|
-
}
|
|
382
|
-
brokerUrl;
|
|
383
|
-
token;
|
|
384
|
-
privateKey;
|
|
385
|
-
kid;
|
|
386
|
-
currentNonce;
|
|
387
|
-
requestCounter = 0;
|
|
388
|
-
endpoint;
|
|
389
|
-
/**
|
|
390
|
-
* Expose the DPoP signing key so peer modules sharing auth state
|
|
391
|
-
* (e.g. tandem/event-stream.ts) can sign their own requests.
|
|
392
|
-
*/
|
|
393
|
-
getPrivateKey() {
|
|
394
|
-
return this.privateKey;
|
|
395
|
-
}
|
|
396
|
-
/**
|
|
397
|
-
* Expose the bot_key_id (kid) for DPoP proof headers. Paired with
|
|
398
|
-
* getPrivateKey() so peer modules can construct proofs without
|
|
399
|
-
* threading the key material through their own constructors.
|
|
400
|
-
*/
|
|
401
|
-
getKid() {
|
|
402
|
-
return this.kid;
|
|
403
|
-
}
|
|
404
|
-
/**
|
|
405
|
-
* Derive a deterministic session ID from the gateway token.
|
|
406
|
-
* session_id = sha256(token + "proxy").slice(0, 16)
|
|
407
|
-
*/
|
|
408
|
-
static deriveSessionId(token) {
|
|
409
|
-
const hash = crypto2.createHash("sha256").update(token + "proxy").digest("hex");
|
|
410
|
-
return hash.slice(0, 16);
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Send a JSON-RPC 2.0 request to the gateway, handling DPoP auth and
|
|
414
|
-
* nonce retry transparently. A 403 `step_up_required` (deprecated feature,
|
|
415
|
-
* owner ruling 2026-06-10) is no longer polled — it surfaces immediately as
|
|
416
|
-
* a structured tool error via translateHttpError.
|
|
417
|
-
*
|
|
418
|
-
* `signal` (ROUND-3 MAJOR A) is a REAL AbortSignal threaded into the
|
|
419
|
-
* underlying `fetch` option — NOT placed inside `params`/`arguments`. A
|
|
420
|
-
* caller with a per-call timeout budget (e.g. resubscribeMemberships) passes
|
|
421
|
-
* its controller's signal here so a hung backend aborts at the budget instead
|
|
422
|
-
* of hanging forever. Putting the signal in `arguments` (the round-2 bug) both
|
|
423
|
-
* left fetch un-aborted AND serialized a junk `{}` onto the wire body.
|
|
424
|
-
*/
|
|
425
|
-
async sendRpc(method, params = {}, signal) {
|
|
426
|
-
const rpcRequest = {
|
|
427
|
-
jsonrpc: "2.0",
|
|
428
|
-
id: ++this.requestCounter,
|
|
429
|
-
method,
|
|
430
|
-
params
|
|
431
|
-
};
|
|
432
|
-
return this.executeWithRetries(rpcRequest, signal);
|
|
433
|
-
}
|
|
434
|
-
async executeWithRetries(rpcRequest, signal) {
|
|
435
|
-
let response;
|
|
436
|
-
try {
|
|
437
|
-
response = await this.sendHttpRequest(rpcRequest, signal);
|
|
438
|
-
} catch (err) {
|
|
439
|
-
return translateNetworkError(err);
|
|
440
|
-
}
|
|
441
|
-
this.trackNonce(response);
|
|
442
|
-
if (response.status === 401) {
|
|
443
|
-
const body = await this.tryParseErrorBody(response);
|
|
444
|
-
if (body?.error === "use_dpop_nonce") {
|
|
445
|
-
console.error("[gateway] Nonce expired, retrying with fresh nonce...");
|
|
446
|
-
try {
|
|
447
|
-
response = await this.sendHttpRequest(rpcRequest, signal);
|
|
448
|
-
} catch (err) {
|
|
449
|
-
return translateNetworkError(err);
|
|
450
|
-
}
|
|
451
|
-
this.trackNonce(response);
|
|
452
|
-
} else {
|
|
453
|
-
const translated = translateHttpError(401, body?.error);
|
|
454
|
-
if (translated) return translated;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
if (response.status === 403) {
|
|
458
|
-
const body = await this.tryParseErrorBody(response);
|
|
459
|
-
const translated = translateHttpError(403, body?.error, body?.trigger);
|
|
460
|
-
if (translated) return translated;
|
|
461
|
-
}
|
|
462
|
-
if (!response.ok) {
|
|
463
|
-
const body = await this.tryParseErrorBody(response);
|
|
464
|
-
const translated = translateHttpError(response.status, body?.error);
|
|
465
|
-
if (translated) return translated;
|
|
466
|
-
return {
|
|
467
|
-
content: [{ type: "text", text: `Gateway error: ${response.status}` }],
|
|
468
|
-
isError: true
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
const rpcResponse = await response.json();
|
|
472
|
-
if (rpcResponse.error) {
|
|
473
|
-
return translateJsonRpcError(rpcResponse.error);
|
|
474
|
-
}
|
|
475
|
-
const result = rpcResponse.result;
|
|
476
|
-
return result ?? { content: [{ type: "text", text: "No result" }] };
|
|
477
|
-
}
|
|
478
|
-
async sendHttpRequest(rpcRequest, signal) {
|
|
479
|
-
const proof = await createDPoPProof(
|
|
480
|
-
this.privateKey,
|
|
481
|
-
this.kid,
|
|
482
|
-
"POST",
|
|
483
|
-
this.endpoint,
|
|
484
|
-
this.currentNonce,
|
|
485
|
-
this.token
|
|
486
|
-
);
|
|
487
|
-
return fetch(this.endpoint, {
|
|
488
|
-
method: "POST",
|
|
489
|
-
headers: {
|
|
490
|
-
"Content-Type": "application/json",
|
|
491
|
-
Authorization: `DPoP ${this.token}`,
|
|
492
|
-
DPoP: proof,
|
|
493
|
-
"Mcp-Session-Id": MCP_SESSION_ID
|
|
494
|
-
},
|
|
495
|
-
body: JSON.stringify(rpcRequest),
|
|
496
|
-
// ROUND-3 MAJOR A: the caller's AbortSignal rides HERE (a real fetch
|
|
497
|
-
// option), never inside the JSON-RPC body. `undefined` is a valid value
|
|
498
|
-
// for the fetch `signal` option (no abort wired).
|
|
499
|
-
...signal ? { signal } : {}
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
trackNonce(response) {
|
|
503
|
-
const nonce = response.headers.get("DPoP-Nonce");
|
|
504
|
-
if (nonce) {
|
|
505
|
-
this.currentNonce = nonce;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
async tryParseErrorBody(response) {
|
|
509
|
-
try {
|
|
510
|
-
return await response.json();
|
|
511
|
-
} catch {
|
|
512
|
-
return null;
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
};
|
|
26
|
+
import path2 from "path";
|
|
516
27
|
|
|
517
28
|
// src/tool-registry.ts
|
|
518
29
|
var ToolRegistry = class {
|
|
@@ -590,15 +101,15 @@ import {
|
|
|
590
101
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
591
102
|
|
|
592
103
|
// src/version.ts
|
|
593
|
-
import
|
|
594
|
-
import
|
|
104
|
+
import fs from "fs";
|
|
105
|
+
import path from "path";
|
|
595
106
|
import { fileURLToPath } from "url";
|
|
596
107
|
var FALLBACK_VERSION = "0.0.0-unknown";
|
|
597
108
|
function resolveVersion() {
|
|
598
109
|
try {
|
|
599
|
-
const here =
|
|
110
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
600
111
|
const parsed = JSON.parse(
|
|
601
|
-
|
|
112
|
+
fs.readFileSync(path.join(here, "..", "package.json"), "utf8")
|
|
602
113
|
);
|
|
603
114
|
return typeof parsed?.version === "string" && parsed.version ? parsed.version : FALLBACK_VERSION;
|
|
604
115
|
} catch (err) {
|
|
@@ -625,7 +136,19 @@ function buildChannelInstructions(_tandemMembershipCount, eventLogPath) {
|
|
|
625
136
|
const advice = "\n\nPrefer (2) at session start \u2014 it's the default no-allowlist wake mechanism. (1) supplements it when channels are enabled; (3) is for one-shot blocking waits.";
|
|
626
137
|
return intro + monitorSection + listenSection + advice;
|
|
627
138
|
}
|
|
628
|
-
function
|
|
139
|
+
async function executeToolCall(registry, name, args, hooks) {
|
|
140
|
+
const rawResult = await registry.callTool(name, args);
|
|
141
|
+
const result = translateToolCallResult(rawResult);
|
|
142
|
+
if (name === "tandem_join" && !result.isError) {
|
|
143
|
+
try {
|
|
144
|
+
hooks?.onTandemJoin?.();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error("[mcp] onTandemJoin hook failed:", err?.message ?? String(err));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath, hooks) {
|
|
629
152
|
const capabilities = { tools: {} };
|
|
630
153
|
if (adapter.supportsChannels) {
|
|
631
154
|
capabilities.experimental = { "claude/channel": {} };
|
|
@@ -647,8 +170,7 @@ function createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLog
|
|
|
647
170
|
});
|
|
648
171
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
649
172
|
const { name, arguments: args } = request.params;
|
|
650
|
-
const
|
|
651
|
-
const result = translateToolCallResult(rawResult);
|
|
173
|
+
const result = await executeToolCall(registry, name, args ?? {}, hooks);
|
|
652
174
|
return { content: result.content, isError: result.isError };
|
|
653
175
|
});
|
|
654
176
|
return server;
|
|
@@ -751,7 +273,7 @@ var unknownAdapter = {
|
|
|
751
273
|
};
|
|
752
274
|
|
|
753
275
|
// src/index.ts
|
|
754
|
-
var DEFAULT_KEYSTORE_PATH =
|
|
276
|
+
var DEFAULT_KEYSTORE_PATH = path2.join(os.homedir(), ".kojee", "keypair.json");
|
|
755
277
|
function isDPoPEnrollmentError(err) {
|
|
756
278
|
const msg = String(err?.message ?? err ?? "").toLowerCase();
|
|
757
279
|
if (msg.includes("invalid or expired") && msg.includes("token")) return false;
|
|
@@ -776,6 +298,7 @@ async function listTandemIds(gateway) {
|
|
|
776
298
|
return list.map((t) => {
|
|
777
299
|
if (typeof t === "string") return t;
|
|
778
300
|
const obj = t;
|
|
301
|
+
if (obj?.my_membership?.is_member !== true) return void 0;
|
|
779
302
|
return obj?.tandem_id ?? obj?.id;
|
|
780
303
|
}).filter((id) => typeof id === "string" && id.length > 0);
|
|
781
304
|
} catch {
|
|
@@ -793,6 +316,19 @@ async function startProxy(config) {
|
|
|
793
316
|
console.error(
|
|
794
317
|
`[kojee-mcp] Ready \u2014 ${registry.toolCount} tools available from ${config.url}`
|
|
795
318
|
);
|
|
319
|
+
let activeStreamHandle = null;
|
|
320
|
+
const { createJoinReconnectScheduler } = await import("./reconnect-scheduler-JSXCJKQP.js");
|
|
321
|
+
const joinReconnect = createJoinReconnectScheduler({
|
|
322
|
+
reconnect: () => activeStreamHandle?.reconnect()
|
|
323
|
+
});
|
|
324
|
+
const onTandemJoin = () => joinReconnect.requestReconnect();
|
|
325
|
+
const { ensureJoinTandems } = await import("./ensure-join-7AEDJMPE.js");
|
|
326
|
+
await ensureJoinTandems({
|
|
327
|
+
gateway,
|
|
328
|
+
env: process.env["KOJEE_TANDEMS"],
|
|
329
|
+
listTandems: () => listTandemIds(gateway),
|
|
330
|
+
onJoined: () => joinReconnect.requestReconnect()
|
|
331
|
+
});
|
|
796
332
|
let tandemMembershipCount = -1;
|
|
797
333
|
try {
|
|
798
334
|
const bootIds = await listTandemIds(gateway);
|
|
@@ -804,16 +340,16 @@ async function startProxy(config) {
|
|
|
804
340
|
let server;
|
|
805
341
|
if (adapter.supportsChannels) {
|
|
806
342
|
const { EventQueue } = await import("./event-queue-5YVJFR3E.js");
|
|
807
|
-
const { startHookServer } = await import("./hook-server-
|
|
343
|
+
const { startHookServer } = await import("./hook-server-37E2LUKJ.js");
|
|
808
344
|
const {
|
|
809
345
|
writeDiscoveryByKey,
|
|
810
346
|
cleanupDiscoveryByKey,
|
|
811
347
|
sweepStaleDiscovery
|
|
812
348
|
} = await import("./session-discovery-FNMJGFPM.js");
|
|
813
349
|
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
|
|
814
|
-
const { resubscribeMemberships } = await import("./resubscribe-
|
|
815
|
-
const { resolveWebhookConfig } = await import("./webhook-config-
|
|
816
|
-
const { createWebhookSink } = await import("./webhook-sink-
|
|
350
|
+
const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
|
|
351
|
+
const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
|
|
352
|
+
const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
|
|
817
353
|
sweepStaleDiscovery();
|
|
818
354
|
sweepStaleEventLogs();
|
|
819
355
|
const ccPid = await findClaudeAncestorPid();
|
|
@@ -826,6 +362,11 @@ async function startProxy(config) {
|
|
|
826
362
|
void eventLog.appendStatus(`status=webhook error="${webhookResolution.error}"`).catch(() => {
|
|
827
363
|
});
|
|
828
364
|
}
|
|
365
|
+
if (webhookResolution.warning) {
|
|
366
|
+
console.error(`[kojee-mcp] webhook sink WARNING: ${webhookResolution.warning}`);
|
|
367
|
+
void eventLog.appendStatus(`status=webhook warning="${webhookResolution.warning}"`).catch(() => {
|
|
368
|
+
});
|
|
369
|
+
}
|
|
829
370
|
const webhookSink = webhookResolution.enabled && webhookResolution.config ? createWebhookSink(webhookResolution.config, {
|
|
830
371
|
// Route delivery/failure observability to the STATUS sink.
|
|
831
372
|
log: (line) => {
|
|
@@ -838,13 +379,29 @@ async function startProxy(config) {
|
|
|
838
379
|
void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
|
|
839
380
|
});
|
|
840
381
|
}
|
|
841
|
-
server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path
|
|
382
|
+
server = createMcpServer(registry, adapter, tandemMembershipCount, eventLog.path, {
|
|
383
|
+
onTandemJoin
|
|
384
|
+
});
|
|
385
|
+
const { issueControlToken, controlTokenPath } = await import("./control-token-4BUCTYQB.js");
|
|
386
|
+
let controlToken = null;
|
|
387
|
+
try {
|
|
388
|
+
controlToken = issueControlToken();
|
|
389
|
+
} catch (err) {
|
|
390
|
+
console.error(
|
|
391
|
+
"[kojee-mcp] control token write failed \u2014 POST /send disabled; GET /poll and /status left UNGATED (degrade open):",
|
|
392
|
+
err.message
|
|
393
|
+
);
|
|
394
|
+
}
|
|
842
395
|
const queue = new EventQueue();
|
|
843
396
|
let streamHandle = null;
|
|
844
397
|
const hookServer = await startHookServer({
|
|
845
398
|
port: 0,
|
|
846
399
|
queue,
|
|
847
400
|
adapter,
|
|
401
|
+
// 0.5.4 hardening: the same bearer gates POST /send AND the data-bearing
|
|
402
|
+
// reads (GET /poll, GET /status). When token issuance failed both stay
|
|
403
|
+
// available-but-degraded: /send answers 503, the reads stay open.
|
|
404
|
+
...controlToken !== null ? { controlToken, send: { gateway, authToken: controlToken } } : {},
|
|
848
405
|
getStreamState: () => streamHandle ? streamHandle.getState() : {
|
|
849
406
|
connected: false,
|
|
850
407
|
connectedSince: null,
|
|
@@ -869,6 +426,9 @@ async function startProxy(config) {
|
|
|
869
426
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
870
427
|
brokerUrl: config.url,
|
|
871
428
|
eventLogPath: eventLog.path,
|
|
429
|
+
// Advertise where the POST /send bearer lives so local consumers
|
|
430
|
+
// (native gateway plugins) can find it without guessing ~/.kojee.
|
|
431
|
+
...controlToken !== null ? { controlTokenPath: controlTokenPath() } : {},
|
|
872
432
|
// Stamp the auth mode so `kojee-mcp doctor` renders the pairing check
|
|
873
433
|
// honestly: a token-mode box has no ~/.kojee/config.json by design and
|
|
874
434
|
// must not hard-fail on "paired config: MISSING". Defaults to "paired"
|
|
@@ -921,6 +481,7 @@ async function startProxy(config) {
|
|
|
921
481
|
};
|
|
922
482
|
})()
|
|
923
483
|
});
|
|
484
|
+
activeStreamHandle = streamHandle;
|
|
924
485
|
const cancelStream = streamHandle;
|
|
925
486
|
process.stdin.on("end", () => {
|
|
926
487
|
cancelStream();
|
|
@@ -934,9 +495,9 @@ async function startProxy(config) {
|
|
|
934
495
|
});
|
|
935
496
|
} else if (needsWebhookEventStream()) {
|
|
936
497
|
const { startEventLog, sweepStaleEventLogs } = await import("./event-log-RSTM4PLL.js");
|
|
937
|
-
const { resolveWebhookConfig } = await import("./webhook-config-
|
|
938
|
-
const { createWebhookSink } = await import("./webhook-sink-
|
|
939
|
-
const { resubscribeMemberships } = await import("./resubscribe-
|
|
498
|
+
const { resolveWebhookConfig } = await import("./webhook-config-O4WMQ532.js");
|
|
499
|
+
const { createWebhookSink } = await import("./webhook-sink-NWGCUDGY.js");
|
|
500
|
+
const { resubscribeMemberships } = await import("./resubscribe-G5OGDZJD.js");
|
|
940
501
|
sweepStaleEventLogs();
|
|
941
502
|
const ccPid = await findClaudeAncestorPid();
|
|
942
503
|
const projectDir = process.env["CLAUDE_PROJECT_DIR"];
|
|
@@ -948,6 +509,11 @@ async function startProxy(config) {
|
|
|
948
509
|
void eventLog.appendStatus(`status=webhook error="${webhookResolution.error}"`).catch(() => {
|
|
949
510
|
});
|
|
950
511
|
}
|
|
512
|
+
if (webhookResolution.warning) {
|
|
513
|
+
console.error(`[kojee-mcp] webhook sink WARNING: ${webhookResolution.warning}`);
|
|
514
|
+
void eventLog.appendStatus(`status=webhook warning="${webhookResolution.warning}"`).catch(() => {
|
|
515
|
+
});
|
|
516
|
+
}
|
|
951
517
|
const webhookSink = webhookResolution.enabled && webhookResolution.config ? createWebhookSink(webhookResolution.config, {
|
|
952
518
|
log: (line) => {
|
|
953
519
|
void eventLog.appendStatus(`status=webhook ${line}`).catch(() => {
|
|
@@ -959,7 +525,9 @@ async function startProxy(config) {
|
|
|
959
525
|
void eventLog.appendStatus(`status=webhook enabled ${webhookSink.configSummary()}`).catch(() => {
|
|
960
526
|
});
|
|
961
527
|
}
|
|
962
|
-
server = createMcpServer(registry, adapter, tandemMembershipCount
|
|
528
|
+
server = createMcpServer(registry, adapter, tandemMembershipCount, void 0, {
|
|
529
|
+
onTandemJoin
|
|
530
|
+
});
|
|
963
531
|
process.on("exit", () => eventLog.cleanup());
|
|
964
532
|
const streamHandle = await startEventStream({
|
|
965
533
|
brokerUrl: config.url,
|
|
@@ -981,6 +549,7 @@ async function startProxy(config) {
|
|
|
981
549
|
};
|
|
982
550
|
})()
|
|
983
551
|
});
|
|
552
|
+
activeStreamHandle = streamHandle;
|
|
984
553
|
process.stdin.on("end", () => {
|
|
985
554
|
streamHandle();
|
|
986
555
|
void webhookSink?.stop();
|
|
@@ -1021,7 +590,7 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
|
1021
590
|
"[kojee-mcp] Auth failed, attempting recovery with fresh enrollment..."
|
|
1022
591
|
);
|
|
1023
592
|
try {
|
|
1024
|
-
if (
|
|
593
|
+
if (fs2.existsSync(keystorePath)) fs2.unlinkSync(keystorePath);
|
|
1025
594
|
} catch (unlinkErr) {
|
|
1026
595
|
console.error("[kojee-mcp] Could not remove stale keystore:", unlinkErr);
|
|
1027
596
|
}
|
|
@@ -1030,7 +599,7 @@ async function enrollAndDiscover(config, keystorePath, isRetry = false) {
|
|
|
1030
599
|
}
|
|
1031
600
|
|
|
1032
601
|
export {
|
|
1033
|
-
AuthModule,
|
|
1034
602
|
VERSION,
|
|
603
|
+
listTandemIds,
|
|
1035
604
|
startProxy
|
|
1036
605
|
};
|