@okostream/mcp 0.9.1 → 0.9.2

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/env.js CHANGED
@@ -3,9 +3,16 @@ import { homedir } from "node:os";
3
3
  import { delimiter, isAbsolute, join, parse, resolve } from "node:path";
4
4
  import { DEFAULT_EXCHANGE_URL, DEFAULT_FIREBASE_CONFIG } from "./firebaseDefaults.js";
5
5
  config({ path: join(homedir(), ".env"), quiet: true });
6
+ const USER_CONFIG_PLACEHOLDER = /^\$\{user_config\.[^{}\s]+\}$/;
7
+ function normalizeOptionalEnv(value) {
8
+ const normalized = value?.trim();
9
+ if (!normalized || USER_CONFIG_PLACEHOLDER.test(normalized))
10
+ return null;
11
+ return normalized;
12
+ }
6
13
  function readOptionalEnv(...names) {
7
14
  for (const name of names) {
8
- const value = process.env[name]?.trim();
15
+ const value = normalizeOptionalEnv(process.env[name]);
9
16
  if (value)
10
17
  return value;
11
18
  }
@@ -13,33 +20,39 @@ function readOptionalEnv(...names) {
13
20
  }
14
21
  function readEnvWithDefault(defaultValue, ...names) {
15
22
  for (const name of names) {
16
- const value = process.env[name]?.trim();
23
+ const value = normalizeOptionalEnv(process.env[name]);
17
24
  if (value)
18
25
  return value;
19
26
  }
20
27
  return defaultValue;
21
28
  }
29
+ function disableImageRoots(reason) {
30
+ console.error(`[oko-mcp] Local image reads disabled: ${reason}`);
31
+ return [];
32
+ }
22
33
  function readImageRoots() {
23
- const rawRoots = process.env.OKO_IMAGE_ROOTS?.trim();
34
+ const rawRoots = readOptionalEnv("OKO_IMAGE_ROOTS");
24
35
  if (!rawRoots)
25
36
  return [];
26
- return rawRoots.split(delimiter).map((value) => {
37
+ const roots = [];
38
+ for (const value of rawRoots.split(delimiter)) {
27
39
  const root = value.trim();
28
40
  if (!root) {
29
- throw new Error("OKO_IMAGE_ROOTS contains an empty directory entry.");
41
+ return disableImageRoots("OKO_IMAGE_ROOTS contains an empty directory entry.");
30
42
  }
31
43
  const expanded = root === "~" || root.startsWith("~/")
32
44
  ? join(homedir(), root.slice(2))
33
45
  : root;
34
46
  if (!isAbsolute(expanded)) {
35
- throw new Error(`OKO_IMAGE_ROOTS entries must be absolute paths: ${root}`);
47
+ return disableImageRoots("OKO_IMAGE_ROOTS entries must be absolute paths.");
36
48
  }
37
49
  const resolved = resolve(expanded);
38
50
  if (resolved === parse(resolved).root) {
39
- throw new Error("OKO_IMAGE_ROOTS must not allow an entire filesystem root.");
51
+ return disableImageRoots("OKO_IMAGE_ROOTS must not allow an entire filesystem root.");
40
52
  }
41
- return resolved;
42
- });
53
+ roots.push(resolved);
54
+ }
55
+ return roots;
43
56
  }
44
57
  export const env = {
45
58
  apiKey: readOptionalEnv("OKO_API_KEY"),
@@ -0,0 +1,37 @@
1
+ export function summarizeSessionRepair(input) {
2
+ const fixed = !input.verificationError
3
+ && input.firestorePointer === null
4
+ && input.rtdbPointer === null;
5
+ const fulfilled = new Set(input.attempts
6
+ .filter((attempt) => attempt.status === "fulfilled")
7
+ .map((attempt) => attempt.path));
8
+ const cleared = [
9
+ ...(input.firestorePointer === null ? ["events.activeAnalyticsSessionId"] : []),
10
+ ...(input.rtdbPointer === null ? ["ctrl/activeSessionId"] : []),
11
+ ...(input.sessionId && fulfilled.has(`activeSessions/${input.sessionId}`)
12
+ ? [`activeSessions/${input.sessionId}`]
13
+ : []),
14
+ ...(input.sessionId
15
+ && input.deleteRtdbSessionData
16
+ && fulfilled.has(`events/${input.eventId}/sessions/${input.sessionId}`)
17
+ ? [`events/${input.eventId}/sessions/${input.sessionId}`]
18
+ : []),
19
+ ];
20
+ const warnings = input.attempts
21
+ .filter((attempt) => attempt.status === "rejected")
22
+ .map((attempt) => `${attempt.path}: ${attempt.error || "cleanup failed"}`);
23
+ if (input.verificationError) {
24
+ warnings.push(`postcondition verification: ${input.verificationError}`);
25
+ }
26
+ return {
27
+ eventId: input.eventId,
28
+ sessionId: input.sessionId,
29
+ fixed,
30
+ cleared,
31
+ warnings,
32
+ remainingPointers: {
33
+ firestore: input.firestorePointer,
34
+ rtdb: input.rtdbPointer,
35
+ },
36
+ };
37
+ }
package/dist/tools.js CHANGED
@@ -13,6 +13,7 @@ import { env } from "./env.js";
13
13
  import { ipLiteralFamily, isPublicUnicastAddress, normalizeIpLiteralHostname, } from "./networkPolicy.js";
14
14
  import { chatRtdb, firestore, functions, rtdb } from "./firebase.js";
15
15
  import { OKO_CONNECTION_STATUSES } from "./onboarding.js";
16
+ import { summarizeSessionRepair } from "./sessionRepair.js";
16
17
  import { canonicalTelegramChannelSchema, telegramChannelSchema, } from "./telegramChannelSchema.js";
17
18
  import { csvCell, requireConfirm, safeString, serialize, textResult } from "./utils.js";
18
19
  import { detectAndNormalize, extractIframeSrc } from "./videoUrl.js";
@@ -2404,24 +2405,65 @@ export function registerTools(server) {
2404
2405
  const sessionId = safeString(event.activeAnalyticsSessionId) || safeString(ctrlSnap.val());
2405
2406
  requireConfirm(confirm, "oko_fix_orphan_session", { eventId, sessionId: sessionId || null, deleteRtdbSessionData });
2406
2407
  const cleanup = [
2407
- updateDoc(doc(firestore, "events", eventId), { activeAnalyticsSessionId: deleteField() }),
2408
- remove(ref(rtdb, `events/${eventId}/ctrl/activeSessionId`)),
2408
+ {
2409
+ path: "events.activeAnalyticsSessionId",
2410
+ promise: updateDoc(doc(firestore, "events", eventId), { activeAnalyticsSessionId: deleteField() }),
2411
+ },
2412
+ {
2413
+ path: "ctrl/activeSessionId",
2414
+ promise: remove(ref(rtdb, `events/${eventId}/ctrl/activeSessionId`)),
2415
+ },
2409
2416
  ];
2410
- if (sessionId)
2411
- cleanup.push(remove(ref(rtdb, `activeSessions/${sessionId}`)));
2412
- if (sessionId && deleteRtdbSessionData)
2413
- cleanup.push(remove(ref(rtdb, `events/${eventId}/sessions/${sessionId}`)));
2414
- const results = await Promise.allSettled(cleanup);
2415
- const warnings = results
2416
- .filter((result) => result.status === "rejected")
2417
- .map((result) => result.reason instanceof Error ? result.reason.message : String(result.reason));
2418
- return textResult({
2417
+ if (sessionId) {
2418
+ cleanup.push({
2419
+ path: `activeSessions/${sessionId}`,
2420
+ promise: remove(ref(rtdb, `activeSessions/${sessionId}`)),
2421
+ });
2422
+ }
2423
+ if (sessionId && deleteRtdbSessionData) {
2424
+ cleanup.push({
2425
+ path: `events/${eventId}/sessions/${sessionId}`,
2426
+ promise: remove(ref(rtdb, `events/${eventId}/sessions/${sessionId}`)),
2427
+ });
2428
+ }
2429
+ const settled = await Promise.allSettled(cleanup.map((operation) => operation.promise));
2430
+ const attempts = settled.map((result, index) => {
2431
+ const operation = cleanup[index];
2432
+ if (!operation)
2433
+ throw new Error("Session repair result has no matching cleanup operation.");
2434
+ return {
2435
+ path: operation.path,
2436
+ status: result.status,
2437
+ ...(result.status === "rejected" ? {
2438
+ error: result.reason instanceof Error ? result.reason.message : String(result.reason),
2439
+ } : {}),
2440
+ };
2441
+ });
2442
+ let firestorePointer = null;
2443
+ let rtdbPointer = null;
2444
+ let verificationError;
2445
+ try {
2446
+ const [eventAfter, ctrlAfter] = await Promise.all([
2447
+ getDoc(doc(firestore, "events", eventId)),
2448
+ get(ref(rtdb, `events/${eventId}/ctrl/activeSessionId`)),
2449
+ ]);
2450
+ firestorePointer = eventAfter.exists()
2451
+ ? safeString(eventAfter.data().activeAnalyticsSessionId) || null
2452
+ : null;
2453
+ rtdbPointer = safeString(ctrlAfter.val()) || null;
2454
+ }
2455
+ catch (error) {
2456
+ verificationError = error instanceof Error ? error.message : String(error);
2457
+ }
2458
+ return textResult(summarizeSessionRepair({
2419
2459
  eventId,
2420
2460
  sessionId: sessionId || null,
2421
- fixed: warnings.length === 0,
2422
- cleared: ["events.activeAnalyticsSessionId", "ctrl/activeSessionId", ...(sessionId ? [`activeSessions/${sessionId}`] : []), ...(sessionId && deleteRtdbSessionData ? [`events/${eventId}/sessions/${sessionId}`] : [])],
2423
- warnings,
2424
- });
2461
+ deleteRtdbSessionData,
2462
+ attempts,
2463
+ firestorePointer,
2464
+ rtdbPointer,
2465
+ verificationError,
2466
+ }));
2425
2467
  });
2426
2468
  server.registerTool("oko_delete_session", {
2427
2469
  title: "Delete analytics session",
package/dist/version.js CHANGED
@@ -1 +1,2 @@
1
- export const OKO_MCP_VERSION = "0.9.1";
1
+ // Generated by scripts/generateMcpVersion.mjs from mcp/package.json. Do not edit.
2
+ export const OKO_MCP_VERSION = "0.9.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.9.1",
3
+ "version": "0.9.2",
4
4
  "description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
5
5
  "license": "UNLICENSED",
6
6
  "author": {
@@ -31,7 +31,7 @@
31
31
  "oko-mcp": "dist/server.js"
32
32
  },
33
33
  "scripts": {
34
- "prebuild": "node ../scripts/generateMcpContract.mjs --check",
34
+ "prebuild": "node ../scripts/generateMcpVersion.mjs && node ../scripts/generateMcpContract.mjs --check",
35
35
  "build": "tsc && node -e \"require('node:fs').chmodSync('dist/server.js', 0o755)\"",
36
36
  "test": "npm run build && node --test test/*.test.mjs",
37
37
  "smoke:tarball": "node test/tarball-smoke.mjs",