@okostream/mcp 0.12.11 → 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 +77 -19
  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.11";
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;
@@ -118576,17 +118589,48 @@ function assertScheduleHasDate(fresh, patch) {
118576
118589
  return;
118577
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.");
118578
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
+ }
118579
118617
  function assertFormBindingsExist(fresh, patch) {
118580
- const formsAfterPatch = valueAfterPatch(fresh, patch, "forms");
118581
- const knownFormIds = new Set((Array.isArray(formsAfterPatch) ? formsAfterPatch : []).map((form) => typeof form?.id === "string" ? form.id : null).filter((id) => Boolean(id)));
118582
- for (const field2 of ["blocks.promoBanner.formId", "blocks.primaryCta.formId"]) {
118618
+ const formsAfter = formIdSet(valueAfterPatch(fresh, patch, "forms"));
118619
+ for (const field2 of BLOCK_FORM_BINDING_FIELDS) {
118583
118620
  if (!Object.prototype.hasOwnProperty.call(patch, field2))
118584
118621
  continue;
118585
118622
  const value = patch[field2];
118586
- if (typeof value === "string" && value && !knownFormIds.has(value)) {
118623
+ if (typeof value === "string" && value && !formsAfter.has(value)) {
118587
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.`);
118588
118625
  }
118589
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." : "."));
118590
118634
  }
118591
118635
  function configureBlocksScopes(args) {
118592
118636
  return Object.keys(telegramBlocksPatch(args)).length > 0 ? ["setup:write", "telegram:write"] : ["setup:write"];
@@ -118600,7 +118644,7 @@ function registerEventTools(server2) {
118600
118644
  "limit: maximum events to return, default 30, max 100.",
118601
118645
  "cursor: pagination cursor returned as nextCursor by the previous page; reuse it with the same filters.",
118602
118646
  "workspaceId: optional workspace filter.",
118603
- '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.',
118604
118648
  "from/to: optional ISO timestamps bounding meta.eventDate; sorting switches to eventDate descending and undated events are excluded."
118605
118649
  ],
118606
118650
  returns: "events plus count, limit, hasMore/has_more, nextCursor/next_cursor.",
@@ -118648,7 +118692,10 @@ function registerEventTools(server2) {
118648
118692
  let events;
118649
118693
  if (args.detail === "full") {
118650
118694
  const eventDocuments = await Promise.all(pageItems.map((item) => getDoc(doc(firestore, "events", item.id))));
118651
- 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
+ }] : []);
118652
118699
  } else {
118653
118700
  events = pageItems.map((item) => {
118654
118701
  const data = serialize(item.data());
@@ -118668,7 +118715,7 @@ function registerEventTools(server2) {
118668
118715
  description: toolDescription({
118669
118716
  summary: "Read one OKO event visible to the organizer.",
118670
118717
  args: ["eventId: OKO event document id."],
118671
- 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.",
118672
118719
  examples: ["Use after oko_create_webinar to verify setup and return the event URL to the user."],
118673
118720
  notes: [
118674
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."
@@ -118680,7 +118727,11 @@ function registerEventTools(server2) {
118680
118727
  }, async ({ eventId }) => {
118681
118728
  const { uid } = await authed(["events:read"]);
118682
118729
  const data = await readEvent(eventId, uid, "read");
118683
- return textResult({ id: eventId, ...serialize(data), ...eventLinks(eventId) });
118730
+ return textResult({
118731
+ id: eventId,
118732
+ ...serialize(eventDocumentForOutput(data)),
118733
+ ...eventLinks(eventId)
118734
+ });
118684
118735
  });
118685
118736
  server2.registerTool("oko_validate_event_setup", {
118686
118737
  title: "Validate event setup",
@@ -118830,7 +118881,7 @@ function registerEventTools(server2) {
118830
118881
  args: [
118831
118882
  "eventId: event to edit.",
118832
118883
  "chat/promoBanner/primaryCta/videoPlaceholder: optional block patches.",
118833
- "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.",
118834
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."
118835
118886
  ],
118836
118887
  returns: "event id and the list of Firestore fields updated.",
@@ -118840,7 +118891,8 @@ function registerEventTools(server2) {
118840
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.",
118841
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.',
118842
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.",
118843
- "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."
118844
118896
  ]
118845
118897
  }),
118846
118898
  inputSchema: {
@@ -119792,6 +119844,7 @@ function registerLiveTools(server2) {
119792
119844
  examples: ["Use visible=false to hide the CTA live; use visible=null to return to setup defaults."],
119793
119845
  notes: [
119794
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.",
119795
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."
119796
119849
  ]
119797
119850
  }),
@@ -119846,10 +119899,10 @@ function registerLiveTools(server2) {
119846
119899
  server2.registerTool("oko_refresh_viewers", {
119847
119900
  title: "Refresh viewers",
119848
119901
  description: toolDescription({
119849
- 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.",
119850
119903
  args: ["eventId: event whose viewers should refresh.", "confirm: must be true."],
119851
- returns: "eventId and refreshed=true.",
119852
- 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."]
119853
119906
  }),
119854
119907
  inputSchema: { eventId: eventIdField, confirm: external_exports.boolean().optional().describe("Required true to refresh connected viewers.") },
119855
119908
  outputSchema: basicEventOutputSchema,
@@ -119858,8 +119911,13 @@ function registerLiveTools(server2) {
119858
119911
  const { uid } = await authed(["live:control"]);
119859
119912
  await readEvent(eventId, uid, "live");
119860
119913
  requireConfirm(confirm, "oko_refresh_viewers", { eventId });
119861
- await runTransaction(ref(rtdb, `events/${eventId}/ctrl/refreshTick`), (current) => (current ?? 0) + 1);
119862
- 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 });
119863
119921
  });
119864
119922
  server2.registerTool("oko_set_live_badge", {
119865
119923
  title: "Set live badge",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okostream/mcp",
3
- "version": "0.12.11",
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": {