@okostream/mcp 0.9.0 → 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/telegramChannelSchema.js +13 -0
- package/dist/tools.js +60 -26
- 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
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const telegramChannelSchema = z.object({
|
|
3
|
+
chatId: z.number().int(),
|
|
4
|
+
title: z.string().max(200).optional(),
|
|
5
|
+
username: z.string().max(120).nullable().optional(),
|
|
6
|
+
}).strict();
|
|
7
|
+
export const canonicalTelegramChannelSchema = telegramChannelSchema.extend({
|
|
8
|
+
workspaceIds: z.array(z.string()).max(50),
|
|
9
|
+
owned: z.boolean(),
|
|
10
|
+
subscriberCount: z.number().int().nonnegative().nullable().optional(),
|
|
11
|
+
health: z.enum(["connected", "needs_attention", "unknown"]).optional(),
|
|
12
|
+
healthCheckedAt: z.number().int().nonnegative().nullable().optional(),
|
|
13
|
+
}).strict();
|
package/dist/tools.js
CHANGED
|
@@ -13,6 +13,8 @@ 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";
|
|
17
|
+
import { canonicalTelegramChannelSchema, telegramChannelSchema, } from "./telegramChannelSchema.js";
|
|
16
18
|
import { csvCell, requireConfirm, safeString, serialize, textResult } from "./utils.js";
|
|
17
19
|
import { detectAndNormalize, extractIframeSrc } from "./videoUrl.js";
|
|
18
20
|
const boolOrNull = z.boolean().nullable();
|
|
@@ -206,7 +208,7 @@ const workspaceAccessOutputSchema = z.object({
|
|
|
206
208
|
invites: z.array(unknownRecordSchema),
|
|
207
209
|
}).passthrough();
|
|
208
210
|
const telegramChannelsOutputSchema = z.object({
|
|
209
|
-
channels: z.array(
|
|
211
|
+
channels: z.array(canonicalTelegramChannelSchema),
|
|
210
212
|
count: z.number().int().nonnegative(),
|
|
211
213
|
}).passthrough();
|
|
212
214
|
const telegramPostsOutputSchema = z.object({
|
|
@@ -273,15 +275,6 @@ const cardItemInputSchema = z.object({
|
|
|
273
275
|
buttonUrl: z.string().max(MAX_CARD_BUTTON_URL_LENGTH).nullable().optional(),
|
|
274
276
|
formId: cardIdSchema.nullable().optional(),
|
|
275
277
|
}).strict();
|
|
276
|
-
const telegramChannelSchema = z.object({
|
|
277
|
-
chatId: z.number().int(),
|
|
278
|
-
title: z.string().max(200).optional(),
|
|
279
|
-
username: z.string().max(120).nullable().optional(),
|
|
280
|
-
}).strict();
|
|
281
|
-
const canonicalTelegramChannelSchema = telegramChannelSchema.extend({
|
|
282
|
-
workspaceIds: z.array(z.string()).max(50),
|
|
283
|
-
owned: z.boolean(),
|
|
284
|
-
}).strict();
|
|
285
278
|
const profileDisplayNameSchema = z.string().max(40).nullable();
|
|
286
279
|
const timezoneSchema = z.string()
|
|
287
280
|
.min(1)
|
|
@@ -2412,24 +2405,65 @@ export function registerTools(server) {
|
|
|
2412
2405
|
const sessionId = safeString(event.activeAnalyticsSessionId) || safeString(ctrlSnap.val());
|
|
2413
2406
|
requireConfirm(confirm, "oko_fix_orphan_session", { eventId, sessionId: sessionId || null, deleteRtdbSessionData });
|
|
2414
2407
|
const cleanup = [
|
|
2415
|
-
|
|
2416
|
-
|
|
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
|
+
},
|
|
2417
2416
|
];
|
|
2418
|
-
if (sessionId)
|
|
2419
|
-
cleanup.push(
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
.
|
|
2426
|
-
|
|
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({
|
|
2427
2459
|
eventId,
|
|
2428
2460
|
sessionId: sessionId || null,
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2461
|
+
deleteRtdbSessionData,
|
|
2462
|
+
attempts,
|
|
2463
|
+
firestorePointer,
|
|
2464
|
+
rtdbPointer,
|
|
2465
|
+
verificationError,
|
|
2466
|
+
}));
|
|
2433
2467
|
});
|
|
2434
2468
|
server.registerTool("oko_delete_session", {
|
|
2435
2469
|
title: "Delete analytics session",
|
|
@@ -2937,7 +2971,7 @@ export function registerTools(server) {
|
|
|
2937
2971
|
"eventId: optional event; when provided, include channels explicitly shared to that event's workspace.",
|
|
2938
2972
|
"includeWorkspaceChannels: deprecated compatibility field; eventId is required to resolve workspace sharing safely.",
|
|
2939
2973
|
],
|
|
2940
|
-
returns: "canonical channels with
|
|
2974
|
+
returns: "canonical channels with identity, workspace sharing, ownership, subscriber count, and connection health.",
|
|
2941
2975
|
examples: ["Use before oko_publish_telegram_post so the user can choose a channel by title instead of numeric id."],
|
|
2942
2976
|
}),
|
|
2943
2977
|
inputSchema: {
|
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",
|