@okostream/mcp 0.10.0 → 0.11.0
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/tools.js +191 -13
- package/dist/utils.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -940,7 +940,7 @@ function validateEventSetup(eventId, event) {
|
|
|
940
940
|
add(errors, "title_missing", "title", "Event title is required.");
|
|
941
941
|
const video = asRecord(event.video);
|
|
942
942
|
if (!safeString(video.embedUrl).trim()) {
|
|
943
|
-
add(warnings, "video_missing", "video.embedUrl", "No normalized video URL is configured.");
|
|
943
|
+
add(warnings, "video_missing", "video.embedUrl", "No normalized video URL is configured. This warning is for the organizer only and never appears as page text above other blocks. Without a video URL the player slot shows a small 'video not configured' notice, but when the video placeholder block is enabled and switched on, the placeholder fully covers that slot and viewers see only the placeholder.");
|
|
944
944
|
}
|
|
945
945
|
const meta = asRecord(event.meta);
|
|
946
946
|
const eventDateMs = toMillis(meta.eventDate);
|
|
@@ -1156,6 +1156,35 @@ function timestampCursorFrom(value) {
|
|
|
1156
1156
|
const millis = toMillis(value);
|
|
1157
1157
|
return millis == null ? null : String(millis);
|
|
1158
1158
|
}
|
|
1159
|
+
function parseIsoBound(value, label) {
|
|
1160
|
+
if (value === undefined)
|
|
1161
|
+
return null;
|
|
1162
|
+
const parsed = Date.parse(value);
|
|
1163
|
+
if (!Number.isFinite(parsed)) {
|
|
1164
|
+
throw new Error(`Invalid ${label}: expected an ISO timestamp such as 2026-08-28T00:00:00+03:00.`);
|
|
1165
|
+
}
|
|
1166
|
+
return Timestamp.fromMillis(parsed);
|
|
1167
|
+
}
|
|
1168
|
+
// Компактная карточка события из полного (сериализованного) документа —
|
|
1169
|
+
// той же формы, что grant-проекция в eventAccess.
|
|
1170
|
+
function compactEventFromFullDoc(event) {
|
|
1171
|
+
const meta = (event.meta && typeof event.meta === "object" ? event.meta : {});
|
|
1172
|
+
const telegram = (event.telegram && typeof event.telegram === "object" ? event.telegram : {});
|
|
1173
|
+
return {
|
|
1174
|
+
id: event.id,
|
|
1175
|
+
ownerId: event.ownerId ?? null,
|
|
1176
|
+
workspaceId: event.workspaceId ?? null,
|
|
1177
|
+
title: event.title ?? null,
|
|
1178
|
+
createdAt: event.createdAt ?? null,
|
|
1179
|
+
eventDate: meta.eventDate ?? null,
|
|
1180
|
+
speakerName: meta.speakerName ?? null,
|
|
1181
|
+
pinned: event.pinned === true,
|
|
1182
|
+
telegramEnabled: telegram.enabled === true,
|
|
1183
|
+
trashed: Boolean(event.trashedAt),
|
|
1184
|
+
trashedAt: event.trashedAt ?? null,
|
|
1185
|
+
activeAnalyticsSessionId: event.activeAnalyticsSessionId ?? null,
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1159
1188
|
function compactAnalyticsSession(raw, options) {
|
|
1160
1189
|
const session = serialize(raw);
|
|
1161
1190
|
const timeline = Array.isArray(session.timeline) ? session.timeline : null;
|
|
@@ -1603,24 +1632,40 @@ export function registerTools(server) {
|
|
|
1603
1632
|
summary: "List OKO events visible to the authenticated organizer.",
|
|
1604
1633
|
args: [
|
|
1605
1634
|
"limit: maximum events to return, default 30, max 100.",
|
|
1606
|
-
"cursor:
|
|
1635
|
+
"cursor: pagination cursor returned as nextCursor by the previous page; reuse it with the same filters.",
|
|
1607
1636
|
"workspaceId: optional workspace filter.",
|
|
1637
|
+
'detail: "compact" (default) returns the light card projection; "full" returns complete event documents.',
|
|
1638
|
+
"from/to: optional ISO timestamps bounding meta.eventDate; sorting switches to eventDate descending and undated events are excluded.",
|
|
1608
1639
|
],
|
|
1609
1640
|
returns: "events plus count, limit, hasMore/has_more, nextCursor/next_cursor.",
|
|
1610
|
-
examples: [
|
|
1641
|
+
examples: [
|
|
1642
|
+
"Use limit=10 first; if hasMore is true, call again with cursor=nextCursor.",
|
|
1643
|
+
'For "today\'s webinar" pass from/to covering the day; use detail="full" only when block contents are needed.',
|
|
1644
|
+
],
|
|
1611
1645
|
}),
|
|
1612
1646
|
inputSchema: {
|
|
1613
1647
|
limit: limitField(),
|
|
1614
1648
|
cursor: cursorField,
|
|
1615
1649
|
workspaceId: z.string().optional().describe("Optional workspace id to filter returned events."),
|
|
1650
|
+
detail: z.enum(["compact", "full"]).default("compact").describe('Response shape: "compact" card projection (default) or "full" event documents.'),
|
|
1651
|
+
from: z.string().optional().describe("Include only events with meta.eventDate at or after this ISO timestamp."),
|
|
1652
|
+
to: z.string().optional().describe("Include only events with meta.eventDate at or before this ISO timestamp."),
|
|
1616
1653
|
},
|
|
1617
1654
|
outputSchema: eventsOutputSchema,
|
|
1618
1655
|
annotations: readOnlyTool,
|
|
1619
1656
|
}, async (args) => {
|
|
1620
1657
|
const { uid } = await authed(["events:read"]);
|
|
1658
|
+
const fromTs = parseIsoBound(args.from, "from");
|
|
1659
|
+
const toTs = parseIsoBound(args.to, "to");
|
|
1660
|
+
const dateFiltered = fromTs !== null || toTs !== null;
|
|
1661
|
+
const orderField = dateFiltered ? "eventDate" : "createdAt";
|
|
1621
1662
|
const constraints = [
|
|
1622
|
-
|
|
1623
|
-
|
|
1663
|
+
// При датном фильтре workspaceId отсеивается по загруженной странице:
|
|
1664
|
+
// составного индекса grants(workspaceId+eventDate) нет намеренно.
|
|
1665
|
+
...(!dateFiltered && args.workspaceId ? [where("workspaceId", "==", args.workspaceId)] : []),
|
|
1666
|
+
...(fromTs ? [where("eventDate", ">=", fromTs)] : []),
|
|
1667
|
+
...(toTs ? [where("eventDate", "<=", toTs)] : []),
|
|
1668
|
+
orderBy(orderField, "desc"),
|
|
1624
1669
|
limit(args.limit + 1),
|
|
1625
1670
|
];
|
|
1626
1671
|
if (args.cursor)
|
|
@@ -1632,12 +1677,29 @@ export function registerTools(server) {
|
|
|
1632
1677
|
});
|
|
1633
1678
|
if (snap !== null) {
|
|
1634
1679
|
const { page, hasMore } = pickPage(snap.docs, args.limit);
|
|
1635
|
-
const
|
|
1636
|
-
const
|
|
1637
|
-
?
|
|
1638
|
-
:
|
|
1639
|
-
|
|
1640
|
-
|
|
1680
|
+
const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data()[orderField]) : null;
|
|
1681
|
+
const pageItems = dateFiltered && args.workspaceId
|
|
1682
|
+
? page.filter((item) => item.data().workspaceId === args.workspaceId)
|
|
1683
|
+
: page;
|
|
1684
|
+
let events;
|
|
1685
|
+
if (args.detail === "full") {
|
|
1686
|
+
const eventDocuments = await Promise.all(pageItems.map((item) => (getDoc(doc(firestore, "events", item.id)))));
|
|
1687
|
+
events = eventDocuments.flatMap((item) => (item.exists()
|
|
1688
|
+
? [{ id: item.id, ...serialize(item.data()) }]
|
|
1689
|
+
: []));
|
|
1690
|
+
}
|
|
1691
|
+
else {
|
|
1692
|
+
// Компактный режим отдаёт grant-проекцию как есть — без чтения
|
|
1693
|
+
// полных документов событий.
|
|
1694
|
+
events = pageItems.map((item) => {
|
|
1695
|
+
const data = serialize(item.data());
|
|
1696
|
+
delete data.schemaVersion;
|
|
1697
|
+
delete data.uid;
|
|
1698
|
+
delete data.eventId;
|
|
1699
|
+
return { id: item.id, ...data };
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
return textResult({ events, ...cursorMeta(events.length, args.limit, hasMore, nextCursor) }, { truncationHint: 'Use a smaller limit, cursor pagination, or detail="compact".' });
|
|
1641
1703
|
}
|
|
1642
1704
|
const legacyConstraints = [
|
|
1643
1705
|
where("accessibleBy", "array-contains", uid),
|
|
@@ -1649,14 +1711,26 @@ export function registerTools(server) {
|
|
|
1649
1711
|
}
|
|
1650
1712
|
const legacySnap = await getDocs(query(collection(firestore, "events"), ...legacyConstraints));
|
|
1651
1713
|
const { page, hasMore } = pickPage(legacySnap.docs, args.limit);
|
|
1714
|
+
const fromMs = fromTs ? fromTs.toMillis() : null;
|
|
1715
|
+
const toMs = toTs ? toTs.toMillis() : null;
|
|
1652
1716
|
const events = page
|
|
1653
1717
|
.map((item) => ({
|
|
1654
1718
|
id: item.id,
|
|
1655
1719
|
...serialize(item.data()),
|
|
1656
1720
|
}))
|
|
1657
|
-
.filter((event) => !args.workspaceId || event.workspaceId === args.workspaceId)
|
|
1721
|
+
.filter((event) => !args.workspaceId || event.workspaceId === args.workspaceId)
|
|
1722
|
+
.filter((event) => {
|
|
1723
|
+
if (fromMs === null && toMs === null)
|
|
1724
|
+
return true;
|
|
1725
|
+
const meta = (event.meta && typeof event.meta === "object" ? event.meta : {});
|
|
1726
|
+
const ms = typeof meta.eventDate === "string" ? Date.parse(meta.eventDate) : NaN;
|
|
1727
|
+
if (!Number.isFinite(ms))
|
|
1728
|
+
return false;
|
|
1729
|
+
return (fromMs === null || ms >= fromMs) && (toMs === null || ms <= toMs);
|
|
1730
|
+
})
|
|
1731
|
+
.map((event) => (args.detail === "full" ? event : compactEventFromFullDoc(event)));
|
|
1658
1732
|
const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data().createdAt) : null;
|
|
1659
|
-
return textResult({ events, ...cursorMeta(events.length, args.limit, hasMore, nextCursor) });
|
|
1733
|
+
return textResult({ events, ...cursorMeta(events.length, args.limit, hasMore, nextCursor) }, { truncationHint: 'Use a smaller limit, cursor pagination, or detail="compact".' });
|
|
1660
1734
|
});
|
|
1661
1735
|
server.registerTool("oko_get_event", {
|
|
1662
1736
|
title: "Get event",
|
|
@@ -1820,6 +1894,10 @@ export function registerTools(server) {
|
|
|
1820
1894
|
],
|
|
1821
1895
|
returns: "event id and the list of Firestore fields updated.",
|
|
1822
1896
|
examples: ["Use after oko_create_webinar to set chat visibility, CTA/form binding, and other audience blocks."],
|
|
1897
|
+
notes: [
|
|
1898
|
+
"A block toggle and its image are independent: enabling a block here works with an image already uploaded via oko_upload_image, and uploading an image never enables the block by itself.",
|
|
1899
|
+
"promoBanner has no text field by design: the banner is image+link. blocks.promoBanner.text seen in event documents is legacy and is never rendered on the event page.",
|
|
1900
|
+
],
|
|
1823
1901
|
}),
|
|
1824
1902
|
inputSchema: {
|
|
1825
1903
|
eventId: eventIdField,
|
|
@@ -2001,6 +2079,7 @@ export function registerTools(server) {
|
|
|
2001
2079
|
'Use kind="card" with cardId after the card item already exists.',
|
|
2002
2080
|
],
|
|
2003
2081
|
notes: [
|
|
2082
|
+
"Uploading an image does not enable its block: toggle the block independently via oko_configure_blocks, in either order.",
|
|
2004
2083
|
"The MCP server reads local paths on the organizer machine.",
|
|
2005
2084
|
"Remote URLs must be public HTTPS images, max 15MB; private/local hosts are blocked.",
|
|
2006
2085
|
"For card images, imageId is the cardId so OKO CF4 can update the correct item.",
|
|
@@ -2393,6 +2472,105 @@ export function registerTools(server) {
|
|
|
2393
2472
|
await set(ref(rtdb, `events/${eventId}/ctrl/refreshTick`), Date.now());
|
|
2394
2473
|
return textResult({ eventId, refreshed: true });
|
|
2395
2474
|
});
|
|
2475
|
+
server.registerTool("oko_set_live_badge", {
|
|
2476
|
+
title: "Set live badge",
|
|
2477
|
+
description: toolDescription({
|
|
2478
|
+
summary: "Control the public 'Live' badge on the event page.",
|
|
2479
|
+
args: [
|
|
2480
|
+
"eventId: event to control.",
|
|
2481
|
+
"mode: session (follows the analytics session, product default), manual (badge shown now for durationMinutes), schedule (shown around meta.eventDate), hidden (never shown).",
|
|
2482
|
+
"durationMinutes: badge window for manual/schedule, 15-1440 minutes, default 120.",
|
|
2483
|
+
"confirm: must be true.",
|
|
2484
|
+
],
|
|
2485
|
+
returns: "eventId, mode, durationMinutes.",
|
|
2486
|
+
examples: ['Use mode="manual" during an unscheduled stream; return to mode="session" afterwards.'],
|
|
2487
|
+
notes: ["The badge is a page indicator only; it never starts or stops video or analytics."],
|
|
2488
|
+
}),
|
|
2489
|
+
inputSchema: {
|
|
2490
|
+
eventId: eventIdField,
|
|
2491
|
+
mode: z.enum(["session", "manual", "schedule", "hidden"]).describe("Live badge mode."),
|
|
2492
|
+
durationMinutes: z.number().int().min(15).max(1440).optional().describe("Badge window in minutes for manual/schedule modes; default 120."),
|
|
2493
|
+
confirm: z.boolean().optional().describe("Required true to change the public live badge."),
|
|
2494
|
+
},
|
|
2495
|
+
outputSchema: basicEventOutputSchema,
|
|
2496
|
+
annotations: idempotentWriteTool,
|
|
2497
|
+
}, async ({ eventId, mode, durationMinutes, confirm }) => {
|
|
2498
|
+
const { uid } = await authed(["live:control"]);
|
|
2499
|
+
await readEvent(eventId, uid, "live");
|
|
2500
|
+
requireConfirm(confirm, "oko_set_live_badge", { eventId, mode, durationMinutes: durationMinutes ?? null });
|
|
2501
|
+
const value = { mode, durationMinutes: durationMinutes ?? 120 };
|
|
2502
|
+
await set(ref(rtdb, `events/${eventId}/ctrl/liveBadge`), value);
|
|
2503
|
+
return textResult({ eventId, ...value });
|
|
2504
|
+
});
|
|
2505
|
+
server.registerTool("oko_set_chat_mode", {
|
|
2506
|
+
title: "Set chat mode",
|
|
2507
|
+
description: toolDescription({
|
|
2508
|
+
summary: "Set who can send chat messages on the public event page.",
|
|
2509
|
+
args: [
|
|
2510
|
+
"eventId: event to control.",
|
|
2511
|
+
"mode: public (anyone), auth (signed-in viewers only), owner (organizers only), off (input closed, history stays visible), or null to reset to the product default (public).",
|
|
2512
|
+
"confirm: must be true.",
|
|
2513
|
+
],
|
|
2514
|
+
returns: "eventId and the effective chat mode.",
|
|
2515
|
+
examples: ['Use mode="off" to pause a chat storm; use mode=null or "public" to reopen.'],
|
|
2516
|
+
notes: [
|
|
2517
|
+
'This is the full replacement for the legacy pause via oko_set_block_visibility(block="chat"): prefer oko_set_chat_mode.',
|
|
2518
|
+
"History always stays visible to viewers; mode only gates sending.",
|
|
2519
|
+
],
|
|
2520
|
+
}),
|
|
2521
|
+
inputSchema: {
|
|
2522
|
+
eventId: eventIdField,
|
|
2523
|
+
mode: z.enum(["public", "auth", "owner", "off"]).nullable().describe("Chat mode; null resets the override to the default."),
|
|
2524
|
+
confirm: z.boolean().optional().describe("Required true to change the live chat mode."),
|
|
2525
|
+
},
|
|
2526
|
+
outputSchema: basicEventOutputSchema,
|
|
2527
|
+
annotations: idempotentWriteTool,
|
|
2528
|
+
}, async ({ eventId, mode, confirm }) => {
|
|
2529
|
+
const { uid } = await authed(["live:control"]);
|
|
2530
|
+
await readEvent(eventId, uid, "live");
|
|
2531
|
+
requireConfirm(confirm, "oko_set_chat_mode", { eventId, mode });
|
|
2532
|
+
const target = ref(rtdb, `events/${eventId}/ctrl/chatMode`);
|
|
2533
|
+
if (mode === null)
|
|
2534
|
+
await remove(target);
|
|
2535
|
+
else
|
|
2536
|
+
await set(target, mode);
|
|
2537
|
+
return textResult({ eventId, mode });
|
|
2538
|
+
});
|
|
2539
|
+
server.registerTool("oko_force_viewer_reload", {
|
|
2540
|
+
title: "Force viewer reload",
|
|
2541
|
+
description: toolDescription({
|
|
2542
|
+
summary: "Force a hard page reload for every connected viewer (emergency recovery).",
|
|
2543
|
+
args: ["eventId: event whose viewer pages must reload.", "confirm: must be true."],
|
|
2544
|
+
returns: "eventId and reloaded=true.",
|
|
2545
|
+
examples: ["Use only when viewer pages are stuck (broken bundle or state) and oko_refresh_viewers did not help."],
|
|
2546
|
+
notes: [
|
|
2547
|
+
"Server enforces a 120-second cooldown per event; a repeated call returns the seconds left.",
|
|
2548
|
+
"Viewers reload with up to 4 seconds of jitter. Prefer oko_refresh_viewers for ordinary content updates.",
|
|
2549
|
+
],
|
|
2550
|
+
}),
|
|
2551
|
+
inputSchema: {
|
|
2552
|
+
eventId: eventIdField,
|
|
2553
|
+
confirm: z.boolean().optional().describe("Required true to hard-reload all viewer pages."),
|
|
2554
|
+
},
|
|
2555
|
+
outputSchema: basicEventOutputSchema,
|
|
2556
|
+
annotations: writeTool,
|
|
2557
|
+
}, async ({ eventId, confirm }) => {
|
|
2558
|
+
const { uid } = await authed(["live:control"]);
|
|
2559
|
+
await readEvent(eventId, uid, "live");
|
|
2560
|
+
requireConfirm(confirm, "oko_force_viewer_reload", { eventId });
|
|
2561
|
+
const forceReload = httpsCallable(functions, "forceViewerReload");
|
|
2562
|
+
try {
|
|
2563
|
+
await forceReload({ eventId });
|
|
2564
|
+
}
|
|
2565
|
+
catch (error) {
|
|
2566
|
+
const details = error.details;
|
|
2567
|
+
if (typeof details?.retryInSec === "number") {
|
|
2568
|
+
throw new Error(`Force reload is cooling down: retry in ${details.retryInSec} seconds.`);
|
|
2569
|
+
}
|
|
2570
|
+
throw error;
|
|
2571
|
+
}
|
|
2572
|
+
return textResult({ eventId, reloaded: true });
|
|
2573
|
+
});
|
|
2396
2574
|
server.registerTool("oko_start_session", {
|
|
2397
2575
|
title: "Start analytics session",
|
|
2398
2576
|
description: toolDescription({
|
package/dist/utils.js
CHANGED
|
@@ -52,7 +52,7 @@ export function textResult(data, options = {}) {
|
|
|
52
52
|
structuredContent = {
|
|
53
53
|
...compactForText(structuredContent),
|
|
54
54
|
truncated: true,
|
|
55
|
-
truncationMessage: `Response JSON exceeded ${maxChars} characters. ${options.truncationHint ?? "Use limit
|
|
55
|
+
truncationMessage: `Response JSON exceeded ${maxChars} characters. ${options.truncationHint ?? "Use a smaller limit or cursor pagination."}`,
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
return {
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/generateMcpVersion.mjs from mcp/package.json. Do not edit.
|
|
2
|
-
export const OKO_MCP_VERSION = "0.
|
|
2
|
+
export const OKO_MCP_VERSION = "0.11.0";
|