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.
Files changed (37) hide show
  1. package/README.md +112 -5
  2. package/dist/{chunk-YEC7IHIG.js → chunk-2BDAM3TH.js} +92 -523
  3. package/dist/chunk-2MIISF2W.js +35 -0
  4. package/dist/chunk-3XDJOHMZ.js +223 -0
  5. package/dist/{chunk-ZW4SW7LJ.js → chunk-64EOLZNI.js} +14 -5
  6. package/dist/chunk-6SK6ITFE.js +142 -0
  7. package/dist/chunk-GI2CKKBL.js +46 -0
  8. package/dist/chunk-HIZ4NDWN.js +141 -0
  9. package/dist/chunk-LDZXU3DW.js +170 -0
  10. package/dist/{resubscribe-SLZNA76S.js → chunk-OT2GILXC.js} +1 -0
  11. package/dist/{chunk-WBMX4CHB.js → chunk-UEGQGXPY.js} +57 -40
  12. package/dist/chunk-V5VZPYMZ.js +185 -0
  13. package/dist/{chunk-C6GZ2L2W.js → chunk-X672ZN7V.js} +5 -2
  14. package/dist/cli.js +47 -24
  15. package/dist/{codex-stop-hook-JOTBCS5K.js → codex-stop-hook-SWA53ECG.js} +1 -1
  16. package/dist/control-token-4BUCTYQB.js +13 -0
  17. package/dist/{doctor-TSHOMT5X.js → doctor-QCQDFLEH.js} +30 -17
  18. package/dist/{doctor-codex-BMI5JOO6.js → doctor-codex-NZ53ROQA.js} +12 -5
  19. package/dist/ensure-join-7AEDJMPE.js +96 -0
  20. package/dist/gateway-client-93P1E0CZ.d.ts +92 -0
  21. package/dist/{hook-server-QF5JVUHV.js → hook-server-37E2LUKJ.js} +91 -0
  22. package/dist/index.d.ts +18 -15
  23. package/dist/index.js +9 -3
  24. package/dist/lib.d.ts +427 -0
  25. package/dist/lib.js +44 -0
  26. package/dist/reconnect-scheduler-JSXCJKQP.js +26 -0
  27. package/dist/resubscribe-G5OGDZJD.js +6 -0
  28. package/dist/send-cli-C2F4WTBN.js +72 -0
  29. package/dist/{stop-hook-SEPWWETV.js → stop-hook-TRAMQYNE.js} +16 -8
  30. package/dist/{tail-stream-BYKO4DW6.js → tail-stream-VUZBYKXS.js} +4 -3
  31. package/dist/{user-prompt-submit-hook-ARPEO6FF.js → user-prompt-submit-hook-ZD2XKN7U.js} +7 -1
  32. package/dist/webhook-config-O4WMQ532.js +20 -0
  33. package/dist/{webhook-sink-7OYZBWXA.js → webhook-sink-NWGCUDGY.js} +28 -5
  34. package/dist/{wizard-7KHD5JT4.js → wizard-OSOAY4GO.js} +64 -27
  35. package/package.json +11 -2
  36. package/dist/chunk-F7L25L2J.js +0 -60
  37. 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
- buildCatchUpNote,
7
- buildMonitorSpawn,
8
- buildReplyRecipe
9
- } from "./chunk-C6GZ2L2W.js";
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-WBMX4CHB.js";
18
+ } from "./chunk-UEGQGXPY.js";
15
19
  import {
16
- secureDir,
17
- secureFile
18
- } from "./chunk-BLEGIR35.js";
20
+ translateToolCallResult
21
+ } from "./chunk-LDZXU3DW.js";
19
22
 
20
23
  // src/index.ts
21
- import fs3 from "fs";
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 path from "path";
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 fs2 from "fs";
594
- import path2 from "path";
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 = path2.dirname(fileURLToPath(import.meta.url));
110
+ const here = path.dirname(fileURLToPath(import.meta.url));
600
111
  const parsed = JSON.parse(
601
- fs2.readFileSync(path2.join(here, "..", "package.json"), "utf8")
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 createMcpServer(registry, adapter, tandemMembershipCount = -1, eventLogPath) {
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 rawResult = await registry.callTool(name, args ?? {});
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 = path3.join(os2.homedir(), ".kojee", "keypair.json");
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-QF5JVUHV.js");
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-SLZNA76S.js");
815
- const { resolveWebhookConfig } = await import("./webhook-config-5TLLX7RA.js");
816
- const { createWebhookSink } = await import("./webhook-sink-7OYZBWXA.js");
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-5TLLX7RA.js");
938
- const { createWebhookSink } = await import("./webhook-sink-7OYZBWXA.js");
939
- const { resubscribeMemberships } = await import("./resubscribe-SLZNA76S.js");
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 (fs3.existsSync(keystorePath)) fs3.unlinkSync(keystorePath);
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
  };