@okostream/mcp 0.12.10 → 0.12.12

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.
Files changed (2) hide show
  1. package/dist/server.js +128 -37
  2. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -116291,7 +116291,7 @@ function sanitizeScopes(scopes) {
116291
116291
  }
116292
116292
 
116293
116293
  // build/version.js
116294
- var OKO_MCP_VERSION = "0.12.10";
116294
+ var OKO_MCP_VERSION = "0.12.12";
116295
116295
 
116296
116296
  // build/auth.js
116297
116297
  var state = null;
@@ -116416,9 +116416,8 @@ async function exchangeApiKey() {
116416
116416
  }
116417
116417
  throw status === 401 ? new OkoAuthError("invalid_key", "OKO API key is invalid or revoked. Create a fresh key in the OKO MCP install center and replace OKO_API_KEY.") : new OkoAuthError("unavailable", [
116418
116418
  `OKO organizer authentication failed with HTTP ${status}.`,
116419
- serverRefusalNote(errorBody),
116420
- "Check your connection and try again."
116421
- ].filter(Boolean).join(" "));
116419
+ serverRefusalNote(errorBody) ?? "Check your connection and try again."
116420
+ ].join(" "));
116422
116421
  }
116423
116422
  const data = await response.json();
116424
116423
  if (typeof data.customToken !== "string") {
@@ -116631,6 +116630,20 @@ var EVENT_ACCESS_FIELD_BY_MODE = {
116631
116630
  moderate: "chatModerators",
116632
116631
  notify: "notificationSenders"
116633
116632
  };
116633
+ var EVENT_ACCESS_COMPOSITION_FIELDS = [
116634
+ ...Object.values(EVENT_ACCESS_FIELD_BY_MODE),
116635
+ "moderators",
116636
+ "participantUids"
116637
+ ];
116638
+ function eventDocumentForOutput(data) {
116639
+ const projected = {};
116640
+ for (const [key, value] of Object.entries(data)) {
116641
+ if (EVENT_ACCESS_COMPOSITION_FIELDS.includes(key))
116642
+ continue;
116643
+ projected[key] = value;
116644
+ }
116645
+ return projected;
116646
+ }
116634
116647
  function grantAllowsMode(data, mode) {
116635
116648
  const field2 = EVENT_ACCESS_FIELD_BY_MODE[mode];
116636
116649
  return asRecord(data.capabilities)[field2] === true;
@@ -117195,16 +117208,43 @@ async function updateCardImageInTransaction(eventId, cardId, patch) {
117195
117208
  });
117196
117209
  });
117197
117210
  }
