@okostream/mcp 0.9.3 → 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 +217 -19
- package/dist/utils.js +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
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;
|
|
@@ -1343,7 +1372,7 @@ export function registerTools(server) {
|
|
|
1343
1372
|
],
|
|
1344
1373
|
returns: "uid, updated field names, and the resulting profile snapshot.",
|
|
1345
1374
|
examples: ["Use when the organizer asks to change their visible OKO name or timezone."],
|
|
1346
|
-
notes: ["
|
|
1375
|
+
notes: ["Edits the organizer profile that owns this key. Requires workspace:write: the key holder is not necessarily the organizer, so a read-only key must not rewrite the owner's name, avatar or timezone."],
|
|
1347
1376
|
}),
|
|
1348
1377
|
inputSchema: {
|
|
1349
1378
|
displayName: profileDisplayNameSchema.optional().describe("Display name, max 40 chars. Null clears it."),
|
|
@@ -1352,7 +1381,7 @@ export function registerTools(server) {
|
|
|
1352
1381
|
outputSchema: profileOutputSchema,
|
|
1353
1382
|
annotations: idempotentWriteTool,
|
|
1354
1383
|
}, async ({ displayName, timezone }) => {
|
|
1355
|
-
const { uid } = await authed([]);
|
|
1384
|
+
const { uid } = await authed(["workspace:write"]);
|
|
1356
1385
|
const patch = { updatedAt: serverTimestamp() };
|
|
1357
1386
|
if (displayName !== undefined)
|
|
1358
1387
|
patch.displayName = displayName?.trim() || null;
|
|
@@ -1464,11 +1493,17 @@ export function registerTools(server) {
|
|
|
1464
1493
|
]);
|
|
1465
1494
|
const members = [];
|
|
1466
1495
|
for (const share of sharesSnap.docs) {
|
|
1467
|
-
|
|
1496
|
+
// Правила разрешают читать чужой профиль только владельцу платформы,
|
|
1497
|
+
// поэтому на любом рабочем пространстве с участниками это чтение
|
|
1498
|
+
// заканчивается отказом. Без обработки инструмент падал целиком вместо
|
|
1499
|
+
// того, чтобы вернуть состав команды без необязательных профилей.
|
|
1500
|
+
const profile = await getDoc(doc(firestore, "userProfiles", share.id))
|
|
1501
|
+
.then((snap) => (snap.exists() ? serialize(snap.data()) : null))
|
|
1502
|
+
.catch(() => null);
|
|
1468
1503
|
members.push({
|
|
1469
1504
|
uid: share.id,
|
|
1470
1505
|
...serialize(share.data()),
|
|
1471
|
-
profile
|
|
1506
|
+
profile,
|
|
1472
1507
|
});
|
|
1473
1508
|
}
|
|
1474
1509
|
const invites = invitesSnap.docs.map((invite) => ({ code: invite.id, ...serialize(invite.data()) }));
|
|
@@ -1597,24 +1632,40 @@ export function registerTools(server) {
|
|
|
1597
1632
|
summary: "List OKO events visible to the authenticated organizer.",
|
|
1598
1633
|
args: [
|
|
1599
1634
|
"limit: maximum events to return, default 30, max 100.",
|
|
1600
|
-
"cursor:
|
|
1635
|
+
"cursor: pagination cursor returned as nextCursor by the previous page; reuse it with the same filters.",
|
|
1601
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.",
|
|
1602
1639
|
],
|
|
1603
1640
|
returns: "events plus count, limit, hasMore/has_more, nextCursor/next_cursor.",
|
|
1604
|
-
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
|
+
],
|
|
1605
1645
|
}),
|
|
1606
1646
|
inputSchema: {
|
|
1607
1647
|
limit: limitField(),
|
|
1608
1648
|
cursor: cursorField,
|
|
1609
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."),
|
|
1610
1653
|
},
|
|
1611
1654
|
outputSchema: eventsOutputSchema,
|
|
1612
1655
|
annotations: readOnlyTool,
|
|
1613
1656
|
}, async (args) => {
|
|
1614
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";
|
|
1615
1662
|
const constraints = [
|
|
1616
|
-
|
|
1617
|
-
|
|
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"),
|
|
1618
1669
|
limit(args.limit + 1),
|
|
1619
1670
|
];
|
|
1620
1671
|
if (args.cursor)
|
|
@@ -1626,12 +1677,29 @@ export function registerTools(server) {
|
|
|
1626
1677
|
});
|
|
1627
1678
|
if (snap !== null) {
|
|
1628
1679
|
const { page, hasMore } = pickPage(snap.docs, args.limit);
|
|
1629
|
-
const
|
|
1630
|
-
const
|
|
1631
|
-
?
|
|
1632
|
-
:
|
|
1633
|
-
|
|
1634
|
-
|
|
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".' });
|
|
1635
1703
|
}
|
|
1636
1704
|
const legacyConstraints = [
|
|
1637
1705
|
where("accessibleBy", "array-contains", uid),
|
|
@@ -1643,14 +1711,26 @@ export function registerTools(server) {
|
|
|
1643
1711
|
}
|
|
1644
1712
|
const legacySnap = await getDocs(query(collection(firestore, "events"), ...legacyConstraints));
|
|
1645
1713
|
const { page, hasMore } = pickPage(legacySnap.docs, args.limit);
|
|
1714
|
+
const fromMs = fromTs ? fromTs.toMillis() : null;
|
|
1715
|
+
const toMs = toTs ? toTs.toMillis() : null;
|
|
1646
1716
|
const events = page
|
|
1647
1717
|
.map((item) => ({
|
|
1648
1718
|
id: item.id,
|
|
1649
1719
|
...serialize(item.data()),
|
|
1650
1720
|
}))
|
|
1651
|
-
.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)));
|
|
1652
1732
|
const nextCursor = hasMore ? timestampCursorFrom(page.at(-1)?.data().createdAt) : null;
|
|
1653
|
-
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".' });
|
|
1654
1734
|
});
|
|
1655
1735
|
server.registerTool("oko_get_event", {
|
|
1656
1736
|
title: "Get event",
|
|
@@ -1743,9 +1823,12 @@ export function registerTools(server) {
|
|
|
1743
1823
|
title: "Update event metadata",
|
|
1744
1824
|
description: toolDescription({
|
|
1745
1825
|
summary: "Update safe top-level event/setup metadata fields.",
|
|
1746
|
-
args: ["eventId plus any subset of title, videoUrl, eventDate, speakerName, description, pinned."],
|
|
1826
|
+
args: ["eventId plus any subset of title, videoUrl, eventDate, speakerName, description, pinned, sessionMode, durationMinutes."],
|
|
1747
1827
|
returns: "event id and the list of Firestore fields updated.",
|
|
1748
1828
|
examples: ["Use for title/date/speaker/video edits; use oko_configure_blocks for blocks and forms."],
|
|
1829
|
+
notes: [
|
|
1830
|
+
"sessionMode 'schedule' makes OKO start and end the analytics session automatically at eventDate for durationMinutes; 'manual' keeps the Start/End buttons and a Telegram reminder 10 minutes before eventDate. This is still an analytics session, not an event status.",
|
|
1831
|
+
],
|
|
1749
1832
|
}),
|
|
1750
1833
|
inputSchema: {
|
|
1751
1834
|
eventId: eventIdField,
|
|
@@ -1755,6 +1838,8 @@ export function registerTools(server) {
|
|
|
1755
1838
|
speakerName: z.string().max(160).nullable().optional().describe("New speaker display name, or null to clear."),
|
|
1756
1839
|
description: z.string().max(3000).nullable().optional().describe("New description, or null to clear."),
|
|
1757
1840
|
pinned: z.boolean().optional().describe("Whether this event is pinned in the dashboard."),
|
|
1841
|
+
sessionMode: z.enum(["manual", "schedule"]).optional().describe("Analytics session mode: manual Start/End buttons, or automatic start/end on schedule at eventDate."),
|
|
1842
|
+
durationMinutes: z.number().int().min(15).max(1440).optional().describe("Event duration in minutes (15-1440) used by schedule mode to end the session."),
|
|
1758
1843
|
},
|
|
1759
1844
|
outputSchema: updateOutputSchema,
|
|
1760
1845
|
annotations: writeTool,
|
|
@@ -1765,7 +1850,9 @@ export function registerTools(server) {
|
|
|
1765
1850
|
if (args.videoUrl !== undefined
|
|
1766
1851
|
|| args.eventDate !== undefined
|
|
1767
1852
|
|| args.speakerName !== undefined
|
|
1768
|
-
|| args.description !== undefined
|
|
1853
|
+
|| args.description !== undefined
|
|
1854
|
+
|| args.sessionMode !== undefined
|
|
1855
|
+
|| args.durationMinutes !== undefined) {
|
|
1769
1856
|
requiredScopes.add("setup:write");
|
|
1770
1857
|
}
|
|
1771
1858
|
if (requiredScopes.size === 0)
|
|
@@ -1788,6 +1875,10 @@ export function registerTools(server) {
|
|
|
1788
1875
|
patch["meta.description"] = args.description?.trim() || null;
|
|
1789
1876
|
if (args.pinned !== undefined)
|
|
1790
1877
|
patch.pinned = args.pinned;
|
|
1878
|
+
if (args.sessionMode !== undefined)
|
|
1879
|
+
patch.analyticsSessionMode = args.sessionMode;
|
|
1880
|
+
if (args.durationMinutes !== undefined)
|
|
1881
|
+
patch["meta.durationMinutes"] = args.durationMinutes;
|
|
1791
1882
|
await updateDoc(doc(firestore, "events", args.eventId), patch);
|
|
1792
1883
|
return textResult({ id: args.eventId, updated: Object.keys(patch) });
|
|
1793
1884
|
});
|
|
@@ -1803,6 +1894,10 @@ export function registerTools(server) {
|
|
|
1803
1894
|
],
|
|
1804
1895
|
returns: "event id and the list of Firestore fields updated.",
|
|
1805
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
|
+
],
|
|
1806
1901
|
}),
|
|
1807
1902
|
inputSchema: {
|
|
1808
1903
|
eventId: eventIdField,
|
|
@@ -1984,6 +2079,7 @@ export function registerTools(server) {
|
|
|
1984
2079
|
'Use kind="card" with cardId after the card item already exists.',
|
|
1985
2080
|
],
|
|
1986
2081
|
notes: [
|
|
2082
|
+
"Uploading an image does not enable its block: toggle the block independently via oko_configure_blocks, in either order.",
|
|
1987
2083
|
"The MCP server reads local paths on the organizer machine.",
|
|
1988
2084
|
"Remote URLs must be public HTTPS images, max 15MB; private/local hosts are blocked.",
|
|
1989
2085
|
"For card images, imageId is the cardId so OKO CF4 can update the correct item.",
|
|
@@ -2328,6 +2424,9 @@ export function registerTools(server) {
|
|
|
2328
2424
|
args: ["eventId: event to control.", "block: chat, banner, cta, cards, or placeholder.", "visible: true/false, or null to reset override.", "confirm: must be true."],
|
|
2329
2425
|
returns: "eventId, block, and effective override value.",
|
|
2330
2426
|
examples: ["Use visible=false to hide the CTA live; use visible=null to return to setup defaults."],
|
|
2427
|
+
notes: [
|
|
2428
|
+
"block=chat with visible=false PAUSES message input: viewers keep seeing the chat history but cannot send. It does not hide the chat panel.",
|
|
2429
|
+
],
|
|
2331
2430
|
}),
|
|
2332
2431
|
inputSchema: {
|
|
2333
2432
|
eventId: eventIdField,
|
|
@@ -2373,6 +2472,105 @@ export function registerTools(server) {
|
|
|
2373
2472
|
await set(ref(rtdb, `events/${eventId}/ctrl/refreshTick`), Date.now());
|
|
2374
2473
|
return textResult({ eventId, refreshed: true });
|
|
2375
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
|
+
});
|
|
2376
2574
|
server.registerTool("oko_start_session", {
|
|
2377
2575
|
title: "Start analytics session",
|
|
2378
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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@okostream/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": {
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"dotenv": "^17.2.3",
|
|
51
51
|
"firebase": "^12.16.0",
|
|
52
52
|
"ipaddr.js": "2.4.0",
|
|
53
|
-
"undici": "7.
|
|
53
|
+
"undici": "7.29.0",
|
|
54
54
|
"zod": "^3.25.76"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|