@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 +22 -9
- package/dist/sessionRepair.js +37 -0
- package/dist/tools.js +57 -15
- package/dist/version.js +2 -1
- package/package.json +2 -2
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]
|
|
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]
|
|
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 =
|
|
34
|
+
const rawRoots = readOptionalEnv("OKO_IMAGE_ROOTS");
|
|
24
35
|
if (!rawRoots)
|
|
25
36
|
return [];
|
|
26
|
-
|
|
37
|
+
const roots = [];
|
|
38
|
+
for (const value of rawRoots.split(delimiter)) {
|
|
27
39
|
const root = value.trim();
|
|
28
40
|
if (!root) {
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
+
return disableImageRoots("OKO_IMAGE_ROOTS must not allow an entire filesystem root.");
|
|
40
52
|
}
|
|
41
|
-
|
|
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
|
-
|
|
2408
|
-
|
|
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(
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
.
|
|
2418
|
-
|
|
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
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|