117198
- async function writeEventPatchWithCards(eventId, basePatch, buildItems) {
117211
+ function descendPath(root, segments) {
117212
+ let current = root;
117213
+ for (const segment of segments) {
117214
+ if (current === null || typeof current !== "object")
117215
+ return void 0;
117216
+ current = current[segment];
117217
+ }
117218
+ return current;
117219
+ }
117220
+ function valueAfterPatch(fresh, patch, path) {
117221
+ if (Object.prototype.hasOwnProperty.call(patch, path))
117222
+ return patch[path];
117223
+ const segments = path.split(".");
117224
+ for (let cut = segments.length - 1; cut > 0; cut--) {
117225
+ const ancestor = segments.slice(0, cut).join(".");
117226
+ if (!Object.prototype.hasOwnProperty.call(patch, ancestor))
117227
+ continue;
117228
+ return descendPath(patch[ancestor], segments.slice(cut));
117229
+ }
117230
+ return descendPath(fresh, segments);
117231
+ }
117232
+ function scheduleMissesDate(sessionMode, eventDate) {
117233
+ return sessionMode === "schedule" && !eventDate;
117234
+ }
117235
+ async function writeEventPatchWithCards(eventId, basePatch, buildItems, validate = null) {
117199
117236
  const eventRef = doc(firestore, "events", eventId);
117200
117237
  await runTransaction2(firestore, async (transaction) => {
117201
117238
  const snap = await transaction.get(eventRef);
117202
117239
  if (!snap.exists())
117203
117240
  throw new Error("Event not found.");
117241
+ const fresh = snap.data();
117204
117242
  const patch = { ...basePatch };
117205
117243
  if (buildItems) {
117206
- patch["blocks.cards.items"] = buildItems(snap.data());
117244
+ patch["blocks.cards.items"] = buildItems(fresh);
117207
117245
  }
117246
+ if (validate)
117247
+ validate(fresh, patch);
117208
117248
  if (Object.keys(patch).length === 0)
117209
117249
  return;
117210
117250
  transaction.update(eventRef, patch);
@@ -118425,6 +118465,9 @@ function validateEventSetup(eventId, event) {
118425
118465
  } else if (eventDateMs < Date.now() - 60 * 60 * 1e3) {
118426
118466
  add(warnings, "date_in_past", "meta.eventDate", "Configured webinar date appears to be in the past.");
118427
118467
  }
118468
+ if (scheduleMissesDate(event.analyticsSessionMode, meta2.eventDate)) {
118469
+ add(errors, "schedule_missing_date", "analyticsSessionMode", "Session mode is 'schedule' but meta.eventDate is empty, so OKO can never start or end the session automatically and the reminder is never sent. Set eventDate, or switch sessionMode to 'manual' \u2014 oko_update_event accepts both fields in one call.");
118470
+ }
118428
118471
  const duplicateFormIds = /* @__PURE__ */ new Set();
118429
118472
  const seenFormIds = /* @__PURE__ */ new Set();
118430
118473
  forms.forEach((form, index) => {
@@ -118540,6 +118583,55 @@ function telegramBlocksPatch(args) {
118540
118583
  "telegram.channelIds": args.telegramChannelIds
118541
118584
  };
118542
118585
  }
118586
+ function assertScheduleHasDate(fresh, patch) {
118587
+ const missing = scheduleMissesDate(valueAfterPatch(fresh, patch, "analyticsSessionMode"), valueAfterPatch(fresh, patch, "meta.eventDate"));
118588
+ if (!missing)
118589
+ return;
118590
+ throw new Error("sessionMode 'schedule' requires meta.eventDate: without a date OKO can never start the session automatically. Set eventDate in the same call, or switch sessionMode to 'manual' in the same call to clear the date.");
118591
+ }
118592
+ var BLOCK_FORM_BINDING_FIELDS = [
118593
+ "blocks.promoBanner.formId",
118594
+ "blocks.primaryCta.formId"
118595
+ ];
118596
+ function formIdSet(formsValue) {
118597
+ return new Set((Array.isArray(formsValue) ? formsValue : []).map((form) => typeof form?.id === "string" ? form.id : null).filter((id) => Boolean(id)));
118598
+ }
118599
+ function formBindingsAfterPatch(fresh, patch) {
118600
+ const bindings = [];
118601
+ for (const field2 of BLOCK_FORM_BINDING_FIELDS) {
118602
+ const value = valueAfterPatch(fresh, patch, field2);
118603
+ if (typeof value === "string" && value)
118604
+ bindings.push({ field: field2, formId: value });
118605
+ }
118606
+ const cards = valueAfterPatch(fresh, patch, "blocks.cards.items");
118607
+ if (Array.isArray(cards)) {
118608
+ cards.forEach((card, index) => {
118609
+ const formId = card && typeof card === "object" && !Array.isArray(card) ? card.formId : null;
118610
+ if (typeof formId === "string" && formId) {
118611
+ bindings.push({ field: `blocks.cards.items[${index}].formId`, formId });
118612
+ }
118613
+ });
118614
+ }
118615
+ return bindings;
118616
+ }
118617
+ function assertFormBindingsExist(fresh, patch) {
118618
+ const formsAfter = formIdSet(valueAfterPatch(fresh, patch, "forms"));
118619
+ for (const field2 of BLOCK_FORM_BINDING_FIELDS) {
118620
+ if (!Object.prototype.hasOwnProperty.call(patch, field2))
118621
+ continue;
118622
+ const value = patch[field2];
118623
+ if (typeof value === "string" && value && !formsAfter.has(value)) {
118624
+ throw new Error(`${field2}: form "${value}" not found in event.forms. Create the form in the same call (or beforehand) \u2014 a button bound to a missing form does nothing for viewers.`);
118625
+ }
118626
+ }
118627
+ const formsBefore = formIdSet(valueAfterPatch(fresh, {}, "forms"));
118628
+ const orphaned = formBindingsAfterPatch(fresh, patch).filter((binding) => !formsAfter.has(binding.formId) && formsBefore.has(binding.formId));
118629
+ if (orphaned.length === 0)
118630
+ return;
118631
+ const listed = orphaned.map((binding) => `${binding.field} -> "${binding.formId}"`).join(", ");
118632
+ const cardsAffected = orphaned.some((binding) => binding.field.startsWith("blocks.cards."));
118633
+ throw new Error(`This call removes a form that buttons still point at: ${listed}. A button bound to a missing form does nothing for viewers, so the removal is refused while something still points at it. Unbind them first: pass promoBanner: {formId: null} and/or primaryCta: {formId: null} in this same call` + (cardsAffected ? ", and clear the card bindings with oko_configure_cards before removing the form." : "."));
118634
+ }
118543
118635
  function configureBlocksScopes(args) {
118544
118636
  return Object.keys(telegramBlocksPatch(args)).length > 0 ? ["setup:write", "telegram:write"] : ["setup:write"];
118545
118637
  }
@@ -118552,7 +118644,7 @@ function registerEventTools(server2) {
118552
118644
  "limit: maximum events to return, default 30, max 100.",
118553
118645
  "cursor: pagination cursor returned as nextCursor by the previous page; reuse it with the same filters.",
118554
118646
  "workspaceId: optional workspace filter.",
118555
- 'detail: "compact" (default) returns the light card projection; "full" returns complete event documents.',
118647
+ 'detail: "compact" (default) returns the light card projection; "full" returns event documents without the access composition, same shape as oko_get_event.',
118556
118648
  "from/to: optional ISO timestamps bounding meta.eventDate; sorting switches to eventDate descending and undated events are excluded."
118557
118649
  ],
118558
118650
  returns: "events plus count, limit, hasMore/has_more, nextCursor/next_cursor.",
@@ -118600,7 +118692,10 @@ function registerEventTools(server2) {
118600
118692
  let events;
118601
118693
  if (args.detail === "full") {
118602
118694
  const eventDocuments = await Promise.all(pageItems.map((item) => getDoc(doc(firestore, "events", item.id))));
118603
- events = eventDocuments.flatMap((item) => item.exists() ? [{ id: item.id, ...serialize(item.data()) }] : []);
118695
+ events = eventDocuments.flatMap((item) => item.exists() ? [{
118696
+ id: item.id,
118697
+ ...serialize(eventDocumentForOutput(item.data()))
118698
+ }] : []);
118604
118699
  } else {
118605
118700
  events = pageItems.map((item) => {
118606
118701
  const data = serialize(item.data());
@@ -118620,7 +118715,7 @@ function registerEventTools(server2) {
118620
118715
  description: toolDescription({
118621
118716
  summary: "Read one OKO event visible to the organizer.",
118622
118717
  args: ["eventId: OKO event document id."],
118623
- returns: "full event document fields plus id and public event URL.",
118718
+ returns: "event document fields plus id and public event URL. The event's access composition (the historical uid arrays) is never returned; ask oko_list_workspace_access for who has access.",
118624
118719
  examples: ["Use after oko_create_webinar to verify setup and return the event URL to the user."],
118625
118720
  notes: [
118626
118721
  "blocks.chat.mode and blocks.chat.visibility are both legacy: the event page reads neither \u2014 the chat panel exists exactly when blocks.chat.enabled is true, and sending is opened or closed live with oko_set_chat_mode. Ignore both fields."
@@ -118632,7 +118727,11 @@ function registerEventTools(server2) {
118632
118727
  }, async ({ eventId }) => {
118633
118728
  const { uid } = await authed(["events:read"]);
118634
118729
  const data = await readEvent(eventId, uid, "read");
118635
- return textResult({ id: eventId, ...serialize(data), ...eventLinks(eventId) });
118730
+ return textResult({
118731
+ id: eventId,
118732
+ ...serialize(eventDocumentForOutput(data)),
118733
+ ...eventLinks(eventId)
118734
+ });
118636
118735
  });
118637
118736
  server2.registerTool("oko_validate_event_setup", {
118638
118737
  title: "Validate event setup",
@@ -118719,7 +118818,8 @@ function registerEventTools(server2) {
118719
118818
  returns: "event id and the list of Firestore fields updated.",
118720
118819
  examples: ["Use for title/date/speaker/video edits; use oko_configure_blocks for blocks and forms."],
118721
118820
  notes: [
118722
- "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."
118821
+ "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.",
118822
+ "sessionMode 'schedule' and eventDate travel together: setting the mode without a date, or clearing the date while the mode stays, is refused. Change both in one call."
118723
118823
  ]
118724
118824
  }),
118725
118825
  inputSchema: {
@@ -118745,13 +118845,7 @@ function registerEventTools(server2) {
118745
118845
  if (requiredScopes.size === 0)
118746
118846
  throw new Error("Provide at least one field to update.");
118747
118847
  const { uid } = await authed([...requiredScopes]);
118748
- const current = await readEvent(args.eventId, uid, "edit");
118749
- if (args.sessionMode === "schedule") {
118750
- const dateAfterPatch = args.eventDate !== void 0 ? args.eventDate : current.meta?.eventDate ?? null;
118751
- if (!dateAfterPatch) {
118752
- throw new Error("sessionMode 'schedule' requires meta.eventDate: without a date OKO can never start the session automatically. Set eventDate in the same call, or keep sessionMode 'manual'.");
118753
- }
118754
- }
118848
+ await readEvent(args.eventId, uid, "edit");
118755
118849
  const patch = {};
118756
118850
  if (args.title !== void 0)
118757
118851
  patch.title = args.title.trim();
@@ -118776,7 +118870,7 @@ function registerEventTools(server2) {
118776
118870
  patch.analyticsSessionMode = args.sessionMode;
118777
118871
  if (args.durationMinutes !== void 0)
118778
118872
  patch["meta.durationMinutes"] = args.durationMinutes;
118779
- await updateDoc(doc(firestore, "events", args.eventId), patch);
118873
+ await writeEventPatchWithCards(args.eventId, patch, null, assertScheduleHasDate);
118780
118874
  return textResult({ id: args.eventId, updated: Object.keys(patch) });
118781
118875
  });
118782
118876
  server2.registerTool("oko_configure_blocks", {
@@ -118787,7 +118881,7 @@ function registerEventTools(server2) {
118787
118881
  args: [
118788
118882
  "eventId: event to edit.",
118789
118883
  "chat/promoBanner/primaryCta/videoPlaceholder: optional block patches.",
118790
- "forms: replaces configured lead forms when provided.",
118884
+ "forms: replaces configured lead forms when provided; removing a form that a button still points at is refused.",
118791
118885
  "telegramChannelIds: replaces configured Telegram channels when provided; needs the telegram:write scope AND the notification permission on the event, and is refused before anything is written when either is missing."
118792
118886
  ],
118793
118887
  returns: "event id and the list of Firestore fields updated.",
@@ -118797,7 +118891,8 @@ function registerEventTools(server2) {
118797
118891
  "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.",
118798
118892
  'videoPlaceholder.enabled only allows the placeholder; viewers see it after oko_set_block_visibility with block "placeholder" and visible true (a live override, stored outside the event document). Chat, CTA, banner and cards appear as soon as they are enabled here.',
118799
118893
  "Setup toggle vs live override: enabled=false removes a block from the page for everyone and persists (its uploaded image stays); oko_set_block_visibility hides or shows an enabled block temporarily and is the tool for the live show. Uploading an image never enables a block.",
118800
- "telegramChannelIds is the only field here that is not a page setting: it decides which channel carries this event's messages, and the automatic reminder goes out under the event owner's name. Omit the field to configure blocks with setup:write alone; pass it only with a key that holds telegram:write and for an organizer who may send notifications for this event."
118894
+ "telegramChannelIds is the only field here that is not a page setting: it decides which channel carries this event's messages, and the automatic reminder goes out under the event owner's name. Omit the field to configure blocks with setup:write alone; pass it only with a key that holds telegram:write and for an organizer who may send notifications for this event.",
118895
+ "forms and the buttons bound to them travel together in both directions: binding a button to a form that does not exist is refused, and so is replacing forms in a way that removes a form the banner, the CTA or a card button still points at. Unbind in the same call with promoBanner/primaryCta formId:null; card bindings are cleared with oko_configure_cards first. A binding that was already broken before the call is not touched and does not block it."
118801
118896
  ]
118802
118897
  }),
118803
118898
  inputSchema: {
@@ -118874,17 +118969,7 @@ function registerEventTools(server2) {
118874
118969
  }))
118875
118970
  }));
118876
118971
  Object.assign(patch, telegramPatch);
118877
- const formsAfterPatch = patch.forms ?? current.forms;
118878
- const knownFormIds = new Set((Array.isArray(formsAfterPatch) ? formsAfterPatch : []).map((form) => typeof form?.id === "string" ? form.id : null).filter((id) => Boolean(id)));
118879
- for (const [field2, value] of [
118880
- ["blocks.promoBanner.formId", patch["blocks.promoBanner.formId"]],
118881
- ["blocks.primaryCta.formId", patch["blocks.primaryCta.formId"]]
118882
- ]) {
118883
- if (typeof value === "string" && value && !knownFormIds.has(value)) {
118884
- throw new Error(`${field2}: form "${value}" not found in event.forms. Create the form in the same call (or beforehand) \u2014 a button bound to a missing form does nothing for viewers.`);
118885
- }
118886
- }
118887
- await updateDoc(doc(firestore, "events", args.eventId), patch);
118972
+ await writeEventPatchWithCards(args.eventId, patch, null, assertFormBindingsExist);
118888
118973
  const hints = [];
118889
118974
  if (args.chat?.visibility !== void 0) {
118890
118975
  hints.push("blocks.chat.visibility was stored but the event page does not read it: the chat panel exists exactly when blocks.chat.enabled is true, and sending is opened or closed live with oko_set_chat_mode. Drop the field.");
@@ -119759,6 +119844,7 @@ function registerLiveTools(server2) {
119759
119844
  examples: ["Use visible=false to hide the CTA live; use visible=null to return to setup defaults."],
119760
119845
  notes: [
119761
119846
  "block=chat with visible=false PAUSES message input: viewers keep seeing the chat history but cannot send. It does not hide the chat panel.",
119847
+ "Chat is the only block whose override the SERVER enforces: it is written as the chat mode and mirrored into the chat security rules, so a paused chat refuses messages from every client. The other four overrides are page presentation only \u2014 they change what viewers see, not what the server accepts.",
119762
119848
  "Live overrides sit on top of setup: a block disabled in setup (oko_configure_blocks enabled=false) stays absent whatever the override; visible=null returns an enabled block to its setup default. Use overrides for the live show, setup toggles for permanent changes."
119763
119849
  ]
119764
119850
  }),
@@ -119813,10 +119899,10 @@ function registerLiveTools(server2) {
119813
119899
  server2.registerTool("oko_refresh_viewers", {
119814
119900
  title: "Refresh viewers",
119815
119901
  description: toolDescription({
119816
- summary: "Trigger a live viewer refresh tick for an event.",
119902
+ summary: "Refresh what viewers see: recount the online viewers and make open event pages re-read their config.",
119817
119903
  args: ["eventId: event whose viewers should refresh.", "confirm: must be true."],
119818
- returns: "eventId and refreshed=true.",
119819
- examples: ["Use after a setup/live change if viewers should reload state immediately."]
119904
+ returns: "eventId, refreshed=true, and online \u2014 the recounted number of viewers present.",
119905
+ examples: ["Use after a setup/live change if viewers should reload state immediately.", "Use when the online counter looks wrong: it is recounted from the actual presence nodes."]
119820
119906
  }),
119821
119907
  inputSchema: { eventId: eventIdField, confirm: external_exports.boolean().optional().describe("Required true to refresh connected viewers.") },
119822
119908
  outputSchema: basicEventOutputSchema,
@@ -119825,8 +119911,13 @@ function registerLiveTools(server2) {
119825
119911
  const { uid } = await authed(["live:control"]);
119826
119912
  await readEvent(eventId, uid, "live");
119827
119913
  requireConfirm(confirm, "oko_refresh_viewers", { eventId });
119828
- await runTransaction(ref(rtdb, `events/${eventId}/ctrl/refreshTick`), (current) => (current ?? 0) + 1);
119829
- return textResult({ eventId, refreshed: true });
119914
+ const resyncPresence = httpsCallable(functions, "resyncPresence");
119915
+ const [resynced] = await Promise.all([
119916
+ resyncPresence({ eventId }),
119917
+ runTransaction(ref(rtdb, `events/${eventId}/ctrl/refreshTick`), (current) => (current ?? 0) + 1)
119918
+ ]);
119919
+ const online = typeof resynced.data?.online === "number" ? resynced.data.online : null;
119920
+ return textResult({ eventId, refreshed: true, online });
119830
119921
  });
119831
119922
  server2.registerTool("oko_set_live_badge", {
119832
119923
  title: "Set live badge",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.12.10",
3
+ "version": "0.12.12",
4
4
  "description": "Control OKO Stream webinars from Claude, Codex, and other MCP clients.",
5
5
  "license": "UNLICENSED",
6
6
  "author": {